diff options
author | Spencer Jackson <spencer.jackson@mongodb.com> | 2016-12-07 16:11:57 -0500 |
---|---|---|
committer | Spencer Jackson <spencer.jackson@mongodb.com> | 2017-02-02 17:48:07 -0500 |
commit | 47da0b53f9cd27aeec1d2822780784866269a47d (patch) | |
tree | 39bcdaf2374bab9d0186f07030d1b32c936cdd31 | |
parent | c742c4ebe789fafa3fccd4b25332a7df0782b252 (diff) | |
download | mongo-47da0b53f9cd27aeec1d2822780784866269a47d.tar.gz |
SERVER-26952: Cache SCRAM-SHA-1 ClientKey
31 files changed, 675 insertions, 344 deletions
diff --git a/buildscripts/resmokeconfig/suites/multiversion_auth.yml b/buildscripts/resmokeconfig/suites/multiversion_auth.yml new file mode 100644 index 00000000000..11197f0a970 --- /dev/null +++ b/buildscripts/resmokeconfig/suites/multiversion_auth.yml @@ -0,0 +1,32 @@ +# Section that is ignored by resmoke.py. +config_variables: +- &keyFile jstests/libs/authTestsKey +- &keyFileData Thiskeyisonlyforrunningthesuitewithauthenticationdontuseitinanytestsdirectly + +selector: + js_test: + roots: + - jstests/multiVersion/*.js + exclude_files: + # Multi storageEngine tests + - jstests/multiVersion/mixed_storage_version_replication.js + - jstests/multiVersion/transitioning_to_and_from_WT.js + # TODO: SERVER-21578 + - jstests/multiVersion/balancer_multiVersion_detect.js + +# Multiversion tests start their own mongod's. +executor: + js_test: + config: + shell_options: + global_vars: + TestData: + auth: true + authMechanism: SCRAM-SHA-1 + keyFile: *keyFile + keyFileData: *keyFileData + authenticationDatabase: local + authenticationMechanism: SCRAM-SHA-1 + password: *keyFileData + username: __system + nodb: '' diff --git a/etc/evergreen.yml b/etc/evergreen.yml index 93f3c620e4e..45fa231ee49 100644 --- a/etc/evergreen.yml +++ b/etc/evergreen.yml @@ -2224,6 +2224,17 @@ tasks: run_multiple_jobs: true - <<: *task_template + name: multiversion_auth + commands: + - func: "do setup" + - func: "do multiversion setup" + - func: "run tests" + vars: + task_path_suffix: /data/multiversion + resmoke_args: --suites=multiversion_auth --storageEngine=mmapv1 --excludeWithAnyTags=requires_wiredtiger + run_multiple_jobs: true + +- <<: *task_template name: multiversion_WT commands: - func: "do setup" @@ -3421,6 +3432,7 @@ buildvariants: - name: mmap - name: mongosTest - name: multiversion + - name: multiversion_auth - name: multiversion_WT - name: multiversion_multistorage_engine - name: noPassthrough @@ -4885,6 +4897,7 @@ buildvariants: - name: mmap - name: mongosTest - name: multiversion + - name: multiversion_auth - name: multiversion_WT - name: multiversion_multistorage_engine - name: noPassthrough @@ -5774,6 +5787,7 @@ buildvariants: - name: mmap - name: mongosTest - name: multiversion + - name: multiversion_auth - name: multiversion_WT - name: multiversion_multistorage_engine - name: noPassthrough @@ -6005,6 +6019,7 @@ buildvariants: - name: mmap - name: mongosTest - name: multiversion + - name: multiversion_auth - name: multiversion_WT - name: multiversion_multistorage_engine - name: noPassthrough @@ -8467,6 +8482,7 @@ buildvariants: - name: mmap - name: mongosTest - name: multiversion + - name: multiversion_auth - name: multiversion_WT - name: multiversion_multistorage_engine - name: noPassthrough @@ -8647,6 +8663,7 @@ buildvariants: - name: mmap - name: mongosTest - name: multiversion + - name: multiversion_auth - name: multiversion_WT - name: multiversion_multistorage_engine - name: noPassthrough diff --git a/jstests/libs/test_background_ops.js b/jstests/libs/test_background_ops.js index db2361d67c8..dd2f75a9da5 100644 --- a/jstests/libs/test_background_ops.js +++ b/jstests/libs/test_background_ops.js @@ -82,6 +82,10 @@ var getResult = function(mongo, name) { * Overrides the parallel shell code in mongo */ function startParallelShell(jsCode, port) { + if (TestData) { + jsCode = "TestData = " + tojson(TestData) + ";" + jsCode; + } + var x; if (port) { x = startMongoProgramNoConnect("mongo", "--port", port, "--eval", jsCode); diff --git a/jstests/multiVersion/libs/multi_rs.js b/jstests/multiVersion/libs/multi_rs.js index bbe5a02b0b0..364e8945902 100644 --- a/jstests/multiVersion/libs/multi_rs.js +++ b/jstests/multiVersion/libs/multi_rs.js @@ -59,6 +59,7 @@ ReplSetTest.prototype.upgradeNode = function(node, opts = {}, user, pwd) { if (user != undefined) { assert.eq(1, node.getDB("admin").auth(user, pwd)); } + jsTest.authenticate(node); var isMaster = node.getDB('admin').runCommand({isMaster: 1}); diff --git a/jstests/multiVersion/major_version_upgrade.js b/jstests/multiVersion/major_version_upgrade.js index d0289c6af80..bc46e191a33 100644 --- a/jstests/multiVersion/major_version_upgrade.js +++ b/jstests/multiVersion/major_version_upgrade.js @@ -117,6 +117,12 @@ for (let i = 0; i < versions.length; i++) { let version = versions[i]; let mongodOptions = Object.extend({binVersion: version.binVersion}, defaultOptions); + var changedAuthMechanism = false; + if (TestData.authMechanism === "SCRAM-SHA-1" && version.binVersion === "2.6") { + TestData.authMechanism = undefined; + DB.prototype._defaultAuthenticationMechanism = "MONGODB-CR"; + changedAuthMechanism = true; + } // Start a mongod with specified version. let conn = MongoRunner.runMongod(mongodOptions); @@ -164,6 +170,12 @@ } // Shutdown the current mongod. MongoRunner.stopMongod(conn); + + if (version.binVersion === "2.6" && changedAuthMechanism) { + TestData.authMechanism = "SCRAM-SHA-1"; + DB.prototype._defaultAuthenticationMechanism = "SCRAM-SHA-1"; + changedAuthMechanisms = false; + } } // Replica Sets @@ -173,6 +185,12 @@ n2: {binVersion: versions[0].binVersion}, n3: {binVersion: versions[0].binVersion}, }; + var changedAuthMechanisms = false; + if (TestData.authMechanism === "SCRAM-SHA-1" && versions[0].binVersion === "2.6") { + TestData.authMechanism = undefined; + DB.prototype._defaultAuthenticationMechanism = "MONGODB-CR"; + changedAuthMechanism = true; + } let rst = new ReplSetTest({nodes}); @@ -184,6 +202,11 @@ // outlined at the top of this test file. for (let i = 0; i < versions.length; i++) { let version = versions[i]; + if (version.binVersion != "2.6" && changedAuthMechanism) { + TestData.authMechanism = "SCRAM-SHA-1"; + DB.prototype._defaultAuthenticationMechanism = "SCRAM-SHA-1"; + changedAuthMechanisms = false; + } // Connect to the primary running the old version to ensure that the test can insert and // create indices. diff --git a/src/mongo/client/SConscript b/src/mongo/client/SConscript index 9a8cf3284be..d6e2c4ce6c0 100644 --- a/src/mongo/client/SConscript +++ b/src/mongo/client/SConscript @@ -62,6 +62,7 @@ saslClientSource = [ 'sasl_client_session.cpp', 'sasl_plain_client_conversation.cpp', 'sasl_scramsha1_client_conversation.cpp', + 'scram_sha1_client_cache.cpp', ] # Add in actual sasl dependencies if sasl is enabled, otherwise diff --git a/src/mongo/client/authenticate.cpp b/src/mongo/client/authenticate.cpp index 3f0320a2f4f..410caff4aae 100644 --- a/src/mongo/client/authenticate.cpp +++ b/src/mongo/client/authenticate.cpp @@ -241,7 +241,7 @@ bool isFailedAuthOk(const AuthResponse& response) { void auth(RunCommandHook runCommand, const BSONObj& params, - StringData hostname, + const HostAndPort& hostname, StringData clientName, AuthCompletionHandler handler) { std::string mechanism; @@ -285,7 +285,7 @@ void auth(RunCommandHook runCommand, void asyncAuth(RunCommandHook runCommand, const BSONObj& params, - StringData hostname, + const HostAndPort& hostname, StringData clientName, AuthCompletionHandler handler) { auth(runCommand, params, hostname, clientName, std::move(handler)); @@ -294,7 +294,7 @@ void asyncAuth(RunCommandHook runCommand, } // namespace void authenticateClient(const BSONObj& params, - StringData hostname, + const HostAndPort& hostname, StringData clientName, RunCommandHook runCommand, AuthCompletionHandler handler) { diff --git a/src/mongo/client/authenticate.h b/src/mongo/client/authenticate.h index 6ad596477bf..552d43cdafb 100644 --- a/src/mongo/client/authenticate.h +++ b/src/mongo/client/authenticate.h @@ -92,7 +92,7 @@ extern const char* const kMechanismScramSha1; * tantamount to authentication failure, but may also indicate more serious problems. */ void authenticateClient(const BSONObj& params, - StringData hostname, + const HostAndPort& hostname, StringData clientSubjectName, RunCommandHook runCommand, AuthCompletionHandler handler = AuthCompletionHandler()); diff --git a/src/mongo/client/authenticate_test.cpp b/src/mongo/client/authenticate_test.cpp index fd7e62206e5..7cfa42b4449 100644 --- a/src/mongo/client/authenticate_test.cpp +++ b/src/mongo/client/authenticate_test.cpp @@ -179,29 +179,31 @@ public: TEST_F(AuthClientTest, MongoCR) { auto params = loadMongoCRConversation(); - auth::authenticateClient(std::move(params), "", "", _runCommandCallback); + auth::authenticateClient(std::move(params), HostAndPort(), "", _runCommandCallback); } TEST_F(AuthClientTest, asyncMongoCR) { auto params = loadMongoCRConversation(); - auth::authenticateClient( - std::move(params), "", "", _runCommandCallback, [this](auth::AuthResponse response) { - ASSERT(response.isOK()); - }); + auth::authenticateClient(std::move(params), + HostAndPort(), + "", + _runCommandCallback, + [this](auth::AuthResponse response) { ASSERT(response.isOK()); }); } #ifdef MONGO_CONFIG_SSL TEST_F(AuthClientTest, X509) { auto params = loadX509Conversation(); - auth::authenticateClient(std::move(params), "", _username, _runCommandCallback); + auth::authenticateClient(std::move(params), HostAndPort(), _username, _runCommandCallback); } TEST_F(AuthClientTest, asyncX509) { auto params = loadX509Conversation(); - auth::authenticateClient( - std::move(params), "", _username, _runCommandCallback, [this](auth::AuthResponse response) { - ASSERT(response.isOK()); - }); + auth::authenticateClient(std::move(params), + HostAndPort(), + _username, + _runCommandCallback, + [this](auth::AuthResponse response) { ASSERT(response.isOK()); }); } #endif diff --git a/src/mongo/client/dbclient.cpp b/src/mongo/client/dbclient.cpp index 14d8a47d8a9..ee729358ae2 100644 --- a/src/mongo/client/dbclient.cpp +++ b/src/mongo/client/dbclient.cpp @@ -447,7 +447,7 @@ void DBClientWithCommands::_auth(const BSONObj& params) { auth::authenticateClient( params, - HostAndPort(getServerAddress()).host(), + HostAndPort(getServerAddress()), clientName, [this](RemoteCommandRequest request, auth::AuthCompletionHandler handler) { BSONObj info; diff --git a/src/mongo/client/native_sasl_client_session.cpp b/src/mongo/client/native_sasl_client_session.cpp index 7976c09e413..8f8d4fe91af 100644 --- a/src/mongo/client/native_sasl_client_session.cpp +++ b/src/mongo/client/native_sasl_client_session.cpp @@ -33,6 +33,7 @@ #include "mongo/client/sasl_client_conversation.h" #include "mongo/client/sasl_plain_client_conversation.h" #include "mongo/client/sasl_scramsha1_client_conversation.h" +#include "mongo/client/scram_sha1_client_cache.h" #include "mongo/util/mongoutils/str.h" namespace mongo { @@ -47,6 +48,9 @@ MONGO_INITIALIZER(NativeSaslClientContext)(InitializerContext* context) { return Status::OK(); } +// Global cache for SCRAM-SHA-1 credentials +SCRAMSHA1ClientCache* scramsha1ClientCache = new SCRAMSHA1ClientCache; + } // namespace NativeSaslClientSession::NativeSaslClientSession() @@ -63,7 +67,7 @@ Status NativeSaslClientSession::initialize() { if (mechanism == "PLAIN") { _saslConversation.reset(new SaslPLAINClientConversation(this)); } else if (mechanism == "SCRAM-SHA-1") { - _saslConversation.reset(new SaslSCRAMSHA1ClientConversation(this)); + _saslConversation.reset(new SaslSCRAMSHA1ClientConversation(this, scramsha1ClientCache)); } else { return Status(ErrorCodes::BadValue, mongoutils::str::stream() << "SASL mechanism " << mechanism diff --git a/src/mongo/client/sasl_client_authenticate.cpp b/src/mongo/client/sasl_client_authenticate.cpp index ecb7dfd7d1e..1d305d44d05 100644 --- a/src/mongo/client/sasl_client_authenticate.cpp +++ b/src/mongo/client/sasl_client_authenticate.cpp @@ -39,7 +39,7 @@ namespace mongo { using namespace mongoutils; void (*saslClientAuthenticate)(auth::RunCommandHook runCommand, - StringData hostname, + const HostAndPort& hostname, const BSONObj& saslParameters, auth::AuthCompletionHandler handler) = nullptr; diff --git a/src/mongo/client/sasl_client_authenticate.h b/src/mongo/client/sasl_client_authenticate.h index 8e52bee407d..cde16cd3afe 100644 --- a/src/mongo/client/sasl_client_authenticate.h +++ b/src/mongo/client/sasl_client_authenticate.h @@ -68,7 +68,7 @@ class BSONObj; * returned. */ extern void (*saslClientAuthenticate)(auth::RunCommandHook runCommand, - StringData hostname, + const HostAndPort& hostname, const BSONObj& saslParameters, auth::AuthCompletionHandler handler); diff --git a/src/mongo/client/sasl_client_authenticate_impl.cpp b/src/mongo/client/sasl_client_authenticate_impl.cpp index 8a06acedb31..d4755a22775 100644 --- a/src/mongo/client/sasl_client_authenticate_impl.cpp +++ b/src/mongo/client/sasl_client_authenticate_impl.cpp @@ -116,7 +116,7 @@ Status extractPassword(const BSONObj& saslParameters, * Returns Status::OK() on success. */ Status configureSession(SaslClientSession* session, - StringData hostname, + const HostAndPort& hostname, StringData targetDatabase, const BSONObj& saslParameters) { std::string mechanism; @@ -134,10 +134,11 @@ Status configureSession(SaslClientSession* session, session->setParameter(SaslClientSession::parameterServiceName, value); status = bsonExtractStringFieldWithDefault( - saslParameters, saslCommandServiceHostnameFieldName, hostname, &value); + saslParameters, saslCommandServiceHostnameFieldName, hostname.host(), &value); if (!status.isOK()) return status; session->setParameter(SaslClientSession::parameterServiceHostname, value); + session->setParameter(SaslClientSession::parameterServiceHostAndPort, hostname.toString()); status = bsonExtractStringField(saslParameters, saslCommandUserFieldName, &value); if (!status.isOK()) @@ -247,7 +248,7 @@ void asyncSaslConversation(auth::RunCommandHook runCommand, * "client". */ void saslClientAuthenticateImpl(auth::RunCommandHook runCommand, - StringData hostname, + const HostAndPort& hostname, const BSONObj& saslParameters, auth::AuthCompletionHandler handler) { int saslLogLevel = getSaslClientLogLevel(saslParameters); diff --git a/src/mongo/client/sasl_client_session.h b/src/mongo/client/sasl_client_session.h index c93a843c9d7..3cda8fba89c 100644 --- a/src/mongo/client/sasl_client_session.h +++ b/src/mongo/client/sasl_client_session.h @@ -63,6 +63,7 @@ public: enum Parameter { parameterServiceName = 0, parameterServiceHostname, + parameterServiceHostAndPort, parameterMechanism, parameterUser, parameterPassword, diff --git a/src/mongo/client/sasl_scramsha1_client_conversation.cpp b/src/mongo/client/sasl_scramsha1_client_conversation.cpp index 0e2f61f2414..2b52f1a28a2 100644 --- a/src/mongo/client/sasl_scramsha1_client_conversation.cpp +++ b/src/mongo/client/sasl_scramsha1_client_conversation.cpp @@ -34,6 +34,7 @@ #include "mongo/base/parse_number.h" #include "mongo/client/sasl_client_session.h" +#include "mongo/client/scram_sha1_client_cache.h" #include "mongo/platform/random.h" #include "mongo/util/base64.h" #include "mongo/util/mongoutils/str.h" @@ -46,13 +47,12 @@ using std::unique_ptr; using std::string; SaslSCRAMSHA1ClientConversation::SaslSCRAMSHA1ClientConversation( - SaslClientSession* saslClientSession) - : SaslClientConversation(saslClientSession), _step(0), _authMessage(""), _clientNonce("") {} - -SaslSCRAMSHA1ClientConversation::~SaslSCRAMSHA1ClientConversation() { - // clear the _saltedPassword memory - memset(_saltedPassword, 0, scram::hashSize); -} + SaslClientSession* saslClientSession, SCRAMSHA1ClientCache* clientCache) + : SaslClientConversation(saslClientSession), + _step(0), + _authMessage(), + _clientCache(clientCache), + _clientNonce() {} StatusWith<bool> SaslSCRAMSHA1ClientConversation::step(StringData inputData, std::string* outputData) { @@ -159,7 +159,7 @@ StatusWith<bool> SaslSCRAMSHA1ClientConversation::_secondStep(const std::vector< } std::string salt = input[1].substr(2); - int iterationCount; + size_t iterationCount; Status status = parseNumberFromStringWithBase(input[2].substr(2), 10, &iterationCount); if (status != Status::OK()) { @@ -179,14 +179,30 @@ StatusWith<bool> SaslSCRAMSHA1ClientConversation::_secondStep(const std::vector< return StatusWith<bool>(ex.toStatus()); } - scram::generateSaltedPassword( - _saslClientSession->getParameter(SaslClientSession::parameterPassword), - reinterpret_cast<const unsigned char*>(decodedSalt.c_str()), - decodedSalt.size(), - iterationCount, - _saltedPassword); + scram::SCRAMPresecrets presecrets( + _saslClientSession->getParameter(SaslClientSession::parameterPassword).toString(), + std::vector<std::uint8_t>(decodedSalt.begin(), decodedSalt.end()), + iterationCount); + + StatusWith<HostAndPort> targetHost = HostAndPort::parse( + _saslClientSession->getParameter(SaslClientSession::parameterServiceHostAndPort)); + + if (targetHost.isOK()) { + auto cachedSecrets = _clientCache->getCachedSecrets(targetHost.getValue(), presecrets); + + if (cachedSecrets) { + _credentials = *cachedSecrets; + } else { + _credentials = scram::generateSecrets(presecrets); + + _clientCache->setCachedSecrets( + std::move(targetHost.getValue()), std::move(presecrets), _credentials); + } + } else { + _credentials = scram::generateSecrets(presecrets); + } - std::string clientProof = scram::generateClientProof(_saltedPassword, _authMessage); + std::string clientProof = scram::generateClientProof(_credentials, _authMessage); StringBuilder sb; sb << "c=biws,r=" << nonce << ",p=" << clientProof; @@ -227,7 +243,7 @@ StatusWith<bool> SaslSCRAMSHA1ClientConversation::_thirdStep(const std::vector<s } bool validServerSignature = - scram::verifyServerSignature(_saltedPassword, _authMessage, input[0].substr(2)); + scram::verifyServerSignature(_credentials, _authMessage, input[0].substr(2)); if (!validServerSignature) { *outputData = "e=Invalid server signature"; diff --git a/src/mongo/client/sasl_scramsha1_client_conversation.h b/src/mongo/client/sasl_scramsha1_client_conversation.h index ccc89eb2243..dcf8a5686e4 100644 --- a/src/mongo/client/sasl_scramsha1_client_conversation.h +++ b/src/mongo/client/sasl_scramsha1_client_conversation.h @@ -38,6 +38,8 @@ #include "mongo/crypto/mechanism_scram.h" namespace mongo { + +class SCRAMSHA1ClientCache; /** * Client side authentication session for SASL PLAIN. */ @@ -48,9 +50,8 @@ public: /** * Implements the client side of a SASL PLAIN mechanism session. **/ - explicit SaslSCRAMSHA1ClientConversation(SaslClientSession* saslClientSession); - - virtual ~SaslSCRAMSHA1ClientConversation(); + SaslSCRAMSHA1ClientConversation(SaslClientSession* saslClientSession, + SCRAMSHA1ClientCache* clientCache); /** * Takes one step in a SCRAM-SHA-1 conversation. @@ -79,7 +80,10 @@ private: int _step; std::string _authMessage; - unsigned char _saltedPassword[scram::hashSize]; + + // Secrets and secrets cache + scram::SCRAMSecrets _credentials; + SCRAMSHA1ClientCache* const _clientCache; // client and server nonce concatenated std::string _clientNonce; diff --git a/src/mongo/client/scram_sha1_client_cache.cpp b/src/mongo/client/scram_sha1_client_cache.cpp new file mode 100644 index 00000000000..a087f27ca0a --- /dev/null +++ b/src/mongo/client/scram_sha1_client_cache.cpp @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2017 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 <http://www.gnu.org/licenses/>. + * + * 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/client/scram_sha1_client_cache.h" + +namespace mongo { + +boost::optional<scram::SCRAMSecrets> SCRAMSHA1ClientCache::getCachedSecrets( + const HostAndPort& target, const scram::SCRAMPresecrets& presecrets) const { + const stdx::lock_guard<stdx::mutex> lock(_hostToSecretsMutex); + + // Search the cache for a record associated with the host we're trying to connect to. + auto foundSecret = _hostToSecrets.find(target); + if (foundSecret != _hostToSecrets.end()) { + // Presecrets contain parameters provided by the server, which may change. If the + // cached presecrets don't match the presecrets we have on hand, we must not return the + // stale cached secrets. We'll need to rerun the SCRAM computation. + const scram::SCRAMPresecrets& foundPresecrets = foundSecret->second.first; + if (foundPresecrets == presecrets) { + return foundSecret->second.second; + } + } + return {}; +} + +void SCRAMSHA1ClientCache::setCachedSecrets(HostAndPort target, + scram::SCRAMPresecrets presecrets, + scram::SCRAMSecrets secrets) { + const stdx::lock_guard<stdx::mutex> lock(_hostToSecretsMutex); + + decltype(_hostToSecrets)::iterator it; + bool insertionSuccessful; + auto cacheRecord = std::make_pair(std::move(presecrets), std::move(secrets)); + // Insert the presecrets, and the secrets we computed for them into the cache + std::tie(it, insertionSuccessful) = _hostToSecrets.emplace(std::move(target), cacheRecord); + // If there was already a cache entry for the target HostAndPort, we should overwrite it. + // We have fresher presecrets and secrets. + if (!insertionSuccessful) { + it->second = std::move(cacheRecord); + } +} + +} // namespace mongo diff --git a/src/mongo/client/scram_sha1_client_cache.h b/src/mongo/client/scram_sha1_client_cache.h new file mode 100644 index 00000000000..4d5fdb3b323 --- /dev/null +++ b/src/mongo/client/scram_sha1_client_cache.h @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2017 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 <http://www.gnu.org/licenses/>. + * + * 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. + */ +#pragma once + +#include <boost/optional.hpp> +#include <string> + +#include "mongo/crypto/mechanism_scram.h" +#include "mongo/stdx/mutex.h" +#include "mongo/stdx/unordered_map.h" +#include "mongo/util/net/hostandport.h" + +namespace mongo { + +/** + * A cache for the intermediate steps of the SCRAM-SHA-1 computation. + * + * Clients wishing to authenticate to a server using SCRAM-SHA-1 + * must produce a set of credential objects from their password, + * a salt, and an iteration count. The computation to generate these + * is very expensive, proportional to the iteration count. The high + * cost of this computation prevents brute force attacks on + * intercepted SCRAM authentication data, or a stolen password + * database. The inputs to the function are unlikely to frequently + * change. Caching the relationship between the inputs and the + * resulting output should make repeated authentication attempts + * to a single server much faster. + * + * This is explicitly permitted by RFC5802, section 5.1: + * + * "Note that a client implementation MAY cache + * ClientKey&ServerKey (or just SaltedPassword) for later + * reauthentication to the same service, as it is likely that the + * server is going to advertise the same salt value upon + * reauthentication. This might be useful for mobile clients where + * CPU usage is a concern." + */ +class SCRAMSHA1ClientCache { +public: + /** + * Returns precomputed SCRAMSecrets, if one has already been + * stored for the specified hostname and the provided presecrets + * match those recorded for the hostname. Otherwise, no secrets + * are returned. + */ + boost::optional<scram::SCRAMSecrets> getCachedSecrets( + const HostAndPort& target, const scram::SCRAMPresecrets& presecrets) const; + + /** + * Records a set of precomputed SCRAMSecrets for the specified + * host, along with the presecrets used to generate them. + */ + void setCachedSecrets(HostAndPort target, + scram::SCRAMPresecrets presecrets, + scram::SCRAMSecrets secrets); + +private: + mutable stdx::mutex _hostToSecretsMutex; + stdx::unordered_map<HostAndPort, std::pair<scram::SCRAMPresecrets, scram::SCRAMSecrets>> + _hostToSecrets; +}; + +} // namespace mongo diff --git a/src/mongo/crypto/SConscript b/src/mongo/crypto/SConscript index 7d2aae22ce9..c57bccedd31 100644 --- a/src/mongo/crypto/SConscript +++ b/src/mongo/crypto/SConscript @@ -13,16 +13,29 @@ env.SConscript( ], ) -env.Library('crypto_tom', - ['crypto_tom.cpp'], - LIBDEPS=['tom/tomcrypt']) +env.Library( + target='crypto_tom', + source=[ + 'crypto_tom.cpp' + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + 'tom/tomcrypt' + ]) env.Library('crypto_openssl', - ['crypto_openssl.cpp']) + source=[ + 'crypto_openssl.cpp' + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + ]) env.Library('scramauth', ['mechanism_scram.cpp'], LIBDEPS=['$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/base/secure_allocator', + '$BUILD_DIR/mongo/util/secure_zero_memory', 'crypto_${MONGO_CRYPTO}']) env.CppUnitTest('crypto_test', diff --git a/src/mongo/crypto/crypto.h b/src/mongo/crypto/crypto.h index 7b08e371a84..1f010980078 100644 --- a/src/mongo/crypto/crypto.h +++ b/src/mongo/crypto/crypto.h @@ -28,24 +28,26 @@ #pragma once +#include <array> #include <cstddef> namespace mongo { +// Storage container for a SHA1 hash +using SHA1Hash = std::array<std::uint8_t, 20>; + namespace crypto { /* * Computes a SHA-1 hash of 'input'. */ -bool sha1(const unsigned char* input, const size_t inputLen, unsigned char* output); +SHA1Hash sha1(const unsigned char* input, const size_t inputLen); /* * Computes a HMAC SHA-1 keyed hash of 'input' using the key 'key' */ -bool hmacSha1(const unsigned char* key, - const size_t keyLen, - const unsigned char* input, - const size_t inputLen, - unsigned char* output, - unsigned int* outputLen); +SHA1Hash hmacSha1(const unsigned char* key, + const size_t keyLen, + const unsigned char* input, + const size_t inputLen); } // namespace crypto } // namespace mongo diff --git a/src/mongo/crypto/crypto_openssl.cpp b/src/mongo/crypto/crypto_openssl.cpp index ca6844acb9e..5c552826090 100644 --- a/src/mongo/crypto/crypto_openssl.cpp +++ b/src/mongo/crypto/crypto_openssl.cpp @@ -29,6 +29,8 @@ #include "mongo/platform/basic.h" #include "mongo/config.h" +#include "mongo/crypto/crypto.h" +#include "mongo/util/assert_util.h" #include "mongo/util/scopeguard.h" #ifndef MONGO_CONFIG_SSL @@ -44,32 +46,30 @@ namespace crypto { /* * Computes a SHA-1 hash of 'input'. */ -bool sha1(const unsigned char* input, const size_t inputLen, unsigned char* output) { +SHA1Hash sha1(const unsigned char* input, const size_t inputLen) { + SHA1Hash output; + EVP_MD_CTX digestCtx; EVP_MD_CTX_init(&digestCtx); ON_BLOCK_EXIT(EVP_MD_CTX_cleanup, &digestCtx); - if (1 != EVP_DigestInit_ex(&digestCtx, EVP_sha1(), NULL)) { - return false; - } - - if (1 != EVP_DigestUpdate(&digestCtx, input, inputLen)) { - return false; - } - - return (1 == EVP_DigestFinal_ex(&digestCtx, output, NULL)); + fassert(40379, + EVP_DigestInit_ex(&digestCtx, EVP_sha1(), NULL) == 1 && + EVP_DigestUpdate(&digestCtx, input, inputLen) == 1 && + EVP_DigestFinal_ex(&digestCtx, output.data(), NULL) == 1); + return output; } /* * Computes a HMAC SHA-1 keyed hash of 'input' using the key 'key' */ -bool hmacSha1(const unsigned char* key, - const size_t keyLen, - const unsigned char* input, - const size_t inputLen, - unsigned char* output, - unsigned int* outputLen) { - return HMAC(EVP_sha1(), key, keyLen, input, inputLen, output, outputLen); +SHA1Hash hmacSha1(const unsigned char* key, + const size_t keyLen, + const unsigned char* input, + const size_t inputLen) { + SHA1Hash output; + fassert(40380, HMAC(EVP_sha1(), key, keyLen, input, inputLen, output.data(), NULL) != NULL); + return output; } } // namespace crypto diff --git a/src/mongo/crypto/crypto_test.cpp b/src/mongo/crypto/crypto_test.cpp index b3a1a6badb7..4857e36cab1 100644 --- a/src/mongo/crypto/crypto_test.cpp +++ b/src/mongo/crypto/crypto_test.cpp @@ -31,12 +31,11 @@ namespace mongo { namespace { -const int digestLen = 20; // SHA-1 test vectors from http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/SHA_All.pdf const struct { const char* msg; - unsigned char hash[digestLen]; + SHA1Hash hash; } sha1Tests[] = {{"abc", {0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50, 0xc2, 0x6c, 0x9c, 0xd0, 0xd8, 0x9d}}, @@ -45,15 +44,12 @@ const struct { 0x4A, 0xA1, 0xF9, 0x51, 0x29, 0xE5, 0xE5, 0x46, 0x70, 0xF1}}}; TEST(CryptoVectors, SHA1) { - unsigned char sha1Result[digestLen]; size_t numTests = sizeof(sha1Tests) / sizeof(sha1Tests[0]); for (size_t i = 0; i < numTests; i++) { - ASSERT(crypto::sha1(reinterpret_cast<const unsigned char*>(sha1Tests[i].msg), - strlen(sha1Tests[i].msg), - sha1Result)) + SHA1Hash result = crypto::sha1(reinterpret_cast<const unsigned char*>(sha1Tests[i].msg), + strlen(sha1Tests[i].msg)); + ASSERT(0 == memcmp(sha1Tests[i].hash.data(), result.data(), result.size())) << "Failed SHA1 iteration " << i; - ASSERT(0 == memcmp(sha1Tests[i].hash, sha1Result, digestLen)) << "Failed SHA1 iteration " - << i; } } @@ -65,7 +61,7 @@ const struct { int keyLen; unsigned char data[maxDataSize]; int dataLen; - unsigned char hash[digestLen]; + SHA1Hash hash; } hmacSha1Tests[] = { // RFC test case 1 {{0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, @@ -117,19 +113,13 @@ const struct { 0x56, 0x37, 0xce, 0x8a, 0x3b, 0x55, 0xed, 0x40, 0x21, 0x12}}}; TEST(CryptoVectors, HMACSHA1) { - unsigned char hmacSha1Result[digestLen]; - unsigned int hashLen = digestLen; - size_t numTests = sizeof(hmacSha1Tests) / sizeof(hmacSha1Tests[0]); for (size_t i = 0; i < numTests; i++) { - ASSERT(crypto::hmacSha1(hmacSha1Tests[i].key, - hmacSha1Tests[i].keyLen, - hmacSha1Tests[i].data, - hmacSha1Tests[i].dataLen, - hmacSha1Result, - &hashLen)) - << "Failed HMAC-SHA1 iteration " << i; - ASSERT(0 == memcmp(hmacSha1Tests[i].hash, hmacSha1Result, digestLen)) + SHA1Hash result = crypto::hmacSha1(hmacSha1Tests[i].key, + hmacSha1Tests[i].keyLen, + hmacSha1Tests[i].data, + hmacSha1Tests[i].dataLen); + ASSERT(0 == memcmp(hmacSha1Tests[i].hash.data(), result.data(), result.size())) << "Failed HMAC-SHA1 iteration " << i; } } diff --git a/src/mongo/crypto/crypto_tom.cpp b/src/mongo/crypto/crypto_tom.cpp index a629dff945d..904eb5925e3 100644 --- a/src/mongo/crypto/crypto_tom.cpp +++ b/src/mongo/crypto/crypto_tom.cpp @@ -29,6 +29,8 @@ #include "mongo/platform/basic.h" #include "mongo/config.h" +#include "mongo/crypto/crypto.h" +#include "mongo/util/assert_util.h" #ifdef MONGO_CONFIG_SSL #error This file should not be included if compiling with SSL support @@ -41,33 +43,26 @@ namespace crypto { /* * Computes a SHA-1 hash of 'input'. */ -bool sha1(const unsigned char* input, const size_t inputLen, unsigned char* output) { - hash_state hashState; - if (sha1_init(&hashState) != CRYPT_OK) { - return false; - } - if (sha1_process(&hashState, input, inputLen) != CRYPT_OK) { - return false; - } - if (sha1_done(&hashState, output) != CRYPT_OK) { - return false; - } +SHA1Hash sha1(const unsigned char* input, const size_t inputLen) { + SHA1Hash output; - return true; + hash_state hashState; + fassert(40381, + sha1_init(&hashState) == CRYPT_OK && + sha1_process(&hashState, input, inputLen) == CRYPT_OK && + sha1_done(&hashState, output.data()) == CRYPT_OK); + return output; } /* * Computes a HMAC SHA-1 keyed hash of 'input' using the key 'key' */ -bool hmacSha1(const unsigned char* key, - const size_t keyLen, - const unsigned char* input, - const size_t inputLen, - unsigned char* output, - unsigned int* outputLen) { - if (!key || !input || !output) { - return false; - } +SHA1Hash hmacSha1(const unsigned char* key, + const size_t keyLen, + const unsigned char* input, + const size_t inputLen) { + invariant(key && input); + SHA1Hash output; static int hashId = -1; if (hashId == -1) { @@ -76,12 +71,10 @@ bool hmacSha1(const unsigned char* key, } unsigned long sha1HashLen = 20; - if (hmac_memory(hashId, key, keyLen, input, inputLen, output, &sha1HashLen) != CRYPT_OK) { - return false; - } - - *outputLen = sha1HashLen; - return true; + fassert(40382, + hmac_memory(hashId, key, keyLen, input, inputLen, output.data(), &sha1HashLen) == + CRYPT_OK); + return output; } } // namespace crypto diff --git a/src/mongo/crypto/mechanism_scram.cpp b/src/mongo/crypto/mechanism_scram.cpp index 5bc797ef704..57a2eed3dd0 100644 --- a/src/mongo/crypto/mechanism_scram.cpp +++ b/src/mongo/crypto/mechanism_scram.cpp @@ -35,26 +35,56 @@ #include "mongo/crypto/crypto.h" #include "mongo/platform/random.h" #include "mongo/util/base64.h" +#include "mongo/util/secure_zero_memory.h" namespace mongo { namespace scram { using std::unique_ptr; +namespace { +/** + * Compare two arrays of bytes for equality in constant time. + * + * This means that the function runs for the same amount of time even if they differ. Unlike memcmp, + * this function does not exit on the first difference. + * + * Returns true if the two arrays are equal. + * + * TODO: evaluate if LTO inlines or changes the code flow of this function. + */ +NOINLINE_DECL +bool consttimeMemEqual(volatile const unsigned char* s1, // NOLINT - using volatile to + volatile const unsigned char* s2, // NOLINT - disable compiler optimizations + size_t length) { + unsigned int ret = 0; + + for (size_t i = 0; i < length; ++i) { + ret |= s1[i] ^ s2[i]; + } + + return (1 & ((ret - 1) >> 8)); +} +} // namespace + +std::string hashToBase64(const SecureHandle<SHA1Hash>& hash) { + return base64::encode(reinterpret_cast<const char*>(hash->data()), hash->size()); +} + // Compute the SCRAM step Hi() as defined in RFC5802 -static void HMACIteration(const unsigned char input[], - size_t inputLen, - const unsigned char salt[], - size_t saltLen, - unsigned int iterationCount, - unsigned char output[]) { - unsigned char intermediateDigest[hashSize]; - unsigned char startKey[hashSize]; - // Placeholder for HMAC return size, will always be scram::hashSize for HMAC SHA-1 - unsigned int hashLen = 0; - - uassert(17450, "invalid salt length provided", saltLen + 4 == hashSize); - memcpy(startKey, salt, saltLen); +static SHA1Hash HMACIteration(const unsigned char input[], + size_t inputLen, + const unsigned char salt[], + size_t saltLen, + unsigned int iterationCount) { + SHA1Hash output; + SHA1Hash intermediateDigest; + // Reserve a 20 byte block for the initial key. We use 16 byte salts, and must reserve an extra + // 4 bytes for a suffix mandated by RFC5802. + std::array<std::uint8_t, 20> startKey; + + uassert(17450, "invalid salt length provided", saltLen + 4 == startKey.size()); + std::copy(salt, salt + saltLen, startKey.begin()); startKey[saltLen] = 0; startKey[saltLen + 1] = 0; @@ -62,72 +92,64 @@ static void HMACIteration(const unsigned char input[], startKey[saltLen + 3] = 1; // U1 = HMAC(input, salt + 0001) - fassert(17494, crypto::hmacSha1(input, inputLen, startKey, saltLen + 4, output, &hashLen)); - - memcpy(intermediateDigest, output, hashSize); + output = crypto::hmacSha1(input, inputLen, startKey.data(), startKey.size()); + intermediateDigest = output; // intermediateDigest contains Ui and output contains the accumulated XOR:ed result for (size_t i = 2; i <= iterationCount; i++) { - unsigned char intermediateOutput[hashSize]; - fassert(17495, - crypto::hmacSha1( - input, inputLen, intermediateDigest, hashSize, intermediateOutput, &hashLen)); - memcpy(intermediateDigest, intermediateOutput, hashSize); - for (size_t k = 0; k < hashSize; k++) { + intermediateDigest = + crypto::hmacSha1(input, inputLen, intermediateDigest.data(), intermediateDigest.size()); + for (size_t k = 0; k < output.size(); k++) { output[k] ^= intermediateDigest[k]; } } + + return output; } // Iterate the hash function to generate SaltedPassword -void generateSaltedPassword(StringData hashedPassword, - const unsigned char* salt, - const int saltLen, - const int iterationCount, - unsigned char saltedPassword[hashSize]) { +SHA1Hash generateSaltedPassword(const SCRAMPresecrets& presecrets) { // saltedPassword = Hi(hashedPassword, salt) - HMACIteration(reinterpret_cast<const unsigned char*>(hashedPassword.rawData()), - hashedPassword.size(), - salt, - saltLen, - iterationCount, - saltedPassword); + SHA1Hash saltedPassword = + HMACIteration(reinterpret_cast<const unsigned char*>(presecrets.hashedPassword.c_str()), + presecrets.hashedPassword.size(), + presecrets.salt.data(), + presecrets.salt.size(), + presecrets.iterationCount); + + return saltedPassword; +} + +SCRAMSecrets generateSecrets(const SCRAMPresecrets& presecrets) { + SHA1Hash saltedPassword = generateSaltedPassword(presecrets); + return generateSecrets(saltedPassword); } -void generateSecrets(const std::string& hashedPassword, - const unsigned char salt[], - size_t saltLen, - size_t iterationCount, - unsigned char storedKey[hashSize], - unsigned char serverKey[hashSize]) { - unsigned char saltedPassword[hashSize]; - unsigned char clientKey[hashSize]; - unsigned int hashLen = 0; - - generateSaltedPassword(hashedPassword, salt, saltLen, iterationCount, saltedPassword); - - // clientKey = HMAC(saltedPassword, "Client Key") - fassert(17498, - crypto::hmacSha1(saltedPassword, - hashSize, - reinterpret_cast<const unsigned char*>(clientKeyConst.data()), - clientKeyConst.size(), - clientKey, - &hashLen)); - - // storedKey = H(clientKey) - fassert(17499, crypto::sha1(clientKey, hashSize, storedKey)); - - // serverKey = HMAC(saltedPassword, "Server Key") - fassert(17500, - crypto::hmacSha1(saltedPassword, - hashSize, - reinterpret_cast<const unsigned char*>(serverKeyConst.data()), - serverKeyConst.size(), - serverKey, - &hashLen)); +SCRAMSecrets generateSecrets(const SHA1Hash& saltedPassword) { + SCRAMSecrets credentials; + + // ClientKey := HMAC(saltedPassword, "Client Key") + credentials.clientKey = + crypto::hmacSha1(saltedPassword.data(), + saltedPassword.size(), + reinterpret_cast<const unsigned char*>(clientKeyConst.data()), + clientKeyConst.size()); + + // StoredKey := H(clientKey) + credentials.storedKey = + crypto::sha1(credentials.clientKey->data(), credentials.clientKey->size()); + + // ServerKey := HMAC(SaltedPassword, "Server Key") + credentials.serverKey = + crypto::hmacSha1(saltedPassword.data(), + saltedPassword.size(), + reinterpret_cast<const unsigned char*>(serverKeyConst.data()), + serverKeyConst.size()); + + return credentials; } + BSONObj generateCredentials(const std::string& hashedPassword, int iterationCount) { const int saltLenQWords = 2; @@ -142,18 +164,15 @@ BSONObj generateCredentials(const std::string& hashedPassword, int iterationCoun base64::encode(reinterpret_cast<char*>(userSalt), sizeof(userSalt)); // Compute SCRAM secrets serverKey and storedKey - unsigned char storedKey[hashSize]; - unsigned char serverKey[hashSize]; + auto secrets = generateSecrets( + SCRAMPresecrets(hashedPassword, + std::vector<std::uint8_t>(reinterpret_cast<std::uint8_t*>(userSalt), + reinterpret_cast<std::uint8_t*>(userSalt) + + saltLenQWords * sizeof(uint64_t)), + iterationCount)); - generateSecrets(hashedPassword, - reinterpret_cast<unsigned char*>(userSalt), - saltLenQWords * sizeof(uint64_t), - iterationCount, - storedKey, - serverKey); - - std::string encodedStoredKey = base64::encode(reinterpret_cast<char*>(storedKey), hashSize); - std::string encodedServerKey = base64::encode(reinterpret_cast<char*>(serverKey), hashSize); + std::string encodedStoredKey = hashToBase64(secrets.storedKey); + std::string encodedServerKey = hashToBase64(secrets.serverKey); return BSON(iterationCountFieldName << iterationCount << saltFieldName << encodedUserSalt << storedKeyFieldName @@ -162,99 +181,66 @@ BSONObj generateCredentials(const std::string& hashedPassword, int iterationCoun << encodedServerKey); } -std::string generateClientProof(const unsigned char saltedPassword[hashSize], +std::string generateClientProof(const SCRAMSecrets& clientCredentials, const std::string& authMessage) { - // ClientKey := HMAC(saltedPassword, "Client Key") - unsigned char clientKey[hashSize]; - unsigned int hashLen = 0; - fassert(18689, - crypto::hmacSha1(saltedPassword, - hashSize, - reinterpret_cast<const unsigned char*>(clientKeyConst.data()), - clientKeyConst.size(), - clientKey, - &hashLen)); - - // StoredKey := H(clientKey) - unsigned char storedKey[hashSize]; - fassert(18701, crypto::sha1(clientKey, hashSize, storedKey)); - // ClientSignature := HMAC(StoredKey, AuthMessage) - unsigned char clientSignature[hashSize]; - fassert(18702, - crypto::hmacSha1(storedKey, - hashSize, - reinterpret_cast<const unsigned char*>(authMessage.c_str()), - authMessage.size(), - clientSignature, - &hashLen)); + SHA1Hash clientSignature = + crypto::hmacSha1(clientCredentials.storedKey->data(), + clientCredentials.storedKey->size(), + reinterpret_cast<const unsigned char*>(authMessage.c_str()), + authMessage.size()); // ClientProof := ClientKey XOR ClientSignature - unsigned char clientProof[hashSize]; - for (size_t i = 0; i < hashSize; i++) { - clientProof[i] = clientKey[i] ^ clientSignature[i]; - } - - return base64::encode(reinterpret_cast<char*>(clientProof), hashSize); -} - -/** - * Compare two arrays of bytes for equality in constant time. - * - * This means that the function runs for the same amount of time even if they differ. Unlike memcmp, - * this function does not exit on the first difference. - * - * Returns true if the two arrays are equal. - * - * TODO: evaluate if LTO inlines or changes the code flow of this function. - */ -NOINLINE_DECL -bool memequal(volatile const unsigned char* s1, // NOLINT - using volatile to - volatile const unsigned char* s2, // NOLINT - disable compiler optimizations - size_t length) { - unsigned char ret = 0; - - for (size_t i = 0; i < length; ++i) { - ret |= s1[i] ^ s2[i]; + SHA1Hash clientProof; + for (size_t i = 0; i < clientCredentials.clientKey->size(); i++) { + clientProof[i] = (*clientCredentials.clientKey)[i] ^ clientSignature[i]; } - return ret == 0; + return hashToBase64(clientProof); } -bool verifyServerSignature(const unsigned char saltedPassword[hashSize], +bool verifyServerSignature(const SCRAMSecrets& clientCredentials, const std::string& authMessage, const std::string& receivedServerSignature) { - // ServerKey := HMAC(SaltedPassword, "Server Key") - unsigned int hashLen; - unsigned char serverKey[hashSize]; - fassert(18703, - crypto::hmacSha1(saltedPassword, - hashSize, - reinterpret_cast<const unsigned char*>(serverKeyConst.data()), - serverKeyConst.size(), - serverKey, - &hashLen)); - // ServerSignature := HMAC(ServerKey, AuthMessage) - unsigned char serverSignature[hashSize]; - fassert(18704, - crypto::hmacSha1(serverKey, - hashSize, - reinterpret_cast<const unsigned char*>(authMessage.c_str()), - authMessage.size(), - serverSignature, - &hashLen)); - - std::string encodedServerSignature = - base64::encode(reinterpret_cast<char*>(serverSignature), sizeof(serverSignature)); + SHA1Hash serverSignature = + crypto::hmacSha1(clientCredentials.serverKey->data(), + clientCredentials.serverKey->size(), + reinterpret_cast<const unsigned char*>(authMessage.c_str()), + authMessage.size()); + + std::string encodedServerSignature = hashToBase64(serverSignature); if (encodedServerSignature.size() != receivedServerSignature.size()) { return false; } - return memequal(reinterpret_cast<const unsigned char*>(encodedServerSignature.c_str()), - reinterpret_cast<const unsigned char*>(receivedServerSignature.c_str()), - encodedServerSignature.size()); + return consttimeMemEqual( + reinterpret_cast<const unsigned char*>(encodedServerSignature.c_str()), + reinterpret_cast<const unsigned char*>(receivedServerSignature.c_str()), + encodedServerSignature.size()); +} + +bool verifyClientProof(StringData clientProof, StringData storedKey, StringData authMessage) { + // ClientSignature := HMAC(StoredKey, AuthMessage) + SHA1Hash clientSignature = + crypto::hmacSha1(reinterpret_cast<const unsigned char*>(storedKey.rawData()), + storedKey.size(), + reinterpret_cast<const unsigned char*>(authMessage.rawData()), + authMessage.size()); + + // ClientKey := ClientSignature XOR ClientProof + SHA1Hash clientKey; + for (size_t i = 0; i < clientKey.size(); i++) { + clientKey[i] = clientSignature[i] ^ clientProof.rawData()[i]; + } + + // StoredKey := H(ClientKey) + SHA1Hash computedStoredKey = crypto::sha1(clientKey.data(), clientKey.size()); + + return consttimeMemEqual(reinterpret_cast<const unsigned char*>(storedKey.rawData()), + computedStoredKey.data(), + computedStoredKey.size()); } } // namespace scram diff --git a/src/mongo/crypto/mechanism_scram.h b/src/mongo/crypto/mechanism_scram.h index 65e77b38f74..1d3b86f2fdd 100644 --- a/src/mongo/crypto/mechanism_scram.h +++ b/src/mongo/crypto/mechanism_scram.h @@ -30,12 +30,15 @@ #include <string> +#include "mongo/base/secure_allocator.h" #include "mongo/base/status.h" +#include "mongo/crypto/crypto.h" #include "mongo/db/jsobj.h" namespace mongo { namespace scram { -const unsigned int hashSize = 20; +// Convert a SHA1Hash into a base64 encoded string. +std::string hashToBase64(const SecureHandle<SHA1Hash>& hash); const std::string serverKeyConst = "Server Key"; const std::string clientKeyConst = "Client Key"; @@ -46,24 +49,53 @@ const std::string storedKeyFieldName = "storedKey"; const std::string serverKeyFieldName = "serverKey"; /* + * The precursors necessary to perform the computation which produces SCRAMSecrets. + * These are the original password, its salt, and the number of times it must be + * hashed to produce the SaltedPassword used to generate the rest of the SCRAMSecrets. + */ +struct SCRAMPresecrets { + SCRAMPresecrets(std::string hashedPassword, + std::vector<std::uint8_t> salt, + size_t iterationCount) + : hashedPassword(std::move(hashedPassword)), + salt(std::move(salt)), + iterationCount(iterationCount) {} + + std::string hashedPassword; + std::vector<std::uint8_t> salt; + size_t iterationCount; +}; + +inline bool operator==(const SCRAMPresecrets& lhs, const SCRAMPresecrets& rhs) { + return lhs.hashedPassword == rhs.hashedPassword && lhs.salt == rhs.salt && + lhs.iterationCount == rhs.iterationCount; +} + +/* * Computes the SaltedPassword from password, salt and iterationCount. */ -void generateSaltedPassword(StringData hashedPassword, - const unsigned char* salt, - const int saltLen, - const int iterationCount, - unsigned char saltedPassword[hashSize]); +SHA1Hash generateSaltedPassword(const SCRAMPresecrets& presecrets); + +/* + * Stores all of the keys, generated from a password, needed for a client or server to perform a + * SCRAM handshake. This structure will secureZeroMemory itself on destruction. + */ +struct SCRAMSecrets { + SecureHandle<SHA1Hash> clientKey; + SecureHandle<SHA1Hash> storedKey; + SecureHandle<SHA1Hash> serverKey; +}; /* - * Computes the SCRAM secrets storedKey and serverKey using the salt 'salt' + * Computes the SCRAM secrets clientKey, storedKey, and serverKey using the salt 'salt' * and iteration count 'iterationCount' as defined in RFC5802 (server side). */ -void generateSecrets(const std::string& hashedPassword, - const unsigned char salt[], - size_t saltLen, - size_t iterationCount, - unsigned char storedKey[hashSize], - unsigned char serverKey[hashSize]); +SCRAMSecrets generateSecrets(const SCRAMPresecrets& presecrets); + +/* + * Computes the ClientKey and StoredKey from SaltedPassword (client side). + */ +SCRAMSecrets generateSecrets(const SHA1Hash& saltedPassword); /* * Generates the user salt and the SCRAM secrets storedKey and serverKey as @@ -72,9 +104,9 @@ void generateSecrets(const std::string& hashedPassword, BSONObj generateCredentials(const std::string& hashedPassword, int iterationCount); /* - * Computes the ClientProof from SaltedPassword and authMessage (client side). + * Computes the ClientProof from ClientKey, StoredKey, and authMessage (client side). */ -std::string generateClientProof(const unsigned char saltedPassword[hashSize], +std::string generateClientProof(const SCRAMSecrets& clientCredentials, const std::string& authMessage); /* @@ -89,8 +121,14 @@ bool validatePassword(const std::string& hashedPassword, /* * Verifies ServerSignature (client side). */ -bool verifyServerSignature(const unsigned char saltedPassword[hashSize], +bool verifyServerSignature(const SCRAMSecrets& clientCredentials, const std::string& authMessage, const std::string& serverSignature); + +/* + * Verifies ClientProof (server side). + */ +bool verifyClientProof(StringData clientProof, StringData storedKey, StringData authMessage); + } // namespace scram } // namespace mongo diff --git a/src/mongo/db/auth/sasl_plain_server_conversation.cpp b/src/mongo/db/auth/sasl_plain_server_conversation.cpp index 31945f6e9a6..cf7f2cdeaa5 100644 --- a/src/mongo/db/auth/sasl_plain_server_conversation.cpp +++ b/src/mongo/db/auth/sasl_plain_server_conversation.cpp @@ -28,7 +28,6 @@ #include "mongo/db/auth/sasl_plain_server_conversation.h" -#include "mongo/base/secure_allocator.h" #include "mongo/crypto/mechanism_scram.h" #include "mongo/db/auth/sasl_authentication_session.h" #include "mongo/util/base64.h" @@ -115,18 +114,16 @@ StatusWith<bool> SaslPLAINServerConversation::step(StringData inputData, std::st } } else { // Handle schemaVersion28SCRAM (SCRAM only mode) - unsigned char storedKey[scram::hashSize]; - unsigned char serverKey[scram::hashSize]; - - scram::generateSecrets( + std::string decodedSalt = base64::decode(creds.scram.salt); + scram::SCRAMSecrets secrets = scram::generateSecrets(scram::SCRAMPresecrets( authDigest, - reinterpret_cast<const unsigned char*>(base64::decode(creds.scram.salt).c_str()), - 16, - creds.scram.iterationCount, - storedKey, - serverKey); + std::vector<std::uint8_t>(reinterpret_cast<const std::uint8_t*>(decodedSalt.c_str()), + reinterpret_cast<const std::uint8_t*>(decodedSalt.c_str()) + + 16), + creds.scram.iterationCount)); if (creds.scram.storedKey != - base64::encode(reinterpret_cast<const char*>(storedKey), scram::hashSize)) { + base64::encode(reinterpret_cast<const char*>(secrets.storedKey->data()), + secrets.storedKey->size())) { return StatusWith<bool>(ErrorCodes::AuthenticationFailed, mongoutils::str::stream() << "Incorrect user name or password"); } diff --git a/src/mongo/db/auth/sasl_scramsha1_server_conversation.cpp b/src/mongo/db/auth/sasl_scramsha1_server_conversation.cpp index ed812ddb27f..a98dc6d7dcc 100644 --- a/src/mongo/db/auth/sasl_scramsha1_server_conversation.cpp +++ b/src/mongo/db/auth/sasl_scramsha1_server_conversation.cpp @@ -281,60 +281,23 @@ StatusWith<bool> SaslSCRAMSHA1ServerConversation::_secondStep(const std::vector< // 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<const unsigned char*>(decodedStoredKey.c_str()), - scram::hashSize, - reinterpret_cast<const unsigned char*>(_authMessage.c_str()), - _authMessage.size(), - clientSignature, - &hashLen)); - - fassert(18658, hashLen == scram::hashSize); - - try { - clientProof = base64::decode(clientProof); - } catch (const DBException& ex) { - return StatusWith<bool>(ex.toStatus()); - } - const unsigned char* decodedClientProof = - reinterpret_cast<const unsigned char*>(clientProof.c_str()); - - // ClientKey := ClientSignature XOR ClientProof - unsigned char clientKey[scram::hashSize]; - for (size_t i = 0; i < scram::hashSize; i++) { - clientKey[i] = clientSignature[i] ^ decodedClientProof[i]; - } - - // StoredKey := H(ClientKey) - unsigned char computedStoredKey[scram::hashSize]; - fassert(18659, crypto::sha1(clientKey, scram::hashSize, computedStoredKey)); - - if (memcmp(decodedStoredKey.c_str(), computedStoredKey, scram::hashSize) != 0) { + if (!scram::verifyClientProof( + base64::decode(clientProof), base64::decode(_creds.scram.storedKey), _authMessage)) { return StatusWith<bool>(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<const unsigned char*>(decodedServerKey.c_str()), - scram::hashSize, - reinterpret_cast<const unsigned char*>(_authMessage.c_str()), - _authMessage.size(), - serverSignature, - &hashLen)); - - fassert(18661, hashLen == scram::hashSize); + SHA1Hash serverSignature = + crypto::hmacSha1(reinterpret_cast<const unsigned char*>(decodedServerKey.c_str()), + decodedServerKey.size(), + reinterpret_cast<const unsigned char*>(_authMessage.c_str()), + _authMessage.size()); StringBuilder sb; - sb << "v=" << base64::encode(reinterpret_cast<char*>(serverSignature), scram::hashSize); + sb << "v=" << scram::hashToBase64(serverSignature); *outputData = sb.str(); return StatusWith<bool>(false); diff --git a/src/mongo/db/auth/sasl_scramsha1_test.cpp b/src/mongo/db/auth/sasl_scramsha1_test.cpp index 72b817bc95d..b7a214af68e 100644 --- a/src/mongo/db/auth/sasl_scramsha1_test.cpp +++ b/src/mongo/db/auth/sasl_scramsha1_test.cpp @@ -29,6 +29,7 @@ #include "mongo/platform/basic.h" #include "mongo/client/native_sasl_client_session.h" +#include "mongo/client/scram_sha1_client_cache.h" #include "mongo/crypto/mechanism_scram.h" #include "mongo/db/auth/authorization_manager.h" #include "mongo/db/auth/authz_manager_external_state_mock.h" @@ -246,6 +247,8 @@ protected: saslClientSession->setParameter(NativeSaslClientSession::parameterServiceName, "mongodb"); saslClientSession->setParameter(NativeSaslClientSession::parameterServiceHostname, "MockServer.test"); + saslClientSession->setParameter(NativeSaslClientSession::parameterServiceHostAndPort, + "MockServer.test:27017"); } }; @@ -435,4 +438,79 @@ TEST_F(SCRAMSHA1Fixture, testMONGODBCR) { ASSERT_EQ(goalState, runSteps(saslServerSession.get(), saslClientSession.get())); } +TEST(SCRAMSHA1Cache, testGetFromEmptyCache) { + SCRAMSHA1ClientCache cache; + std::string saltStr("saltsaltsaltsalt"); + std::vector<std::uint8_t> salt(saltStr.begin(), saltStr.end()); + HostAndPort host("localhost:27017"); + + ASSERT_FALSE(cache.getCachedSecrets(host, scram::SCRAMPresecrets("aaa", salt, 10000))); +} + + +TEST(SCRAMSHA1Cache, testSetAndGet) { + SCRAMSHA1ClientCache cache; + std::string saltStr("saltsaltsaltsalt"); + std::string badSaltStr("s@lts@lts@lts@lt"); + std::vector<std::uint8_t> salt(saltStr.begin(), saltStr.end()); + std::vector<std::uint8_t> badSalt(badSaltStr.begin(), badSaltStr.end()); + HostAndPort host("localhost:27017"); + + auto secret = scram::generateSecrets(scram::SCRAMPresecrets("aaa", salt, 10000)); + cache.setCachedSecrets(host, scram::SCRAMPresecrets("aaa", salt, 10000), secret); + auto cachedSecret = cache.getCachedSecrets(host, scram::SCRAMPresecrets("aaa", salt, 10000)); + ASSERT_TRUE(cachedSecret); + ASSERT_TRUE(std::equal( + secret.clientKey->begin(), secret.clientKey->end(), cachedSecret->clientKey->begin())); + ASSERT_TRUE(std::equal( + secret.serverKey->begin(), secret.serverKey->end(), cachedSecret->serverKey->begin())); + ASSERT_TRUE(std::equal( + secret.storedKey->begin(), secret.storedKey->end(), cachedSecret->storedKey->begin())); +} + + +TEST(SCRAMSHA1Cache, testSetAndGetWithDifferentParameters) { + SCRAMSHA1ClientCache cache; + std::string saltStr("saltsaltsaltsalt"); + std::string badSaltStr("s@lts@lts@lts@lt"); + std::vector<std::uint8_t> salt(saltStr.begin(), saltStr.end()); + std::vector<std::uint8_t> badSalt(badSaltStr.begin(), badSaltStr.end()); + HostAndPort host("localhost:27017"); + + auto secret = scram::generateSecrets(scram::SCRAMPresecrets("aaa", salt, 10000)); + cache.setCachedSecrets(host, scram::SCRAMPresecrets("aaa", salt, 10000), secret); + + ASSERT_FALSE(cache.getCachedSecrets(HostAndPort("localhost:27018"), + scram::SCRAMPresecrets("aaa", salt, 10000))); + ASSERT_FALSE(cache.getCachedSecrets(host, scram::SCRAMPresecrets("aab", salt, 10000))); + ASSERT_FALSE(cache.getCachedSecrets(host, scram::SCRAMPresecrets("aaa", badSalt, 10000))); + ASSERT_FALSE(cache.getCachedSecrets(host, scram::SCRAMPresecrets("aaa", salt, 10001))); +} + + +TEST(SCRAMSHA1Cache, testSetAndReset) { + SCRAMSHA1ClientCache cache; + StringData saltStr("saltsaltsaltsalt"); + std::vector<std::uint8_t> salt(saltStr.begin(), saltStr.end()); + HostAndPort host("localhost:27017"); + + auto secret = scram::generateSecrets(scram::SCRAMPresecrets("aaa", salt, 10000)); + cache.setCachedSecrets(host, scram::SCRAMPresecrets("aaa", salt, 10000), secret); + auto newSecret = scram::generateSecrets(scram::SCRAMPresecrets("aab", salt, 10000)); + cache.setCachedSecrets(host, scram::SCRAMPresecrets("aab", salt, 10000), newSecret); + + ASSERT_FALSE(cache.getCachedSecrets(host, scram::SCRAMPresecrets("aaa", salt, 10000))); + auto cachedSecret = cache.getCachedSecrets(host, scram::SCRAMPresecrets("aab", salt, 10000)); + ASSERT_TRUE(cachedSecret); + ASSERT_TRUE(std::equal(newSecret.clientKey->begin(), + newSecret.clientKey->end(), + cachedSecret->clientKey->begin())); + ASSERT_TRUE(std::equal(newSecret.serverKey->begin(), + newSecret.serverKey->end(), + cachedSecret->serverKey->begin())); + ASSERT_TRUE(std::equal(newSecret.storedKey->begin(), + newSecret.storedKey->end(), + cachedSecret->storedKey->begin())); +} + } // namespace mongo diff --git a/src/mongo/executor/network_interface_asio_auth.cpp b/src/mongo/executor/network_interface_asio_auth.cpp index 84600d6bde6..1f2039dbd1e 100644 --- a/src/mongo/executor/network_interface_asio_auth.cpp +++ b/src/mongo/executor/network_interface_asio_auth.cpp @@ -218,8 +218,7 @@ void NetworkInterfaceASIO::_authenticate(AsyncOp* op) { }; auto params = getInternalUserAuthParams(); - auth::authenticateClient( - params, op->request().target.host(), clientName, runCommandHook, authHook); + auth::authenticateClient(params, op->request().target, clientName, runCommandHook, authHook); } } // namespace executor diff --git a/src/mongo/shell/utils.js b/src/mongo/shell/utils.js index 215d40de685..2b8bfde5bbc 100644 --- a/src/mongo/shell/utils.js +++ b/src/mongo/shell/utils.js @@ -278,7 +278,15 @@ jsTest.authenticateNodes = function(nodes) { assert.soonNoExcept(function() { for (var i = 0; i < nodes.length; i++) { // Don't try to authenticate to arbiters - res = nodes[i].getDB("admin").runCommand({replSetGetStatus: 1}); + try { + res = nodes[i].getDB("admin").runCommand({replSetGetStatus: 1}); + } catch (e) { + // ReplicaSet tests which don't use auth are allowed to have nodes crash during + // startup. To allow tests which use to behavior to work with auth, + // attempting authentication against a dead node should be non-fatal. + print("Caught exception getting replSetStatus while authenticating: " + e); + continue; + } if (res.myState == 7) { continue; } |