summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Shuvalov <andrew.shuvalov@mongodb.com>2020-11-15 00:22:54 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-11-15 01:46:45 +0000
commitfe88fed31376d6e2dc95af46342fb3c87c164ab1 (patch)
treea5eef28d9370d8743dbf0186aebd52fb712e1074
parent828266b8c7b99e54aa172ccb89aadcdb7d1e6a37 (diff)
downloadmongo-fe88fed31376d6e2dc95af46342fb3c87c164ab1.tar.gz
SERVER-51599: Allow creating an SSLConnectionContext from in-memory certificates
-rw-r--r--src/mongo/shell/check_log.js35
-rw-r--r--src/mongo/transport/transport_layer_asio.cpp2
-rw-r--r--src/mongo/transport/transport_layer_asio_test.cpp4
-rw-r--r--src/mongo/util/net/SConscript1
-rw-r--r--src/mongo/util/net/ssl_manager.cpp22
-rw-r--r--src/mongo/util/net/ssl_manager.h57
-rw-r--r--src/mongo/util/net/ssl_manager_apple.cpp11
-rw-r--r--src/mongo/util/net/ssl_manager_openssl.cpp314
-rw-r--r--src/mongo/util/net/ssl_manager_test.cpp189
-rw-r--r--src/mongo/util/net/ssl_manager_windows.cpp12
-rw-r--r--src/mongo/util/net/ssl_options.h1
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_ptr<SSLManagerInterfac
Status status = newSSLContext->manager->initSSLContext(
newSSLContext->ingress->native_handle(),
sslParams,
+ TransientSSLParams(),
SSLManagerInterface::ConnectionDirection::kIncoming);
if (!status.isOK()) {
return status;
@@ -1219,6 +1220,7 @@ Status TransportLayerASIO::rotateCertificates(std::shared_ptr<SSLManagerInterfac
Status status = newSSLContext->manager->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<Latch> 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<transport::SessionHandle> old_sessions;
{
stdx::unique_lock<Latch> 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<SSLManagerInterface> 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<char> 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<std::string> 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<std::string> 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<SSLManagerInterface> {
public:
+ /**
+ * Creates an instance of SSLManagerInterface.
+ * Note: as we normally have one instance of the manager, it cannot take TransientSSLParams.
+ */
static std::shared_ptr<SSLManagerInterface> 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<std::pair<::SSLProtocol, ::SSLProtocol>> 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>((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<SSLManagerOpenSSL> {
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<StringData> fetchCachedPasswordNoPrompt() {
+ stdx::lock_guard<Latch> 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<StringData> keyFile,
+ std::optional<StringData> 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<StringData> keyFile,
+ std::optional<StringData> 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<StringData> 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<StringData>* 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<PasswordFetcher*>(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<StringData>* 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<size_t>(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<void*>(&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<StringData> 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<char*>(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<void*>(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<char*>(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<StringData> keyFile,
+ std::optional<StringData> 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<void*>(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<StringData> keyFile,
+ std::optional<StringData> 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<unsigned char*>(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 <fstream>
+
#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<Latch> 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<transport::SessionHandle> old_sessions;
+ {
+ stdx::unique_lock<Latch> 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<Latch> lock(_mutex);
+ return _sessions.size();
+ }
+
+ Future<DbResponse> handleRequest(OperationContext* opCtx,
+ const Message& request) noexcept override {
+ MONGO_UNREACHABLE;
+ }
+
+ void setTransportLayer(transport::TransportLayer* tl) {
+ _transport = tl;
+ }
+
+ void waitForConnect() {
+ stdx::unique_lock<Latch> lock(_mutex);
+ _cv.wait(lock, [&] { return !_sessions.empty(); });
+ }
+
+private:
+ mutable Mutex _mutex = MONGO_MAKE_LATCH("::_mutex");
+ stdx::condition_variable _cv;
+ std::vector<transport::SessionHandle> _sessions;
+ transport::TransportLayer* _transport = nullptr;
+};
+
+std::string LoadFile(const std::string& name) {
+ std::ifstream input(name);
+ std::string str((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
+ 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<SSLManagerInterface> manager =
+ SSLManagerInterface::create(params, true /* isSSLServer */);
+
+ ServiceEntryPointUtil sepu;
+
+ auto options = [] {
+ ServerGlobalParams params;
+ params.noUnixSocket = true;
+ transport::TransportLayerASIO::Options opts(&params);
+ 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([&params] { 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<SSLManagerInterface> manager =
+ SSLManagerInterface::create(params, false /* isSSLServer */);
+
+ ServiceEntryPointUtil sepu;
+
+ auto options = [] {
+ ServerGlobalParams params;
+ params.noUnixSocket = true;
+ transport::TransportLayerASIO::Options opts(&params);
+ 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<SSLManagerInterface> manager =
+ SSLManagerInterface::create(params, false /* isSSLServer */);
+
+ auto egress = std::make_unique<asio::ssl::context>(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<SSLManagerInterface> manager =
+ SSLManagerInterface::create(params, false /* isSSLServer */);
+
+ auto egress = std::make_unique<asio::ssl::context>(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<SSLManagerInterface> manager =
+ SSLManagerInterface::create(params, true /* isSSLServer */);
+
+ auto egress = std::make_unique<asio::ssl::context>(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.