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:
parent
7139513a1c
commit
5f89b25908
57
jstests/libs/client_password_protected.pem
Normal file
57
jstests/libs/client_password_protected.pem
Normal 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-----
|
||||
1
jstests/libs/client_password_protected.pem.digest.sha1
Normal file
1
jstests/libs/client_password_protected.pem.digest.sha1
Normal file
@ -0,0 +1 @@
|
||||
7E414923FB657EDC6C45136FFEB8A29125ED110C
|
||||
1
jstests/libs/client_password_protected.pem.digest.sha256
Normal file
1
jstests/libs/client_password_protected.pem.digest.sha256
Normal file
@ -0,0 +1 @@
|
||||
DDCBC0984AED440FDBA3ADC1B297D60FF9A908BB78FD2C565C5970C3BB4764B3
|
||||
@ -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");
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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);
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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, {})
|
||||
|
||||
@ -61,7 +61,8 @@ public:
|
||||
MONGO_UNIMPLEMENTED;
|
||||
}
|
||||
|
||||
Status rotateCertificates(const SSLConfiguration& sslConfig) override {
|
||||
Status rotateCertificates(const SSLConfiguration& sslConfig,
|
||||
const SSLManagerInterface& sslManager) override {
|
||||
MONGO_UNIMPLEMENTED;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user