From 5f89b25908cfbbffb334aa32819a161021ae0e48 Mon Sep 17 00:00:00 2001 From: Sam Frank Date: Thu, 7 May 2026 15:44:11 -0400 Subject: [PATCH] SERVER-125887 Add support for encrypted PEM files for gRPC egress (#53238) Co-authored-by: Erwin Pe GitOrigin-RevId: 071e3964bf43e7fa728a5df6390e0b960e335bdf --- jstests/libs/client_password_protected.pem | 57 ++++++++++++ .../client_password_protected.pem.digest.sha1 | 1 + ...lient_password_protected.pem.digest.sha256 | 1 + jstests/ssl/libs/ssl_helpers.js | 1 + jstests/ssl/x509/certs.yml | 12 +++ ...es_password_protected_certs_with_mongot.js | 92 +++++++++++++++++++ src/mongo/transport/grpc/client.cpp | 30 ++++-- src/mongo/transport/grpc/client.h | 7 +- src/mongo/transport/grpc/grpc_client_test.cpp | 59 ++++++++++++ .../grpc/grpc_transport_layer_impl.cpp | 7 +- .../grpc/grpc_transport_layer_test.cpp | 32 ++++++- src/mongo/transport/grpc/mock_client.h | 3 +- src/mongo/transport/grpc/test_fixtures.h | 3 + src/mongo/transport/test_fixtures.h | 11 ++- src/mongo/util/net/ssl_manager.h | 11 +++ src/mongo/util/net/ssl_manager_openssl.cpp | 47 ++++++++++ src/mongo/util/net/ssl_manager_test.cpp | 78 ++++++++++++++++ x509/main_certs_def.bzl | 28 ++++++ 18 files changed, 465 insertions(+), 15 deletions(-) create mode 100644 jstests/libs/client_password_protected.pem create mode 100644 jstests/libs/client_password_protected.pem.digest.sha1 create mode 100644 jstests/libs/client_password_protected.pem.digest.sha256 create mode 100644 jstests/with_mongot/search_mocked/ssl/mongod_uses_password_protected_certs_with_mongot.js diff --git a/jstests/libs/client_password_protected.pem b/jstests/libs/client_password_protected.pem new file mode 100644 index 00000000000..beaf5ab5475 --- /dev/null +++ b/jstests/libs/client_password_protected.pem @@ -0,0 +1,57 @@ +# Autogenerated file, do not edit. +# Generate using jstests/ssl/x509/mkcert.py --config jstests/ssl/x509/certs.yml client_password_protected.pem +# +# Client certificate using an encrypted private key. + +-----BEGIN CERTIFICATE----- +MIIDsDCCApigAwIBAgIEfFChFjANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV +UzERMA8GA1UECAwITmV3IFlvcmsxFjAUBgNVBAcMDU5ldyBZb3JrIENpdHkxEDAO +BgNVBAoMB01vbmdvREIxDzANBgNVBAsMBktlcm5lbDEXMBUGA1UEAwwOS2VybmVs +IFRlc3QgQ0EwHhcNMjYwNDI5MjM1NTE0WhcNMjgwNzMxMjM1NTE0WjBwMQswCQYD +VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxFjAUBgNVBAcMDU5ldyBZb3JrIENp +dHkxEDAOBgNVBAoMB01vbmdvREIxEzARBgNVBAsMCktlcm5lbFVzZXIxDzANBgNV +BAMMBmNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOKX9jEC +f1LiqN7BPNsEED21HXrehoJzOFmfEaCUJLtN/1J9r8ITDdXjkn9zS+X9TpDGAxQV +FOkZs7FHdLetFWDFm1OdZKIlySY8tKBYD6N6nso1R1vNmQVNw5wGj6yl0uir2v3k +9NsAlQoe3ijidDj6RbsfoLAK74maA26A5h7+5wgiekcb/zcICLFQ+BoBA+N2RVM3 +plzlanOkln18NwQEW0mbDHbYmN9xiroA049C0C/ecoMcEhz27A+DZd34uIQTEMxA +eZZ4eEOyDraWRpGqzVNv42/uk0e16B79ATrsveStzqZgrMGxNhq4r301pD8XgVqi +G7/NCRj8SG0MEOsCAwEAAaNOMEwwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwEwYD +VR0lBAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFJ0rjXp58BHb6dcJVbokPN2NtofF +MA0GCSqGSIb3DQEBCwUAA4IBAQDENVOR1a0lDZSzrCec6n3ZaWlWBZEbSvrz9pYw +HHFsxsNehpyNlJ+uinHOap7/gnaDzCAzkuxUBlZ8zx0XA0JzSxktQv2kj7svqXz8 +5ncAYK873YtWdX/dytWZm337Rv85LEwJU8/MKGZVIgCyPOGcp+ECCTsrQUN/Js0J +NHd6AAOJc6EOR9a/9W18kEcVYauxLwEBuGs+YTluO08CYZffdglP8IRtmwmxipsa +Y8HMh2PO+EEYB8PKE3L8GlRWgsUHCHauWgurqDUL9gZOJqTC9JH8d8d3A905w6eg +GqpYVsZ0bAdadSvkcCt8bh71TMhKef/F8sIOhHC4D6i/FISz +-----END CERTIFICATE----- +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIotWfAkvkErICAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBApG3OwKAVkSGOTZTyDBIQQBIIE +0Eh4kS+bglFWfOMbTm39e9s6pKYXa2JM69YO2+B+YoUzpIrHv0PCyV/ste/8ajzj +yxOJIKsZki8iO+PRrqccqI6Pfa3RCOwAiv73UKdaGbEKlYYWXCXdZyml+TGpkeOL +hGYyGQ8ZcmG+JlbQH1NDCf0W4x0nWUkzZVBT4+khZztR4JULh287wEJe4olCw8+S +cPgVJJsYdtl2tX1ciRn6TLEXa8DipaiW/TA6Ot+2P3VaHNwohlTpG8pIwZrJVh27 +xgQWLb2pLpy4tPG0z+869Ylymb0rNnd/niEBg5m4fOArOK84/LaRj7DJLXtDafuM +wnFR1BFH+Vn+CDlZGiTeEweOx4jcg6w78jJFYa993sH8SUDzZn4qZGvOm0JuzJv0 +6e3qQAw4mXm77NsUwhtwhPJdkwFdvUDtRG31tuRLnC69XykeV4u/095te8EiANoW +Tue3EVzuqHTqkE7lwctU2f51u8Oz09h0GrUP0N6/R1W4XALHU5EQA4mPltquaenS +EuovU13HnBGw0Dh+xUcJopv1TQ4IYvKngiJYP6lQYquJCFM8t6RMrBZ2maHonaPu +neRVs5mrggL7kmEgnNBrTVbztYqFbX8upyGSFLewm52H3M/rurrlq7PvVP07U2WU +0Stt71Gs66cS1FTN7QnCukgJ9sT8fBUvOeei5H93Pi0wCyy4D/0y++h6Y+muxslt +EbNTggGWrmNWK1OJfYPNbdNTOrI4B2vvyNiD38WbOcF+I4tfpJCXiXo9/9gbBfSe +jfjO02Xta24ruI3w6n/GSK4gpNTGJk81GrkNWUaZ/6DyhpbU3cpdffbBT5xMaEDO +ivtMgv5BvFmhp7QOK9ki0UXKGAelTavDnG2nM7jasahHoz12ih5KtvuQ2W8WOdLT +nbnP/TZARBP06g6ejHosHjRc0xlJPU8deZ6+xNS601hSUVeT5A5jGnthasYtb9PJ +TvNDva0NGVGMo3k10QeRaNZh+qMvzFXVZu4jFeOdMgiT1ZP11oCrTmTB4n0VIMQA +TEvHZ72bnzqrDht3Amnqt5CqmqN4Kyl5P9MEUgM4hjlgefGLQqKHnzRb8Mk/UqcJ +J8o6TcP5Mr5lQpHpEbskP6ECYfJcbK6uY6RqXx2y1w+t6SwuKw2ai1zZWc7fmrzm +ImlI1lTTmRVmaDyozGIm1c8i1CNZEsmrjjJzfPfRLIVGBAkiizrXHZcX7Wy3/X3N +Cn2Xm5lDx7vPuWZu2pp+4vzK3p/YbmDavGmB6dsJotQSXLXkqpot98ImQ4jMrxu/ +cywGeyrs4GH30lobnDrLXSko+MFRItLiQMsng/IuWPEX5SqpzQy8bqk6Uo64kPBj +oXuu+XKAwuw1xrlVIPt2bj6dBz8i+MUTJrVAhh2hAgU/ExiGzicoEdXipveOtnDn +EQ6xjUDIsDNoZcW0uIOlEgxycB4vm41QjLNUIajFuuNN/VRu+jr1ToJmqNtFHzz5 +SOBQ38qD4jHoRxpQojGHFUpEySsxs3bTPmq/2ymjKWd37u3wguc0XSxycCNu62Tn +qbPGt17pPqVU8MOcUIF3wfYus70Em4zJr4cJtghr+cYCTn+5uBrt0+PfkpC0hmPv +XvVBYNbonFkLQFjEdg5BiYodhvAP8+4WXNKpJwPxs/ae +-----END ENCRYPTED PRIVATE KEY----- diff --git a/jstests/libs/client_password_protected.pem.digest.sha1 b/jstests/libs/client_password_protected.pem.digest.sha1 new file mode 100644 index 00000000000..7535a740ec3 --- /dev/null +++ b/jstests/libs/client_password_protected.pem.digest.sha1 @@ -0,0 +1 @@ +7E414923FB657EDC6C45136FFEB8A29125ED110C \ No newline at end of file diff --git a/jstests/libs/client_password_protected.pem.digest.sha256 b/jstests/libs/client_password_protected.pem.digest.sha256 new file mode 100644 index 00000000000..c8f7f57aad0 --- /dev/null +++ b/jstests/libs/client_password_protected.pem.digest.sha256 @@ -0,0 +1 @@ +DDCBC0984AED440FDBA3ADC1B297D60FF9A908BB78FD2C565C5970C3BB4764B3 \ No newline at end of file diff --git a/jstests/ssl/libs/ssl_helpers.js b/jstests/ssl/libs/ssl_helpers.js index 6702ac75cfe..502d8f8e5c5 100644 --- a/jstests/ssl/libs/ssl_helpers.js +++ b/jstests/ssl/libs/ssl_helpers.js @@ -17,6 +17,7 @@ export var TRUSTED_SERVER_CERT = getX509Path("trusted-server.pem"); export var CA_CERT = getX509Path("ca.pem"); export var TRUSTED_CA_CERT = getX509Path("trusted-ca.pem"); export var CLIENT_CERT = getX509Path("client.pem"); +export var CLIENT_PASSWORD_PROTECTED_CERT = getX509Path("client_password_protected.pem"); export var TRUSTED_CLIENT_CERT = getX509Path("trusted-client.pem"); export var DH_PARAM = "jstests/libs/8k-prime.dhparam"; export var CLUSTER_CERT = getX509Path("cluster_cert.pem"); diff --git a/jstests/ssl/x509/certs.yml b/jstests/ssl/x509/certs.yml index 84e514d00b4..b66ec3ffb16 100644 --- a/jstests/ssl/x509/certs.yml +++ b/jstests/ssl/x509/certs.yml @@ -224,6 +224,18 @@ certs: - {role: backup, db: admin} - {role: readAnyDatabase, db: admin} + - name: "client_password_protected.pem" + description: Client certificate using an encrypted private key. + Subject: {OU: "KernelUser", CN: "client"} + Issuer: "ca.pem" + passphrase: "qwerty" + pkcs1: true + extensions: + basicConstraints: {CA: false} + subjectKeyIdentifier: hash + keyUsage: [digitalSignature, keyEncipherment] + extendedKeyUsage: [clientAuth] + - name: "cluster_cert.pem" description: Alternate cert for use in intra-cluster communication. Subject: {CN: "clustertest"} diff --git a/jstests/with_mongot/search_mocked/ssl/mongod_uses_password_protected_certs_with_mongot.js b/jstests/with_mongot/search_mocked/ssl/mongod_uses_password_protected_certs_with_mongot.js new file mode 100644 index 00000000000..12c86a33f85 --- /dev/null +++ b/jstests/with_mongot/search_mocked/ssl/mongod_uses_password_protected_certs_with_mongot.js @@ -0,0 +1,92 @@ +/** + * Check that mongod can use the password protected intracluster certificates for + * communication with mongot. + */ +import {getUUIDFromListCollections} from "jstests/libs/uuid_util.js"; +import {CA_CERT, SERVER_CERT, CLIENT_PASSWORD_PROTECTED_CERT} from "jstests/ssl/libs/ssl_helpers.js"; +import {MongotMock} from "jstests/with_mongot/mongotmock/lib/mongotmock.js"; + +function runOneTest(mongodTlsOpts, badPassword) { + // Set up mongotmock and point the mongod to it. + const mongotmock = new MongotMock(); + mongotmock.start({tlsMode: "requireTLS"}); + const mongotConn = mongotmock.getConnection(); + + const opts = Object.assign( + { + sslMode: "requireSSL", + tlsAllowInvalidCertificates: "", + setParameter: {mongotHost: mongotConn.host, searchTLSMode: "requireTLS"}, + }, + mongodTlsOpts, + ); + + if (badPassword) { + assert.throws(() => MongoRunner.runMongod(opts)); + mongotmock.stop(); + return; + } + + const conn = MongoRunner.runMongod(opts); + + const db = conn.getDB("test"); + const collName = "search"; + const coll = db.getCollection(collName); + const searchQuery = { + query: "cakes", + path: "title", + }; + const pipeline = [{$search: searchQuery}]; + coll.drop(); + assert.commandWorked(coll.insert({"_id": 1, "title": "cakes"})); + + const collUUID = getUUIDFromListCollections(db, collName); + // Give mongotmock some stuff to return. + { + const cursorId = NumberLong(123); + const searchCmd = {search: collName, collectionUUID: collUUID, query: searchQuery, $db: "test"}; + const history = [ + { + expectedCommand: searchCmd, + response: { + cursor: { + id: NumberLong(0), + ns: "test." + collName, + nextBatch: [{_id: 1, $searchScore: 0.321}], + }, + ok: 1, + }, + }, + ]; + + assert.commandWorked(mongotConn.adminCommand({setMockResponses: 1, cursorId: cursorId, history: history})); + } + + // Perform a $search query. + let cursor = db[collName].aggregate(pipeline); + + const expected = [{"_id": 1, "title": "cakes"}]; + assert.eq(expected, cursor.toArray()); + + MongoRunner.stopMongod(conn); + mongotmock.stop(); +} + +const optsClusterFile = { + sslCAFile: CA_CERT, + sslPEMKeyFile: SERVER_CERT, + tlsClusterCAFile: CA_CERT, + tlsClusterFile: CLIENT_PASSWORD_PROTECTED_CERT, +}; +// Test usage of password protected cluster file +runOneTest({...optsClusterFile, tlsClusterPassword: "foobar"}, true); +runOneTest({...optsClusterFile, tlsClusterPassword: "qwerty"}, false); + +const optsPEMKeyFile = { + sslCAFile: CA_CERT, + sslPEMKeyFile: CLIENT_PASSWORD_PROTECTED_CERT, +}; + +// Test usage of password protected PEM key file +runOneTest({...optsPEMKeyFile, tlsCertificateKeyFilePassword: "foobar"}, true); +runOneTest({...optsPEMKeyFile, tlsCertificateKeyFilePassword: "qwerty"}, false); diff --git a/src/mongo/transport/grpc/client.cpp b/src/mongo/transport/grpc/client.cpp index 655990c0f92..f2867a75ae7 100644 --- a/src/mongo/transport/grpc/client.cpp +++ b/src/mongo/transport/grpc/client.cpp @@ -642,7 +642,7 @@ public: std::shared_ptr manager = nullptr; if (SSLManagerCoordinator::get() && (manager = SSLManagerCoordinator::get()->getSSLManager())) { - _loadTlsCertificates(manager->getSSLConfiguration()); + _loadTlsCertificates(manager->getSSLConfiguration(), *manager); } _prunerService.start(_svcCtx, _pool); } @@ -657,9 +657,10 @@ public: } #ifdef MONGO_CONFIG_SSL - Status rotateCertificates(const SSLConfiguration& sslConfig) try { + Status rotateCertificates(const SSLConfiguration& sslConfig, + const SSLManagerInterface& sslManager) try { LOGV2_DEBUG(9886801, 3, "Rotating certificates used for creating gRPC channels"); - _loadTlsCertificates(sslConfig); + _loadTlsCertificates(sslConfig, sslManager); return Status::OK(); } catch (const DBException& ex) { return ex.toStatus(); @@ -714,7 +715,8 @@ private: } #ifdef MONGO_CONFIG_SSL - void _loadTlsCertificates(const SSLConfiguration& sslConfig) { + void _loadTlsCertificates(const SSLConfiguration& sslConfig, + const SSLManagerInterface& manager) { auto cache = [&]() -> boost::optional { if (!_options.tlsCAFile && !_options.tlsCertificateKeyFile) { return boost::none; @@ -722,7 +724,20 @@ private: std::vector<::grpc::experimental::IdentityKeyCertPair> certKeyPairs; if (_options.tlsCertificateKeyFile) { - auto sslPair = util::parsePEMKeyFile(_options.tlsCertificateKeyFile.get()); + ::grpc::SslServerCredentialsOptions::PemKeyCertPair sslPair; + + auto certificateKeyFileContents = + uassertStatusOK(ssl_util::readPEMFile(_options.tlsCertificateKeyFile.get())); + sslPair.cert_chain = certificateKeyFileContents; + auto swDecrypted = + manager.decryptPEMKey(certificateKeyFileContents, + _options.tlsCertificatePassword.value_or(StringData{})); + if (swDecrypted == ErrorCodes::NotImplemented) { + sslPair.private_key = certificateKeyFileContents; + } else { + sslPair.private_key = uassertStatusOK(std::move(swDecrypted)); + } + certKeyPairs.push_back( {std::move(sslPair.private_key), std::move(sslPair.cert_chain)}); } @@ -843,8 +858,9 @@ void GRPCClient::appendStats(GRPCConnectionStats& stats) const { } #ifdef MONGO_CONFIG_SSL -Status GRPCClient::rotateCertificates(const SSLConfiguration& config) { - return static_cast(*_stubFactory).rotateCertificates(config); +Status GRPCClient::rotateCertificates(const SSLConfiguration& config, + const SSLManagerInterface& sslManager) { + return static_cast(*_stubFactory).rotateCertificates(config, sslManager); } #endif diff --git a/src/mongo/transport/grpc/client.h b/src/mongo/transport/grpc/client.h index fb5cd3ab8c6..aa8e93e1176 100644 --- a/src/mongo/transport/grpc/client.h +++ b/src/mongo/transport/grpc/client.h @@ -90,7 +90,8 @@ public: virtual void appendStats(GRPCConnectionStats& stats) const = 0; #ifdef MONGO_CONFIG_SSL - virtual Status rotateCertificates(const SSLConfiguration& sslConfig) = 0; + virtual Status rotateCertificates(const SSLConfiguration& sslConfig, + const SSLManagerInterface& sslManager) = 0; #endif struct ConnectOptions { @@ -270,6 +271,7 @@ public: struct Options { boost::optional tlsCAFile; boost::optional tlsCertificateKeyFile; + boost::optional tlsCertificatePassword; bool tlsAllowInvalidCertificates = false; bool tlsAllowInvalidHostnames = false; }; @@ -283,7 +285,8 @@ public: void shutdown() override; void appendStats(GRPCConnectionStats& stats) const override; #ifdef MONGO_CONFIG_SSL - Status rotateCertificates(const SSLConfiguration& sslConfig) override; + Status rotateCertificates(const SSLConfiguration& sslConfig, + const SSLManagerInterface& sslManager) override; #endif void dropConnections(const Status& status) override; diff --git a/src/mongo/transport/grpc/grpc_client_test.cpp b/src/mongo/transport/grpc/grpc_client_test.cpp index 3e67d4e5ed1..d484451fe35 100644 --- a/src/mongo/transport/grpc/grpc_client_test.cpp +++ b/src/mongo/transport/grpc/grpc_client_test.cpp @@ -256,6 +256,65 @@ TEST_F(GRPCClientTest, GRPCClientConnectWithInvalidCertificate) { CommandServiceTestFixtures::makeEchoHandler(), clientThreadBody, std::move(options)); } +TEST_F(GRPCClientTest, GRPCClientConnectWithEncryptedCertificate) { + auto options = CommandServiceTestFixtures::makeServerOptions(); + + auto clientThreadBody = [&](auto& server, auto& monitor) { + GRPCClient::Options options; + options.tlsCAFile = "jstests/libs/ca.pem"; + options.tlsCertificateKeyFile = "jstests/libs/client_password_protected.pem"; + options.tlsCertificatePassword = "qwerty"; + + auto client = makeClient(std::move(options)); + client->start(); + + auto session = client + ->connect(server.getListeningAddresses().at(0), + getReactor(), + CommandServiceTestFixtures::kDefaultConnectTimeout, + {}) + .get(); + assertEchoSucceeds(*session); + ASSERT_OK(session->finish()); + }; + + CommandServiceTestFixtures::runWithServer( + CommandServiceTestFixtures::makeEchoHandler(), clientThreadBody, std::move(options)); +} + +TEST_F(GRPCClientTest, GRPCClientConnectWithIncorrectCertificatePasswordShouldFail) { + auto options = CommandServiceTestFixtures::makeServerOptions(); + + auto clientThreadBody = [&](auto& server, auto& monitor) { + GRPCClient::Options options; + options.tlsCAFile = "jstests/libs/ca.pem"; + options.tlsCertificateKeyFile = "jstests/libs/client_password_protected.pem"; + options.tlsCertificatePassword = "wrong!"; + + auto client = makeClient(std::move(options)); + ASSERT_THROWS_CODE(client->start(), DBException, ErrorCodes::InvalidSSLConfiguration); + }; + + CommandServiceTestFixtures::runWithServer( + CommandServiceTestFixtures::makeEchoHandler(), clientThreadBody, std::move(options)); +} + +TEST_F(GRPCClientTest, GRPCClientConnectMissingCertificatePasswordShouldFail) { + auto options = CommandServiceTestFixtures::makeServerOptions(); + + auto clientThreadBody = [&](auto& server, auto& monitor) { + GRPCClient::Options options; + options.tlsCAFile = "jstests/libs/ca.pem"; + options.tlsCertificateKeyFile = "jstests/libs/client_password_protected.pem"; + + auto client = makeClient(std::move(options)); + ASSERT_THROWS_CODE(client->start(), DBException, ErrorCodes::InvalidSSLConfiguration); + }; + + CommandServiceTestFixtures::runWithServer( + CommandServiceTestFixtures::makeEchoHandler(), clientThreadBody, std::move(options)); +} + TEST_F(GRPCClientTest, GRPCClientConnectNoClientCertificate) { auto options = CommandServiceTestFixtures::makeServerOptions(); options.tlsAllowConnectionsWithoutCertificates = true; diff --git a/src/mongo/transport/grpc/grpc_transport_layer_impl.cpp b/src/mongo/transport/grpc/grpc_transport_layer_impl.cpp index e627e2becb7..31a2ea1c0f1 100644 --- a/src/mongo/transport/grpc/grpc_transport_layer_impl.cpp +++ b/src/mongo/transport/grpc/grpc_transport_layer_impl.cpp @@ -203,8 +203,10 @@ Status GRPCTransportLayerImpl::setup() { } if (!sslGlobalParams.sslClusterFile.empty()) { _clientOptions.tlsCertificateKeyFile = sslGlobalParams.sslClusterFile; + _clientOptions.tlsCertificatePassword = sslGlobalParams.sslClusterPassword; } else if (!sslGlobalParams.sslPEMKeyFile.empty()) { _clientOptions.tlsCertificateKeyFile = sslGlobalParams.sslPEMKeyFile; + _clientOptions.tlsCertificatePassword = sslGlobalParams.sslPEMKeyPassword; } _clientOptions.tlsAllowInvalidHostnames = sslGlobalParams.sslAllowInvalidHostnames; _clientOptions.tlsAllowInvalidCertificates = @@ -444,7 +446,8 @@ Status GRPCTransportLayerImpl::rotateCertificates(std::shared_ptrrotateCertificates(manager->getSSLConfiguration()); + if (auto status = + _defaultClient->rotateCertificates(manager->getSSLConfiguration(), *manager); !status.isOK()) { LOGV2_DEBUG( 9886803, 1, "Failed to rotate egress gRPC TLS certificates", "error"_attr = status); @@ -456,7 +459,7 @@ Status GRPCTransportLayerImpl::rotateCertificates(std::shared_ptrrotateCertificates(manager->getSSLConfiguration()); + if (auto status = c->rotateCertificates(manager->getSSLConfiguration(), *manager); !status.isOK()) { LOGV2_DEBUG(10026100, 1, diff --git a/src/mongo/transport/grpc/grpc_transport_layer_test.cpp b/src/mongo/transport/grpc/grpc_transport_layer_test.cpp index c0dc159aaf2..3c1b90fae25 100644 --- a/src/mongo/transport/grpc/grpc_transport_layer_test.cpp +++ b/src/mongo/transport/grpc/grpc_transport_layer_test.cpp @@ -280,6 +280,31 @@ TEST_F(GRPCTransportLayerTest, setupIngressWithoutTLSShouldFail) { ASSERT_EQ(ErrorCodes::InvalidOptions, tl->setup()); } +TEST_F(GRPCTransportLayerTest, startupEgressWithClusterPassword) { + sslGlobalParams.sslClusterPassword = "qwerty"; + sslGlobalParams.sslClusterFile = "jstests/libs/password_protected.pem"; + createAndStartupTL(false, true); +} + +TEST_F(GRPCTransportLayerTest, startupEgressWithPEMKeyPassword) { + sslGlobalParams.sslPEMKeyPassword = "qwerty"; + sslGlobalParams.sslPEMKeyFile = "jstests/libs/password_protected.pem"; + createAndStartupTL(false, true); +} + +TEST_F(GRPCTransportLayerTest, startupEgressWithIncorrectSSLPasswordShouldFail) { + sslGlobalParams.sslPEMKeyPassword = "wrong!"; + sslGlobalParams.sslPEMKeyFile = "jstests/libs/password_protected.pem"; + + auto options = CommandServiceTestFixtures::makeTLOptions(); + options.enableIngress = false; + options.enableEgress = true; + auto tl = makeTL(makeNoopRPCHandler(), std::move(options)); + ASSERT_OK(tl->setup()); + ASSERT_EQ(ErrorCodes::InvalidSSLConfiguration, tl->start()); + tl->shutdown(); +} + using GRPCTransportLayerTestDeathTest = GRPCTransportLayerTest; DEATH_TEST_F(GRPCTransportLayerTestDeathTest, setupWithPortConflictShouldFail, @@ -929,7 +954,9 @@ TEST_F(RotateCertificatesGRPCTransportLayerTest, SSLConfiguration newConfig{}; newConfig.serverCertificateExpirationDate = Date_t::fromDurationSinceEpoch(Milliseconds(1234)); - ASSERT_EQ(client->rotateCertificates(newConfig), ErrorCodes::InvalidSSLConfiguration); + ASSERT_EQ(client->rotateCertificates(newConfig, + *(SSLManagerCoordinator::get()->getSSLManager())), + ErrorCodes::InvalidSSLConfiguration); // Make sure we can still connect with the initial certs used before the bad // rotation. @@ -999,7 +1026,8 @@ TEST_F(RotateCertificatesGRPCTransportLayerTest, ClientUsesOldCertsUntilRotate) SSLConfiguration newConfig{}; newConfig.serverCertificateExpirationDate = Date_t::fromMillisSinceEpoch(1234); - ASSERT_OK(client->rotateCertificates(newConfig)); + ASSERT_OK(client->rotateCertificates(newConfig, + *(SSLManagerCoordinator::get()->getSSLManager()))); auto swSession = client ->connect(addr, reactor, CommandServiceTestFixtures::kDefaultConnectTimeout, {}) diff --git a/src/mongo/transport/grpc/mock_client.h b/src/mongo/transport/grpc/mock_client.h index 292d0b4bf9a..cee3323f916 100644 --- a/src/mongo/transport/grpc/mock_client.h +++ b/src/mongo/transport/grpc/mock_client.h @@ -61,7 +61,8 @@ public: MONGO_UNIMPLEMENTED; } - Status rotateCertificates(const SSLConfiguration& sslConfig) override { + Status rotateCertificates(const SSLConfiguration& sslConfig, + const SSLManagerInterface& sslManager) override { MONGO_UNIMPLEMENTED; } diff --git a/src/mongo/transport/grpc/test_fixtures.h b/src/mongo/transport/grpc/test_fixtures.h index 3e7f48205e8..ad1f8fc667a 100644 --- a/src/mongo/transport/grpc/test_fixtures.h +++ b/src/mongo/transport/grpc/test_fixtures.h @@ -55,6 +55,7 @@ #include "mongo/util/modules.h" #include "mongo/util/net/hostandport.h" #include "mongo/util/net/socket_utils.h" +#include "mongo/util/net/ssl_manager.h" #include "mongo/util/net/ssl_util.h" #include "mongo/util/scopeguard.h" #include "mongo/util/uuid.h" @@ -181,6 +182,7 @@ public: static constexpr auto kMaxThreads = 100; static constexpr auto kServerCertificateKeyFile = "jstests/libs/server_SAN.pem"; static constexpr auto kClientCertificateKeyFile = "jstests/libs/client.pem"; + static constexpr auto kClientCertificatePassword = ""; static constexpr auto kClientSelfSignedCertificateKeyFile = "jstests/libs/client-self-signed.pem"; static constexpr auto kCAFile = "jstests/libs/ca.pem"; @@ -426,6 +428,7 @@ public: options.emplace(); options->tlsCAFile = kCAFile; options->tlsCertificateKeyFile = kClientCertificateKeyFile; + options->tlsCertificatePassword = kClientCertificatePassword; } ::grpc::SslCredentialsOptions sslOps; diff --git a/src/mongo/transport/test_fixtures.h b/src/mongo/transport/test_fixtures.h index 3c0df4111b5..932c1a0bc6e 100644 --- a/src/mongo/transport/test_fixtures.h +++ b/src/mongo/transport/test_fixtures.h @@ -297,7 +297,7 @@ inline std::unique_ptr copyCertsToTempDir(std::string caFil }; /** - * RAII type that caches the sslGlobalParams sslCAFile, sslPEMKeyFile, and sslMode on construction, + * RAII type that caches the included sslGlobalParams on construction, * and restores them to the cached values on destruction. */ class SSLGlobalParamsGuard { @@ -305,18 +305,27 @@ public: SSLGlobalParamsGuard() { _sslCAFile = sslGlobalParams.sslCAFile; _sslPEMKeyFile = sslGlobalParams.sslPEMKeyFile; + _sslPEMKeyPassword = sslGlobalParams.sslPEMKeyPassword; + _sslClusterFile = sslGlobalParams.sslClusterFile; + _sslClusterPassword = sslGlobalParams.sslClusterPassword; _sslMode = sslGlobalParams.sslMode.load(); } ~SSLGlobalParamsGuard() { sslGlobalParams.sslCAFile = _sslCAFile; sslGlobalParams.sslPEMKeyFile = _sslPEMKeyFile; + sslGlobalParams.sslPEMKeyPassword = _sslPEMKeyPassword; + sslGlobalParams.sslClusterFile = _sslClusterFile; + sslGlobalParams.sslClusterPassword = _sslClusterPassword; sslGlobalParams.sslMode.store(_sslMode); } private: std::string _sslCAFile; std::string _sslPEMKeyFile; + std::string _sslPEMKeyPassword; + std::string _sslClusterFile; + std::string _sslClusterPassword; int _sslMode; }; diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h index 710cb15ce8a..06bd6cd93ab 100644 --- a/src/mongo/util/net/ssl_manager.h +++ b/src/mongo/util/net/ssl_manager.h @@ -405,6 +405,17 @@ public: * SSL connecctions. */ virtual SSLInformationToLog getSSLInformationToLog() const = 0; + + /** + * Decrypt the raw contents of a PEM key file which was encrypted with `password`. Only + * implemented for the OpenSSL variant; other implementations return NotImplemented. + * TODO SERVER-126149: Replace/remove this function. + */ + virtual StatusWith decryptPEMKey(StringData pemContents, + StringData password) const { + return Status(ErrorCodes::NotImplemented, + "decryptPEMKey is not supported on this platform"); + } }; /** diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp index a69d198d3f8..888496f1021 100644 --- a/src/mongo/util/net/ssl_manager_openssl.cpp +++ b/src/mongo/util/net/ssl_manager_openssl.cpp @@ -161,6 +161,9 @@ constexpr std::uint8_t ffdhe3072_g = 0x02; using UniqueBIO = std::unique_ptr>; +using UniqueEVP_PKEY = + std::unique_ptr>; + #ifdef MONGO_CONFIG_HAVE_SSL_EC_KEY_NEW using UniqueEC_KEY = std::unique_ptr>; @@ -1327,6 +1330,8 @@ public: SSLInformationToLog getSSLInformationToLog() const final; + StatusWith decryptPEMKey(StringData pemContents, StringData password) const final; + std::shared_ptr getOcspStaplingContext() { std::lock_guard guard(_sharedResponseMutex); return _ocspStaplingContext; @@ -3817,4 +3822,46 @@ SSLInformationToLog SSLManagerOpenSSL::getSSLInformationToLog() const { return info; } +StatusWith SSLManagerOpenSSL::decryptPEMKey(StringData pemContents, + StringData password) const { + str::uassertNoEmbeddedNulBytes(password); + + UniqueBIO inBIO(::BIO_new_mem_buf(pemContents.data(), pemContents.size())); + if (!inBIO) { + return Status(ErrorCodes::InvalidSSLConfiguration, + fmt::format("Failed to allocate inBIO object. error: {}", + getSSLErrorMessage(ERR_get_error()))); + } + + // If `cb` is NULL, `PEM_read_bio_PrivateKey` interprets `u` as a NUL-terminated string + // containing the password. Calling `toString()` is necessary as `password` does not have to be + // NUL-terminated. + std::string passwordStr{password}; + void* userdata = static_cast(passwordStr.data()); + UniqueEVP_PKEY pkey(::PEM_read_bio_PrivateKey(inBIO.get(), nullptr, nullptr, userdata)); + if (!pkey) { + return Status( + ErrorCodes::InvalidSSLConfiguration, + fmt::format("Failed to read PEM key: {}", getSSLErrorMessage(ERR_get_error()))); + } + + UniqueBIO outBIO(BIO_new(BIO_s_mem())); + if (!outBIO) { + return Status(ErrorCodes::InvalidSSLConfiguration, + fmt::format("Failed to allocate outBIO object. error: {}", + getSSLErrorMessage(ERR_get_error()))); + } + + if (PEM_write_bio_PrivateKey(outBIO.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr) != + 1) { + return Status(ErrorCodes::InvalidSSLConfiguration, + fmt::format("Failed to serialize decrypted PEM key: {}", + getSSLErrorMessage(ERR_get_error()))); + } + + char* data = nullptr; + long len = BIO_get_mem_data(outBIO.get(), &data); + return std::string(data, len); +} + } // namespace mongo diff --git a/src/mongo/util/net/ssl_manager_test.cpp b/src/mongo/util/net/ssl_manager_test.cpp index fc96cb23cd6..be8c4f0a174 100644 --- a/src/mongo/util/net/ssl_manager_test.cpp +++ b/src/mongo/util/net/ssl_manager_test.cpp @@ -1150,6 +1150,84 @@ TEST(SSLManager, WindowsReusableBufferOps) { #endif // MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS +// Test decryptPEMKey +#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL +TEST(SSLManager, OpenSSLDecryptBadPEMKey) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + + std::shared_ptr manager = + SSLManagerInterface::create(params, true /* isSSLServer */); + + auto sw = manager->decryptPEMKey("badPEMContents"_sd, "password"_sd); + ASSERT_NOT_OK(sw.getStatus()); + ASSERT_EQ(sw.getStatus().code(), ErrorCodes::InvalidSSLConfiguration); +} + +TEST(SSLManager, OpenSSLDecryptPEMKeyEmbeddedNullInPasswordNotAllowed) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + + std::shared_ptr manager = + SSLManagerInterface::create(params, true /* isSSLServer */); + + ASSERT_THROWS_CODE( + manager->decryptPEMKey("whatever"_sd, "password\0extrastuff"_sd), DBException, 9527900); +} + +TEST(SSLManager, OpenSSLDecryptPEMKeyPasswordWithoutNulTermination) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + + auto pemContents = loadFile("jstests/libs/password_protected.pem"); + auto fullStr = "qwerty_extra_bytes"_sd; + StringData password = fullStr.substr(0, 6); // no null termination + + std::shared_ptr manager = + SSLManagerInterface::create(params, true /* isSSLServer */); + + ASSERT_OK(manager->decryptPEMKey(pemContents, password)); +} + +TEST(SSLManager, OpenSSLDecryptPEMKeyEmptyPasswordShouldFail) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + + auto pemContents = loadFile("jstests/libs/password_protected.pem"); + auto password = ""_sd; + + std::shared_ptr manager = + SSLManagerInterface::create(params, true /* isSSLServer */); + + auto sw = manager->decryptPEMKey(pemContents, password); + ASSERT_NOT_OK(sw.getStatus()); + ASSERT_EQ(sw.getStatus().code(), ErrorCodes::InvalidSSLConfiguration); +} + +#else +TEST(SSLManager, NonOpenSSLDecryptPEMKeyNotImplemented) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + + std::shared_ptr manager = + SSLManagerInterface::create(params, true /* isSSLServer */); + + auto sw = manager->decryptPEMKey("pemContents"_sd, "password"_sd); + ASSERT_NOT_OK(sw.getStatus()); + ASSERT_EQ(sw.getStatus().code(), ErrorCodes::NotImplemented); +} +#endif // MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL + #ifdef MONGO_CONFIG_SSL TEST(SSLManager, CheckCertificateInTransientManager) { diff --git a/x509/main_certs_def.bzl b/x509/main_certs_def.bzl index e340b1bf8cc..de99a018912 100644 --- a/x509/main_certs_def.bzl +++ b/x509/main_certs_def.bzl @@ -531,6 +531,34 @@ certs_def = json.encode({ }, }, }, + { + "name": "client_password_protected.pem", + "description": "Server cerificate using an encrypted private key.", + "Subject": { + "OU": "KernelUser", + "CN": "client", + }, + "keyfile": "pkcs1_encrypted_key.pem", + "passphrase": "qwerty", + "Issuer": "ca.pem", + "extensions": { + "basicConstraints": { + "CA": False, + }, + "subjectKeyIdentifier": "hash", + "keyUsage": [ + "digitalSignature", + "keyEncipherment", + ], + "extendedKeyUsage": [ + "clientAuth", + ], + "subjectAltName": { + "DNS": "localhost", + "IP": "127.0.0.1", + }, + }, + }, { "name": "password_protected.pem", "description": "Server cerificate using an encrypted private key.",