SERVER-125887 Add support for encrypted PEM files for gRPC egress (#53238)

Co-authored-by: Erwin Pe <erwin.pe@mongodb.com>
GitOrigin-RevId: 071e3964bf43e7fa728a5df6390e0b960e335bdf
This commit is contained in:
Sam Frank 2026-05-07 15:44:11 -04:00 committed by MongoDB Bot
parent 7139513a1c
commit 5f89b25908
18 changed files with 465 additions and 15 deletions

View File

@ -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-----

View File

@ -0,0 +1 @@
7E414923FB657EDC6C45136FFEB8A29125ED110C

View File

@ -0,0 +1 @@
DDCBC0984AED440FDBA3ADC1B297D60FF9A908BB78FD2C565C5970C3BB4764B3

View File

@ -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");

View File

@ -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"}

View File

@ -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);

View File

@ -642,7 +642,7 @@ public:
std::shared_ptr<SSLManagerInterface> 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<TLSCache> {
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<StubFactoryImpl&>(*_stubFactory).rotateCertificates(config);
Status GRPCClient::rotateCertificates(const SSLConfiguration& config,
const SSLManagerInterface& sslManager) {
return static_cast<StubFactoryImpl&>(*_stubFactory).rotateCertificates(config, sslManager);
}
#endif

View File

@ -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<StringData> tlsCAFile;
boost::optional<StringData> tlsCertificateKeyFile;
boost::optional<StringData> 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;

View File

@ -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;

View File

@ -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_ptr<SSLManagerInte
}
if (_defaultClient) {
if (auto status = _defaultClient->rotateCertificates(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_ptr<SSLManagerInte
std::lock_guard lk(_mutex);
for (auto&& clientEntry : _clients) {
if (auto c = clientEntry.client.lock()) {
if (auto status = c->rotateCertificates(manager->getSSLConfiguration());
if (auto status = c->rotateCertificates(manager->getSSLConfiguration(), *manager);
!status.isOK()) {
LOGV2_DEBUG(10026100,
1,

View File

@ -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, {})

View File

@ -61,7 +61,8 @@ public:
MONGO_UNIMPLEMENTED;
}
Status rotateCertificates(const SSLConfiguration& sslConfig) override {
Status rotateCertificates(const SSLConfiguration& sslConfig,
const SSLManagerInterface& sslManager) override {
MONGO_UNIMPLEMENTED;
}

View File

@ -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;

View File

@ -297,7 +297,7 @@ inline std::unique_ptr<TempCertificatesDir> 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;
};

View File

@ -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<std::string> decryptPEMKey(StringData pemContents,
StringData password) const {
return Status(ErrorCodes::NotImplemented,
"decryptPEMKey is not supported on this platform");
}
};
/**

View File

@ -161,6 +161,9 @@ constexpr std::uint8_t ffdhe3072_g = 0x02;
using UniqueBIO = std::unique_ptr<BIO, OpenSSLDeleter<decltype(::BIO_free), ::BIO_free>>;
using UniqueEVP_PKEY =
std::unique_ptr<EVP_PKEY, OpenSSLDeleter<decltype(::EVP_PKEY_free), ::EVP_PKEY_free>>;
#ifdef MONGO_CONFIG_HAVE_SSL_EC_KEY_NEW
using UniqueEC_KEY =
std::unique_ptr<EC_KEY, OpenSSLDeleter<decltype(::EC_KEY_free), ::EC_KEY_free>>;
@ -1327,6 +1330,8 @@ public:
SSLInformationToLog getSSLInformationToLog() const final;
StatusWith<std::string> decryptPEMKey(StringData pemContents, StringData password) const final;
std::shared_ptr<OCSPStaplingContext> getOcspStaplingContext() {
std::lock_guard<std::mutex> guard(_sharedResponseMutex);
return _ocspStaplingContext;
@ -3817,4 +3822,46 @@ SSLInformationToLog SSLManagerOpenSSL::getSSLInformationToLog() const {
return info;
}
StatusWith<std::string> 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<void*>(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

View File

@ -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<SSLManagerInterface> 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<SSLManagerInterface> 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<SSLManagerInterface> 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<SSLManagerInterface> 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<SSLManagerInterface> 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) {

View File

@ -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.",