/* * Copyright (C) 2014 MongoDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kAccessControl #include "mongo/platform/basic.h" #include "mongo/db/auth/sasl_scramsha1_server_conversation.h" #include #include #include "mongo/crypto/crypto.h" #include "mongo/crypto/mechanism_scram.h" #include "mongo/platform/random.h" #include "mongo/util/base64.h" #include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" #include "mongo/util/password_digest.h" #include "mongo/util/text.h" namespace mongo { SaslSCRAMSHA1ServerConversation::SaslSCRAMSHA1ServerConversation( SaslAuthenticationSession* saslAuthSession) : SaslServerConversation(saslAuthSession), _step(0), _authMessage(""), _nonce("") { } StatusWith SaslSCRAMSHA1ServerConversation::step(const StringData& inputData, std::string* outputData) { std::vector input = StringSplitter::split(inputData.toString(), ","); _step++; if (_step > 3 || _step <= 0) { return StatusWith(ErrorCodes::AuthenticationFailed, mongoutils::str::stream() << "Invalid SCRAM-SHA-1 authentication step: " << _step); } if (_step == 1) { return _firstStep(input, outputData); } if (_step == 2) { return _secondStep(input, outputData); } *outputData = ""; return StatusWith(true); } /* * RFC 5802 specifies that in SCRAM user names characters ',' and '=' are encoded as * =2C and =3D respectively. */ static void decodeSCRAMUsername(std::string& user) { boost::replace_all(user, "=2C", ","); boost::replace_all(user, "=3D", "="); } /* * Parse client-first-message of the form: * n,a=authzid,n=encoded-username,r=client-nonce * * Generate server-first-message on the form: * r=client-nonce|server-nonce,s=user-salt,i=iteration-count * * NOTE: we are ignoring the authorization ID part of the message */ StatusWith SaslSCRAMSHA1ServerConversation::_firstStep(std::vector& input, std::string* outputData) { std::string authzId = ""; if (input.size() == 4) { /* The second entry a=authzid is optional. If provided it will be * validated against the encoded username. * * The two allowed input forms are: * n,,n=encoded-username,r=client-nonce * n,a=authzid,n=encoded-username,r=client-nonce */ if (!str::startsWith(input[1], "a=") || input[1].size() < 3) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect SCRAM-SHA-1 authzid: " << input[1]); } authzId = input[1].substr(2); input.erase(input.begin() + 1); } if (input.size() != 3) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect number of arguments for first SCRAM-SHA-1 client message, got " << input.size() << " expected 4"); } else if (input[0] != "n") { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect SCRAM-SHA-1 client message prefix: " << input[0]); } else if (!str::startsWith(input[1], "n=") || input[1].size() < 3) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect SCRAM-SHA-1 user name: " << input[1]); } else if(!str::startsWith(input[2], "r=") || input[2].size() < 6) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect SCRAM-SHA-1 client nonce: " << input[2]); } // add client-first-message-bare to _authMessage _authMessage += input[1] + "," + input[2] + ","; _user = input[1].substr(2); if (!authzId.empty() && _user != authzId) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "SCRAM-SHA-1 user name " << _user << " does not match authzid " << authzId); } decodeSCRAMUsername(_user); std::string clientNonce = input[2].substr(2); // The authentication database is also the source database for the user. User* userObj; Status status = _saslAuthSession->getAuthorizationSession()->getAuthorizationManager(). acquireUser(_saslAuthSession->getOpCtxt(), UserName(_user, _saslAuthSession->getAuthenticationDatabase()), &userObj); if (!status.isOK()) { return StatusWith(status); } _creds = userObj->getCredentials(); UserName userName = userObj->getName(); _saslAuthSession->getAuthorizationSession()->getAuthorizationManager(). releaseUser(userObj); // Check for authentication attempts of the __system user on // systems started without a keyfile. if (userName == internalSecurity.user->getName() && _creds.scram.salt.empty()) { return StatusWith(ErrorCodes::AuthenticationFailed, "It is not possible to authenticate as the __system user " "on servers started without --keyFile parameter"); } // Generate SCRAM credentials on the fly for mixed MONGODB-CR/SCRAM mode. if (_creds.scram.salt.empty() && !_creds.password.empty()) { // Use a default value of 5000 for the scramIterationCount when in mixed mode, // overriding the default value (10000) used for SCRAM mode or the user-given value. const int mixedModeScramIterationCount = 5000; BSONObj scramCreds = scram::generateCredentials(_creds.password, mixedModeScramIterationCount); _creds.scram.iterationCount = scramCreds[scram::iterationCountFieldName].Int(); _creds.scram.salt = scramCreds[scram::saltFieldName].String(); _creds.scram.storedKey = scramCreds[scram::storedKeyFieldName].String(); _creds.scram.serverKey = scramCreds[scram::serverKeyFieldName].String(); } // Generate server-first-message // Create text-based nonce as base64 encoding of a binary blob of length multiple of 3 const int nonceLenQWords = 3; uint64_t binaryNonce[nonceLenQWords]; scoped_ptr sr(SecureRandom::create()); binaryNonce[0] = sr->nextInt64(); binaryNonce[1] = sr->nextInt64(); binaryNonce[2] = sr->nextInt64(); _nonce = clientNonce + base64::encode(reinterpret_cast(binaryNonce), sizeof(binaryNonce)); StringBuilder sb; sb << "r=" << _nonce << ",s=" << _creds.scram.salt << ",i=" << _creds.scram.iterationCount; *outputData = sb.str(); // add server-first-message to authMessage _authMessage += *outputData + ","; return StatusWith(false); } /** * Parse client-final-message of the form: * c=channel-binding(base64),r=client-nonce|server-nonce,p=ClientProof * * Generate successful authentication server-final-message on the form: * v=ServerSignature * * or failed authentication server-final-message on the form: * e=message * * NOTE: we are ignoring the channel binding part of the message **/ StatusWith SaslSCRAMSHA1ServerConversation::_secondStep(const std::vector& input, std::string* outputData) { if (input.size() != 3) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect number of arguments for second SCRAM-SHA-1 client message, got " << input.size() << " expected 3"); } else if (!str::startsWith(input[0], "c=") || input[0].size() < 3) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect SCRAM-SHA-1 channel binding: " << input[0]); } else if (!str::startsWith(input[1], "r=") || input[1].size() < 6) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect SCRAM-SHA-1 client|server nonce: " << input[1]); } else if(!str::startsWith(input[2], "p=") || input[2].size() < 3) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Incorrect SCRAM-SHA-1 ClientProof: " << input[2]); } // add client-final-message-without-proof to authMessage _authMessage += input[0] + "," + input[1]; // Concatenated nonce sent by client should equal the one in server-first-message std::string nonce = input[1].substr(2); if (nonce != _nonce) { return StatusWith(ErrorCodes::BadValue, mongoutils::str::stream() << "Unmatched SCRAM-SHA-1 nonce received from client in second step, expected " << _nonce << " but received " << nonce); } std::string clientProof = input[2].substr(2); // Do server side computations, compare storedKeys and generate client-final-message // AuthMessage := client-first-message-bare + "," + // server-first-message + "," + // client-final-message-without-proof // ClientSignature := HMAC(StoredKey, AuthMessage) // ClientKey := ClientSignature XOR ClientProof // ServerSignature := HMAC(ServerKey, AuthMessage) unsigned int hashLen = 0; unsigned char clientSignature[scram::hashSize]; std::string decodedStoredKey = base64::decode(_creds.scram.storedKey); // ClientSignature := HMAC(StoredKey, AuthMessage) fassert(18662, crypto::hmacSha1( reinterpret_cast(decodedStoredKey.c_str()), scram::hashSize, reinterpret_cast(_authMessage.c_str()), _authMessage.size(), clientSignature, &hashLen)); fassert(18658, hashLen == scram::hashSize); try { clientProof = base64::decode(clientProof); } catch (const DBException& ex) { return StatusWith(ex.toStatus()); } const unsigned char *decodedClientProof = reinterpret_cast(clientProof.c_str()); // ClientKey := ClientSignature XOR ClientProof unsigned char clientKey[scram::hashSize]; for(size_t i=0; i(ErrorCodes::AuthenticationFailed, mongoutils::str::stream() << "SCRAM-SHA-1 authentication failed, storedKey mismatch"); } // ServerSignature := HMAC(ServerKey, AuthMessage) unsigned char serverSignature[scram::hashSize]; std::string decodedServerKey = base64::decode(_creds.scram.serverKey); fassert(18660, crypto::hmacSha1( reinterpret_cast(decodedServerKey.c_str()), scram::hashSize, reinterpret_cast(_authMessage.c_str()), _authMessage.size(), serverSignature, &hashLen)); fassert(18661, hashLen == scram::hashSize); StringBuilder sb; sb << "v=" << base64::encode(reinterpret_cast(serverSignature), scram::hashSize); *outputData = sb.str(); return StatusWith(false); } } // namespace mongo