mirror of
https://github.com/outbackdingo/sysadm.git
synced 2026-01-27 10:20:26 +00:00
Completely re-do the SSL authentication systems. Now it is a two-stage auth system, where the server generates a random string, sends it to the client for encryptions with their private SSL key, then gets sent back to the server where the new string is decrypted with the known SSL keys and compared to teh original for accuracy.
This commit is contained in:
@@ -17,6 +17,14 @@
|
||||
#include <pwd.h>
|
||||
#include <login_cap.h>
|
||||
|
||||
//Stuff for OpenSSL to work
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/err.h>
|
||||
|
||||
//Internal defines
|
||||
// -- token management
|
||||
#define TIMEOUTSECS 900 // (15 minutes) time before a token becomes invalid
|
||||
@@ -69,10 +77,10 @@ bool AuthorizationManager::hasFullAccess(QString token){
|
||||
}
|
||||
|
||||
//SSL Certificate register/revoke/list
|
||||
bool AuthorizationManager::RegisterCertificate(QString token, QSslCertificate cert){
|
||||
bool AuthorizationManager::RegisterCertificate(QString token, QString pubkey, QString nickname, QString email){
|
||||
if(!checkAuth(token)){ return false; }
|
||||
QString user = hashID(token).section("::::",2,2); //get the user name from the currently-valid token
|
||||
CONFIG->setValue("RegisteredCerts/"+user+"/"+QString(cert.publicKey().toPem()), cert.toText());
|
||||
CONFIG->setValue("RegisteredCerts/"+user+"/"+pubkey, "Nickname: "+nickname+", Email: "+email);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -158,49 +166,6 @@ QString AuthorizationManager::LoginUP(QHostAddress host, QString user, QString p
|
||||
}
|
||||
}
|
||||
|
||||
QString AuthorizationManager::LoginUC(QHostAddress host, QString user, QList<QSslCertificate> certs){
|
||||
//Login w/ username & SSL certificate
|
||||
bool localhost = ( (host== QHostAddress::LocalHost) || (host== QHostAddress::LocalHostIPv6) || (host.toString()=="::ffff:127.0.0.1") );
|
||||
bool ok = false;
|
||||
//First check that the user is valid on the system and part of the operator group
|
||||
bool isOperator = false;
|
||||
if(user!="root" && user!="toor"){
|
||||
QStringList groups = getUserGroups(user);
|
||||
if(groups.contains("wheel")){ isOperator = true; } //full-access user
|
||||
else if(!groups.contains("operator")){
|
||||
return ""; //user not allowed access if not in either of the wheel/operator groups
|
||||
}
|
||||
}else{ isOperator = true; }
|
||||
//qDebug() << "Check username/certificate combination" << user << localhost;
|
||||
|
||||
//Need to check the registered certificates for the designated user
|
||||
if(!localhost || user=="root" || user=="toor"){
|
||||
for(int i=0; i<certs.length() && !ok; i++){
|
||||
if(CONFIG->contains("RegisteredCerts/"+user+"/"+QString(certs[i].publicKey().toPem()) ) ){
|
||||
//Cert was registered - check expiration info
|
||||
// TO-DO
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
ok = true; //allow local access for users without password
|
||||
}
|
||||
|
||||
qDebug() << "User Login Attempt:" << user << " Success:" << ok << " IP:" << host.toString();
|
||||
LogManager::log(LogManager::HOST, QString("User Login Attempt: ")+user+" Success: "+(ok?"true":"false")+" IP: "+host.toString() );
|
||||
if(!ok){
|
||||
//invalid login
|
||||
//Bump the fail count for this host
|
||||
bool overlimit = BumpFailCount(host.toString());
|
||||
if(overlimit){ emit BlockHost(host); }
|
||||
return (overlimit ? "REFUSED" : "");
|
||||
}else{
|
||||
//valid login - generate a new token for it
|
||||
ClearHostFail(host.toString());
|
||||
return generateNewToken(isOperator, user);
|
||||
}
|
||||
}
|
||||
|
||||
QString AuthorizationManager::LoginService(QHostAddress host, QString service){
|
||||
bool localhost = ( (host== QHostAddress::LocalHost) || (host== QHostAddress::LocalHostIPv6) || (host.toString()=="::ffff:127.0.0.1") );
|
||||
|
||||
@@ -228,6 +193,80 @@ QString AuthorizationManager::LoginService(QHostAddress host, QString service){
|
||||
}else{ return generateNewToken(false, service); }//services are never given operator privileges
|
||||
}
|
||||
|
||||
//Stage 1 SSL Login Check: Generation of random string for this user
|
||||
QString AuthorizationManager::GenerateEncCheckString(){
|
||||
QString key;
|
||||
for(int i=0; i<TOKENLENGTH; i++){
|
||||
key.append( AUTHCHARS.at( qrand() % AUTHCHARS.length() ) );
|
||||
}
|
||||
if(HASH.contains("SSL_CHECK_STRING/"+key)){ key = GenerateEncCheckString(); } //get a different one
|
||||
else{
|
||||
//insert this new key into the hash for later
|
||||
HASH.insert("SSL_CHECK_STRING/"+key, QDateTime::currentDateTime().addSecs(30) ); //only keep a key "alive" for 30 seconds
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
//Stage 2 SSL Login Check: Verify that the returned/encrypted string can be decoded and matches the initial random string
|
||||
QString AuthorizationManager::LoginUC(QHostAddress host, QString encstring){
|
||||
//Login w/ SSL certificate
|
||||
bool ok = false;
|
||||
|
||||
//First clean out any old strings/keys
|
||||
QStringList pubkeys = QStringList(HASH.keys()).filter("SSL_CHECK_STRING/"); //temporary, re-use variable below
|
||||
for(int i=0; i<pubkeys.length(); i++){
|
||||
//Check expiration time on each initial string
|
||||
if(QDateTime::currentDateTime() > HASH[pubkeys[i]]){
|
||||
//Note: normally only 1 request per user at a time, but it is possible for a couple different clients to try
|
||||
// and authenticate as the same user (but different keys) at nearly the same time - so keep a short valid-string time frame (<30 seconds)
|
||||
// to mitigate this possibility (need to prevent the second user-auth request from invalidating the first before the first auth handshake is finished)
|
||||
HASH.remove(pubkeys[i]); //initstring expired - go ahead and remove it to reduce calc time later
|
||||
}
|
||||
}
|
||||
//Now re-use the "pubkeys" variable for the public SSL keys
|
||||
QString user;
|
||||
pubkeys = CONFIG->allKeys().filter("RegisteredCerts/"); //Format: "RegisteredCerts/<user>/<key>"
|
||||
for(int i=0; i<pubkeys.length() && !ok; i++){
|
||||
//Decrypt the string with this pubkey - and compare to the outstanding initstrings
|
||||
QString key = DecryptSSLString(encstring, pubkeys[i].section("/",2,50000));
|
||||
if(HASH.contains("SSL_CHECK_STRING/"+key)){
|
||||
//Valid reponse found
|
||||
ok = true;
|
||||
//Remove the initstring from the hash (already used)
|
||||
HASH.remove("SSL_CHECK_STRING/"+key);
|
||||
user = pubkeys[i].section("/",1,1);
|
||||
}
|
||||
}
|
||||
bool isOperator = false;
|
||||
if(ok){
|
||||
//First check that the user is valid on the system and part of the operator group
|
||||
|
||||
if(user!="root" && user!="toor"){
|
||||
QStringList groups = getUserGroups(user);
|
||||
if(groups.contains("wheel")){ isOperator = true; } //full-access user
|
||||
else if(!groups.contains("operator")){
|
||||
return ""; //user not allowed access if not in either of the wheel/operator groups
|
||||
}
|
||||
}else{ isOperator = true; }
|
||||
|
||||
}
|
||||
if(user.isEmpty()){ ok = false; }
|
||||
|
||||
qDebug() << "User Login Attempt:" << user << " Success:" << ok << " IP:" << host.toString();
|
||||
LogManager::log(LogManager::HOST, QString("User Login Attempt: ")+user+" Success: "+(ok?"true":"false")+" IP: "+host.toString() );
|
||||
if(!ok){
|
||||
//invalid login
|
||||
//Bump the fail count for this host
|
||||
bool overlimit = BumpFailCount(host.toString());
|
||||
if(overlimit){ emit BlockHost(host); }
|
||||
return (overlimit ? "REFUSED" : "");
|
||||
}else{
|
||||
//valid login - generate a new token for it
|
||||
ClearHostFail(host.toString());
|
||||
return generateNewToken(isOperator, user);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// PRIVATE
|
||||
// =========================
|
||||
@@ -288,6 +327,18 @@ void AuthorizationManager::ClearHostFail(QString host){
|
||||
for(int i=0; i<keys.length(); i++){ IPFAIL.remove(keys[i]); }
|
||||
}
|
||||
|
||||
QString AuthorizationManager::DecryptSSLString(QString encstring, QString pubkey){
|
||||
unsigned char decode[4098] = {};
|
||||
RSA *rsa= NULL;
|
||||
BIO *keybio = NULL;
|
||||
keybio = BIO_new_mem_buf(pubkey.toLatin1().data(), -1);
|
||||
if(keybio==NULL){ return ""; }
|
||||
rsa = PEM_read_bio_RSA_PUBKEY(keybio, &rsa,NULL, NULL);
|
||||
bool ok = (-1 != RSA_public_decrypt(encstring.length(), (unsigned char*)(encstring.toLatin1().data()), decode, rsa, RSA_PKCS1_PADDING) );
|
||||
if(!ok){ return ""; }
|
||||
else{ return QString::fromLatin1( (char *)(decode) ).simplified(); }
|
||||
}
|
||||
|
||||
/*
|
||||
========== PAM FUNCTIONS ==========
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,7 @@ public:
|
||||
bool hasFullAccess(QString token); //see if the token is associated with a full-access account
|
||||
|
||||
//SSL Certificate register/revoke/list (should only run if the current token is valid)
|
||||
bool RegisterCertificate(QString token, QSslCertificate cert); //if token is valid, register the given cert for future logins
|
||||
bool RegisterCertificate(QString token, QString pubkey, QString nickname, QString email); //if token is valid, register the given cert for future logins
|
||||
bool RevokeCertificate(QString token, QString key, QString user=""); //user will be the current user if not empty - cannot touch other user's certs without full perms on current session
|
||||
void ListCertificates(QString token, QJsonObject *out);
|
||||
|
||||
@@ -28,9 +28,14 @@ public:
|
||||
|
||||
// == Token Generation functions
|
||||
QString LoginUP(QHostAddress host, QString user, QString pass); //Login w/ username & password
|
||||
QString LoginUC(QHostAddress host, QString user, QList<QSslCertificate> certs); //Login w/ username & SSL certificate
|
||||
QString LoginService(QHostAddress host, QString service); //Login a particular automated service
|
||||
|
||||
//Stage 1 SSL Login Check: Generation of random string for this user
|
||||
QString GenerateEncCheckString();
|
||||
//Stage 2 SSL Login Check: Verify that the returned/encrypted string can be decoded and matches the initial random string
|
||||
QString LoginUC(QHostAddress host, QString encstring);
|
||||
|
||||
|
||||
private:
|
||||
QHash<QString, QDateTime> HASH;
|
||||
QHash <QString, QDateTime> IPFAIL;
|
||||
@@ -49,6 +54,9 @@ private:
|
||||
else{ return tmp.first(); }
|
||||
}
|
||||
|
||||
//SSL Decrypt function
|
||||
QString DecryptSSLString(QString encstring, QString pubkey);
|
||||
|
||||
//PAM login/check files
|
||||
bool pam_checkPW(QString user, QString pass);
|
||||
void pam_logFailure(int ret);
|
||||
|
||||
@@ -70,8 +70,12 @@ void DProcess::cmdFinished(int ret, QProcess::ExitStatus status){
|
||||
proclog.append( this->readAllStandardOutput() );
|
||||
//Now run any additional commands
|
||||
//qDebug() << "Proc Finished:" << ID << success << proclog;
|
||||
if(success){ startProc(); }//will emit the finished signal as needed if no more commands
|
||||
else{
|
||||
if(success && !cmds.isEmpty()){ startProc(); }
|
||||
else if(success){
|
||||
t_finished = QDateTime::currentDateTime();
|
||||
emit ProcFinished(ID);
|
||||
emit Finished(ID, ret, proclog);
|
||||
}else{
|
||||
if(status==QProcess::NormalExit){
|
||||
proclog.append("\n[Command Failed: " + QString::number(ret)+" ]");
|
||||
}else{
|
||||
@@ -79,6 +83,7 @@ void DProcess::cmdFinished(int ret, QProcess::ExitStatus status){
|
||||
}
|
||||
t_finished = QDateTime::currentDateTime();
|
||||
emit ProcFinished(ID);
|
||||
emit Finished(ID, ret, proclog);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
|
||||
// == Simple Process class for running sequential commands ==
|
||||
// == INTERNAL ONLY - Do not use directly ==
|
||||
class DProcess : public QProcess{
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -42,7 +41,8 @@ private slots:
|
||||
void cmdFinished(int, QProcess::ExitStatus);
|
||||
|
||||
signals:
|
||||
void ProcFinished(QString ID);
|
||||
void ProcFinished(QString); //ID
|
||||
void Finished(QString, int, QString); //ID, retcode, log
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ RestOutputStruct::ExitCode WebSocket::EvaluateBackendRequest(const RestInputStru
|
||||
AvailableSubsystems(IN.fullaccess, &avail);
|
||||
if(!avail.contains(namesp+"/"+name)){ return RestOutputStruct::NOTFOUND; }
|
||||
}
|
||||
|
||||
qDebug() << "Evaluate Backend Request:" << namesp << name;
|
||||
//Go through and forward this request to the appropriate sub-system
|
||||
if(namesp=="sysadm" && name=="settings"){
|
||||
return EvaluateSysadmSettingsRequest(IN.args, out);
|
||||
@@ -125,24 +125,25 @@ RestOutputStruct::ExitCode WebSocket::EvaluateBackendRequest(const RestInputStru
|
||||
|
||||
// === SYSADM SETTINGS ===
|
||||
RestOutputStruct::ExitCode WebSocket::EvaluateSysadmSettingsRequest(const QJsonValue in_args, QJsonObject *out){
|
||||
qDebug() << "sysadm/settings Request:" << in_args;
|
||||
if(!in_args.isObject()){ return RestOutputStruct::BADREQUEST; }
|
||||
QJsonObject argsO = in_args.toObject();
|
||||
QStringList keys = argsO.keys();
|
||||
qDebug() << " - keys:" << keys;
|
||||
if(!keys.contains("action")){ return RestOutputStruct::BADREQUEST; }
|
||||
QString act = argsO.value("action").toString();
|
||||
bool ok = false;
|
||||
if(act=="register_ssl_cert" && keys.contains("pub_key")){
|
||||
//Additional arguments: "pub_key" (String), and the cert with that key must already be loaded into the connection
|
||||
QString pub_key = argsO.value("pub_key").toString();\
|
||||
//Now find the currently-loaded certificate with the given public key
|
||||
QList<QSslCertificate> certs;
|
||||
if(SOCKET!=0){ certs = SOCKET->sslConfiguration().peerCertificateChain(); }
|
||||
else if(TSOCKET!=0){ certs = TSOCKET->peerCertificateChain(); }
|
||||
for(int i=0; i<certs.length() && !ok; i++){
|
||||
if(certs[i].publicKey().toPem()==pub_key){
|
||||
//Certificate found - register it
|
||||
ok = AUTHSYSTEM->RegisterCertificate(SockAuthToken, certs[i]);
|
||||
}
|
||||
//Required arguments: "pub_key" (String)
|
||||
//Optional arguments: "nickname" (String), "email" (String)
|
||||
QString pub_key, nickname, email;
|
||||
pub_key = argsO.value("pub_key").toString();
|
||||
if(keys.contains("nickname")){ nickname = argsO.value("nickname").toString(); }
|
||||
if(keys.contains("email")){ email = argsO.value("email").toString(); }
|
||||
|
||||
if(!pub_key.isEmpty()){
|
||||
ok = AUTHSYSTEM->RegisterCertificate(SockAuthToken, pub_key, nickname, email);
|
||||
if(!ok){ return RestOutputStruct::FORBIDDEN; }
|
||||
}
|
||||
}else if(act=="list_ssl_certs"){
|
||||
AUTHSYSTEM->ListCertificates(SockAuthToken, out);
|
||||
|
||||
@@ -145,7 +145,7 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
|
||||
}
|
||||
|
||||
//Now check the body of the message and do what it needs
|
||||
if(out.in_struct.namesp.toLower() == "rpc"){
|
||||
if(out.in_struct.namesp.toLower() == "rpc"){
|
||||
if(out.in_struct.name.startsWith("auth")){
|
||||
//Now perform authentication based on type of auth given
|
||||
//Note: This sets/changes the current SockAuthToken
|
||||
@@ -157,15 +157,18 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
|
||||
QString user, pass;
|
||||
if(out.in_struct.args.toObject().contains("username")){ user = JsonValueToString(out.in_struct.args.toObject().value("username")); }
|
||||
if(out.in_struct.args.toObject().contains("password")){ pass = JsonValueToString(out.in_struct.args.toObject().value("password")); }
|
||||
if(!pass.isEmpty()){
|
||||
|
||||
//Use the given password
|
||||
SockAuthToken = AUTHSYSTEM->LoginUP(host, user, pass);
|
||||
}else{
|
||||
//No password - use the current SSL certificates instead
|
||||
QList<QSslCertificate> certs;
|
||||
if(SOCKET!=0){ certs = SOCKET->sslConfiguration().peerCertificateChain(); }
|
||||
else if(TSOCKET!=0){ certs = TSOCKET->peerCertificateChain(); }
|
||||
SockAuthToken = AUTHSYSTEM->LoginUC(host, user, certs);
|
||||
}else if(out.in_struct.name=="auth_ssl" && out.in_struct.args.isObject() ){
|
||||
if(!out.in_struct.args.toObject().contains("encrypted_string")){
|
||||
//Stage 1: Send the client a random string to encrypt with their SSL key
|
||||
QString key = AUTHSYSTEM->GenerateEncCheckString();
|
||||
QJsonObject obj; obj.insert("test_string", key);
|
||||
out.CODE = RestOutputStruct::PARTIALCONTENT;
|
||||
}else{
|
||||
//Stage 2: Check the returned encrypted/string
|
||||
SockAuthToken = AUTHSYSTEM->LoginUC(host, JsonValueToString(out.in_struct.args.toObject().value("encrypted_string")) );
|
||||
}
|
||||
}else if(out.in_struct.name == "auth_token" && out.in_struct.args.isObject()){
|
||||
SockAuthToken = JsonValueToString(out.in_struct.args.toObject().value("token"));
|
||||
@@ -193,17 +196,17 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
|
||||
}else if( AUTHSYSTEM->checkAuth(SockAuthToken) ){ //validate current Authentication token
|
||||
//Now provide access to the various subsystems
|
||||
// First get/set the permissions flag into the input structure
|
||||
out.in_struct.fullaccess = AUTHSYSTEM->hasFullAccess(SockAuthToken);
|
||||
out.in_struct.fullaccess = AUTHSYSTEM->hasFullAccess(SockAuthToken);
|
||||
//Pre-set any output fields
|
||||
QJsonObject outargs;
|
||||
QJsonObject outargs;
|
||||
out.CODE = EvaluateBackendRequest(out.in_struct, &outargs);
|
||||
out.out_args = outargs;
|
||||
}else{
|
||||
out.out_args = outargs;
|
||||
}else{
|
||||
//Bad/No authentication
|
||||
out.CODE = RestOutputStruct::UNAUTHORIZED;
|
||||
}
|
||||
|
||||
}else if(out.in_struct.namesp.toLower() == "events"){
|
||||
}else if(out.in_struct.namesp.toLower() == "events"){
|
||||
if( AUTHSYSTEM->checkAuth(SockAuthToken) ){ //validate current Authentication token
|
||||
//Pre-set any output fields
|
||||
QJsonObject outargs;
|
||||
@@ -239,7 +242,7 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
|
||||
out.CODE = RestOutputStruct::UNAUTHORIZED;
|
||||
}
|
||||
//Other namespace - check whether auth has already been established before continuing
|
||||
}else if( AUTHSYSTEM->checkAuth(SockAuthToken) ){ //validate current Authentication token
|
||||
}else if( AUTHSYSTEM->checkAuth(SockAuthToken) ){ //validate current Authentication token
|
||||
//Now provide access to the various subsystems
|
||||
// First get/set the permissions flag into the input structure
|
||||
out.in_struct.fullaccess = AUTHSYSTEM->hasFullAccess(SockAuthToken);
|
||||
@@ -247,10 +250,11 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){
|
||||
QJsonObject outargs;
|
||||
out.CODE = EvaluateBackendRequest(out.in_struct, &outargs);
|
||||
out.out_args = outargs;
|
||||
}else{
|
||||
}else{
|
||||
//Error in inputs - assemble the return error message
|
||||
out.CODE = RestOutputStruct::UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
|
||||
//If this is a REST input - go ahead and format the output header
|
||||
if(out.CODE == RestOutputStruct::OK){
|
||||
out.Header << "Content-Type: text/json; charset=utf-8";
|
||||
|
||||
@@ -27,7 +27,6 @@ private:
|
||||
QString SockID, SockAuthToken, SockPeerIP;
|
||||
AuthorizationManager *AUTHSYSTEM;
|
||||
QList<EventWatcher::EVENT_TYPE> ForwardEvents;
|
||||
|
||||
void sendReply(QString msg);
|
||||
|
||||
//Main connection comminucations procedure
|
||||
|
||||
@@ -37,4 +37,4 @@ INSTALLS += target
|
||||
|
||||
QMAKE_LIBDIR = /usr/local/lib/qt5 /usr/local/lib
|
||||
INCLUDEPATH += /usr/local/include
|
||||
LIBS += -L/usr/local/lib -lpam -lutil
|
||||
LIBS += -L/usr/local/lib -lpam -lutil -lssl -lcrypto
|
||||
|
||||
Reference in New Issue
Block a user