From 0f9a73de2f818cb99946d754bcd9bc0bb294b12b Mon Sep 17 00:00:00 2001 From: dlavigne Date: Tue, 3 May 2016 14:20:44 -0400 Subject: [PATCH 1/3] Add identify RPC call. --- api/connection.rst | 56 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/api/connection.rst b/api/connection.rst index 9d3fc89..1e71afc 100644 --- a/api/connection.rst +++ b/api/connection.rst @@ -410,7 +410,7 @@ When submitting a job to the dispatcher, keep the following points in mind: * A chain of commands is useful for multi-step operations but is not considered a replacement for a good shell script on the server. -.. index:: query, rpc +.. index:: query, identify, rpc .. _Server Subsystems: @@ -418,7 +418,7 @@ Server Subsystems ================= An RPC query can be issued to probe all the known subsystems and return which ones are currently available and what level of read and write access the user has. -A query contains the following parameters: +An RPC query contains the following parameters: +---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ | **Parameter** | **Value** | **Description** | @@ -487,3 +487,55 @@ A query contains the following parameters: "name": "response", "namespace": "rpc" } + +To identify the type of SysAdm system, use an "identify" query. Possible identities are "server", "bridge", and "client". + +This type of query contains the following parameters: + ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ +| **Parameter** | **Value** | **Description** | +| | | | ++=================================+===============+======================================================================================================================+ +| id | | any unique value for the request; examples include a hash, checksum, or uuid | +| | | | ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ +| name | identify | | +| | | | ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ +| namespace | rpc | | +| | | | ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ +| args | | can be any data | +| | | | ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ + +**REST Request** + +.. code-block:: json + + PUT /rpc/identify + {} + +**WebSocket Request** + +.. code-block:: json + + { + "args" : {}, + "namespace" : "rpc", + "id" : "fooid", + "name" : "identify" + } + +**Response** + +.. code-block:: json + + { + "args": { + "type": "server" + }, + "id": "fooid", + "name": "response", + "namespace": "rpc" + } \ No newline at end of file From 45cec48f3c08117ebc8d9b0296d3e30c981f3b5d Mon Sep 17 00:00:00 2001 From: Ken Moore Date: Wed, 4 May 2016 14:45:59 -0400 Subject: [PATCH 2/3] Setup the authorization manager for the sysadm-bridge. The SSL authentication procedures for Bridge<-->System should now be completely functional. --- src/bridge/AuthorizationManager.cpp | 247 ++++++++++++++++++++++++++++ src/bridge/AuthorizationManager.h | 64 +++++++ src/bridge/BridgeConnection.cpp | 37 ++++- src/bridge/BridgeConnection.h | 1 + src/bridge/BridgeServer.cpp | 4 +- src/bridge/bridge.pro | 9 +- src/bridge/globals.h | 5 + src/bridge/main.cpp | 10 +- 8 files changed, 361 insertions(+), 16 deletions(-) create mode 100644 src/bridge/AuthorizationManager.cpp create mode 100644 src/bridge/AuthorizationManager.h diff --git a/src/bridge/AuthorizationManager.cpp b/src/bridge/AuthorizationManager.cpp new file mode 100644 index 0000000..ce33122 --- /dev/null +++ b/src/bridge/AuthorizationManager.cpp @@ -0,0 +1,247 @@ +// =============================== +// PC-BSD REST/JSON API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#include "AuthorizationManager.h" + +#include "globals.h" + +// Stuff for PAM to work +#include +#include +#include +#include +#include +#include +#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 +#define AUTHCHARS QString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") +#define TOKENLENGTH 40 + +AuthorizationManager::AuthorizationManager() : QObject(){ + HASH.clear(); + IPFAIL.clear(); + //initialize the random number generator (need to generate auth tokens) + qsrand(QDateTime::currentMSecsSinceEpoch()); +} + +AuthorizationManager::~AuthorizationManager(){ + +} + +// == Token Interaction functions == +void AuthorizationManager::clearAuth(QString token){ + if(token.isEmpty() || token.length() < TOKENLENGTH){ return; } //not a valid token + //clear an authorization token + QString id = hashID(token); + //qDebug() << "Clear Auth:" << id; + if(!id.isEmpty()){ HASH.remove(id); } +} + +bool AuthorizationManager::checkAuth(QString token){ + //see if the given token is valid + bool ok = false; + QString id = hashID(token); + if(!id.isEmpty()){ + //Also verify that the token has not timed out + ok = (HASH[id] > QDateTime::currentDateTime()); + if(ok){ HASH.insert(id, QDateTime::currentDateTime().addSecs(TIMEOUTSECS)); } //valid - bump the timestamp + } + return ok; +} + +//SSL Certificate register/revoke/list +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 + //NOTE: The public key should be a base64 encoded string + CONFIG->setValue("RegisteredCerts/"+user+"/"+pubkey, "Nickname: "+nickname+"\nEmail: "+email+"\nDate Registered: "+QDateTime::currentDateTime().toString(Qt::ISODate) ); + return true; +} + +bool AuthorizationManager::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 + QString cuser = hashID(token).section("::::",2,2); + + //Check that the given cert exists first + if( !CONFIG->contains("RegisteredCerts/"+cuser+"/"+key) ){ return false; } + CONFIG->remove("RegisteredCerts/"+cuser+"/"+key); + return true; +} + +void AuthorizationManager::ListCertificates(QString token, QJsonObject *out){ + QStringList keys; //Format: "RegisteredCerts//" + //Read all user's certs + keys = CONFIG->allKeys().filter("RegisteredCerts/"); + //qDebug() << "Found SSL Keys to List:" << keys; + + keys.sort(); + //Now put the known keys into the output structure arranged by username/key + QJsonObject user; QString username; + for(int i=0; iinsert(username, user); user = QJsonObject(); } //save the current info to the output + username = keys[i].section("/",1,1); //save the new username for later + } + QString info = CONFIG->value(keys[i]).toString(); + QString key = keys[i].section("/",2,-1);//just in case the key has additional "/" in it + user.insert(key,info); + } + if(!user.isEmpty() && !username.isEmpty()){ out->insert(username, user); } +} + +//Generic functions +int AuthorizationManager::checkAuthTimeoutSecs(QString token){ + //Return the number of seconds that a token is valid for + if(!HASH.contains(token)){ return 0; } //invalid token + return QDateTime::currentDateTime().secsTo( HASH[token] ); +} + + +// == Token Generation functions + +//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//" + //qDebug() << " - Check pubkeys";// << pubkeys; + for(int i=0; i::::" + QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::"); + int fails = 0; + if(!keys.isEmpty()){ + //Take the existing key/value and put a new one in (this limits the filter to 1 value maximum) + QDateTime last = IPFAIL.take(keys[0]); + if(last.addSecs(CONFIG->value("blacklist_settings/block_minutes",60).toInt()*60) > QDateTime::currentDateTime() ){ + fails = keys[0].section("::::",1,1).toInt(); + } + } + fails++; + IPFAIL.insert(host+"::::"+QString::number(fails), QDateTime::currentDateTime() ); + return (fails>=CONFIG->value("blacklist_settings/fails_to_block",2).toInt()); +} + +void AuthorizationManager::ClearHostFail(QString host){ + QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::"); + for(int i=0; i July 2015 +// ================================= +#ifndef _PCBSD_REST_AUTHORIZATION_MANAGER_H +#define _PCBSD_REST_AUTHORIZATION_MANAGER_H + +#include +#include +#include +#include +#include +#include + +class AuthorizationManager : public QObject{ + Q_OBJECT +public: + AuthorizationManager(); + ~AuthorizationManager(); + + // == Token Interaction functions == + void clearAuth(QString token); //clear an authorization token + bool checkAuth(QString token); //see if the given token is valid + + //SSL Certificate register/revoke/list (should only run if the current token is valid) + 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); + + int checkAuthTimeoutSecs(QString token); //Return the number of seconds that a token is valid for + + //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; + + QString generateNewToken(bool isOperator, QString name); + + //Failure count management + bool BumpFailCount(QString host); + void ClearHostFail(QString host); + + //token->hashID filter simplification + QString hashID(QString token){ + QStringList tmp = QStringList(HASH.keys()).filter(token+"::::"); + if(tmp.isEmpty()){ return ""; } + else{ return tmp.first(); } + } + + //SSL Decrypt function + QString DecryptSSLString(QString encstring, QString pubkey); + +signals: + void BlockHost(QHostAddress); //block a host address temporarily + +}; + +#endif diff --git a/src/bridge/BridgeConnection.cpp b/src/bridge/BridgeConnection.cpp index 32b2e40..151feec 100644 --- a/src/bridge/BridgeConnection.cpp +++ b/src/bridge/BridgeConnection.cpp @@ -77,7 +77,7 @@ QStringList BridgeConnection::JsonArrayToStringList(QJsonArray array){ void BridgeConnection::InjectMessage(QString msg){ //See if this message is directed to the bridge itself, or a client - if(msg.startsWith("{")){ + if(msg.startsWith("{") || !AUTHSYSTEM->checkAuth(SockAuthToken) ){ HandleAPIMessage(msg); }else{ //Need to read the destination off the message first @@ -119,10 +119,35 @@ void BridgeConnection::HandleAPIMessage(QString msg){ out.insert("id", JM.value("id")); out.insert("namespace", namesp); out.insert("name","reponse"); - QJsonObject outargs; + QJsonValue outargs; //There is only a short list of API calls the bridge is capable of: if(namesp == "rpc" && name=="identify"){ - outargs.insert("type","bridge"); + QJsonObject tmp; + tmp.insert("type","bridge"); + outargs = tmp; + + }else if(namesp == "rpc" && name=="auth_ssl"){ + if(!args.contains("encrypted_string")){ + //Stage 1 - send a random string to encrypt + QString key = AUTHSYSTEM->GenerateEncCheckString(); + QJsonObject obj; obj.insert("test_string", key); + outargs = obj; + }else{ + //Stage 2 - verify returned encrypted string + SockAuthToken = AUTHSYSTEM->LoginUC(SOCKET->peerAddress(),args.value("encrypted_string").toString() ); + if(AUTHSYSTEM->checkAuth(SockAuthToken)){ + QJsonArray array; + array.append(SockAuthToken); + array.append(AUTHSYSTEM->checkAuthTimeoutSecs(SockAuthToken)); + outargs = array; + }else{ + out.insert("name","error"); + outargs = "unauthorized"; + } + } + }else if(AUTHSYSTEM->checkAuth(SockAuthToken)){ + //Valid auth - a couple more API calls available here + }else{ out.insert("name","error"); //unknown API call } @@ -142,10 +167,10 @@ void BridgeConnection::checkIdle(){ } void BridgeConnection::checkAuth(){ - //if(!AUTHSYSTEM->checkAuth(SockAuthToken)){ + if(!AUTHSYSTEM->checkAuth(SockAuthToken)){ //Still not authorized - disconnect - //checkIdle(); - //} + checkIdle(); + } } void BridgeConnection::SocketClosing(){ diff --git a/src/bridge/BridgeConnection.h b/src/bridge/BridgeConnection.h index 0e7ab70..9bc4f8d 100644 --- a/src/bridge/BridgeConnection.h +++ b/src/bridge/BridgeConnection.h @@ -30,6 +30,7 @@ private: void InjectMessage(QString msg); void HandleAPIMessage(QString msg); + private slots: void checkIdle(); //see if the currently-connected client is idle void checkAuth(); //see if the currently-connected client has authed yet diff --git a/src/bridge/BridgeServer.cpp b/src/bridge/BridgeServer.cpp index 1cc5bb9..3adf362 100644 --- a/src/bridge/BridgeServer.cpp +++ b/src/bridge/BridgeServer.cpp @@ -13,7 +13,7 @@ BridgeServer::BridgeServer() : QWebSocketServer("sysadm-bridge", QWebSocketServer::SecureMode){ //Setup all the various settings //AUTH = new AuthorizationManager(); - //connect(AUTH, SIGNAL(BlockHost(QHostAddress)), this, SLOT(BlackListConnection(QHostAddress)) ); + connect(AUTHSYSTEM, SIGNAL(BlockHost(QHostAddress)), this, SLOT(BlackListConnection(QHostAddress)) ); } BridgeServer::~BridgeServer(){ @@ -92,7 +92,7 @@ bool BridgeServer::allowConnection(QHostAddress addr){ if(!CONFIG->contains(key) ){ return true; } //not in the list //Address on the list - see if the timeout has expired QDateTime dt = CONFIG->value(key,QDateTime()).toDateTime(); - if(dt.addSecs(CONFIG->value("blacklist_settings/blockmins",60).toInt()*60) < QDateTime::currentDateTime()){ + if(dt.addSecs(CONFIG->value("blacklist_settings/block_minutes",60).toInt()*60) < QDateTime::currentDateTime()){ //This entry has timed out - go ahead and allow it CONFIG->remove(key); //make the next connection check for this IP faster again return true; diff --git a/src/bridge/bridge.pro b/src/bridge/bridge.pro index 8d8c277..99d41c0 100644 --- a/src/bridge/bridge.pro +++ b/src/bridge/bridge.pro @@ -6,12 +6,13 @@ QT = core network websockets HEADERS += globals.h \ BridgeServer.h \ - BridgeConnection.h + BridgeConnection.h \ + AuthorizationManager.h SOURCES += main.cpp \ BridgeServer.cpp \ - BridgeConnection.cpp - + BridgeConnection.cpp \ + AuthorizationManager.cpp TARGET=sysadm-bridge @@ -25,4 +26,4 @@ INSTALLS += target scripts QMAKE_LIBDIR = /usr/local/lib/qt5 /usr/local/lib INCLUDEPATH += /usr/local/include -LIBS += -L/usr/local/lib -lpam -lutil -lssl -lcrypto +LIBS += -L/usr/local/lib -lutil -lssl -lcrypto diff --git a/src/bridge/globals.h b/src/bridge/globals.h index 890eaa4..a4a2a4c 100644 --- a/src/bridge/globals.h +++ b/src/bridge/globals.h @@ -37,4 +37,9 @@ #define SETTINGSFILE "/var/db/sysadm-bridge.ini" +#define SYSADM_BRIDGE +#include "AuthorizationManager.h" + extern QSettings* CONFIG; +extern AuthorizationManager* AUTHSYSTEM; + diff --git a/src/bridge/main.cpp b/src/bridge/main.cpp index 4a7a9aa..012a6d4 100644 --- a/src/bridge/main.cpp +++ b/src/bridge/main.cpp @@ -12,9 +12,9 @@ #define DEBUG 0 -//Create any global classes +//Create any global classes/settings QSettings *CONFIG = new QSettings(SETTINGSFILE, QSettings::IniFormat); - +AuthorizationManager *AUTHSYSTEM = new AuthorizationManager(); //Set the defail values for the global config variables /*int BlackList_BlockMinutes = 60; @@ -59,7 +59,9 @@ int main( int argc, char ** argv ) if(!info.contains("=")){ continue; } //invalid format QString var = info.section("=",0,0); QString val = info.section("=",1,-1); qDebug() << "Changing bridge setting:" << info; - if(var=="blacklist/blockmins"){ CONFIG->setValue("blacklist_settings/blockmins",val.toInt()); } + if(var=="blacklist/block_minutes"){ CONFIG->setValue("blacklist_settings/block_minutes",val.toInt()); } + else if(var=="blacklist/fails_to_block"){ CONFIG->setValue("blacklist_settings/fails_to_block",val.toInt()); } + //else if(var=="blacklist_settings/block_minutes"){ CONFIG->setValue("blacklist_settings/block_minutes",val.toInt()); } } else if( (QString(argv[i])=="-port" || QString(argv[i])=="-p") && (i+1 Date: Wed, 4 May 2016 14:54:04 -0400 Subject: [PATCH 3/3] Enable the 30 second window for a client to successfully authorize before the connection is closed. --- src/bridge/BridgeConnection.cpp | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/bridge/BridgeConnection.cpp b/src/bridge/BridgeConnection.cpp index 151feec..3253ce0 100644 --- a/src/bridge/BridgeConnection.cpp +++ b/src/bridge/BridgeConnection.cpp @@ -16,9 +16,9 @@ BridgeConnection::BridgeConnection(QWebSocket *sock, QString ID){ SOCKET = sock; SockPeerIP = SOCKET->peerAddress().toString(); idletimer = new QTimer(this); - idletimer->setInterval(IDLETIMEOUTMINS*60000); //connection timout for idle sockets + idletimer->setInterval(30000); //connection timout for idle sockets idletimer->setSingleShot(true); - connect(idletimer, SIGNAL(timeout()), this, SLOT(checkgonintendoIdle()) ); + connect(idletimer, SIGNAL(timeout()), this, SLOT(checkAuth()) ); connect(SOCKET, SIGNAL(textMessageReceived(const QString&)), this, SLOT(EvaluateMessage(const QString&)) ); connect(SOCKET, SIGNAL(binaryMessageReceived(const QByteArray&)), this, SLOT(EvaluateMessage(const QByteArray&)) ); connect(SOCKET, SIGNAL(aboutToClose()), this, SLOT(SocketClosing()) ); @@ -175,13 +175,7 @@ void BridgeConnection::checkAuth(){ void BridgeConnection::SocketClosing(){ qDebug() << "Connection Closing: " << SockPeerIP; - if(idletimer->isActive()){ - //This means the client deliberately closed the connection - not the idle timer - //qDebug() << " - Client Closed Connection"; - idletimer->stop(); - }else{ - //qDebug() << "idleTimer not running"; - } + if(idletimer->isActive()){ idletimer->stop(); } //Stop any current requests //Reset the pointer @@ -192,16 +186,12 @@ void BridgeConnection::SocketClosing(){ void BridgeConnection::EvaluateMessage(const QByteArray &msg){ //qDebug() << "New Binary Message:"; - if(idletimer->isActive()){ idletimer->stop(); } - idletimer->start(); InjectMessage( QString(msg) ); //qDebug() << " - Done with Binary Message"; } void BridgeConnection::EvaluateMessage(const QString &msg){ //qDebug() << "New Text Message:"; - if(idletimer->isActive()){ idletimer->stop(); } - idletimer->start(); InjectMessage(msg); //qDebug() << " - Done with Text Message"; }