From fe88fed31376d6e2dc95af46342fb3c87c164ab1 Mon Sep 17 00:00:00 2001 From: Andrew Shuvalov Date: Sun, 15 Nov 2020 00:22:54 +0000 Subject: SERVER-51599: Allow creating an SSLConnectionContext from in-memory certificates --- src/mongo/shell/check_log.js | 35 +++ src/mongo/transport/transport_layer_asio.cpp | 2 + src/mongo/transport/transport_layer_asio_test.cpp | 4 +- src/mongo/util/net/SConscript | 1 + src/mongo/util/net/ssl_manager.cpp | 22 +- src/mongo/util/net/ssl_manager.h | 57 ++++ src/mongo/util/net/ssl_manager_apple.cpp | 11 +- src/mongo/util/net/ssl_manager_openssl.cpp | 314 ++++++++++++++++++---- src/mongo/util/net/ssl_manager_test.cpp | 189 +++++++++++++ src/mongo/util/net/ssl_manager_windows.cpp | 12 +- src/mongo/util/net/ssl_options.h | 1 - 11 files changed, 575 insertions(+), 73 deletions(-) diff --git a/src/mongo/shell/check_log.js b/src/mongo/shell/check_log.js index 925c5e57f2d..6e13c3ba2f8 100644 --- a/src/mongo/shell/check_log.js +++ b/src/mongo/shell/check_log.js @@ -76,6 +76,12 @@ checkLog = (function() { allAttrMatch = false; break; } + } else if (obj.attr[attrKey] !== attrValue && + typeof obj.attr[attrKey] == "object") { + if (!_deepEqual(obj.attr[attrKey], attrValue)) { + allAttrMatch = false; + break; + } } else { if (obj.attr[attrKey] !== attrValue) { allAttrMatch = false; @@ -247,6 +253,35 @@ checkLog = (function() { return (Array.isArray(value) ? `[${serialized.join(',')}]` : `{${serialized.join(',')}}`); }; + // Internal helper to compare objects filed by field. + const _deepEqual = function(object1, object2) { + if (object1 == null || object2 == null) { + return false; + } + const keys1 = Object.keys(object1); + const keys2 = Object.keys(object2); + + if (keys1.length !== keys2.length) { + return false; + } + + for (const key of keys1) { + const val1 = object1[key]; + const val2 = object2[key]; + const areObjects = _isObject(val1) && _isObject(val2); + if (areObjects && !_deepEqual(val1, val2) || !areObjects && val1 !== val2) { + return false; + } + } + + return true; + }; + + // Internal helper to check that the argument is a non-null object. + const _isObject = function(object) { + return object != null && typeof object === 'object'; + }; + return { getGlobalLog: getGlobalLog, checkContainsOnce: checkContainsOnce, diff --git a/src/mongo/transport/transport_layer_asio.cpp b/src/mongo/transport/transport_layer_asio.cpp index 0517f8d9af3..4fd4df156bc 100644 --- a/src/mongo/transport/transport_layer_asio.cpp +++ b/src/mongo/transport/transport_layer_asio.cpp @@ -1199,6 +1199,7 @@ Status TransportLayerASIO::rotateCertificates(std::shared_ptrmanager->initSSLContext( newSSLContext->ingress->native_handle(), sslParams, + TransientSSLParams(), SSLManagerInterface::ConnectionDirection::kIncoming); if (!status.isOK()) { return status; @@ -1219,6 +1220,7 @@ Status TransportLayerASIO::rotateCertificates(std::shared_ptrmanager->initSSLContext( newSSLContext->egress->native_handle(), sslParams, + TransientSSLParams(), SSLManagerInterface::ConnectionDirection::kOutgoing); if (!status.isOK()) { return status; diff --git a/src/mongo/transport/transport_layer_asio_test.cpp b/src/mongo/transport/transport_layer_asio_test.cpp index a7938e031ba..9764e952b2e 100644 --- a/src/mongo/transport/transport_layer_asio_test.cpp +++ b/src/mongo/transport/transport_layer_asio_test.cpp @@ -50,12 +50,12 @@ public: void startSession(transport::SessionHandle session) override { stdx::unique_lock lk(_mutex); _sessions.push_back(std::move(session)); - LOGV2(23032, "started session"); + LOGV2(2303201, "started session"); _cv.notify_one(); } void endAllSessions(transport::Session::TagMask tags) override { - LOGV2(23033, "end all sessions"); + LOGV2(2303301, "end all sessions"); std::vector old_sessions; { stdx::unique_lock lock(_mutex); diff --git a/src/mongo/util/net/SConscript b/src/mongo/util/net/SConscript index ea81c6d254c..adf8b0ec033 100644 --- a/src/mongo/util/net/SConscript +++ b/src/mongo/util/net/SConscript @@ -231,6 +231,7 @@ if get_option('ssl') == 'on': ], LIBDEPS=[ '$BUILD_DIR/mongo/db/server_options_servers', + '$BUILD_DIR/mongo/transport/transport_layer', '$BUILD_DIR/mongo/util/cmdline_utils/cmdline_utils', '$BUILD_DIR/mongo/util/fail_point', 'network', diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp index 73884cabae0..87772d303c8 100644 --- a/src/mongo/util/net/ssl_manager.cpp +++ b/src/mongo/util/net/ssl_manager.cpp @@ -335,14 +335,9 @@ std::shared_ptr SSLManagerCoordinator::getSSLManager() { } void logCert(const CertInformationToLog& cert, StringData certType, const int logNum) { - LOGV2(logNum, - "Certificate information", - "type"_attr = certType, - "subject"_attr = cert.subject.toString(), - "issuer"_attr = cert.issuer.toString(), - "thumbprint"_attr = hexblob::encode(cert.thumbprint.data(), cert.thumbprint.size()), - "notValidBefore"_attr = cert.validityNotBefore.toString(), - "notValidAfter"_attr = cert.validityNotAfter.toString()); + auto attrs = cert.getDynamicAttributes(); + attrs.add("type", certType); + LOGV2(logNum, "Certificate information", attrs); } void logCRL(const CRLInformationToLog& crl, const int logNum) { @@ -353,15 +348,18 @@ void logCRL(const CRLInformationToLog& crl, const int logNum) { "notValidAfter"_attr = crl.validityNotAfter.toString()); } -void logSSLInfo(const SSLInformationToLog& info) { +void logSSLInfo(const SSLInformationToLog& info, + const int logNumPEM, + const int logNumCluster, + const int logNumCrl) { if (!(sslGlobalParams.sslPEMKeyFile.empty())) { - logCert(info.server, "Server", 4913010); + logCert(info.server, "Server", logNumPEM); } if (info.cluster.has_value()) { - logCert(info.cluster.get(), "Cluster", 4913011); + logCert(info.cluster.get(), "Cluster", logNumCluster); } if (info.crl.has_value()) { - logCRL(info.crl.get(), 4913012); + logCRL(info.crl.get(), logNumCrl); } } diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h index ab8447c439f..b7cfd8b8099 100644 --- a/src/mongo/util/net/ssl_manager.h +++ b/src/mongo/util/net/ssl_manager.h @@ -40,6 +40,7 @@ #include "mongo/base/string_data.h" #include "mongo/bson/bsonobj.h" #include "mongo/db/service_context.h" +#include "mongo/logv2/attribute_storage.h" #include "mongo/platform/atomic_word.h" #include "mongo/util/decorable.h" #include "mongo/util/net/sock.h" @@ -77,6 +78,7 @@ Status validateDisableNonTLSConnectionLogging(const bool&); #ifdef MONGO_CONFIG_SSL namespace mongo { struct SSLParams; +struct TransientSSLParams; #if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL typedef SSL_CTX* SSLContextType; @@ -162,8 +164,32 @@ struct CertInformationToLog { SSLX509Name subject; SSLX509Name issuer; std::vector thumbprint; + // The human readable 'thumbprint' encoded with 'hexblob::encode'. + std::string hexEncodedThumbprint; Date_t validityNotBefore; Date_t validityNotAfter; + // If the certificate was loaded from file, this is the file name. If empty, + // it means the certificate came from memory payload. + std::optional keyFile; + // If the certificate targets a particular cluster, this is cluster URI. If empty, + // it means the certificate is the default one for the local cluster. + std::optional targetClusterURI; + + logv2::DynamicAttributes getDynamicAttributes() const { + logv2::DynamicAttributes attrs; + attrs.add("subject", subject); + attrs.add("issuer", issuer); + attrs.add("thumbprint", StringData(hexEncodedThumbprint)); + attrs.add("notValidBefore", validityNotBefore); + attrs.add("notValidAfter", validityNotAfter); + if (keyFile) { + attrs.add("keyFile", StringData(*keyFile)); + } + if (targetClusterURI) { + attrs.add("targetClusterURI", StringData(*targetClusterURI)); + } + return attrs; + } }; struct CRLInformationToLog { @@ -180,6 +206,10 @@ struct SSLInformationToLog { class SSLManagerInterface : public Decorable { public: + /** + * Creates an instance of SSLManagerInterface. + * Note: as we normally have one instance of the manager, it cannot take TransientSSLParams. + */ static std::shared_ptr create(const SSLParams& params, bool isServer); virtual ~SSLManagerInterface(); @@ -232,6 +262,17 @@ public: ERR_error_string_n(code, msg, msglen); return msg; } + + /** + * Utility class to capture a temporary string with SSL error message in DynamicAttributes. + */ + struct CaptureSSLErrorInAttrs { + CaptureSSLErrorInAttrs(logv2::DynamicAttributes& attrs) + : _captured(getSSLErrorMessage(ERR_get_error())) { + attrs.add("error", _captured); + } + std::string _captured; + }; #endif /** @@ -252,6 +293,7 @@ public: */ virtual Status initSSLContext(SSLContextType context, const SSLParams& params, + const TransientSSLParams& transientParams, ConnectionDirection direction) = 0; /** @@ -390,5 +432,20 @@ void recordTLSVersion(TLSVersion version, const HostAndPort& hostForLogging); void tlsEmitWarningExpiringClientCertificate(const SSLX509Name& peer); void tlsEmitWarningExpiringClientCertificate(const SSLX509Name& peer, Days days); +/** + * Logs the SSL information by dispatching to either logCert() or logCRL(). + */ +void logSSLInfo(const SSLInformationToLog& info, + const int logNumPEM = 4913010, + const int logNumCluster = 4913011, + const int logNumCrl = 4913012); + +/** + * Logs the certificate. + * @param certType human-readable description of the certificate type. + */ +void logCert(const CertInformationToLog& cert, StringData certType, const int logNum); +void logCRL(const CRLInformationToLog& crl, const int logNum); + } // namespace mongo #endif // #ifdef MONGO_CONFIG_SSL diff --git a/src/mongo/util/net/ssl_manager_apple.cpp b/src/mongo/util/net/ssl_manager_apple.cpp index 16af08cb0b9..6a20c9bbeda 100644 --- a/src/mongo/util/net/ssl_manager_apple.cpp +++ b/src/mongo/util/net/ssl_manager_apple.cpp @@ -1251,7 +1251,8 @@ public: Status initSSLContext(asio::ssl::apple::Context* context, const SSLParams& params, - ConnectionDirection direction) final; + const TransientSSLParams& transientParams, + ConnectionDirection direction) override final; SSLConnectionInterface* connect(Socket* socket) final; SSLConnectionInterface* accept(Socket* socket, const char* initialBytes, int len) final; @@ -1310,14 +1311,16 @@ SSLManagerApple::SSLManagerApple(const SSLParams& params, bool isServer) _allowInvalidHostnames(params.sslAllowInvalidHostnames), _suppressNoCertificateWarning(params.suppressNoTLSPeerCertificateWarning) { - uassertStatusOK(initSSLContext(&_clientCtx, params, ConnectionDirection::kOutgoing)); + uassertStatusOK( + initSSLContext(&_clientCtx, params, TransientSSLParams(), ConnectionDirection::kOutgoing)); if (_clientCtx.certs) { _sslConfiguration.clientSubjectName = uassertStatusOK(certificateGetSubject(_clientCtx.certs.get())); } if (isServer) { - uassertStatusOK(initSSLContext(&_serverCtx, params, ConnectionDirection::kIncoming)); + uassertStatusOK(initSSLContext( + &_serverCtx, params, TransientSSLParams(), ConnectionDirection::kIncoming)); if (_serverCtx.certs) { uassertStatusOK( _sslConfiguration.setServerSubjectName(uassertStatusOK(certificateGetSubject( @@ -1391,6 +1394,7 @@ StatusWith> parseProtocolRange(const SSL Status SSLManagerApple::initSSLContext(asio::ssl::apple::Context* context, const SSLParams& params, + const TransientSSLParams& transientParams, ConnectionDirection direction) { // Protocol Version. const auto swProto = parseProtocolRange(params); @@ -1792,6 +1796,7 @@ void getCertInfo(CertInformationToLog* info, const ::CFArrayRef cert) { const auto certSha1 = SHA1Block::computeHash({certData}); info->thumbprint = std::vector((char*)certSha1.data(), (char*)certSha1.data() + certSha1.kHashLength); + info->hexEncodedThumbprint = hexblob::encode(info->thumbprint.data(), info->thumbprint.size()); } SSLInformationToLog SSLManagerApple::getSSLInformationToLog() const { diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp index 9fb60e83f6e..d42474657a3 100644 --- a/src/mongo/util/net/ssl_manager_openssl.cpp +++ b/src/mongo/util/net/ssl_manager_openssl.cpp @@ -1128,7 +1128,7 @@ class SSLManagerOpenSSL : public SSLManagerInterface, public std::enable_shared_from_this { public: explicit SSLManagerOpenSSL(const SSLParams& params, bool isServer); - ~SSLManagerOpenSSL() { + ~SSLManagerOpenSSL() final { stopJobs(); } @@ -1138,6 +1138,7 @@ public: */ Status initSSLContext(SSL_CTX* context, const SSLParams& params, + const TransientSSLParams& transientParams, ConnectionDirection direction) final; SSLConnectionInterface* connect(Socket* socket) final; @@ -1241,6 +1242,19 @@ private: return StringData(_password->c_str()); } + /** + * This method can only return a cached password and never prompts. + * @returns cached password if available, error if password is not cached. + */ + StatusWith fetchCachedPasswordNoPrompt() { + stdx::lock_guard lock(_mutex); + if (_password->size()) { + return StringData(_password->c_str()); + } + return Status(ErrorCodes::UnknownError, + "Failed to return a cached password, cannot prompt."); + } + private: Mutex _mutex = MONGO_MAKE_LATCH("PasswordFetcher::_mutex"); SecureString _password; // Protected by _mutex @@ -1299,7 +1313,10 @@ private: * @param info as a pointer to the CertInformationToLog struct to populate * with the information. */ - void _getX509CertInfo(UniqueX509& x509, CertInformationToLog* info) const; + static void _getX509CertInfo(UniqueX509& x509, + CertInformationToLog* info, + std::optional keyFile, + std::optional targetClusterURI); /* * Retrieve and store CRL information from the provided CRL filename. @@ -1316,6 +1333,42 @@ private: /** @return true if was successful, otherwise false */ bool _setupPEM(SSL_CTX* context, const std::string& keyFile, PasswordFetcher* password); + /** + * @param payload in-memory payload of a PEM file + * @return true if was successful, otherwise false + */ + bool _setupPEMFromMemoryPayload(SSL_CTX* context, + const std::string& payload, + PasswordFetcher* password, + StringData targetClusterURI); + + /** + * Setup PEM from BIO, which could be file or memory input abstraction. + * @param inBio input BIO, where smart pointer is created with a custom deleter to call + * 'BIO_free()'. + * @param keyFile if the certificate was loaded from file, this is the file name. If empty, + * it means the certificate came from memory payload. + * @param targetClusterURI If the certificate targets a particular cluster, this is cluster URI. + * If empty, it means the certificate is the default one for the local cluster. + * @return true if was successful, otherwise false + */ + bool _setupPEMFromBIO(SSL_CTX* context, + UniqueBIO inBio, + PasswordFetcher* password, + std::optional keyFile, + std::optional targetClusterURI); + + /** + * Loads a certificate chain from memory into context. + * This method is intended to be a repalcement of API call SSL_CTX_use_certificate_chain_file() + * but using memory instead of file. + * @return true if was successful, otherwise false + */ + static bool _readCertificateChainFromMemory(SSL_CTX* context, + const std::string& payload, + PasswordFetcher* password, + std::optional targetClusterURI); + /* * Set up an SSL context for certificate validation by loading a CA */ @@ -1342,10 +1395,25 @@ private: */ void _flushNetworkBIO(SSLConnectionOpenSSL* conn); + /* + * Utility method to process the result returned by password Fetcher. + */ + static int _processPasswordFetcherOutput(StatusWith* fetcherResult, + char* buf, + int num, + int rwflag); + /** * Callbacks for SSL functions. */ + static int password_cb(char* buf, int num, int rwflag, void* userdata); + + /** + * Special flawor of password callback, which always fails. + * @return -1. + */ + static int always_error_password_cb(char* buf, int num, int rwflag, void* userdata); static int servername_cb(SSL* s, int* al, void* arg); static int verify_cb(int ok, X509_STORE_CTX* ctx); }; @@ -1514,20 +1582,28 @@ SSLManagerOpenSSL::SSLManagerOpenSSL(const SSLParams& params, bool isServer) } int SSLManagerOpenSSL::password_cb(char* buf, int num, int rwflag, void* userdata) { - // Unless OpenSSL misbehaves, num should always be positive - fassert(17314, num > 0); invariant(userdata); - auto pwFetcher = static_cast(userdata); auto swPassword = pwFetcher->fetchPassword(); - if (!swPassword.isOK()) { - LOGV2_ERROR(23239, - "Unable to fetch password: {error}", - "Unable to fetch password", - "error"_attr = swPassword.getStatus()); + return _processPasswordFetcherOutput(&swPassword, buf, num, rwflag); +} + +int SSLManagerOpenSSL::always_error_password_cb(char* buf, int num, int rwflag, void* userdata) { + return -1; +} + +int SSLManagerOpenSSL::_processPasswordFetcherOutput(StatusWith* swPassword, + char* buf, + int num, + int rwflag) { + // Unless OpenSSL misbehaves, num should always be positive + fassert(17314, num > 0); + + if (!swPassword->isOK()) { + LOGV2_ERROR(23239, "Unable to fetch password", "error"_attr = swPassword->getStatus()); return -1; } - StringData password = std::move(swPassword.getValue()); + StringData password = std::move(swPassword->getValue()); const size_t copyCount = std::min(password.size(), static_cast(num)); std::copy_n(password.begin(), copyCount, buf); @@ -2036,6 +2112,7 @@ Milliseconds SSLManagerOpenSSL::updateOcspStaplingContextWithResponse( Status SSLManagerOpenSSL::initSSLContext(SSL_CTX* context, const SSLParams& params, + const TransientSSLParams& transientParams, ConnectionDirection direction) { // SSL_OP_ALL - Activate all bug workaround options, to support buggy client SSL's. // SSL_OP_NO_SSLv2 - Disable SSL v2 support @@ -2097,7 +2174,21 @@ Status SSLManagerOpenSSL::initSSLContext(SSL_CTX* context, << getSSLErrorMessage(ERR_get_error())); } - if (direction == ConnectionDirection::kOutgoing && params.tlsWithholdClientCertificate) { + + if (direction == ConnectionDirection::kOutgoing && + !transientParams.sslClusterPEMPayload.empty()) { + + // Transient params for outgoing connection have priority over global params. + if (!_setupPEMFromMemoryPayload( + context, + transientParams.sslClusterPEMPayload, + &_clusterPEMPassword, + transientParams.targetedClusterConnectionString.toString())) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "Can not set up transient ssl cluster certificate for " + << transientParams.targetedClusterConnectionString); + } + } else if (direction == ConnectionDirection::kOutgoing && params.tlsWithholdClientCertificate) { // Do not send a client certificate if they have been suppressed. } else if (direction == ConnectionDirection::kOutgoing && !params.sslClusterFile.empty()) { @@ -2195,7 +2286,7 @@ bool SSLManagerOpenSSL::_initSynchronousSSLContext(UniqueSSLContext* contextPtr, ConnectionDirection direction) { *contextPtr = UniqueSSLContext(SSL_CTX_new(SSLv23_method())); - uassertStatusOK(initSSLContext(contextPtr->get(), params, direction)); + uassertStatusOK(initSSLContext(contextPtr->get(), params, TransientSSLParams(), direction)); // If renegotiation is needed, don't return from recv() or send() until it's successful. // Note: this is for blocking sockets only. @@ -2211,7 +2302,6 @@ bool SSLManagerOpenSSL::_parseAndValidateCertificate(const std::string& keyFile, BIO* inBIO = BIO_new(BIO_s_file()); if (inBIO == nullptr) { LOGV2_ERROR(23243, - "failed to allocate BIO object: {error}", "Failed to allocate BIO object", "error"_attr = getSSLErrorMessage(ERR_get_error())); return false; @@ -2220,7 +2310,6 @@ bool SSLManagerOpenSSL::_parseAndValidateCertificate(const std::string& keyFile, ON_BLOCK_EXIT([&] { BIO_free(inBIO); }); if (BIO_read_filename(inBIO, keyFile.c_str()) <= 0) { LOGV2_ERROR(23244, - "cannot read key file when setting subject name: {keyFile} {error}", "Cannot read key file when setting subject name", "keyFile"_attr = keyFile, "error"_attr = getSSLErrorMessage(ERR_get_error())); @@ -2231,7 +2320,6 @@ bool SSLManagerOpenSSL::_parseAndValidateCertificate(const std::string& keyFile, inBIO, nullptr, &SSLManagerOpenSSL::password_cb, static_cast(&keyPassword)); if (x509 == nullptr) { LOGV2_ERROR(23245, - "cannot retrieve certificate from keyfile: {keyFile} {error}", "Cannot retrieve certificate from keyfile", "keyFile"_attr = keyFile, "error"_attr = getSSLErrorMessage(ERR_get_error())); @@ -2263,66 +2351,177 @@ bool SSLManagerOpenSSL::_parseAndValidateCertificate(const std::string& keyFile, return true; } +// static +bool SSLManagerOpenSSL::_readCertificateChainFromMemory( + SSL_CTX* context, + const std::string& payload, + PasswordFetcher* password, + std::optional targetClusterURI) { + + logv2::DynamicAttributes errorAttrs; + if (targetClusterURI) { + errorAttrs.add("targetClusterURI", *targetClusterURI); + } + + ERR_clear_error(); // Clear error stack for SSL_CTX_use_certificate(). + + // Note: old versions of SSL take (void*) here but it's still R/O. +#if OPENSSL_VERSION_NUMBER <= 0x1000114fL + UniqueBIO inBio(BIO_new_mem_buf(const_cast(payload.c_str()), payload.length())); +#else + UniqueBIO inBio(BIO_new_mem_buf(payload.c_str(), payload.length())); +#endif + + if (!inBio) { + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(5159905, "Failed to allocate BIO from in memory payload", errorAttrs); + return false; + } + + auto password_cb = + &SSLManagerOpenSSL::always_error_password_cb; // We don't expect a password to be required. + void* userdata = static_cast(password); + UniqueX509 x509cert(PEM_read_bio_X509_AUX(inBio.get(), NULL, password_cb, userdata)); + + if (!x509cert) { + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(5159906, "Failed to read the X509 certificate from memory", errorAttrs); + return false; + } + + CertInformationToLog debugInfo; + _getX509CertInfo(x509cert, &debugInfo, std::nullopt, targetClusterURI); + logCert(debugInfo, "", 5159903); + + // SSL_CTX_use_certificate increments the refcount on cert. + if (1 != SSL_CTX_use_certificate(context, x509cert.get())) { + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(5159907, "Failed to use the X509 certificate loaded from memory", errorAttrs); + return false; + } + + // If we could set up our certificate, now proceed to the CA certificates. + UniqueX509 ca; +#if OPENSSL_VERSION_NUMBER >= 0x100010fFL + SSL_CTX_clear_chain_certs(context); +#else + SSL_CTX_clear_extra_chain_certs(context); +#endif + while ((ca = UniqueX509(PEM_read_bio_X509(inBio.get(), NULL, password_cb, userdata)))) { +#if OPENSSL_VERSION_NUMBER >= 0x100010fFL + if (1 != SSL_CTX_add1_chain_cert(context, ca.get())) { +#else + if (1 != SSL_CTX_add_extra_chain_cert(context, ca.release())) { +#endif + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR( + 5159908, "Failed to use the CA X509 certificate loaded from memory", errorAttrs); + return false; + } + _getX509CertInfo(ca, &debugInfo, std::nullopt, targetClusterURI); + logCert(debugInfo, "", 5159902); + } + // When the while loop ends, it's usually just EOF. + auto err = ERR_peek_last_error(); + if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE) { + ERR_clear_error(); + } else { + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR( + 5159909, "Error remained after scanning all X509 certificates from memory", errorAttrs); + return false; // Some real error. + } + + return true; +} + bool SSLManagerOpenSSL::_setupPEM(SSL_CTX* context, const std::string& keyFile, PasswordFetcher* password) { + logv2::DynamicAttributes errorAttrs; + errorAttrs.add("keyFile", keyFile); + if (SSL_CTX_use_certificate_chain_file(context, keyFile.c_str()) != 1) { - LOGV2_ERROR(23248, - "cannot read certificate file: {keyFile} {error}", - "Cannot read certificate file", - "keyFile"_attr = keyFile, - "error"_attr = getSSLErrorMessage(ERR_get_error())); + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(23248, "Cannot read certificate file", errorAttrs); return false; } - BIO* inBio = BIO_new(BIO_s_file()); + UniqueBIO inBio(BIO_new(BIO_s_file())); + if (!inBio) { - LOGV2_ERROR(23249, - "failed to allocate BIO object: {error}", - "Failed to allocate BIO object", - "error"_attr = getSSLErrorMessage(ERR_get_error())); + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(23249, "Failed to allocate BIO object", errorAttrs); return false; } - const auto bioGuard = makeGuard([&inBio]() { BIO_free(inBio); }); - if (BIO_read_filename(inBio, keyFile.c_str()) <= 0) { - LOGV2_ERROR(23250, - "cannot read PEM key file: {keyFile} {error}", - "Cannot read PEM key file", - "keyFile"_attr = keyFile, - "error"_attr = getSSLErrorMessage(ERR_get_error())); + if (BIO_read_filename(inBio.get(), keyFile.c_str()) <= 0) { + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(23250, "Cannot read PEM key file", errorAttrs); return false; } + return _setupPEMFromBIO(context, std::move(inBio), password, keyFile, std::nullopt); +} + +bool SSLManagerOpenSSL::_setupPEMFromMemoryPayload(SSL_CTX* context, + const std::string& payload, + PasswordFetcher* password, + StringData targetClusterURI) { + logv2::DynamicAttributes errorAttrs; + errorAttrs.add("targetClusterURI", targetClusterURI); + + if (!_readCertificateChainFromMemory(context, payload, password, targetClusterURI)) { + return false; + } +#if OPENSSL_VERSION_NUMBER <= 0x1000114fL + UniqueBIO inBio(BIO_new_mem_buf(const_cast(payload.c_str()), payload.length())); +#else + UniqueBIO inBio(BIO_new_mem_buf(payload.c_str(), payload.length())); +#endif + + if (!inBio) { + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(5159901, "Failed to allocate BIO object from in-memory payload", errorAttrs); + return false; + } + + return _setupPEMFromBIO(context, std::move(inBio), password, std::nullopt, targetClusterURI); +} + +bool SSLManagerOpenSSL::_setupPEMFromBIO(SSL_CTX* context, + UniqueBIO inBio, + PasswordFetcher* password, + std::optional keyFile, + std::optional targetClusterURI) { + logv2::DynamicAttributes errorAttrs; + if (keyFile) { + errorAttrs.add("keyFile", *keyFile); + } + if (targetClusterURI) { + errorAttrs.add("targetClusterURI", *targetClusterURI); + } // Obtain the private key, using our callback to acquire a decryption password if necessary. - decltype(&SSLManagerOpenSSL::password_cb) password_cb = &SSLManagerOpenSSL::password_cb; + auto password_cb = &SSLManagerOpenSSL::password_cb; void* userdata = static_cast(password); - EVP_PKEY* privateKey = PEM_read_bio_PrivateKey(inBio, nullptr, password_cb, userdata); + EVP_PKEY* privateKey = PEM_read_bio_PrivateKey(inBio.get(), nullptr, password_cb, userdata); if (!privateKey) { - LOGV2_ERROR(23251, - "cannot read PEM key file: {keyFile} {error}", - "Cannot read PEM key file", - "keyFile"_attr = keyFile, - "error"_attr = getSSLErrorMessage(ERR_get_error())); + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(23251, "Cannot read PEM key", errorAttrs); return false; } const auto privateKeyGuard = makeGuard([&privateKey]() { EVP_PKEY_free(privateKey); }); if (SSL_CTX_use_PrivateKey(context, privateKey) != 1) { - LOGV2_ERROR(23252, - "cannot use PEM key file: {keyFile} {error}", - "Cannot use PEM key file", - "keyFile"_attr = keyFile, - "error"_attr = getSSLErrorMessage(ERR_get_error())); + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(23252, "Cannot use PEM key", errorAttrs); return false; } // Verify that the certificate and the key go together. if (SSL_CTX_check_private_key(context) != 1) { - LOGV2_ERROR(23253, - "SSL certificate validation failed: {error}", - "SSL certificate validation failed", - "error"_attr = getSSLErrorMessage(ERR_get_error())); + CaptureSSLErrorInAttrs capture(errorAttrs); + LOGV2_ERROR(23253, "SSL certificate validation failed", errorAttrs); return false; } @@ -2983,13 +3182,18 @@ UniqueX509 SSLManagerOpenSSL::_getX509Object(StringData keyFile, constexpr size_t kSHA1HashBytes = 20; -void SSLManagerOpenSSL::_getX509CertInfo(UniqueX509& x509, CertInformationToLog* info) const { +// static +void SSLManagerOpenSSL::_getX509CertInfo(UniqueX509& x509, + CertInformationToLog* info, + std::optional keyFile, + std::optional targetClusterURI) { info->subject = getCertificateSubjectX509Name(x509.get()); info->issuer = convertX509ToSSLX509Name(X509_get_issuer_name(x509.get())); info->thumbprint.resize(kSHA1HashBytes); X509_digest( x509.get(), EVP_sha1(), reinterpret_cast(info->thumbprint.data()), nullptr); + info->hexEncodedThumbprint = hexblob::encode(info->thumbprint.data(), info->thumbprint.size()); auto notBeforeMillis = convertASN1ToMillis(X509_get_notBefore(x509.get())); @@ -3002,6 +3206,11 @@ void SSLManagerOpenSSL::_getX509CertInfo(UniqueX509& x509, CertInformationToLog* info->validityNotAfter = notAfterMillis; uassert(4913004, "date conversion failed", notAfterMillis != Date_t()); + + if (keyFile) + info->keyFile = keyFile->toString(); + if (targetClusterURI) + info->targetClusterURI = targetClusterURI->toString(); } @@ -3052,14 +3261,15 @@ SSLInformationToLog SSLManagerOpenSSL::getSSLInformationToLog() const { if (!(sslGlobalParams.sslPEMKeyFile.empty())) { UniqueX509 serverX509Cert = _getX509Object(sslGlobalParams.sslPEMKeyFile, &_serverPEMPassword); - _getX509CertInfo(serverX509Cert, &info.server); + _getX509CertInfo(serverX509Cert, &info.server, sslGlobalParams.sslPEMKeyFile, std::nullopt); } if (!(sslGlobalParams.sslClusterFile.empty())) { CertInformationToLog clusterInfo; UniqueX509 clusterX509Cert = _getX509Object(sslGlobalParams.sslClusterFile, &_clusterPEMPassword); - _getX509CertInfo(clusterX509Cert, &clusterInfo); + _getX509CertInfo( + clusterX509Cert, &clusterInfo, sslGlobalParams.sslClusterFile, std::nullopt); info.cluster = clusterInfo; } else { info.cluster = boost::none; diff --git a/src/mongo/util/net/ssl_manager_test.cpp b/src/mongo/util/net/ssl_manager_test.cpp index 781707b14a9..f2a5551ea28 100644 --- a/src/mongo/util/net/ssl_manager_test.cpp +++ b/src/mongo/util/net/ssl_manager_test.cpp @@ -29,9 +29,15 @@ #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest +#include + #include "mongo/platform/basic.h" +#include "mongo/transport/service_entry_point.h" +#include "mongo/transport/transport_layer_asio.h" +#include "mongo/util/net/ssl/context_base.hpp" #include "mongo/util/net/ssl_manager.h" +#include "mongo/util/net/ssl_options.h" #include "mongo/config.h" #include "mongo/logv2/log.h" @@ -39,11 +45,76 @@ #if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL #include "mongo/util/net/dh_openssl.h" +#include "mongo/util/net/ssl/context_openssl.hpp" #endif namespace mongo { namespace { + + +// Test implementation needed by ASIO transport. +class ServiceEntryPointUtil : public ServiceEntryPoint { +public: + void startSession(transport::SessionHandle session) override { + stdx::unique_lock lk(_mutex); + _sessions.push_back(std::move(session)); + LOGV2(2303202, "started session"); + _cv.notify_one(); + } + + void endAllSessions(transport::Session::TagMask tags) override { + LOGV2(2303302, "end all sessions"); + std::vector old_sessions; + { + stdx::unique_lock lock(_mutex); + old_sessions.swap(_sessions); + } + old_sessions.clear(); + } + + Status start() override { + return Status::OK(); + } + + bool shutdown(Milliseconds timeout) override { + return true; + } + + void appendStats(BSONObjBuilder*) const override {} + + size_t numOpenSessions() const override { + stdx::unique_lock lock(_mutex); + return _sessions.size(); + } + + Future handleRequest(OperationContext* opCtx, + const Message& request) noexcept override { + MONGO_UNREACHABLE; + } + + void setTransportLayer(transport::TransportLayer* tl) { + _transport = tl; + } + + void waitForConnect() { + stdx::unique_lock lock(_mutex); + _cv.wait(lock, [&] { return !_sessions.empty(); }); + } + +private: + mutable Mutex _mutex = MONGO_MAKE_LATCH("::_mutex"); + stdx::condition_variable _cv; + std::vector _sessions; + transport::TransportLayer* _transport = nullptr; +}; + +std::string LoadFile(const std::string& name) { + std::ifstream input(name); + std::string str((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + return str; +} + TEST(SSLManager, matchHostname) { enum Expected : bool { match = true, mismatch = false }; const struct { @@ -415,5 +486,123 @@ TEST(SSLManager, BadDNParsing) { } } +TEST(SSLManager, RotateCertificatesFromFile) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + // Server is required to have the sslPEMKeyFile. + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + params.sslClusterFile = "jstests/libs/client.pem"; + + std::shared_ptr manager = + SSLManagerInterface::create(params, true /* isSSLServer */); + + ServiceEntryPointUtil sepu; + + auto options = [] { + ServerGlobalParams params; + params.noUnixSocket = true; + transport::TransportLayerASIO::Options opts(¶ms); + return opts; + }(); + transport::TransportLayerASIO tla(options, &sepu); + uassertStatusOK(tla.rotateCertificates(manager, false /* asyncOCSPStaple */)); +} + +TEST(SSLManager, InitContextFromFileShouldFail) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + // Server is required to have the sslPEMKeyFile. + // We force the initialization to fail by omitting this param. + params.sslCAFile = "jstests/libs/ca.pem"; + params.sslClusterFile = "jstests/libs/client.pem"; + +#if MONGO_CONFIG_SSL_PROVIDER != MONGO_CONFIG_SSL_PROVIDER_APPLE + // TODO SERVER-52858: there is no exception on Mac. + ASSERT_THROWS_CODE([¶ms] { SSLManagerInterface::create(params, true /* isSSLServer */); }(), + DBException, + 16942); +#endif +} + +TEST(SSLManager, RotateClusterCertificatesFromFile) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + // Client doesn't need params.sslPEMKeyFile. + params.sslCAFile = "jstests/libs/ca.pem"; + params.sslClusterFile = "jstests/libs/client.pem"; + + std::shared_ptr manager = + SSLManagerInterface::create(params, false /* isSSLServer */); + + ServiceEntryPointUtil sepu; + + auto options = [] { + ServerGlobalParams params; + params.noUnixSocket = true; + transport::TransportLayerASIO::Options opts(¶ms); + return opts; + }(); + transport::TransportLayerASIO tla(options, &sepu); + uassertStatusOK(tla.rotateCertificates(manager, false /* asyncOCSPStaple */)); +} + +#if MONGO_CONFIG_SSL_PROVIDER != MONGO_CONFIG_SSL_PROVIDER_APPLE + +TEST(SSLManager, InitContextFromFile) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + // Client doesn't need params.sslPEMKeyFile. + params.sslClusterFile = "jstests/libs/client.pem"; + + std::shared_ptr manager = + SSLManagerInterface::create(params, false /* isSSLServer */); + + auto egress = std::make_unique(asio::ssl::context::sslv23); + uassertStatusOK(manager->initSSLContext(egress->native_handle(), + params, + TransientSSLParams(), + SSLManagerInterface::ConnectionDirection::kOutgoing)); +} + +TEST(SSLManager, InitContextFromMemory) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslCAFile = "jstests/libs/ca.pem"; + + TransientSSLParams transientParams; + transientParams.sslClusterPEMPayload = LoadFile("jstests/libs/client.pem"); + + std::shared_ptr manager = + SSLManagerInterface::create(params, false /* isSSLServer */); + + auto egress = std::make_unique(asio::ssl::context::sslv23); + uassertStatusOK(manager->initSSLContext(egress->native_handle(), + params, + transientParams, + SSLManagerInterface::ConnectionDirection::kOutgoing)); +} + +TEST(SSLManager, InitServerSideContextFromMemory) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + + TransientSSLParams transientParams; + transientParams.sslClusterPEMPayload = LoadFile("jstests/libs/client.pem"); + + std::shared_ptr manager = + SSLManagerInterface::create(params, true /* isSSLServer */); + + auto egress = std::make_unique(asio::ssl::context::sslv23); + uassertStatusOK(manager->initSSLContext(egress->native_handle(), + params, + transientParams, + SSLManagerInterface::ConnectionDirection::kOutgoing)); +} + +#endif + } // namespace } // namespace mongo diff --git a/src/mongo/util/net/ssl_manager_windows.cpp b/src/mongo/util/net/ssl_manager_windows.cpp index ad938405d62..3c13e7a630f 100644 --- a/src/mongo/util/net/ssl_manager_windows.cpp +++ b/src/mongo/util/net/ssl_manager_windows.cpp @@ -269,7 +269,8 @@ public: */ Status initSSLContext(SCHANNEL_CRED* cred, const SSLParams& params, - ConnectionDirection direction) final; + const TransientSSLParams& transientParams, + ConnectionDirection direction) override final; SSLConnectionInterface* connect(Socket* socket) final; @@ -415,7 +416,8 @@ SSLManagerWindows::SSLManagerWindows(const SSLParams& params, bool isServer) uassertStatusOK(_loadCertificates(params)); - uassertStatusOK(initSSLContext(&_clientCred, params, ConnectionDirection::kOutgoing)); + uassertStatusOK( + initSSLContext(&_clientCred, params, TransientSSLParams(), ConnectionDirection::kOutgoing)); // Certificates may not have been loaded. This typically occurs in unit tests. if (_clientCertificates[0] != nullptr) { @@ -425,7 +427,8 @@ SSLManagerWindows::SSLManagerWindows(const SSLParams& params, bool isServer) // SSL server specific initialization if (isServer) { - uassertStatusOK(initSSLContext(&_serverCred, params, ConnectionDirection::kIncoming)); + uassertStatusOK(initSSLContext( + &_serverCred, params, TransientSSLParams(), ConnectionDirection::kIncoming)); if (_serverCertificates[0] != nullptr) { SSLX509Name subjectName; @@ -1351,6 +1354,7 @@ Status SSLManagerWindows::_loadCertificates(const SSLParams& params) { Status SSLManagerWindows::initSSLContext(SCHANNEL_CRED* cred, const SSLParams& params, + const TransientSSLParams& transientParams, ConnectionDirection direction) { memset(cred, 0, sizeof(*cred)); @@ -1443,6 +1447,7 @@ SSLConnectionInterface* SSLManagerWindows::accept(Socket* socket, void SSLManagerWindows::_handshake(SSLConnectionWindows* conn, bool client) { initSSLContext(conn->_cred, getSSLGlobalParams(), + TransientSSLParams(), client ? SSLManagerInterface::ConnectionDirection::kOutgoing : SSLManagerInterface::ConnectionDirection::kIncoming); @@ -2071,6 +2076,7 @@ Status getCertInfo(CertInformationToLog* info, PCCERT_CONTEXT cert) { str::stream() << "getCertInfo failed to get certificate thumbprint: " << errnoWithDescription(gle)); } + info->hexEncodedThumbprint = hexblob::encode(info->thumbprint.data(), info->thumbprint.size()); info->validityNotBefore = Date_t::fromMillisSinceEpoch(FiletimeToEpocMillis(cert->pCertInfo->NotBefore)); diff --git a/src/mongo/util/net/ssl_options.h b/src/mongo/util/net/ssl_options.h index 755cfb030c3..e58bedcd076 100644 --- a/src/mongo/util/net/ssl_options.h +++ b/src/mongo/util/net/ssl_options.h @@ -134,7 +134,6 @@ struct SSLParams { extern SSLParams sslGlobalParams; - // Additional SSL Params that could be used to augment a particular connection // or have limited lifetime. In all cases, the fields stored here are not appropriate // to be part of sslGlobalParams. -- cgit v1.2.1