Merge pull request #5513 from bwent/domain-server-authentication

Add connection token UUId to sign with username when connecting to domain-server
This commit is contained in:
Stephen Birarda 2015-08-07 10:05:00 -07:00
commit eec382ade8
10 changed files with 162 additions and 90 deletions

View file

@ -579,7 +579,6 @@ const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer
void DomainServer::processConnectRequestPacket(QSharedPointer<NLPacket> packet) {
NodeType_t nodeType;
HifiSockAddr publicSockAddr, localSockAddr;
if (packet->getPayloadSize() == 0) {
return;
@ -625,31 +624,62 @@ void DomainServer::processConnectRequestPacket(QSharedPointer<NLPacket> packet)
return;
}
}
}
QList<NodeType_t> nodeInterestList;
QString username;
QByteArray usernameSignature;
packetStream >> nodeInterestList >> username >> usernameSignature;
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
packetStream >> nodeInterestList;
if (packet->bytesLeftToRead() > 0) {
// try to verify username
packetStream >> username;
}
bool isRestrictingAccess =
_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
// we always let in a user who is sending a packet from our local socket or from the localhost address
bool isLocalUser = (senderSockAddr.getAddress() == DependencyManager::get<LimitedNodeList>()->getLocalSockAddr().getAddress() || senderSockAddr.getAddress() == QHostAddress::LocalHost);
if (isRestrictingAccess && !isLocalUser) {
if (!username.isEmpty()) {
// if there's a username, try to unpack username signature
packetStream >> usernameSignature;
if (usernameSignature.isEmpty()) {
// if user didn't include usernameSignature in connect request, send a connectionToken packet
QUuid& connectionToken = _connectionTokenHash[username.toLower()];
if (connectionToken.isNull()) {
connectionToken = QUuid::createUuid();
}
static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID);
connectionTokenPacket->reset();
connectionTokenPacket->write(connectionToken.toRfc4122());
limitedNodeList->sendUnreliablePacket(*connectionTokenPacket, packet->getSenderSockAddr());
return;
}
}
}
QString reason;
if (!isAssignment && !shouldAllowConnectionFromNode(username, usernameSignature, senderSockAddr, reason)) {
// this is an agent and we've decided we won't let them connect - send them a packet to deny connection
QByteArray utfString = reason.toUtf8();
quint16 payloadSize = utfString.size();
auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, payloadSize + sizeof(payloadSize));
connectionDeniedPacket->writePrimitive(payloadSize);
connectionDeniedPacket->write(utfString);
if (payloadSize > 0) {
connectionDeniedPacket->writePrimitive(payloadSize);
connectionDeniedPacket->write(utfString);
}
// tell client it has been refused.
limitedNodeList->sendPacket(std::move(connectionDeniedPacket), senderSockAddr);
return;
}
@ -736,6 +766,7 @@ void DomainServer::processConnectRequestPacket(QSharedPointer<NLPacket> packet)
}
}
void DomainServer::processListRequestPacket(QSharedPointer<NLPacket> packet, SharedNodePointer sendingNode) {
NodeType_t throwawayNodeType;
@ -766,52 +797,61 @@ unsigned int DomainServer::countConnectedUsers() {
}
bool DomainServer::verifyUsersKey(const QString& username,
const QByteArray& usernameSignature,
QString& reasonReturn) {
bool DomainServer::verifyUserSignature(const QString& username,
const QByteArray& usernameSignature,
QString& reasonReturn) {
// it's possible this user can be allowed to connect, but we need to check their username signature
QByteArray publicKeyArray = _userPublicKeys.value(username);
if (!publicKeyArray.isEmpty()) {
const QUuid& connectionToken = _connectionTokenHash.value(username.toLower());
if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) {
// if we do have a public key for the user, check for a signature match
const unsigned char* publicKeyData = reinterpret_cast<const unsigned char*>(publicKeyArray.constData());
// first load up the public key into an RSA struct
RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size());
QByteArray lowercaseUsername = username.toLower().toUtf8();
QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()),
QCryptographicHash::Sha256);
if (rsaPublicKey) {
QByteArray decryptedArray(RSA_size(rsaPublicKey), 0);
int decryptResult =
RSA_public_decrypt(usernameSignature.size(),
reinterpret_cast<const unsigned char*>(usernameSignature.constData()),
reinterpret_cast<unsigned char*>(decryptedArray.data()),
rsaPublicKey, RSA_PKCS1_PADDING);
int decryptResult = RSA_verify(NID_sha256,
reinterpret_cast<const unsigned char*>(usernameWithToken.constData()),
usernameWithToken.size(),
reinterpret_cast<const unsigned char*>(usernameSignature.constData()),
usernameSignature.size(),
rsaPublicKey);
if (decryptResult == 1) {
qDebug() << "Username signature matches for" << username << "- allowing connection.";
if (decryptResult != -1) {
if (username.toLower() == decryptedArray) {
qDebug() << "Username signature matches for" << username << "- allowing connection.";
// free up the public key and remove connection token before we return
RSA_free(rsaPublicKey);
_connectionTokenHash.remove(username);
// free up the public key before we return
RSA_free(rsaPublicKey);
return true;
} else {
qDebug() << "Username signature did not match for" << username << "- denying connection.";
reasonReturn = "Username signature did not match.";
}
return true;
} else {
qDebug() << "Couldn't decrypt user signature for" << username << "- denying connection.";
reasonReturn = "Couldn't decrypt user signature.";
qDebug() << "Error decrypting username signature for " << username << "- denying connection.";
reasonReturn = "Error decrypting username signature.";
// free up the public key, we don't need it anymore
RSA_free(rsaPublicKey);
}
// free up the public key, we don't need it anymore
RSA_free(rsaPublicKey);
} else {
// we can't let this user in since we couldn't convert their public key to an RSA key we could use
qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection.";
reasonReturn = "Couldn't convert data to RSA key.";
}
} else {
qDebug() << "Insufficient data to decrypt username signature - denying connection.";
reasonReturn = "Insufficient data";
}
requestUserPublicKey(username); // no joy. maybe next time?
@ -823,41 +863,40 @@ bool DomainServer::shouldAllowConnectionFromNode(const QString& username,
const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr,
QString& reasonReturn) {
//TODO: improve flow so these bools aren't declared twice
bool isRestrictingAccess =
_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
bool isLocalUser = (senderSockAddr.getAddress() == DependencyManager::get<LimitedNodeList>()->getLocalSockAddr().getAddress() || senderSockAddr.getAddress() == QHostAddress::LocalHost);
// we always let in a user who is sending a packet from our local socket or from the localhost address
if (senderSockAddr.getAddress() == DependencyManager::get<LimitedNodeList>()->getLocalSockAddr().getAddress()
|| senderSockAddr.getAddress() == QHostAddress::LocalHost) {
return true;
}
if (isRestrictingAccess) {
if (isRestrictingAccess && !isLocalUser) {
QStringList allowedUsers =
_settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList();
if (allowedUsers.contains(username, Qt::CaseInsensitive)) {
if (!verifyUsersKey(username, usernameSignature, reasonReturn)) {
if (username.isEmpty()) {
qDebug() << "Connect request denied - no username provided.";
reasonReturn = "No username provided";
return false;
}
if (!verifyUserSignature(username, usernameSignature, reasonReturn)) {
return false;
}
} else {
qDebug() << "Connect request denied for user" << username << "not in allowed users list.";
reasonReturn = "User not on whitelist.";
return false;
}
}
// either we aren't restricting users, or this user is in the allowed list
// if this user is in the editors list, exempt them from the max-capacity check
const QVariant* allowedEditorsVariant =
valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH);
QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList();
if (allowedEditors.contains(username)) {
if (verifyUsersKey(username, usernameSignature, reasonReturn)) {
if (verifyUserSignature(username, usernameSignature, reasonReturn)) {
return true;
}
}

View file

@ -90,7 +90,7 @@ private:
void pingPunchForConnectingPeer(const SharedNetworkPeer& peer);
unsigned int countConnectedUsers();
bool verifyUsersKey (const QString& username, const QByteArray& usernameSignature, QString& reasonReturn);
bool verifyUserSignature (const QString& username, const QByteArray& usernameSignature, QString& reasonReturn);
bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr, QString& reasonReturn);
@ -149,6 +149,8 @@ private:
QSet<QUuid> _webAuthenticationStateSet;
QHash<QUuid, DomainServerWebSessionData> _cookieSessionHash;
QHash<QString, QUuid> _connectionTokenHash;
QHash<QString, QByteArray> _userPublicKeys;

View file

@ -10,11 +10,13 @@
//
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <qjsondocument.h>
#include <QtCore/QDebug>
#include <QtCore/QDataStream>
#include "UUID.h"
#include "NetworkLogging.h"
#include "DataServerAccountInfo.h"
@ -30,8 +32,7 @@ DataServerAccountInfo::DataServerAccountInfo() :
_walletID(),
_balance(0),
_hasBalance(false),
_privateKey(),
_usernameSignature()
_privateKey()
{
}
@ -73,9 +74,6 @@ void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject
void DataServerAccountInfo::setUsername(const QString& username) {
if (_username != username) {
_username = username;
// clear our username signature so it has to be re-created
_usernameSignature = QByteArray();
qCDebug(networking) << "Username changed to" << username;
}
@ -128,8 +126,8 @@ void DataServerAccountInfo::setProfileInfoFromJSON(const QJsonObject& jsonObject
setWalletID(QUuid(user["wallet_id"].toString()));
}
const QByteArray& DataServerAccountInfo::getUsernameSignature() {
if (_usernameSignature.isEmpty()) {
QByteArray DataServerAccountInfo::getUsernameSignature(const QUuid& connectionToken) {
if (!_privateKey.isEmpty()) {
const char* privateKeyData = _privateKey.constData();
RSA* rsaPrivateKey = d2i_RSAPrivateKey(NULL,
@ -137,36 +135,41 @@ const QByteArray& DataServerAccountInfo::getUsernameSignature() {
_privateKey.size());
if (rsaPrivateKey) {
QByteArray lowercaseUsername = _username.toLower().toUtf8();
_usernameSignature.resize(RSA_size(rsaPrivateKey));
QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()),
QCryptographicHash::Sha256);
int encryptReturn = RSA_private_encrypt(lowercaseUsername.size(),
reinterpret_cast<const unsigned char*>(lowercaseUsername.constData()),
reinterpret_cast<unsigned char*>(_usernameSignature.data()),
rsaPrivateKey, RSA_PKCS1_PADDING);
QByteArray usernameSignature(RSA_size(rsaPrivateKey), 0);
unsigned int usernameSignatureSize = 0;
if (encryptReturn == -1) {
qCDebug(networking) << "Error encrypting username signature.";
qCDebug(networking) << "Will re-attempt on next domain-server check in.";
_usernameSignature = QByteArray();
}
int encryptReturn = RSA_sign(NID_sha256,
reinterpret_cast<const unsigned char*>(usernameWithToken.constData()),
usernameWithToken.size(),
reinterpret_cast<unsigned char*>(usernameSignature.data()),
&usernameSignatureSize,
rsaPrivateKey);
// free the private key RSA struct now that we are done with it
RSA_free(rsaPrivateKey);
if (encryptReturn == -1) {
qCDebug(networking) << "Error encrypting username signature.";
qCDebug(networking) << "Will re-attempt on next domain-server check in.";
} else {
qDebug(networking) << "Returning username" << _username << "signed with connection UUID" << uuidStringWithoutCurlyBraces(connectionToken);
return usernameSignature;
}
} else {
qCDebug(networking) << "Could not create RSA struct from QByteArray private key.";
qCDebug(networking) << "Will re-attempt on next domain-server check in.";
}
}
}
return _usernameSignature;
return QByteArray();
}
void DataServerAccountInfo::setPrivateKey(const QByteArray& privateKey) {
_privateKey = privateKey;
// clear our username signature so it has to be re-created
_usernameSignature = QByteArray();
}
QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) {

View file

@ -43,7 +43,7 @@ public:
const QUuid& getWalletID() const { return _walletID; }
void setWalletID(const QUuid& walletID);
const QByteArray& getUsernameSignature();
QByteArray getUsernameSignature(const QUuid& connectionToken);
bool hasPrivateKey() const { return !_privateKey.isEmpty(); }
void setPrivateKey(const QByteArray& privateKey);
@ -73,7 +73,7 @@ private:
qint64 _balance;
bool _hasBalance;
QByteArray _privateKey;
QByteArray _usernameSignature;
};
#endif // hifi_DataServerAccountInfo_h

View file

@ -29,6 +29,7 @@ DomainHandler::DomainHandler(QObject* parent) :
_uuid(),
_sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)),
_assignmentUUID(),
_connectionToken(),
_iceDomainID(),
_iceClientID(),
_iceServerSockAddr(),
@ -43,7 +44,8 @@ DomainHandler::DomainHandler(QObject* parent) :
void DomainHandler::clearConnectionInfo() {
_uuid = QUuid();
_connectionToken = QUuid();
_icePeer.reset();
if (requiresICE()) {

View file

@ -50,9 +50,12 @@ public:
unsigned short getPort() const { return _sockAddr.getPort(); }
void setPort(quint16 port) { _sockAddr.setPort(port); }
const QUuid& getConnectionToken() const { return _connectionToken; }
void setConnectionToken(const QUuid& connectionToken) { _connectionToken = connectionToken; }
const QUuid& getAssignmentUUID() const { return _assignmentUUID; }
void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; }
const QUuid& getICEDomainID() const { return _iceDomainID; }
const QUuid& getICEClientID() const { return _iceClientID; }
@ -114,6 +117,7 @@ private:
QString _hostname;
HifiSockAddr _sockAddr;
QUuid _assignmentUUID;
QUuid _connectionToken;
QUuid _iceDomainID;
QUuid _iceClientID;
HifiSockAddr _iceServerSockAddr;

View file

@ -94,7 +94,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned
packetReceiver.registerListener(PacketType::PingReply, this, "processPingReplyPacket");
packetReceiver.registerListener(PacketType::ICEPing, this, "processICEPingPacket");
packetReceiver.registerListener(PacketType::DomainServerAddedNode, this, "processDomainServerAddedNode");
packetReceiver.registerListener(PacketType::DomainServerConnectionToken, this, "processDomainServerConnectionTokenPacket");
packetReceiver.registerListener(PacketType::ICEServerPeerInformation, &_domainHandler, "processICEResponsePacket");
packetReceiver.registerListener(PacketType::DomainServerRequireDTLS, &_domainHandler, "processDTLSRequirementPacket");
packetReceiver.registerListener(PacketType::ICEPingReply, &_domainHandler, "processICEPingReplyPacket");
@ -274,17 +274,23 @@ void NodeList::sendDomainServerCheckIn() {
// pack our data to send to the domain-server
packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList();
// if this is a connect request, and we can present a username signature, send it along
if (!_domainHandler.isConnected()) {
if (!_domainHandler.isConnected() ) {
DataServerAccountInfo& accountInfo = AccountManager::getInstance().getAccountInfo();
packetStream << accountInfo.getUsername();
const QByteArray& usernameSignature = AccountManager::getInstance().getAccountInfo().getUsernameSignature();
if (!usernameSignature.isEmpty()) {
qCDebug(networking) << "Including username signature in domain connect request.";
packetStream << usernameSignature;
// get connection token from the domain-server
const QUuid& connectionToken = _domainHandler.getConnectionToken();
if (!connectionToken.isNull()) {
const QByteArray& usernameSignature = AccountManager::getInstance().getAccountInfo().getUsernameSignature(connectionToken);
if (!usernameSignature.isEmpty()) {
packetStream << usernameSignature;
}
}
}
@ -361,6 +367,7 @@ void NodeList::sendDSPathQuery(const QString& newPath) {
}
}
void NodeList::processDomainServerPathResponse(QSharedPointer<NLPacket> packet) {
// This is a response to a path query we theoretically made.
// In the future we may want to check that this was actually from our DS and for a query we actually made.
@ -450,6 +457,17 @@ void NodeList::pingPunchForDomainServer() {
}
}
void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer<NLPacket> packet) {
if (_domainHandler.getSockAddr().isNull()) {
// refuse to process this packet if we aren't currently connected to the DS
return;
}
// read in the connection token from the packet, then send domain-server checkin
_domainHandler.setConnectionToken(QUuid::fromRfc4122(packet->read(NUM_BYTES_RFC4122_UUID)));
sendDomainServerCheckIn();
}
void NodeList::processDomainServerList(QSharedPointer<NLPacket> packet) {
if (_domainHandler.getSockAddr().isNull()) {
// refuse to process this packet if we aren't currently connected to the DS

View file

@ -76,6 +76,8 @@ public slots:
void processDomainServerAddedNode(QSharedPointer<NLPacket> packet);
void processDomainServerPathResponse(QSharedPointer<NLPacket> packet);
void processDomainServerConnectionTokenPacket(QSharedPointer<NLPacket> packet);
void processPingPacket(QSharedPointer<NLPacket> packet, SharedNodePointer sendingNode);
void processPingReplyPacket(QSharedPointer<NLPacket> packet, SharedNodePointer sendingNode);

View file

@ -26,7 +26,7 @@ const QSet<PacketType::Value> SEQUENCE_NUMBERED_PACKETS = QSet<PacketType::Value
const QSet<PacketType::Value> NON_SOURCED_PACKETS = QSet<PacketType::Value>()
<< StunResponse << CreateAssignment << RequestAssignment
<< DomainServerRequireDTLS << DomainConnectRequest
<< DomainServerRequireDTLS << DomainConnectRequest << DomainServerConnectionToken
<< DomainList << DomainConnectionDenied
<< DomainServerPathQuery << DomainServerPathResponse
<< DomainServerAddedNode
@ -119,6 +119,7 @@ QString nameForPacketType(PacketType::Value packetType) {
PACKET_TYPE_NAME_LOOKUP(ICEPingReply);
PACKET_TYPE_NAME_LOOKUP(EntityAdd);
PACKET_TYPE_NAME_LOOKUP(EntityEdit);
PACKET_TYPE_NAME_LOOKUP(DomainServerConnectionToken);
default:
return QString("Type: ") + QString::number((int)packetType);
}

View file

@ -73,7 +73,8 @@ namespace PacketType {
EntityQuery,
EntityAdd,
EntityErase,
EntityEdit
EntityEdit,
DomainServerConnectionToken
};
};