summaryrefslogtreecommitdiff
path: root/src/mongo/util/net/ssl_manager_test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/util/net/ssl_manager_test.cpp')
-rw-r--r--src/mongo/util/net/ssl_manager_test.cpp387
1 files changed, 386 insertions, 1 deletions
diff --git a/src/mongo/util/net/ssl_manager_test.cpp b/src/mongo/util/net/ssl_manager_test.cpp
index 1dd6585f7c1..80e83af242a 100644
--- a/src/mongo/util/net/ssl_manager_test.cpp
+++ b/src/mongo/util/net/ssl_manager_test.cpp
@@ -29,15 +29,20 @@
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
+#include <asio.hpp>
+#include <boost/filesystem.hpp>
#include <fstream>
#include "mongo/config.h"
#include "mongo/platform/basic.h"
+#include "mongo/bson/json.h"
#include "mongo/transport/service_entry_point.h"
#include "mongo/transport/transport_layer_asio.h"
#include "mongo/transport/transport_layer_manager.h"
+#include "mongo/util/net/sock_test_utils.h"
#include "mongo/util/net/ssl/context.hpp"
+#include "mongo/util/net/ssl/stream.hpp"
#include "mongo/util/net/ssl_manager.h"
#include "mongo/util/net/ssl_options.h"
@@ -49,10 +54,36 @@
#include "mongo/util/net/ssl/context_openssl.hpp"
#endif
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
+
+namespace fs = boost::filesystem;
namespace mongo {
namespace {
+#define TEST_CERTS_DIR "jstests/libs/"
+// certs & CRLs rooted in ca.pem
+constexpr const char* caFile = TEST_CERTS_DIR "ca.pem";
+constexpr const char* serverKeyFile = TEST_CERTS_DIR "server.pem";
+constexpr const char* clientKeyFile = TEST_CERTS_DIR "client.pem";
+constexpr const char* revokedClientKeyFile = TEST_CERTS_DIR "client_revoked.pem";
+
+constexpr const char* intermediateACaFile = TEST_CERTS_DIR "intermediate-ca.pem";
+constexpr const char* intermediateALeafKeyFile = TEST_CERTS_DIR "server-intermediate-leaf.pem";
+constexpr const char* intermediateBCaFile = TEST_CERTS_DIR "intermediate-ca-B.pem";
+constexpr const char* intermediateBLeafKeyFile = TEST_CERTS_DIR "intermediate-ca-B-leaf.pem";
+constexpr const char* emptyCRL = TEST_CERTS_DIR "crl.pem";
+constexpr const char* expiredCRL = TEST_CERTS_DIR "crl_expired.pem";
+constexpr const char* clientRevokedCRL = TEST_CERTS_DIR "crl_client_revoked.pem";
+constexpr const char* intermediateBRevokedCRL = TEST_CERTS_DIR "crl_intermediate_ca_B_revoked.pem";
+constexpr const char* intermediateBCRL = TEST_CERTS_DIR "crl_from_intermediate_ca_B.pem";
+
+// certs & CRLs rooted in trusted-ca.pem
+constexpr const char* trustedCaFile = TEST_CERTS_DIR "trusted-ca.pem";
+constexpr const char* trustedServerKeyFile = TEST_CERTS_DIR "trusted-server.pem";
+constexpr const char* trustedClientKeyFile = TEST_CERTS_DIR "trusted-client.pem";
+constexpr const char* trustedEmptyCRL = TEST_CERTS_DIR "crl_from_trusted_ca.pem";
+
// Test implementation needed by ASIO transport.
class ServiceEntryPointUtil : public ServiceEntryPoint {
public:
@@ -103,7 +134,7 @@ public:
}
private:
- mutable Mutex _mutex = MONGO_MAKE_LATCH("::_mutex");
+ mutable Mutex _mutex;
stdx::condition_variable _cv;
std::vector<transport::SessionHandle> _sessions;
transport::TransportLayer* _transport = nullptr;
@@ -115,6 +146,76 @@ std::string loadFile(const std::string& name) {
return str;
}
+// Reads the input stream until EOF or a valid PEM block is encountered.
+// Skips private key PEM blocks if includePrivateKeys is true.
+// Returns the parsed PEM block as a string (with newlines), or an empty
+// string if none is found or a read error occurs.
+std::string readOnePEMBlock(std::ifstream& inputStrm, bool includePrivateKeys) {
+ std::string line;
+ for (;;) {
+ std::stringstream output;
+ bool foundBegin = false;
+ bool foundEnd = false;
+ bool discard = false;
+
+ while (!foundBegin && std::getline(inputStrm, line)) {
+ StringData lineSD(line);
+ foundBegin = (lineSD.startsWith("-----BEGIN ") && lineSD.endsWith("-----"));
+ }
+ if (!foundBegin) {
+ return "";
+ }
+
+ discard = (!includePrivateKeys && line.find("PRIVATE KEY") != std::string::npos);
+ output << line << std::endl;
+
+ while (!foundEnd && std::getline(inputStrm, line)) {
+ StringData lineSD(line);
+ output << line << std::endl;
+ foundEnd = (lineSD.startsWith("-----END ") && lineSD.endsWith("-----"));
+ }
+ if (!foundEnd) {
+ return "";
+ }
+ if (!discard) {
+ return output.str();
+ }
+ }
+}
+
+struct PEMFileSpec {
+ std::string path;
+ bool includePrivateKeys{false};
+ void serialize(BSONObjBuilder* bob) const {
+ bob->append("path", path);
+ bob->append("includePrivateKeys", includePrivateKeys);
+ }
+};
+// Given a list of PEM files, this concatenates the PEM blocks in those files
+// (optionally filtering out private keys) and writes the result into a temporary
+// file. Returns the path to the temp file.
+std::string combinePEMFiles(const std::vector<PEMFileSpec>& pemSpecs) {
+ // make a temp file for the output
+ auto path = fs::temp_directory_path() / fs::unique_path("tmpfile_%%%%_%%%%_%%%%_%%%%.pem");
+ std::ofstream outStream(path.string());
+ invariant(outStream.is_open());
+
+ LOGV2(
+ 9476600, "Combining PEM files", "output"_attr = path.string(), "pemFiles"_attr = pemSpecs);
+
+ // read & parse the PEM files; append PEM blocks to output
+ for (auto& pemSpec : pemSpecs) {
+ std::ifstream input(pemSpec.path);
+ std::string pemBlock;
+ do {
+ pemBlock = readOnePEMBlock(input, pemSpec.includePrivateKeys);
+ outStream << pemBlock;
+ } while (!pemBlock.empty());
+ }
+ outStream.close();
+ return path.string();
+}
+
TEST(SSLManager, matchHostname) {
enum Expected : bool { match = true, mismatch = false };
const struct {
@@ -734,6 +835,8 @@ TEST(SSLManager, TransientSSLParamsStressTestWithManager) {
#endif // MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL
+#ifdef MONGO_CONFIG_SSL
+
static bool isSanWarningWritten(const std::vector<std::string>& logLines) {
for (const auto& line : logLines) {
if (std::string::npos !=
@@ -780,6 +883,288 @@ TEST(SSLManager, InitContextNoSanWarning) {
ASSERT_FALSE(isSanWarningWritten(getCapturedTextFormatLogMessages()));
}
+class SSLTestFixture {
+public:
+ SSLTestFixture(const SSLParams& ingressParams,
+ const SSLParams& egressParams,
+ bool ingressIsServer = true,
+ bool egressIsServer = true,
+ const boost::optional<TransientSSLParams>& transientSSLParams = boost::none) {
+ auto serviceContext = ServiceContext::make();
+ setGlobalServiceContext(std::move(serviceContext));
+
+ // SSLManagerWindows uses this global boolean to decide whether to
+ // use unique key container names when setting up the crypto context.
+ // This must be true in order for the handshake to work.
+ isSSLServer = true;
+
+ serverSSLManager = SSLManagerInterface::create(ingressParams, ingressIsServer);
+ clientSSLManager =
+ SSLManagerInterface::create(egressParams, transientSSLParams, egressIsServer);
+
+ serverSSLContext = std::make_shared<asio::ssl::context>(asio::ssl::context::sslv23);
+ clientSSLContext = std::make_shared<asio::ssl::context>(asio::ssl::context::sslv23);
+ uassertStatusOK(
+ serverSSLManager->initSSLContext(serverSSLContext->native_handle(),
+ ingressParams,
+ SSLManagerInterface::ConnectionDirection::kIncoming));
+ uassertStatusOK(
+ clientSSLManager->initSSLContext(clientSSLContext->native_handle(),
+ egressParams,
+ SSLManagerInterface::ConnectionDirection::kOutgoing));
+ }
+
+ void doHandshake() {
+ auto socks = socketPair(SOCK_STREAM);
+
+ serverConn = std::make_shared<ConnectionContext>(socks.first->rawFD(), *serverSSLContext);
+ clientConn = std::make_shared<ConnectionContext>(socks.second->rawFD(), *clientSSLContext);
+ Status serverStatus = Status::OK();
+ Status clientStatus = Status::OK();
+
+ auto serverThread = stdx::thread([this, &serverStatus]() {
+ try {
+ serverConn->sslSocket->handshake(asio::ssl::stream_base::server);
+ } catch (const DBException& ex) {
+ serverStatus = ex.toStatus().withContext("Server handshake failed");
+ }
+ });
+
+ try {
+ clientConn->sslSocket->handshake(asio::ssl::stream_base::client);
+ } catch (const DBException& ex) {
+ clientStatus = ex.toStatus().withContext("Client handshake failed");
+ }
+ serverThread.join();
+
+ // rethrow any handshake errors with context
+ uassertStatusOK(serverStatus);
+ uassertStatusOK(clientStatus);
+ }
+
+ struct IngressEgressValidationResult {
+ StatusWith<SSLPeerInfo> ingress;
+ StatusWith<SSLPeerInfo> egress;
+ };
+ IngressEgressValidationResult runIngressEgressValidation();
+
+ class ConnectionContext {
+ public:
+ ConnectionContext(int fd, asio::ssl::context& ctx) : io_context() {
+ asio::ip::tcp::socket socket(io_context, asio::ip::tcp::v4(), fd);
+ sslSocket =
+ std::make_unique<asio::ssl::stream<decltype(socket)>>(std::move(socket), ctx, "");
+ }
+ asio::io_context io_context;
+ std::unique_ptr<asio::ssl::stream<asio::ip::tcp::socket>> sslSocket;
+ };
+
+ std::shared_ptr<SSLManagerInterface> clientSSLManager;
+ std::shared_ptr<SSLManagerInterface> serverSSLManager;
+
+ std::shared_ptr<asio::ssl::context> clientSSLContext;
+ std::shared_ptr<asio::ssl::context> serverSSLContext;
+
+ std::shared_ptr<ConnectionContext> clientConn;
+ std::shared_ptr<ConnectionContext> serverConn;
+};
+
+SSLTestFixture::IngressEgressValidationResult SSLTestFixture::runIngressEgressValidation() {
+ static const HostAndPort hostForLogging("hostforlogging");
+
+ // Caller must doHandshake beforehand
+ invariant(serverConn);
+ invariant(clientConn);
+
+ IngressEgressValidationResult result{SSLPeerInfo{}, SSLPeerInfo{}};
+
+ // do ingress (server) first
+ try {
+ result.ingress =
+ serverSSLManager
+ ->parseAndValidatePeerCertificate(serverConn->sslSocket->native_handle(),
+ boost::none,
+ "",
+ hostForLogging,
+ nullptr)
+ .get();
+ } catch (const DBException& ex) {
+ result.ingress = ex.toStatus();
+ }
+
+ // do egress (client) next
+ try {
+ result.egress =
+ clientSSLManager
+ ->parseAndValidatePeerCertificate(clientConn->sslSocket->native_handle(),
+ boost::none,
+ "localhost",
+ hostForLogging,
+ nullptr)
+ .get();
+ } catch (const DBException& ex) {
+ result.egress = ex.toStatus();
+ }
+
+ return result;
+}
+
+struct CertValidationTestCase {
+ std::string cafile;
+ std::string clusterCaFile;
+ bool pass;
+ bool allowInvalidCerts{false};
+
+ void serialize(BSONObjBuilder* bob) const {
+ bob->append("CAFile", cafile);
+ bob->append("clusterCAFile", clusterCaFile);
+ bob->append("expectPass", pass);
+ bob->append("allowInvalidCerts", allowInvalidCerts);
+ }
+};
+
+void checkValidationResults(SSLTestFixture::IngressEgressValidationResult& result,
+ bool expectIngressPass,
+ bool expectEgressPass,
+ ErrorCodes::Error expectIngressCode = ErrorCodes::SSLHandshakeFailed,
+ ErrorCodes::Error expectEgressCode = ErrorCodes::SSLHandshakeFailed) {
+ ASSERT_EQ(result.ingress.isOK(), expectIngressPass)
+ << "Ingress validation status: " << result.ingress.getStatus();
+ ASSERT_EQ(result.egress.isOK(), expectEgressPass)
+ << "Egress validation status: " << result.egress.getStatus();
+ if (!result.ingress.isOK()) {
+ ASSERT_EQ(result.ingress.getStatus().code(), expectIngressCode)
+ << "Ingress validation status: " << result.ingress.getStatus();
+ }
+ if (!result.egress.isOK()) {
+ ASSERT_EQ(result.egress.getStatus().code(), expectEgressCode)
+ << "Egress validation status: " << result.egress.getStatus();
+ }
+}
+
+// Tests that validation fails if configured CRL for the issuer of the peer certificate being
+// validated has expired.
+// Caveats:
+// - Apple: CRL unsupported; test disabled
+// - Windows: validation fails, but with misleading error message
+#if MONGO_CONFIG_SSL_PROVIDER != MONGO_CONFIG_SSL_PROVIDER_APPLE
+TEST(SSLManager, expiredCRLTest) {
+ SSLParams clientParams;
+ clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL);
+ clientParams.sslAllowInvalidHostnames = true;
+ clientParams.sslCAFile = caFile;
+ clientParams.sslPEMKeyFile = clientKeyFile;
+ clientParams.sslCRLFile = expiredCRL;
+
+ SSLParams serverParams;
+ serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL);
+ serverParams.sslAllowInvalidHostnames = true;
+ serverParams.sslCAFile = caFile;
+ serverParams.sslPEMKeyFile = serverKeyFile;
+ serverParams.sslCRLFile = expiredCRL;
+
+ SSLTestFixture tf(serverParams, clientParams);
+ tf.doHandshake();
+ auto result = tf.runIngressEgressValidation();
+ checkValidationResults(result, false /*expectIngressPass*/, false /*expectEgressPass*/);
+
+#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS
+ constexpr const char* cause = "revocation server was offline";
+#else
+ constexpr const char* cause = "expired";
+#endif
+ ASSERT_NE(result.ingress.getStatus().reason().find(cause), std::string::npos);
+ ASSERT_NE(result.egress.getStatus().reason().find(cause), std::string::npos);
+}
+
+// Tests basic CRL revocation works on ingress if the client is configured with a revoked key.
+// Caveats:
+// - Apple: CRL unsupported; test disabled
+TEST(SSLManager, basicCRLRevocationTests) {
+ struct TestCase {
+ std::string serverCRLFile;
+ bool serverPass;
+ void serialize(BSONObjBuilder* bob) const {
+ bob->append("serverCRLFile", serverCRLFile);
+ bob->append("serverPass", serverPass);
+ }
+ };
+
+ SSLParams clientParams;
+ clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL);
+ clientParams.sslAllowInvalidHostnames = true;
+ clientParams.sslCAFile = trustedCaFile;
+ clientParams.sslPEMKeyFile = revokedClientKeyFile;
+
+ SSLParams serverParams;
+ serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL);
+ serverParams.sslAllowInvalidHostnames = true;
+ serverParams.sslCAFile = caFile;
+ serverParams.sslPEMKeyFile = trustedServerKeyFile;
+
+ {
+ serverParams.sslCRLFile = emptyCRL;
+ LOGV2(9476702, "Running test case", "CRLFile"_attr = emptyCRL, "pass"_attr = true);
+ SSLTestFixture tf(serverParams, clientParams);
+ tf.doHandshake();
+ auto result = tf.runIngressEgressValidation();
+ checkValidationResults(result, true, true /*expectEgressPass*/);
+ }
+ {
+ serverParams.sslCRLFile = clientRevokedCRL;
+ LOGV2(9476703, "Running test case", "CRLFile"_attr = clientRevokedCRL, "pass"_attr = false);
+ SSLTestFixture tf(serverParams, clientParams);
+ tf.doHandshake();
+ auto result = tf.runIngressEgressValidation();
+ checkValidationResults(result, false, true /*expectEgressPass*/);
+ ASSERT_NE(result.ingress.getStatus().reason().find("revoked"), std::string::npos);
+ }
+}
+
+// Tests whether validation passes if an intermediate CA issuer cert is revoked, but
+// the end-entity cert is not.
+// Caveats:
+// - Apple: CRL unsupported; test disabled
+// - Windows: multiple CRLs (root CRL + intermediate CRL) is not allowed
+// TODO: backport SERVER-95583
+TEST(SSLManager, revocationWithCRLsIntermediateTests) {
+ // intermediate-ca-B.pem + intermediate-ca-B-leaf.pem bundle
+ const std::string intermediateBLeafWithIssuerCertKeyFile = combinePEMFiles(
+ {{intermediateBLeafKeyFile, true /*includePrivKey*/}, {intermediateBCaFile}});
+ // crl_from_intermediate_ca_B.pem + crl_intermediate_ca_B_revoked.pem
+ const std::string crlsFromRootAndIntermediateB =
+ combinePEMFiles({{intermediateBRevokedCRL}, {intermediateBCRL}});
+
+ SSLParams clientParams;
+ clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL);
+ clientParams.sslAllowInvalidHostnames = true;
+ clientParams.sslCAFile = caFile;
+ clientParams.sslPEMKeyFile = clientKeyFile;
+ clientParams.sslCRLFile = crlsFromRootAndIntermediateB;
+
+ SSLParams serverParams;
+ serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL);
+ serverParams.sslAllowInvalidHostnames = true;
+ serverParams.sslCAFile = caFile;
+ serverParams.sslPEMKeyFile = intermediateBLeafWithIssuerCertKeyFile;
+
+#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS
+ ASSERT_THROWS_CODE_AND_WHAT(
+ SSLManagerInterface::create(clientParams, true),
+ DBException,
+ ErrorCodes::InvalidSSLConfiguration,
+ "CertAddCRLContextToStore Failed The object or property already exists.");
+#else
+ SSLTestFixture tf(serverParams, clientParams);
+ tf.doHandshake();
+ auto result = tf.runIngressEgressValidation();
+ checkValidationResults(result, true, false);
+ ASSERT_NE(result.egress.getStatus().reason().find("revoked"), std::string::npos);
+#endif
+}
+
+#endif // MONGO_CONFIG_SSL_PROVIDER != MONGO_CONFIG_SSL_PROVIDER_APPLE
+#endif // MONGO_CONFIG_SSL
} // namespace
} // namespace mongo