From 6cfaf6e5979dc67738a6cb5232f37ce140e6fe19 Mon Sep 17 00:00:00 2001 From: Ken Moore Date: Thu, 18 Feb 2016 17:01:09 -0500 Subject: [PATCH] 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. --- src/server/AuthorizationManager.cpp | 141 +++++++++++++++++++--------- src/server/AuthorizationManager.h | 12 ++- src/server/Dispatcher.cpp | 9 +- src/server/Dispatcher.h | 4 +- src/server/WebBackend.cpp | 25 ++--- src/server/WebSocket.cpp | 36 +++---- src/server/WebSocket.h | 1 - src/server/server.pro | 2 +- 8 files changed, 149 insertions(+), 81 deletions(-) diff --git a/src/server/AuthorizationManager.cpp b/src/server/AuthorizationManager.cpp index 2a20164..3cb65fe 100644 --- a/src/server/AuthorizationManager.cpp +++ b/src/server/AuthorizationManager.cpp @@ -17,6 +17,14 @@ #include #include +//Stuff for OpenSSL to work +#include +#include +#include +#include +#include +#include + //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 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; icontains("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 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//" + for(int i=0; i 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 HASH; QHash 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); diff --git a/src/server/Dispatcher.cpp b/src/server/Dispatcher.cpp index a498f7e..98b1ae3 100644 --- a/src/server/Dispatcher.cpp +++ b/src/server/Dispatcher.cpp @@ -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); } } diff --git a/src/server/Dispatcher.h b/src/server/Dispatcher.h index 8ab12ea..78748e2 100644 --- a/src/server/Dispatcher.h +++ b/src/server/Dispatcher.h @@ -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 }; diff --git a/src/server/WebBackend.cpp b/src/server/WebBackend.cpp index 1092fdd..79a76ea 100644 --- a/src/server/WebBackend.cpp +++ b/src/server/WebBackend.cpp @@ -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 certs; - if(SOCKET!=0){ certs = SOCKET->sslConfiguration().peerCertificateChain(); } - else if(TSOCKET!=0){ certs = TSOCKET->peerCertificateChain(); } - for(int i=0; iRegisterCertificate(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); diff --git a/src/server/WebSocket.cpp b/src/server/WebSocket.cpp index c58b79c..1900bea 100644 --- a/src/server/WebSocket.cpp +++ b/src/server/WebSocket.cpp @@ -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 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"; diff --git a/src/server/WebSocket.h b/src/server/WebSocket.h index b30d837..1eae6a6 100644 --- a/src/server/WebSocket.h +++ b/src/server/WebSocket.h @@ -27,7 +27,6 @@ private: QString SockID, SockAuthToken, SockPeerIP; AuthorizationManager *AUTHSYSTEM; QList ForwardEvents; - void sendReply(QString msg); //Main connection comminucations procedure diff --git a/src/server/server.pro b/src/server/server.pro index e99e9db..c9c768c 100644 --- a/src/server/server.pro +++ b/src/server/server.pro @@ -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