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:
Ken Moore
2016-02-18 17:01:09 -05:00
parent 750f7c8c85
commit 6cfaf6e597
8 changed files with 149 additions and 81 deletions

View File

@@ -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 ==========
*/

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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
};

View File

@@ -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);

View File

@@ -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";

View File

@@ -27,7 +27,6 @@ private:
QString SockID, SockAuthToken, SockPeerIP;
AuthorizationManager *AUTHSYSTEM;
QList<EventWatcher::EVENT_TYPE> ForwardEvents;
void sendReply(QString msg);
//Main connection comminucations procedure

View File

@@ -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