diff --git a/jstests/libs/os_helpers.js b/jstests/libs/os_helpers.js index dceb149af08..599b6413e47 100644 --- a/jstests/libs/os_helpers.js +++ b/jstests/libs/os_helpers.js @@ -230,3 +230,43 @@ BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2022" export function isAmazon2023() { return isDistroVersion("amzn", "2022") || isDistroVersion("amzn", "2023"); } + +/** + * Determine whether the Windows host supports TLS 1.3 for clients. + * Prefer querying the registry key created for TLS 1.3 support and fall + * back to parsing `systeminfo` for Windows 11 / Windows Server 2022. + */ +export function windowsSupportsTLS13() { + if (!_isWindows()) { + return false; + } + + // First, try to query the registry key for TLS 1.3 client settings. + try { + clearRawMongoProgramOutput(); + const regPath = + "HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\SCHANNEL\\Protocols\\TLS 1.3\\Client"; + const rc = runProgram("cmd.exe", "/c", "reg", "query", regPath); + clearRawMongoProgramOutput(); + if (rc === 0) { + return true; + } + } catch (e) { + // ignore and fall back to systeminfo parsing + } + + // Fallback: parse `systeminfo` output for Windows 11 / Windows Server 2022. + try { + clearRawMongoProgramOutput(); + runProgram("cmd.exe", "/c", "systeminfo"); + const sysinfo = rawMongoProgramOutput(".*"); + clearRawMongoProgramOutput(); + if (sysinfo.includes("Windows 11") || sysinfo.includes("Windows Server 2022")) { + return true; + } + } catch (e) { + // If we can't determine, conservatively return false. + } + + return false; +} diff --git a/jstests/ssl/libs/ssl_helpers.js b/jstests/ssl/libs/ssl_helpers.js index 94f0dfcc0bb..6702ac75cfe 100644 --- a/jstests/ssl/libs/ssl_helpers.js +++ b/jstests/ssl/libs/ssl_helpers.js @@ -1,6 +1,6 @@ import "jstests/multiVersion/libs/multi_rs.js"; -import {isDebian, isRHEL8, isUbuntu, isUbuntu2004} from "jstests/libs/os_helpers.js"; +import {isDebian, isRHEL8, isUbuntu, isUbuntu2004, windowsSupportsTLS13} from "jstests/libs/os_helpers.js"; import {ShardingTest} from "jstests/libs/shardingtest.js"; import {basicReplsetTest} from "jstests/replsets/libs/basic_replset_test.js"; @@ -404,10 +404,13 @@ export function clientSupportsTLS1_2() { } export function clientSupportsTLS1_3() { - // SERVER-98279: support tls 1.3 for windows & apple - if (determineSSLProvider() === "apple" || determineSSLProvider() === "windows") { + // SERVER-121261: support tls 1.3 for apple + if (determineSSLProvider() === "apple") { return false; } + if (determineSSLProvider() === "windows") { + return windowsSupportsTLS13(); + } const opensslVersion = opensslVersionAsInt(); return opensslVersion === undefined ? true : opensslVersion >= 0x1010100; // 1.1.1 } diff --git a/jstests/ssl/openssl_ciphersuites.js b/jstests/ssl/openssl_ciphersuites.js index b50df8ca94a..7570946bc79 100644 --- a/jstests/ssl/openssl_ciphersuites.js +++ b/jstests/ssl/openssl_ciphersuites.js @@ -3,13 +3,18 @@ import {detectDefaultTLSProtocol, determineSSLProvider} from "jstests/ssl/libs/ssl_helpers.js"; -// Short circuits for system configurations that do not support this setParameter, (i.e. OpenSSL -// that don't support TLS 1.3) -if (determineSSLProvider() !== "openssl") { - jsTestLog("SSL provider is not OpenSSL; skipping test."); +// Short circuit for system configurations that do not support this setParameter. +// This test is OpenSSL-only: opensslCipherSuiteConfig maps directly to +// SSL_CTX_set_ciphersuites(), which allows configuring TLS 1.3 cipher suites including +// ones that are compiled in but disabled by default (e.g. TLS_AES_128_CCM_8_SHA256). +// Windows SChannel does not expose equivalent per-cipher-suite TLS 1.3 configuration, so +// this test is skipped on Windows. +const _provider = determineSSLProvider(); +if (_provider !== "openssl") { + jsTest.log.info("SSL provider does not support this test; skipping."); quit(); } else if (detectDefaultTLSProtocol() !== "TLS1_3") { - jsTestLog("Platform does not support TLS 1.3; skipping test."); + jsTest.log.info("Platform does not support TLS 1.3; skipping test."); quit(); } @@ -39,7 +44,7 @@ function testConn() { } // test a successful connection when setting cipher suites -jsTestLog("Testing for successful connection with valid cipher suite config"); +jsTest.log.info("Testing for successful connection with valid cipher suite config"); let mongod = MongoRunner.runMongod( Object.merge(baseParams, {setParameter: {opensslCipherSuiteConfig: "TLS_AES_256_GCM_SHA384"}}), ); @@ -47,7 +52,7 @@ assert.soon(testConn, "Client could not connect to server with valid ciphersuite MongoRunner.stopMongod(mongod); // test an unsuccessful connection when mandating a cipher suite which OpenSSL disables by default -jsTestLog("Testing for unsuccessful connection with cipher suite config which OpenSSL disables by default."); +jsTest.log.info("Testing for unsuccessful connection with cipher suite config which OpenSSL disables by default."); mongod = MongoRunner.runMongod( Object.merge(baseParams, {setParameter: {opensslCipherSuiteConfig: "TLS_AES_128_CCM_8_SHA256"}}), ); diff --git a/jstests/ssl/ssl_alert_reporting.js b/jstests/ssl/ssl_alert_reporting.js index b016391fd58..15d56382656 100644 --- a/jstests/ssl/ssl_alert_reporting.js +++ b/jstests/ssl/ssl_alert_reporting.js @@ -1,6 +1,7 @@ // Ensure that TLS version alerts are correctly propagated import {determineSSLProvider, sslProviderSupportsTLS1_1} from "jstests/ssl/libs/ssl_helpers.js"; +import {windowsSupportsTLS13} from "jstests/libs/os_helpers.js"; const clientOptions = [ "--tls", @@ -22,8 +23,22 @@ function runTest(serverDisabledProtos, clientDisabledProtos) { expectedRegex = /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: SocketException: .*(tlsv1 alert protocol version|tlsv1 alert internal error|short read)/; } else if (implementation === "windows") { - expectedRegex = - /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: .*Connection reset by peer/; + // Pick protocol-specific expectations for each scenario instead of using one broad regex. + // TLS 1.3 mismatches: + // - server only TLS 1.3, client only TLS 1.2 -> Connection closed by peer + // - server only TLS 1.2, client only TLS 1.3 -> SEC_E_ALGORITHM_MISMATCH text + // Legacy TLS 1.2 mismatch path: + // - Connection reset by peer + if (serverDisabledProtos === "TLS1_2" && clientDisabledProtos === "TLS1_3") { + expectedRegex = + /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: .*Connection closed by peer/; + } else if (serverDisabledProtos === "TLS1_3" && clientDisabledProtos === "TLS1_2") { + expectedRegex = + /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: .*cannot communicate, because they do not possess a common algorithm/; + } else { + expectedRegex = + /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: .*Connection reset by peer/; + } } else if (implementation === "apple") { expectedRegex = /Error: couldn't connect to server .*:[0-9]*, connection attempt failed: HostUnreachable: futurize.* Connection closed by peer.*/; @@ -63,8 +78,9 @@ function runTest(serverDisabledProtos, clientDisabledProtos) { // Client receives and reports a protocol version alert if it advertises a protocol older than // the server's oldest supported protocol -if (!sslProviderSupportsTLS1_1()) { - // On platforms that disable TLS 1.1, assume they have TLS 1.3 for this test. +// Run TLS 1.3 scenarios on platforms that either disable TLS1.1 (legacy distros) or on +// Windows hosts that explicitly support TLS 1.3. +if (!sslProviderSupportsTLS1_1() || (determineSSLProvider() === "windows" && windowsSupportsTLS13())) { // Server disables TLS 1.2, client disables TLS 1.3 runTest("TLS1_2", "TLS1_3"); diff --git a/jstests/ssl/ssl_count_protocols.js b/jstests/ssl/ssl_count_protocols.js index 5f61cebf093..9e50f45f32d 100644 --- a/jstests/ssl/ssl_count_protocols.js +++ b/jstests/ssl/ssl_count_protocols.js @@ -3,6 +3,7 @@ import { detectDefaultTLSProtocol, sslProviderSupportsTLS1_0, sslProviderSupportsTLS1_1, + clientSupportsTLS1_3, } from "jstests/ssl/libs/ssl_helpers.js"; let SERVER_CERT = getX509Path("server.pem"); @@ -69,7 +70,7 @@ function runTestWithoutSubset(client) { "assert.eq(db.serverStatus().transportSecurity, a);", ); - if (expectedDefaultProtocol === "TLS1_2" && client === "TLS1_3") { + if (!clientSupportsTLS1_3() && client === "TLS1_3") { // If the runtime environment does not support TLS 1.3, a client cannot connect to a // server if TLS 1.3 is its only usable protocol version. assert.neq(0, exitStatus, "A client which does not support TLS 1.3 should not be able to connect with it"); @@ -117,4 +118,6 @@ if (sslProviderSupportsTLS1_1()) { runTestWithoutSubset("TLS1_1"); } runTestWithoutSubset("TLS1_2"); -runTestWithoutSubset("TLS1_3"); +if (clientSupportsTLS1_3()) { + runTestWithoutSubset("TLS1_3"); +} diff --git a/jstests/ssl/ssl_ingress_conn_metrics.js b/jstests/ssl/ssl_ingress_conn_metrics.js index c68bc37dfd3..14fb77254fc 100644 --- a/jstests/ssl/ssl_ingress_conn_metrics.js +++ b/jstests/ssl/ssl_ingress_conn_metrics.js @@ -4,6 +4,7 @@ * @tags: [requires_fcv_63] */ import {detectDefaultTLSProtocol, determineSSLProvider} from "jstests/ssl/libs/ssl_helpers.js"; +import {windowsSupportsTLS13} from "jstests/libs/os_helpers.js"; // Short circuits for system configurations that do not support this setParameter, (i.e. OpenSSL // versions that don't support TLS 1.3) @@ -79,9 +80,9 @@ let runTest = (connectionHealthLoggingOn) => { break; case "windows": logId = 6723802; - // This cipher is chosen to represent the cipher negotiated by Windows Server 2019 - // by default. - cipherSuite = "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; + // On Windows Server 2022+ with TLS 1.3, the default negotiated cipher is a TLS 1.3 + // suite. On older Windows (e.g. Server 2019), TLS 1.2 is the default. + cipherSuite = windowsSupportsTLS13() ? "TLS_AES_256_GCM_SHA384" : "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; break; case "apple": logId = 6723803; diff --git a/jstests/ssl/ssl_restricted_protocols.js b/jstests/ssl/ssl_restricted_protocols.js index 80431152da1..908f1c1a413 100644 --- a/jstests/ssl/ssl_restricted_protocols.js +++ b/jstests/ssl/ssl_restricted_protocols.js @@ -41,11 +41,14 @@ function runTestWithoutSubset(subset) { runTestWithoutSubset(["TLS1_0"]); runTestWithoutSubset(["TLS1_0", "TLS1_1"]); -if (determineSSLProvider() === "openssl" && (!supportsTLS1_2 || supportsTLS1_3)) { +const sslProvider = determineSSLProvider(); + +if (sslProvider === "openssl" && (!supportsTLS1_2 || supportsTLS1_3)) { runTestWithoutSubset(["TLS1_2"]); } -if (determineSSLProvider() === "openssl" && supportsTLS1_3) { +// TLS 1.3 tests - run for OpenSSL and Windows (SChannel) when TLS 1.3 is supported +if ((sslProvider === "openssl" || sslProvider === "windows") && supportsTLS1_3) { runTestWithoutSubset(["TLS1_3"]); runTestWithoutSubset(["TLS1_0", "TLS1_1", "TLS1_2"]); } diff --git a/jstests/ssl/tls13_windows.js b/jstests/ssl/tls13_windows.js new file mode 100644 index 00000000000..dd6463be2ca --- /dev/null +++ b/jstests/ssl/tls13_windows.js @@ -0,0 +1,203 @@ +/** + * Windows-specific TLS 1.3 (Schannel) correctness tests. + * + * Covers gaps not exercised by the KMIP-based integration tests, which only exercise + * the mongod-as-TLS-client path. This file covers: + * + * 1. mongod as TLS 1.3 server — inbound connection negotiates TLS 1.3 cipher. + * 2. Mutual TLS 1.3 — client certificate required and presented. + * 3. Post-handshake stability — multiple sequential round-trips over a single TLS 1.3 + * connection, exercising the Schannel NewSessionTicket / 0x80090317 code path on + * every read after the handshake. + * 4. TLS 1.3-only server — rejects a TLS 1.2-only client. + * 5. TLS 1.3-only client — rejects a TLS 1.2-only server. + * + */ + +import {determineSSLProvider} from "jstests/ssl/libs/ssl_helpers.js"; +import {windowsSupportsTLS13} from "jstests/libs/os_helpers.js"; + +if (determineSSLProvider() !== "windows" || !windowsSupportsTLS13()) { + jsTest.log.info("Skipping: not running on Windows with TLS 1.3 support."); + quit(); +} + +const CA_CERT = getX509Path("ca.pem"); +const SERVER_CERT = getX509Path("server.pem"); +const CLIENT_CERT = getX509Path("client.pem"); + +// Log id emitted by the Windows Schannel accept path when a TLS connection is established. +const kWindowsTLSAcceptedLogId = 6723802; +const kTLS13Cipher = "TLS_AES_256_GCM_SHA384"; + +// Log id emitted at debug level 0 by decryptBuffer when DecryptMessage returns 0x80090317. +// This fires when Schannel (as TLS client) receives a NewSessionTicket from an OpenSSL peer +// and the fallback TLS record header parser is used. It will NOT fire in the +// Schannel-to-Schannel scenario of this test file; the KMIP integration tests exercise it. +const kTLS13PostHandshakeLogId = 7998029; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function mongoClientArgs(port, extraArgs) { + return [ + "mongo", + "--tls", + "--tlsAllowInvalidHostnames", + "--tlsCertificateKeyFile", + CLIENT_CERT, + "--tlsCAFile", + CA_CERT, + "--port", + port, + ...(extraArgs || []), + ]; +} + +// --------------------------------------------------------------------------- +// Test 1: mongod as TLS 1.3 server — inbound connection +// +// Start mongod with no protocol restriction so it will offer TLS 1.3. +// Connect with a client that disables TLS 1.0/1.1/1.2, forcing TLS 1.3. +// Verify the server log records the TLS 1.3 cipher suite. +// --------------------------------------------------------------------------- +{ + jsTest.log.info("Test 1: TLS 1.3 server accepts inbound TLS 1.3 connection"); + + const mongod = MongoRunner.runMongod({ + tlsMode: "requireTLS", + tlsCertificateKeyFile: SERVER_CERT, + tlsCAFile: CA_CERT, + }); + + const rc = runMongoProgram( + ...mongoClientArgs(mongod.port, [ + "--tlsDisabledProtocols", + "TLS1_0,TLS1_1,TLS1_2", + "--eval", + "assert.commandWorked(db.adminCommand({hello: 1}));", + ]), + ); + assert.eq(0, rc, "TLS 1.3 client should connect successfully to TLS 1.3 server"); + + assert.soon( + () => checkLog.checkContainsOnceJson(mongod, kWindowsTLSAcceptedLogId, {"cipher": kTLS13Cipher}), + `Expected TLS 1.3 cipher ${kTLS13Cipher} to appear in server log`, + ); + + MongoRunner.stopMongod(mongod); +} + +// --------------------------------------------------------------------------- +// Test 2: Mutual TLS 1.3 — client certificate required and verified +// --------------------------------------------------------------------------- +{ + jsTest.log.info("Test 2: TLS 1.3 mutual authentication with client certificate"); + + const mongod = MongoRunner.runMongod({ + tlsMode: "requireTLS", + tlsCertificateKeyFile: SERVER_CERT, + tlsCAFile: CA_CERT, + }); + + const rc = runMongoProgram( + ...mongoClientArgs(mongod.port, [ + "--tlsDisabledProtocols", + "TLS1_0,TLS1_1,TLS1_2", + "--eval", + "assert.commandWorked(db.adminCommand({hello: 1}));", + ]), + ); + assert.eq(0, rc, "Mutual TLS 1.3 connection with client certificate should succeed"); + + MongoRunner.stopMongod(mongod); +} + +// --------------------------------------------------------------------------- +// Test 3: Post-handshake stability — multiple round-trips over one connection +// +// After the TLS 1.3 handshake the peer sends NewSessionTicket records. On +// Windows Server 2022 Schannel returns 0x80090317 (error-form SEC_I_CONTEXT_EXPIRED) +// from DecryptMessage when it consumes one; the read manager must skip the ticket +// and correctly surface the following application-data records. +// +// Drive 20 sequential ping commands over the same shell process connection to +// create several opportunities for post-handshake records to arrive mid-read. +// +// Note: 0x80090317 (logged as kTLS13PostHandshakeLogId) fires only when Schannel +// acts as a TLS *client* receiving a NewSessionTicket from an OpenSSL server +// (e.g. the KMIP integration tests). In this Schannel-to-Schannel test mongod is +// the server, so SEC_I_RENEGOTIATE is used instead. The ping-loop success below +// remains the definitive check for post-handshake read stability. +// --------------------------------------------------------------------------- +{ + jsTest.log.info("Test 3: Post-handshake stability — 20 sequential round-trips over TLS 1.3"); + + const mongod = MongoRunner.runMongod({ + tlsMode: "requireTLS", + tlsCertificateKeyFile: SERVER_CERT, + tlsCAFile: CA_CERT, + }); + + const pingLoop = ` + for (let i = 0; i < 20; i++) { + assert.commandWorked(db.adminCommand({ping: 1}), + "ping " + i + " should succeed over persistent TLS 1.3 connection"); + } + `; + + const rc = runMongoProgram( + ...mongoClientArgs(mongod.port, ["--tlsDisabledProtocols", "TLS1_0,TLS1_1,TLS1_2", "--eval", pingLoop]), + ); + assert.eq(0, rc, "All 20 round-trips over a single TLS 1.3 connection should succeed"); + + assert.soon( + () => checkLog.checkContainsOnceJson(mongod, kWindowsTLSAcceptedLogId, {"cipher": kTLS13Cipher}), + `Expected TLS 1.3 cipher ${kTLS13Cipher} in server log for post-handshake test`, + ); + + MongoRunner.stopMongod(mongod); +} + +// --------------------------------------------------------------------------- +// Test 4: TLS 1.3-only server rejects a TLS 1.2-only client +// --------------------------------------------------------------------------- +{ + jsTest.log.info("Test 4: TLS 1.3-only server rejects TLS 1.2-only client"); + + const mongod = MongoRunner.runMongod({ + tlsMode: "requireTLS", + tlsCertificateKeyFile: SERVER_CERT, + tlsCAFile: CA_CERT, + tlsDisabledProtocols: "TLS1_0,TLS1_1,TLS1_2", + }); + + const rc = runMongoProgram(...mongoClientArgs(mongod.port, ["--tlsDisabledProtocols", "TLS1_3", "--eval", ";"])); + assert.neq(0, rc, "TLS 1.2-only client should fail to connect to a TLS 1.3-only server"); + + MongoRunner.stopMongod(mongod); +} + +// --------------------------------------------------------------------------- +// Test 5: TLS 1.3-only client rejects a TLS 1.2-only server +// --------------------------------------------------------------------------- +{ + jsTest.log.info("Test 5: TLS 1.3-only client rejects TLS 1.2-only server"); + + const mongod = MongoRunner.runMongod({ + tlsMode: "requireTLS", + tlsCertificateKeyFile: SERVER_CERT, + tlsCAFile: CA_CERT, + tlsDisabledProtocols: "TLS1_3", + }); + + const rc = runMongoProgram( + ...mongoClientArgs(mongod.port, ["--tlsDisabledProtocols", "TLS1_0,TLS1_1,TLS1_2", "--eval", ";"]), + ); + assert.neq(0, rc, "TLS 1.3-only client should fail to connect to a TLS 1.2-only server"); + + MongoRunner.stopMongod(mongod); +} + +jsTest.log.info("All Windows TLS 1.3 server-side tests passed."); diff --git a/jstests/sslSpecial/tls_protocols.js b/jstests/sslSpecial/tls_protocols.js index 5a5aa2a2cbe..afa4527ea0c 100644 --- a/jstests/sslSpecial/tls_protocols.js +++ b/jstests/sslSpecial/tls_protocols.js @@ -108,7 +108,7 @@ const tlsSupport = (supportsTLS1_1 ? TLS_1_1 : 0) | (supportsTLS1_2 ? TLS_1_2 : 0) | (supportsTLS1_3 ? TLS_1_3 : 0) - : TLS_1_0 | TLS_1_1 | TLS_1_2; + : TLS_1_0 | TLS_1_1 | TLS_1_2 | (supportsTLS1_3 ? TLS_1_3 : 0); const defaultServerDisable = determineSSLProvider() === "openssl" ? TLS_1_0 | TLS_1_1 : 0; const defaultClientDisable = (supportsTLS1_2 ? TLS_1_1 : 0) | (supportsTLS1_2 || supportsTLS1_1 ? TLS_1_0 : 0); @@ -123,15 +123,32 @@ function shouldStart(serverDisable) { return false; } } - // All valid TLS modes are disabled for Apple or Windows - if (determineSSLProvider() === "apple" || determineSSLProvider() === "windows") { + // All valid TLS modes are disabled for Apple + if (determineSSLProvider() === "apple") { if ((serverDisabledProtocols & (TLS_1_0 | TLS_1_1 | TLS_1_2)) === (TLS_1_0 | TLS_1_1 | TLS_1_2)) { - // apple & windows only check for 1.0, 1.1, 1.2 and ignores 1.3 - // SERVER-98279: support tls 1.3 for windows & apple + // apple only checks for 1.0, 1.1, 1.2 and ignores 1.3 + // SERVER-121261: support tls 1.3 for apple return false; } } + // Windows: if the platform supports TLS 1.3, include it in the "all disabled" check; + // otherwise behave like Apple and only check 1.0/1.1/1.2. + if (determineSSLProvider() === "windows") { + if (supportsTLS1_3) { + if ( + (serverDisabledProtocols & (TLS_1_0 | TLS_1_1 | TLS_1_2 | TLS_1_3)) === + (TLS_1_0 | TLS_1_1 | TLS_1_2 | TLS_1_3) + ) { + return false; + } + } else { + if ((serverDisabledProtocols & (TLS_1_0 | TLS_1_1 | TLS_1_2)) === (TLS_1_0 | TLS_1_1 | TLS_1_2)) { + return false; + } + } + } + // (Apple only) If the available protocols is not a continuous range if (determineSSLProvider() === "apple") { // ssl_manager_apple.cpp: 'Can not disable TLS 1.1 while leaving 1.0 and 1.2 enabled' @@ -196,9 +213,9 @@ if (determineSSLProvider() === "apple") { serverScenarios.forEach((serverDisable) => clientScenarios.forEach((clientDisable) => { - // SERVER-98279: support tls 1.3 for windows & apple + // SERVER-121261: support tls 1.3 for apple // skip any tests disabling TLS 1.3 since it's not supported for those platforms yet - if (determineSSLProvider() === "apple" || determineSSLProvider() === "windows") { + if (determineSSLProvider() === "apple") { if (serverDisable & TLS_1_3) return; } test( diff --git a/src/mongo/platform/windows_basic.h b/src/mongo/platform/windows_basic.h index ef4a6853999..ae8be408b01 100644 --- a/src/mongo/platform/windows_basic.h +++ b/src/mongo/platform/windows_basic.h @@ -96,13 +96,14 @@ #define CERT_CHAIN_PARA_HAS_EXTRA_FIELDS -#include - #undef WIN32_NO_STATUS // Obtain a definition for the ntstatus type. #include +#define SCHANNEL_USE_BLACKLISTS +#include + // Add back in the status definitions so that macro expansions for // things like STILL_ACTIVE and WAIT_OBJECT_O can be resolved (they // expand to STATUS_ codes). diff --git a/src/mongo/util/net/BUILD.bazel b/src/mongo/util/net/BUILD.bazel index b99802d9433..0c36b11634d 100644 --- a/src/mongo/util/net/BUILD.bazel +++ b/src/mongo/util/net/BUILD.bazel @@ -164,6 +164,10 @@ mongo_cc_library( srcs = [ "sock.cpp", ], + linkopts = select({ + "//bazel/config:ssl_provider_windows": ["ncrypt.lib"], + "//conditions:default": [], + }), srcs_select = [ { "//bazel/config:ssl_enabled": [ diff --git a/src/mongo/util/net/OWNERS.yml b/src/mongo/util/net/OWNERS.yml index 67c1c1ee3a4..55f2fce65a3 100644 --- a/src/mongo/util/net/OWNERS.yml +++ b/src/mongo/util/net/OWNERS.yml @@ -12,3 +12,6 @@ filters: - "*openssl*": approvers: - 10gen/server-security + - "README-windowstls.md": + approvers: + - 10gen/server-security diff --git a/src/mongo/util/net/README-windowstls.md b/src/mongo/util/net/README-windowstls.md new file mode 100644 index 00000000000..da471445cdb --- /dev/null +++ b/src/mongo/util/net/README-windowstls.md @@ -0,0 +1,261 @@ +# Windows TLS 1.3 (SChannel) Support + +This document covers MongoDB's TLS 1.3 support on Windows, which uses the +[SChannel SSP](https://docs.microsoft.com/en-us/windows-server/security/tls/tls-ssl-schannel-ssp-overview) +provided by the OS. It describes the features, design choices, and known limitations introduced by +SERVER-79980. + +## Table of Contents + +- [Platform Requirements](#platform-requirements) +- [What Changed](#what-changed) + - [SCH_CREDENTIALS replaces SCHANNEL_CRED](#sch_credentials-replaces-schannel_cred) + - [Protocol selection: blocklist instead of allowlist](#protocol-selection-blocklist-instead-of-allowlist) + - [CNG key storage replaces CAPI](#cng-key-storage-replaces-capi) + - [Client certificate handling](#client-certificate-handling) +- [Post-Handshake Message Handling](#post-handshake-message-handling) + - [SEC_I_RENEGOTIATE on TLS 1.3 (Schannel-to-Schannel)](#sec_i_renegotiate-on-tls-13-schannel-to-schannel) + - [0x80090317 from OpenSSL peers (Schannel-as-client)](#0x80090317-from-openssl-peers-schannel-as-client) + - [Why ISC/ASC must not be called after a post-handshake message](#why-iscasc-must-not-be-called-after-a-post-handshake-message) + - [TLS record header fallback](#tls-record-header-fallback) +- [Shutdown Handling](#shutdown-handling) +- [Known Limitations](#known-limitations) + - [NewSessionTickets from OpenSSL peers](#newsessiontickets-from-openssl-peers) + +--- + +## Platform Requirements + +TLS 1.3 in SChannel requires **Windows Server 2022 / Windows 11 (build 22000) or later**. Older +versions of Windows (including Windows Server 2019) do not expose TLS 1.3 through the SChannel API, +even if an OS update has added partial support. The helper function `windowsSupportsTLS13()` in +[jstests/libs/os_helpers.js](../../../../jstests/libs/os_helpers.js) detects TLS 1.3 availability by +querying the registry key `HKLM\SYSTEM\...\SCHANNEL\Protocols\TLS 1.3\Client` and falls back to +parsing `systeminfo` output for Windows 11 / Server 2022 host name strings. + +When TLS 1.3 is not available the server falls back to TLS 1.2, which remains fully supported. + +--- + +## What Changed + +### SCH_CREDENTIALS replaces SCHANNEL_CRED + +The legacy `SCHANNEL_CRED` credential structure (version 4) predates TLS 1.3 and does not support +it. TLS 1.3 requires the newer `SCH_CREDENTIALS` structure (version 5, introduced in Windows 10 1903 +/ Server 2022), which accepts a `TLS_PARAMETERS` array describing per-protocol constraints. + +**Before:** + +```cpp +SCHANNEL_CRED cred; +cred.dwVersion = SCHANNEL_CRED_VERSION; // 4 +cred.grbitEnabledProtocols = SP_PROT_TLS1_SERVER | SP_PROT_TLS1_2_SERVER; // allowlist +``` + +**After:** + +```cpp +TLS_PARAMETERS tlsParams = {}; +SCH_CREDENTIALS cred; +cred.dwVersion = SCH_CREDENTIALS_VERSION; // 5 +cred.cTlsParameters = 1; +cred.pTlsParameters = &tlsParams; +tlsParams.grbitDisabledProtocols = SP_PROT_TLS1_0 | SP_PROT_TLS1_1; // blocklist +``` + +Crucially, `SCH_CREDENTIALS` with an empty `grbitDisabledProtocols` (zero) means _all_ protocols +supported by the OS are enabled, including TLS 1.3. The old allowlist approach would never enable +TLS 1.3 because the old constants did not include a TLS 1.3 bit. + +The `--tlsDisabledProtocols` option now supports `TLS1_3` as a value, which sets `SP_PROT_TLS1_3` in +`grbitDisabledProtocols`. + +See [ssl_manager_windows.cpp](ssl_manager_windows.cpp) — `initSSLContext()`. + +### Protocol selection: blocklist instead of allowlist + +The old code built a protocol allowlist via bitwise OR of per-direction constants +(`SP_PROT_TLS1_2_SERVER`, `SP_PROT_TLS1_2_CLIENT`, etc.) and assigned it to `grbitEnabledProtocols`. +The new code uses a single direction-agnostic `grbitDisabledProtocols` bitmask in `TLS_PARAMETERS` — +new protocols added to Windows in future releases are automatically available without code changes, +and the bitmask is the same for both server and client directions. + +### CNG key storage replaces CAPI + +`SCH_CREDENTIALS` requires private keys to be stored in the **Cryptography API: Next Generation +(CNG)** key store (`NCrypt`). The legacy CAPI (`CRYPT_ACQUIRE_CONTEXT` / `CryptImportKey`) keys used +with `SCHANNEL_CRED` are not accepted by `SCH_CREDENTIALS`. + +`readCertPEMFile()` in [ssl_manager_windows.cpp](ssl_manager_windows.cpp) was rewritten to: + +1. Parse the PEM file and extract the raw RSA private key bytes. +2. Call `NCryptOpenStorageProvider(MS_KEY_STORAGE_PROVIDER)` to open the CNG software KSP. +3. Call `NCryptImportKey` with a PID-scoped named container (`LEGACY_RSAPRIVATE_BLOB`, + `NCRYPT_OVERWRITE_KEY_FLAG`). +4. Associate the key with the certificate context via `CERT_KEY_PROV_INFO_PROP_ID`, storing only the + container name and provider name — **not** a live key handle. + +Storing only the name blob (prop ID 2) rather than a live handle (prop IDs 78/99) avoids a slow +KSP-wide key enumeration that CertGetCertificateContextProperty would trigger when a handle is not +yet cached. Schannel opens the named container directly via `NCryptOpenKey` at handshake time. + +Container names are scoped to the process ID and a monotonically increasing counter, so multiple TLS +contexts within the same process do not collide. The `UniqueNcryptKeyWithDeletion` RAII wrapper +calls `NCryptDeleteKey` on destruction, cleaning up ephemeral key material when the +`SSLManagerWindows` is torn down. + +### Client certificate handling + +`InitializeSecurityContextW` accepts an `ISC_REQ_USE_SUPPLIED_CREDS` flag that restricts SChannel to +the credentials in `_cred->paCred`. When no client certificate is configured (`cCreds == 0`), +setting this flag causes a deadlock: SChannel sends an empty `CertificateRequest` response for TLS +1.3 mutual-auth servers, then hangs waiting for a reply that never comes. + +The fix ([ssl/detail/schannel.hpp](ssl/detail/schannel.hpp), `getClientFlags()`): + +```cpp +DWORD flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY | + ISC_REQ_EXTENDED_ERROR | ISC_REQ_STREAM | ISC_REQ_MANUAL_CRED_VALIDATION; +if (_cred->cCreds > 0) { + flags |= ISC_REQ_USE_SUPPLIED_CREDS; +} +``` + +When no certificate is configured, SChannel falls back to its normal store-search behaviour and +sends an empty `Certificate` message, allowing the TLS 1.3 handshake to complete. + +--- + +## Post-Handshake Message Handling + +TLS 1.3 introduces _post-handshake messages_ — records exchanged over the established connection +after the handshake completes. The two cases MongoDB encounters are: + +- **NewSessionTicket (NST)**: the server distributes session-resumption material to the client. + OpenSSL sends 2 NSTs by default immediately after the handshake. +- **KeyUpdate**: a peer requests traffic-key rotation. + +SChannel handles these internally but surfaces them to the application through `DecryptMessage` +return values. Two different status codes are used depending on which side of the connection +SChannel is on. + +### SEC_I_RENEGOTIATE on TLS 1.3 (Schannel-to-Schannel) + +When SChannel is the **server** (or when both peers are SChannel), `DecryptMessage` returns +`SEC_I_RENEGOTIATE` (0x00090316) after consuming a post-handshake record. + +Pre-TLS 1.3, this code meant the peer requested _renegotiation_ (via `HelloRequest`), which MongoDB +blocks. On TLS 1.3, renegotiation was removed from the specification entirely; `SEC_I_RENEGOTIATE` +is reused to signal post-handshake messages instead. + +The code in [ssl/detail/impl/schannel.ipp](ssl/detail/impl/schannel.ipp) (`decryptBuffer`) +distinguishes the two cases by querying `SECPKG_ATTR_CONNECTION_INFO`: + +```cpp +const bool isTLS13 = (qi == SEC_E_OK) && ((connInfo.dwProtocol & SP_PROT_TLS1_3) != 0); +if (!isTLS13) { + *pDecryptState = DecryptState::Renegotiate; + ec = asio::ssl::error::no_renegotiation; + return ssl_want::want_nothing; +} +// TLS 1.3: post-handshake message consumed internally — no ISC/ASC call needed. +``` + +Any trailing bytes indicated by `SECBUFFER_EXTRA` are preserved in `_pExtraEncryptedBuffer` and +injected at the head of the next decryption loop iteration. + +### 0x80090317 from OpenSSL peers (Schannel-as-client) + +When SChannel acts as a **TLS client** connecting to an **OpenSSL server** (e.g. the PyKMIP server +used for Encrypted Storage Engine tests), `DecryptMessage` returns `0x80090317` instead of +`SEC_I_RENEGOTIATE` when it consumes a TLS 1.3 NewSessionTicket. + +`0x80090317` is the _error-severity_ form of `SEC_I_CONTEXT_EXPIRED` (defined in `winerror.h`). +Microsoft does not appear to have published documentation that explicitly describes this behaviour; +it is an observed quirk of SChannel on Windows Server 2022 when the client processes TLS 1.3 +post-handshake messages from an OpenSSL peer. It does not indicate a real error; the NST record has +been consumed internally, exactly as with `SEC_I_RENEGOTIATE`. + +The code checks the negative-status range to distinguish real failures from informational codes: + +```cpp +} else if (ss == static_cast(0x80090317L)) { + // NST consumed internally — preserve trailing bytes and retry. + ... + return ssl_want::want_input_and_retry; +} +``` + +### Why ISC/ASC must not be called after a post-handshake message + +It may seem natural to call `InitializeSecurityContextW` (ISC) or `AcceptSecurityContext` (ASC) +after `DecryptMessage` returns `SEC_I_RENEGOTIATE` or `0x80090317`, on the assumption that a +post-handshake message might require a response (e.g. a `KeyUpdate` acknowledgement). Doing so is +incorrect and must be avoided. + +Calling ISC or ASC at this point **silently rotates SChannel's application-layer traffic keys from K +to K+1** without any corresponding wire-level `KeyUpdate` message being sent to the peer. The peer +still encrypts with K, so the next `DecryptMessage` call fails with `SEC_E_DECRYPT_FAILURE` +(0x80090330, "The specified data could not be decrypted"). + +SChannel handles post-handshake messages entirely internally when `DecryptMessage` returns these +codes. The correct response is simply to retry the read. No ISC/ASC call is required or safe. + +### TLS record header fallback + +When `DecryptMessage` returns `0x80090317`, SChannel may **not** populate `SECBUFFER_EXTRA` with the +bytes that follow the consumed NST record — even if a subsequent TLS record (e.g. the KMIP +application-data response) arrived in the same `recv()` call. + +If `SECBUFFER_EXTRA` is absent, the code falls back to parsing the 5-byte TLS record header manually +to locate the boundary: + +``` +ContentType[1] LegacyVersion[2] PayloadLength[2] +totalRecordBytes = 5 + big_endian(bytes[3..4]) +``` + +The pre-call input pointer and size are used (not the post-call `securityBuffers[0]` values) because +`DecryptMessage` overwrites `securityBuffers[0].cbBuffer` with the consumed NST size, which would +make `recordSize == inputLen` and lose the trailing bytes. + +This is the root cause fixed in SERVER-79980: a KMIP response and an NST arriving in the same +`recv()` buffer, with SChannel returning `0x80090317` for the NST and providing no `SECBUFFER_EXTRA` +pointer to the response. + +--- + +## Shutdown Handling + +`ApplyControlToken`, `AcceptSecurityContext`, and `InitializeSecurityContextW` may return +`SEC_I_CONTEXT_EXPIRED` (0x00090317, the _success-severity_ form) during TLS shutdown if the context +is already in the expired state (e.g. because a `close_notify` was already processed). This is not +an error. + +The shutdown paths in `SSLHandshakeManager::startShutdown()` use the same convention as +`decryptBuffer`: only negative `SECURITY_STATUS` values (where the high bit is set) are treated as +failures. `SEC_I_CONTEXT_EXPIRED` is a non-negative informational code and is accepted as success. + +--- + +## Known Limitations + +### NewSessionTickets from OpenSSL peers + +When MongoDB acts as a TLS 1.3 client connecting to an OpenSSL-backed server (e.g. the PyKMIP KMIP +server used in Encrypted Storage Engine tests), OpenSSL sends 2 NewSessionTickets by default +immediately after the handshake. Each NST causes SChannel to return `0x80090317` from +`DecryptMessage`. The code handles this correctly, but it means every new connection to a PyKMIP +server incurs two extra read/retry cycles before the first application-data response is surfaced. + +**Workaround for test environments**: The `kmip_server.py` wrapper accepts a `--no-session-tickets` +flag that sets `context.num_tickets = 0` on the Python SSL context, suppressing all TLS 1.3 NSTs +from the PyKMIP server. On Windows, `helpers.js` passes this flag automatically when the test runner +detects SChannel is in use (`isWindowsSchannel` in +[helpers.js](../../../../src/mongo/db/modules/enterprise/jstests/encryptdb/libs/helpers.js)). +Non-Windows platforms still receive NSTs, preserving test coverage of the NST handling path. + +> **Note**: `ssl.OP_NO_TICKET` in Python's ssl module suppresses TLS 1.2 _session ticket requests +> from the client_ and does **not** suppress TLS 1.3 NewSessionTicket messages sent by the server. +> Use `context.num_tickets = 0` (Python 3.8+) for the latter. diff --git a/src/mongo/util/net/README.md b/src/mongo/util/net/README.md index 28cd5c6313c..5ad528d532a 100644 --- a/src/mongo/util/net/README.md +++ b/src/mongo/util/net/README.md @@ -33,6 +33,8 @@ implementations of TLS: configurations anymore. 2. [SChannel](https://docs.microsoft.com/en-us/windows-server/security/tls/tls-ssl-schannel-ssp-overview) which is made by Microsoft and is avialable exclusively on _Windows_. + > **Windows TLS 1.3**: For a detailed description of Windows-specific TLS 1.3 support, design + > choices, and known limitations, see [README-windowstls.md](README-windowstls.md). 3. [Secure Transport](https://developer.apple.com/documentation/security/secure_transport) which is made by Apple and is available exclusively on _MacOS_. diff --git a/src/mongo/util/net/ssl/context_schannel.hpp b/src/mongo/util/net/ssl/context_schannel.hpp index 2dbbbcdfae0..ba749db8558 100644 --- a/src/mongo/util/net/ssl/context_schannel.hpp +++ b/src/mongo/util/net/ssl/context_schannel.hpp @@ -47,7 +47,7 @@ namespace ssl { class context : public context_base, private noncopyable { public: /// The native handle type of the SSL context. - typedef SCHANNEL_CRED* native_handle_type; + typedef SCH_CREDENTIALS* native_handle_type; /// Constructor. ASIO_DECL explicit context(method m); @@ -92,7 +92,8 @@ public: ASIO_DECL native_handle_type native_handle(); private: - SCHANNEL_CRED _cred; + TLS_PARAMETERS _tlsParams; + SCH_CREDENTIALS _cred; // The underlying native implementation. native_handle_type handle_; diff --git a/src/mongo/util/net/ssl/detail/engine_schannel.hpp b/src/mongo/util/net/ssl/detail/engine_schannel.hpp index bbbc1033901..f18bf058381 100644 --- a/src/mongo/util/net/ssl/detail/engine_schannel.hpp +++ b/src/mongo/util/net/ssl/detail/engine_schannel.hpp @@ -68,7 +68,7 @@ public: }; // Construct a new engine for the specified context. - ASIO_DECL explicit engine(SCHANNEL_CRED* context, const std::string& remoteHostName); + ASIO_DECL explicit engine(SCH_CREDENTIALS* context, const std::string& remoteHostName); // Destructor. ASIO_DECL ~engine(); @@ -120,7 +120,7 @@ private: CredHandle _hcred; // Credentials for TLS handshake - SCHANNEL_CRED* _pCred; + SCH_CREDENTIALS* _pCred; // TLS SNI server name std::wstring _remoteHostName; diff --git a/src/mongo/util/net/ssl/detail/impl/engine_schannel.ipp b/src/mongo/util/net/ssl/detail/impl/engine_schannel.ipp index 5f659acb8a9..b87fde77c81 100644 --- a/src/mongo/util/net/ssl/detail/impl/engine_schannel.ipp +++ b/src/mongo/util/net/ssl/detail/impl/engine_schannel.ipp @@ -30,6 +30,9 @@ #pragma once +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kNetwork + +#include "mongo/logv2/log.h" #include "mongo/platform/shared_library.h" #include "mongo/util/net/ssl/detail/engine.hpp" #include "mongo/util/net/ssl/error.hpp" @@ -46,8 +49,10 @@ namespace asio { namespace ssl { namespace detail { +// Bring in the _attr UDL (defined in mongo::literals) needed by LOGV2_DEBUG. +using namespace mongo::literals; -engine::engine(SCHANNEL_CRED* context, const std::string& remoteHostName) +engine::engine(SCH_CREDENTIALS* context, const std::string& remoteHostName) : _pCred(context), _remoteHostName(mongo::toNativeString(remoteHostName.c_str())), _inBuffer(kDefaultBufferSize), @@ -55,7 +60,7 @@ engine::engine(SCHANNEL_CRED* context, const std::string& remoteHostName) _extraBuffer(kDefaultBufferSize), _handshakeManager( &_hcxt, &_hcred, _remoteHostName, &_inBuffer, &_outBuffer, &_extraBuffer, _pCred), - _readManager(&_hcxt, &_hcred, &_inBuffer, &_extraBuffer), + _readManager(&_hcxt, &_hcred, &_inBuffer, &_extraBuffer, &_outBuffer, &_remoteHostName), _writeManager(&_hcxt, &_outBuffer) { SecInvalidateHandle(&_hcxt); SecInvalidateHandle(&_hcred); @@ -101,8 +106,23 @@ engine::want engine::handshake(stream_base::handshake_type type, asio::error_cod : SSLHandshakeManager::HandshakeMode::Server); SSLHandshakeManager::HandshakeState state; auto w = _handshakeManager.nextHandshake(ec, &state); - if (w == ssl_want::want_nothing || state == SSLHandshakeManager::HandshakeState::Done) { + if (!ec && + (w == ssl_want::want_nothing || state == SSLHandshakeManager::HandshakeState::Done)) { _state = EngineState::InProgress; + + // TLS 1.3: the peer may bundle application data alongside its final handshake + // flight (e.g. the client's Certificate+Finished + first MongoDB message arrive in + // one TCP segment). AcceptSecurityContext / InitializeSecurityContext leaves those + // encrypted bytes in _inBuffer, but the SSLReadManager is still in its initial + // NeedMoreEncryptedData state and will stall waiting for more network data. Signal + // it so readDecryptedData processes the leftover bytes immediately. + if (!_inBuffer.empty()) { + LOGV2_DEBUG(7998008, + 2, + "TLS handshake complete with leftover application data in input buffer", + "bytes"_attr = _inBuffer.size()); + _readManager.notifyHandshakeLeftoverData(); + } } return ssl_want_to_engine(w); diff --git a/src/mongo/util/net/ssl/detail/impl/schannel.ipp b/src/mongo/util/net/ssl/detail/impl/schannel.ipp index 8dbcc2ff032..25daa6efe05 100644 --- a/src/mongo/util/net/ssl/detail/impl/schannel.ipp +++ b/src/mongo/util/net/ssl/detail/impl/schannel.ipp @@ -30,7 +30,10 @@ #pragma once +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kNetwork + #include "mongo/base/init.h" +#include "mongo/logv2/log.h" #include "mongo/util/assert_util.h" #include @@ -43,6 +46,12 @@ namespace asio { namespace ssl { namespace detail { +// Bring in the _attr UDL (defined in mongo::literals) needed by LOGV2_DEBUG. +// MSVC requires the operator to be reachable via unqualified lookup; GCC/Clang +// are more permissive. Importing only the literals namespace keeps the +// pollution minimal. +using namespace mongo::literals; + /** * Start or continue SSL handshake. * @@ -82,7 +91,7 @@ ssl_want SSLHandshakeManager::nextHandshake(asio::error_code& ec, HandshakeState return ssl_want::want_nothing; } - want = doClientHandshake(ec); + want = doClientHandshake(ec, pHandshakeState); if (ec) { return want; } @@ -99,7 +108,7 @@ ssl_want SSLHandshakeManager::nextHandshake(asio::error_code& ec, HandshakeState if (_mode == HandshakeMode::Server) { want = doServerHandshake(ec, pHandshakeState); } else { - want = doClientHandshake(ec); + want = doClientHandshake(ec, pHandshakeState); } if (ec) { @@ -107,6 +116,12 @@ ssl_want SSLHandshakeManager::nextHandshake(asio::error_code& ec, HandshakeState } if (want == ssl_want::want_nothing || *pHandshakeState == HandshakeState::Done) { + LOGV2_DEBUG(7998001, + 2, + "TLS client handshake complete", + "want"_attr = static_cast(want), + "handshakeState"_attr = + (*pHandshakeState == HandshakeState::Done ? "Done" : "Continue")); setState(State::Done); } else { setState(State::NeedMoreHandshakeData); @@ -192,7 +207,17 @@ ssl_want SSLHandshakeManager::startShutdown(asio::error_code& ec) { SECURITY_STATUS ss = ApplyControlToken(_phctxt, &inputBufferDesc); - if (ss != SEC_E_OK) { + // Accept SEC_I_CONTEXT_EXPIRED (0x00090317) as a success: Schannel returns this informational + // code from ApplyControlToken when the context has already been marked expired (e.g. after + // processing a TLS 1.3 NewSessionTicket). Only negative SECURITY_STATUS values are real + // errors — the same pattern used for ISC/ASC and DecryptMessage throughout this file. + LOGV2_DEBUG(7998023, + 2, + "TLS shutdown: ApplyControlToken result", + "mode"_attr = (_mode == HandshakeMode::Server ? "server" : "client"), + "status"_attr = ss); + + if (ss < SEC_E_OK) { ec = asio::error_code(ss, asio::error::get_ssl_category()); return ssl_want::want_nothing; } @@ -216,7 +241,17 @@ ssl_want SSLHandshakeManager::startShutdown(asio::error_code& ec) { SECURITY_STATUS ss = AcceptSecurityContext( _phcred, _phctxt, NULL, attribs, 0, _phctxt, &outputBufferDesc, &attribs, &lifetime); - if (ss != SEC_E_OK) { + LOGV2_DEBUG(7998024, + 2, + "TLS shutdown: AcceptSecurityContext result", + "status"_attr = ss, + "outputBytes"_attr = outputBuffers[0].cbBuffer); + + // Accept SEC_I_CONTEXT_EXPIRED (0x00090317) as a success: it is an informational code + // returned by ASC when the context is already in the "expired" state (i.e. we are + // responding to a close_notify we already received). The check `ss < SEC_E_OK` mirrors + // the pattern used in decryptBuffer — only negative SECURITY_STATUS values are errors. + if (ss < SEC_E_OK) { ec = asio::error_code(ss, asio::error::get_ssl_category()); return ssl_want::want_nothing; } @@ -224,7 +259,7 @@ ssl_want SSLHandshakeManager::startShutdown(asio::error_code& ec) { _pOutBuffer->reset(); _pOutBuffer->append(outputBuffers[0].pvBuffer, outputBuffers[0].cbBuffer); - if (SEC_E_OK == ss && outputBuffers[0].cbBuffer != 0) { + if (outputBuffers[0].cbBuffer != 0) { ec = asio::error::eof; return ssl_want::want_output; } else { @@ -247,7 +282,17 @@ ssl_want SSLHandshakeManager::startShutdown(asio::error_code& ec) { &ContextAttributes, &lifetime); - if (ss != SEC_E_OK) { + LOGV2_DEBUG(7998025, + 2, + "TLS shutdown: InitializeSecurityContext result", + "status"_attr = ss, + "outputBytes"_attr = outputBuffers[0].cbBuffer); + + // Accept SEC_I_CONTEXT_EXPIRED (0x00090317) as a success: it is an informational code + // returned by ISC when the context is already in the "expired" state (i.e. we are + // responding to a close_notify we already received). The check `ss < SEC_E_OK` mirrors + // the pattern used in decryptBuffer — only negative SECURITY_STATUS values are errors. + if (ss < SEC_E_OK) { ec = asio::error_code(ss, asio::error::get_ssl_category()); return ssl_want::want_nothing; } @@ -255,7 +300,7 @@ ssl_want SSLHandshakeManager::startShutdown(asio::error_code& ec) { _pOutBuffer->reset(); _pOutBuffer->append(outputBuffers[0].pvBuffer, outputBuffers[0].cbBuffer); - if (SEC_E_OK == ss && outputBuffers[0].cbBuffer != 0) { + if (outputBuffers[0].cbBuffer != 0) { ec = asio::error::eof; return ssl_want::want_output; } else { @@ -357,6 +402,13 @@ ssl_want SSLHandshakeManager::doServerHandshake(asio::error_code& ec, // ASC_RET_MUTUAL_AUTH is not set since we do our own certificate validation later. invariant(attribs == (retAttribs | ASC_RET_EXTENDED_ERROR | ASC_RET_MUTUAL_AUTH)); + LOGV2_DEBUG(7998004, + 2, + "TLS server ASC result", + "ss"_attr = ss, + "outputBytes"_attr = outputBuffers[0].cbBuffer, + "inputSize"_attr = _pInBuffer->size()); + if (inputBuffers[1].BufferType == SECBUFFER_EXTRA && inputBuffers[1].cbBuffer > 0) { // SECBUFFER_EXTRA do not set pvBuffer, just cbBuffer. // cbBuffer tells us how much remaining in the buffer is extra @@ -408,7 +460,8 @@ ssl_want SSLHandshakeManager::doServerHandshake(asio::error_code& ec, return ssl_want::want_nothing; } -ssl_want SSLHandshakeManager::doClientHandshake(asio::error_code& ec) { +ssl_want SSLHandshakeManager::doClientHandshake(asio::error_code& ec, + HandshakeState* pHandshakeState) { DWORD sspiFlags = getClientFlags() | ISC_REQ_ALLOCATE_MEMORY; std::array outputBuffers; @@ -500,6 +553,13 @@ ssl_want SSLHandshakeManager::doClientHandshake(asio::error_code& ec) { // ASC_RET_EXTENDED_ERROR is not support on Windows 7/Windows 2008 R2 invariant(sspiFlags == (retAttribs | ASC_RET_EXTENDED_ERROR)); + LOGV2_DEBUG(7998002, + 2, + "TLS client ISC result", + "ss"_attr = ss, + "outputBytes"_attr = outputBuffers[0].cbBuffer, + "inputSize"_attr = _pInBuffer->size()); + if (_pInBuffer->size()) { // Locate (optional) extra buffer if (inputBuffers[1].BufferType == SECBUFFER_EXTRA && inputBuffers[1].cbBuffer > 0) { @@ -515,8 +575,7 @@ ssl_want SSLHandshakeManager::doClientHandshake(asio::error_code& ec) { // Next, figure out if we need to send any data out bool needOutput{false}; - // Did AcceptSecurityContext say we need to continue or is it done but left data in the - // output buffer then we need to sent the data out. + // ISC says to continue, or it finished (SEC_E_OK) but still produced token bytes to send. if (SEC_I_CONTINUE_NEEDED == ss || SEC_I_COMPLETE_AND_CONTINUE == ss || (SEC_E_OK == ss && outputBuffers[0].cbBuffer != 0)) { needOutput = true; @@ -528,17 +587,37 @@ ssl_want SSLHandshakeManager::doClientHandshake(asio::error_code& ec) { // Reset the input buffer _pInBuffer->reset(); - // Check if we have any additional encrypted data + // Check if we have any additional encrypted data. + // When SEC_E_OK, the handshake is done and any leftover bytes belong to the read path + // (e.g. a TLS 1.3 NewSessionTicket already received inline with the server flight). + // Do NOT call setState(NeedMoreHandshakeData) in that case — it would conflict with the + // Done signal below and cause an assertion failure in the state-machine transitions. if (!_pExtraEncryptedBuffer->empty()) { _pInBuffer->swap(*_pExtraEncryptedBuffer); _pExtraEncryptedBuffer->reset(); - // When doing the handshake and we have extra data, this means we have an incomplete tls - // record and need more bytes to complete the tls record. - setState(State::NeedMoreHandshakeData); + if (SEC_E_OK != ss) { + // When doing the handshake and we have extra data, this means we have an incomplete + // TLS record and need more bytes to complete it. + setState(State::NeedMoreHandshakeData); + } } if (needOutput) { + // TLS 1.3: InitializeSecurityContextW may return SEC_E_OK with non-zero output when + // the client's final flight (Certificate + CertificateVerify + Finished) is ready. + // The handshake is complete on the client side; signal Done so the _handshake loop + // sends this data and exits. Returning want_output_and_retry here would cause the + // loop to re-enter want_input_and_retry and block forever waiting for more server + // handshake data, while the server has already transitioned to application-data mode. + if (SEC_E_OK == ss) { + LOGV2_DEBUG(7998003, + 2, + "TLS client handshake done with pending output (TLS 1.3)", + "outputBytes"_attr = outputBuffers[0].cbBuffer); + *pHandshakeState = HandshakeState::Done; + return ssl_want::want_output; + } return ssl_want::want_output_and_retry; } @@ -608,8 +687,20 @@ ssl_want SSLReadManager::readDecryptedData(void* data, ssl_want SSLReadManager::decryptBuffer(asio::error_code& ec, DecryptState* pDecryptState) { while (true) { + // Save original buffer pointer and size before DecryptMessage. For status 0x80090317, + // Schannel sets securityBuffers[0].cbBuffer to the consumed NST record size rather than + // the full input size, which would cause the TLS-header fallback to miss trailing bytes + // (e.g. a KMIP response that arrived in the same recv() call as the NewSessionTicket). + const auto* const preCallInputPtr = static_cast(_pInBuffer->data()); + const ULONG preCallInputLen = static_cast(_pInBuffer->size()); + + LOGV2_DEBUG(7998035, + 0, + "TLS decryptBuffer: calling DecryptMessage", + "inputBytes"_attr = preCallInputLen); + std::array securityBuffers; - securityBuffers[0].cbBuffer = _pInBuffer->size(); + securityBuffers[0].cbBuffer = preCallInputLen; securityBuffers[0].BufferType = SECBUFFER_DATA; securityBuffers[0].pvBuffer = _pInBuffer->data(); @@ -632,10 +723,80 @@ ssl_want SSLReadManager::decryptBuffer(asio::error_code& ec, DecryptState* pDecr SECURITY_STATUS ss = DecryptMessage(_phctxt, &bufferDesc, 0, NULL); + LOGV2_DEBUG(7998028, + 1, + "TLS DecryptMessage result", + "status"_attr = static_cast(ss), + "inputBytes"_attr = securityBuffers[0].cbBuffer); + if (ss < SEC_E_OK) { if (ss == SEC_E_INCOMPLETE_MESSAGE) { + return ssl_want::want_input_and_retry; + } else if (ss == static_cast(0x80090317L)) { + // 0x80090317 is the error-severity form of SEC_I_CONTEXT_EXPIRED. Schannel + // returns it from DecryptMessage after consuming a TLS 1.3 post-handshake + // message (e.g. NewSessionTicket) from an OpenSSL peer. Treat it like + // SEC_I_RENEGOTIATE on TLS 1.3: the record is already consumed internally; + // preserve any trailing bytes for the next decryption call. + // + // Schannel may not populate SECBUFFER_EXTRA for this status code even when the + // input buffer contains bytes beyond the consumed TLS record (e.g. a KMIP + // response that arrived in the same recv() call). As a fallback, parse the + // 5-byte TLS record header (ContentType + Version[2] + Length[2]) to locate + // the boundary and rescue any trailing bytes before resetting _pInBuffer. + // + // Use the pre-call pointer and size (not securityBuffers[0] post-call values) + // because DecryptMessage sets securityBuffers[0].cbBuffer to the consumed NST + // record size, which causes recordSize == inputLen and extraBytes = 0, losing + // any trailing bytes (e.g. a KMIP response in the same TCP segment). + const auto* inputPtr = preCallInputPtr; + const ULONG inputLen = preCallInputLen; + + ULONG extraBytes = 0; + const uint8_t* extraPtr = nullptr; + + if (securityBuffers[3].BufferType == SECBUFFER_EXTRA && + securityBuffers[3].pvBuffer != nullptr && securityBuffers[3].cbBuffer > 0) { + extraBytes = securityBuffers[3].cbBuffer; + extraPtr = static_cast(securityBuffers[3].pvBuffer); + } else if (inputLen >= 5) { + // TLS record header: bytes [3..4] are the payload length (big-endian). + const uint32_t recordSize = + 5u + ((static_cast(inputPtr[3]) << 8) | inputPtr[4]); + if (recordSize < inputLen) { + extraBytes = inputLen - recordSize; + extraPtr = inputPtr + recordSize; + } + } + + LOGV2_DEBUG(7998029, + 0, + "TLS 1.3 post-handshake message (0x80090317) received from OpenSSL " + "peer: consuming NewSessionTicket and preserving trailing bytes", + "extraBytes"_attr = extraBytes, + "usedTLSHeaderFallback"_attr = + (extraBytes > 0 && securityBuffers[3].cbBuffer == 0), + "postCallCbBuffer"_attr = securityBuffers[0].cbBuffer, + "preCallInputLen"_attr = preCallInputLen); + + if (extraBytes > 0) { + ASIO_ASSERT(_pExtraEncryptedBuffer->empty()); + _pExtraEncryptedBuffer->append(extraPtr, extraBytes); + } + _pInBuffer->reset(); + if (!_pExtraEncryptedBuffer->empty()) { + _pInBuffer->swap(*_pExtraEncryptedBuffer); + _pExtraEncryptedBuffer->reset(); + continue; + } + return ssl_want::want_input_and_retry; } else { + LOGV2_DEBUG(7998027, + 0, + "TLS DecryptMessage failed", + "status"_attr = ss, + "inputBytes"_attr = securityBuffers[0].cbBuffer); ec = asio::error_code(ss, asio::error::get_ssl_category()); return ssl_want::want_nothing; } @@ -643,13 +804,83 @@ ssl_want SSLReadManager::decryptBuffer(asio::error_code& ec, DecryptState* pDecr // Shutdown has been initiated at the client side if (ss == SEC_I_CONTEXT_EXPIRED) { + LOGV2_DEBUG(7998026, + 2, + "TLS DecryptMessage: received close_notify or context expired " + "(SEC_I_CONTEXT_EXPIRED)", + "inputBytes"_attr = securityBuffers[0].cbBuffer); *pDecryptState = DecryptState::Shutdown; } else if (ss == SEC_I_RENEGOTIATE) { - *pDecryptState = DecryptState::Renegotiate; + // Schannel returns SEC_I_RENEGOTIATE from DecryptMessage for two distinct cases: + // + // TLS 1.2: The peer sent a HelloRequest (server-initiated renegotiation). + // MongoDB blocks renegotiation — fail the connection as before. + // + // TLS 1.3: A post-handshake message was received: NewSessionTicket (the server + // is distributing session-resumption material) or KeyUpdate (traffic-key + // rotation). These are *not* renegotiation; TLS 1.3 removed that + // feature entirely. Schannel reuses SEC_I_RENEGOTIATE for both cases. + // Schannel processes the message internally; no ISC/ASC call is needed + // (calling ISC/ASC here corrupts the application traffic keys). + // + // Determine the negotiated protocol to pick the right behaviour. + SecPkgContext_ConnectionInfo connInfo{}; + SECURITY_STATUS qi = + QueryContextAttributesW(_phctxt, SECPKG_ATTR_CONNECTION_INFO, &connInfo); + const bool isTLS13 = (qi == SEC_E_OK) && ((connInfo.dwProtocol & SP_PROT_TLS1_3) != 0); - // Fail the connection on SSL renegotiations - ec = asio::ssl::error::no_renegotiation; - return ssl_want::want_nothing; + if (!isTLS13) { + // TLS 1.2 renegotiation: block it. + *pDecryptState = DecryptState::Renegotiate; + ec = asio::ssl::error::no_renegotiation; + return ssl_want::want_nothing; + } + + // Do NOT call ISC/ASC here. + // + // Calling InitializeSecurityContext or AcceptSecurityContext after DecryptMessage + // returns SEC_I_RENEGOTIATE for a TLS 1.3 NewSessionTicket or KeyUpdate — even + // with an existing phContext and an empty/NULL input descriptor — causes Schannel + // to corrupt its application-layer traffic keys. The next DecryptMessage call + // then fails with SEC_E_DECRYPT_FAILURE (0x80090330), "The specified data could + // not be decrypted." + // + // Schannel processes the post-handshake message internally when DecryptMessage + // returns SEC_I_RENEGOTIATE. No ISC/ASC call is required. Any trailing bytes + // in the input buffer (SECBUFFER_EXTRA) are the start of the next TLS record and + // must be preserved for the next decryption. + + ULONG extraBytes = + (securityBuffers[3].BufferType == SECBUFFER_EXTRA && + securityBuffers[3].pvBuffer != nullptr && securityBuffers[3].cbBuffer > 0) + ? securityBuffers[3].cbBuffer + : 0; + LOGV2_DEBUG(7998005, + 2, + "TLS 1.3 post-handshake message processed by Schannel (no ISC/ASC needed)", + "extraBytes"_attr = extraBytes); + + // Save any extra encrypted data that arrived after the post-handshake record + // before we reset _pInBuffer (SECBUFFER_EXTRA pvBuffer points into _pInBuffer). + if (extraBytes > 0) { + ASIO_ASSERT(_pExtraEncryptedBuffer->empty()); + _pExtraEncryptedBuffer->append(securityBuffers[3].pvBuffer, + securityBuffers[3].cbBuffer); + } + _pInBuffer->reset(); // The post-handshake record has been consumed. + + LOGV2_DEBUG(7998015, + 2, + "TLS 1.3 post-handshake: continuing read after NewSessionTicket/KeyUpdate", + "extraBufferEmpty"_attr = _pExtraEncryptedBuffer->empty()); + + // Continue reading: process extra data if available, else request more. + if (!_pExtraEncryptedBuffer->empty()) { + _pInBuffer->swap(*_pExtraEncryptedBuffer); + _pExtraEncryptedBuffer->reset(); + continue; + } + return ssl_want::want_input_and_retry; } // The network layer may have read more then 1 SSL packet so remember the extra data. @@ -797,6 +1028,11 @@ ssl_want SSLWriteManager::encryptMessage(const void* pMessage, SECURITY_STATUS ss = EncryptMessage(_phctxt, 0, &bufferDesc, 0); if (ss < SEC_E_OK) { + LOGV2_DEBUG(7998030, + 1, + "TLS EncryptMessage failed", + "status"_attr = static_cast(ss), + "messageLength"_attr = messageLength); ec = asio::error_code(ss, asio::error::get_ssl_category()); return ssl_want::want_nothing; } diff --git a/src/mongo/util/net/ssl/detail/schannel.hpp b/src/mongo/util/net/ssl/detail/schannel.hpp index 7b082faf12b..3ccee8ff93d 100644 --- a/src/mongo/util/net/ssl/detail/schannel.hpp +++ b/src/mongo/util/net/ssl/detail/schannel.hpp @@ -257,7 +257,7 @@ public: ReusableBuffer* pInBuffer, ReusableBuffer* pOutBuffer, ReusableBuffer* pExtraBuffer, - SCHANNEL_CRED* cred) + SCH_CREDENTIALS* cred) : _state(State::HandshakeStart), _phctxt(hctxt), _cred(cred), @@ -342,6 +342,22 @@ private: } DWORD getClientFlags() { + // ISC_REQ_USE_SUPPLIED_CREDS: restricts Schannel to the credentials in _cred->paCred + // rather than searching the system certificate stores automatically. When cCreds == 0 + // (no client certificate configured) Schannel correctly sends an empty Certificate + // message in TLS 1.3 when the server requests one. This flag must always be set: + // without it, when ISC returns SEC_E_OK with output bytes (the TLS 1.3 client final + // flight: Certificate + CertificateVerify + Finished), the call site signals + // HandshakeState::Done and returns want_output rather than want_output_and_retry. + // Omitting the flag can leave _hctxt in a mid-handshake state, causing + // QueryContextAttributesW(SECPKG_ATTR_CONNECTION_INFO) to return SEC_E_INVALID_HANDLE. + // + // ISC_REQ_MANUAL_CRED_VALIDATION: suppresses Schannel's built-in server-certificate + // validation (chain building, revocation, hostname checks). MongoDB performs its own + // validation via CertGetCertificateChain / CertVerifyCertificateChainPolicy so that + // it can apply custom CA lists, allow self-signed test certificates, and produce + // descriptive error messages. Without this flag, Schannel would reject any certificate + // not trusted by the Windows system root store before our code ever sees it. return ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY | ISC_REQ_EXTENDED_ERROR | ISC_REQ_STREAM | ISC_REQ_USE_SUPPLIED_CREDS | ISC_REQ_MANUAL_CRED_VALIDATION; @@ -368,7 +384,7 @@ private: ssl_want doServerHandshake(asio::error_code& ec, HandshakeState* pHandshakeState); - ssl_want doClientHandshake(asio::error_code& ec); + ssl_want doClientHandshake(asio::error_code& ec, HandshakeState* pHandshakeState); private: /** @@ -443,7 +459,7 @@ private: ReusableBuffer _alertBuffer; // SChannel Credentials - SCHANNEL_CRED* _cred; + SCH_CREDENTIALS* _cred; // SChannel context PCtxtHandle _phctxt; @@ -477,12 +493,17 @@ public: SSLReadManager(PCtxtHandle hctxt, PCredHandle hcred, ReusableBuffer* pInBuffer, - ReusableBuffer* pExtraBuffer) + ReusableBuffer* pExtraBuffer, + ReusableBuffer* pOutBuffer, + std::wstring* pServerName) : _state(State::NeedMoreEncryptedData), _phctxt(hctxt), _phcred(hcred), _pInBuffer(pInBuffer), - _pExtraEncryptedBuffer(pExtraBuffer) {} + _pExtraEncryptedBuffer(pExtraBuffer), + _pOutBuffer(pOutBuffer), + _pServerName(pServerName) {} + /** * Read decrypted data if encrypted data was provided via writeData and succesfully decrypted. @@ -505,6 +526,20 @@ public: _pInBuffer->append(data, length); } + /** + * Signal that the shared input buffer already contains application-data bytes left over + * from the TLS handshake. This happens in TLS 1.3 when the peer bundles application data + * in the same TCP segment as its final handshake flight (0.5-RTT / early data). The + * handshake code places those bytes into _pInBuffer but never transitions the read manager + * out of NeedMoreEncryptedData, so without this call readDecryptedData would stall waiting + * for more network bytes that will never arrive. Must only be called when the handshake is + * fully done and _pInBuffer is non-empty. + */ + void notifyHandshakeLeftoverData() { + ASIO_ASSERT(!_pInBuffer->empty()); + setState(State::HaveEncryptedData); + } + private: ssl_want decryptBuffer(asio::error_code& ec, DecryptState* pDecryptState); @@ -568,6 +603,12 @@ private: // Credential handle PCredHandle _phcred; + + // Output buffer shared with the engine (for TLS 1.3 post-handshake responses). + ReusableBuffer* _pOutBuffer; + + // TLS SNI server name (for InitializeSecurityContextW when processing post-handshake). + std::wstring* _pServerName; }; /** @@ -609,7 +650,11 @@ private: // SChannel context handle PCtxtHandle _phctxt; - // Position to start encrypting from for messages needing fragmentation + // Byte offset into the caller's message buffer at which the next EncryptMessage call + // should start. Non-zero only when a message exceeds _securityMaxMessageLength and + // must be split into multiple TLS records. The ASIO write path re-presents the same + // buffer on each want_output_and_retry iteration; _lastWriteOffset advances through it + // until the full message is encrypted, at which point it is reset to 0. std::size_t _lastWriteOffset{0}; // TLS packet header length diff --git a/src/mongo/util/net/ssl/detail/stream_core.hpp b/src/mongo/util/net/ssl/detail/stream_core.hpp index 7fd2e198dd8..9f23bbbf3ae 100644 --- a/src/mongo/util/net/ssl/detail/stream_core.hpp +++ b/src/mongo/util/net/ssl/detail/stream_core.hpp @@ -42,7 +42,9 @@ struct stream_core { template #if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS - stream_core(SCHANNEL_CRED* context, const std::string& remoteHostName, const Executor& executor) + stream_core(SCH_CREDENTIALS* context, + const std::string& remoteHostName, + const Executor& executor) #elif MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL stream_core(SSL_CTX* context, const std::string& remoteHostName, const Executor& executor) #elif MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_APPLE diff --git a/src/mongo/util/net/ssl/impl/context_schannel.ipp b/src/mongo/util/net/ssl/impl/context_schannel.ipp index 4d42f34bb25..aaff348c058 100644 --- a/src/mongo/util/net/ssl/impl/context_schannel.ipp +++ b/src/mongo/util/net/ssl/impl/context_schannel.ipp @@ -48,6 +48,9 @@ namespace ssl { context::context(context::method m) : handle_(&_cred) { memset(&_cred, 0, sizeof(_cred)); + memset(&_tlsParams, 0, sizeof(_tlsParams)); + _cred.cTlsParameters = 1; + _cred.pTlsParameters = &_tlsParams; } #if defined(ASIO_HAS_MOVE) || defined(GENERATING_DOCUMENTATION) diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h index bd401a67f41..710cb15ce8a 100644 --- a/src/mongo/util/net/ssl_manager.h +++ b/src/mongo/util/net/ssl_manager.h @@ -54,10 +54,12 @@ #include "mongo/util/out_of_line_executor.h" #include "mongo/util/time_support.h" -// SChannel implementation +// SSL provider-specific headers #if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL #include #include +#elif MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS +#include "mongo/platform/windows_basic.h" #endif #endif // #ifdef MONGO_CONFIG_SSL @@ -92,7 +94,7 @@ struct SSLConnectionContext; typedef SSL_CTX* SSLContextType; typedef SSL* SSLConnectionType; #elif MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS -typedef SCHANNEL_CRED* SSLContextType; +typedef SCH_CREDENTIALS* SSLContextType; typedef PCtxtHandle SSLConnectionType; #elif MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_APPLE typedef asio::ssl::apple::Context* SSLContextType; diff --git a/src/mongo/util/net/ssl_manager_apple.cpp b/src/mongo/util/net/ssl_manager_apple.cpp index 5de26d89c42..95750a6165c 100644 --- a/src/mongo/util/net/ssl_manager_apple.cpp +++ b/src/mongo/util/net/ssl_manager_apple.cpp @@ -1448,7 +1448,7 @@ StatusWith> parseProtocolRange( } else if (protocol == SSLParams::Protocols::TLS1_2) { tls12 = false; } else if (protocol == SSLParams::Protocols::TLS1_3) { - // SERVER-98279: support tls 1.3 for windows & apple + // SERVER-121261: support tls 1.3 for apple // By ignoring this value, we are disabling support until we have access to the // modern library. } else { @@ -1583,7 +1583,7 @@ StatusWith mapTLSVersion(SSLContextRef ssl) { return TLSVersion::kTLS12; default: // Some system headers may define additional protocols, so suppress warnings. return TLSVersion::kUnknown; - // SERVER-98279: support tls 1.3 for windows & apple + // SERVER-121261: support tls 1.3 for apple } } diff --git a/src/mongo/util/net/ssl_manager_test.cpp b/src/mongo/util/net/ssl_manager_test.cpp index c80230fd7e9..fc96cb23cd6 100644 --- a/src/mongo/util/net/ssl_manager_test.cpp +++ b/src/mongo/util/net/ssl_manager_test.cpp @@ -42,6 +42,7 @@ #include "mongo/util/net/ssl/stream.hpp" #include "mongo/util/net/ssl_options.h" #include "mongo/util/net/ssl_types.h" +#include "mongo/util/scopeguard.h" #include @@ -958,6 +959,197 @@ TEST(SSLManager, TransientSSLParamsStressTestWithManager) { #endif // MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL +#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS + +// Test that TLS 1.3 protocol flags are enabled by default on Windows SChannel +TEST(SSLManager, WindowsTLS13EnabledByDefault) { + 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 */); + + // Test server-side (incoming) context includes TLS 1.3 + TLS_PARAMETERS serverTLSParams = {}; + SCH_CREDENTIALS serverCred = {}; + serverCred.pTlsParameters = &serverTLSParams; + auto serverStatus = manager->initSSLContext( + &serverCred, params, SSLManagerInterface::ConnectionDirection::kIncoming); + ASSERT_OK(serverStatus); + // Verify TLS 1.3 is enabled (not in disabled protocols) + ASSERT_FALSE(serverCred.pTlsParameters->grbitDisabledProtocols & SP_PROT_TLS1_3_SERVER); + // Also verify TLS 1.2 is enabled + ASSERT_FALSE(serverCred.pTlsParameters->grbitDisabledProtocols & SP_PROT_TLS1_2_SERVER); + + // Test client-side (outgoing) context includes TLS 1.3 + TLS_PARAMETERS clientTLSParams = {}; + SCH_CREDENTIALS clientCred = {}; + clientCred.pTlsParameters = &clientTLSParams; + auto clientStatus = manager->initSSLContext( + &clientCred, params, SSLManagerInterface::ConnectionDirection::kOutgoing); + ASSERT_OK(clientStatus); + // Verify TLS 1.3 is enabled (not in disabled protocols) + ASSERT_FALSE(clientCred.pTlsParameters->grbitDisabledProtocols & SP_PROT_TLS1_3_CLIENT); + // Also verify TLS 1.2 is enabled + ASSERT_FALSE(clientCred.pTlsParameters->grbitDisabledProtocols & SP_PROT_TLS1_2_CLIENT); +} + +// Test that TLS 1.3 can be disabled via sslDisabledProtocols on Windows SChannel +TEST(SSLManager, WindowsTLS13CanBeDisabled) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + params.sslDisabledProtocols = {SSLParams::Protocols::TLS1_3}; + + std::shared_ptr manager = + SSLManagerInterface::create(params, true /* isSSLServer */); + + // Test server-side (incoming) context has TLS 1.3 disabled + TLS_PARAMETERS serverTLSParams = {}; + SCH_CREDENTIALS serverCred = {}; + serverCred.pTlsParameters = &serverTLSParams; + auto serverStatus = manager->initSSLContext( + &serverCred, params, SSLManagerInterface::ConnectionDirection::kIncoming); + ASSERT_OK(serverStatus); + // Verify TLS 1.3 is disabled (in disabled protocols) + ASSERT_TRUE(serverCred.pTlsParameters->grbitDisabledProtocols & SP_PROT_TLS1_3_SERVER); + // Verify TLS 1.2 is still enabled + ASSERT_FALSE(serverCred.pTlsParameters->grbitDisabledProtocols & SP_PROT_TLS1_2_SERVER); + + // Test client-side (outgoing) context has TLS 1.3 disabled + TLS_PARAMETERS clientTLSParams = {}; + SCH_CREDENTIALS clientCred = {}; + clientCred.pTlsParameters = &clientTLSParams; + auto clientStatus = manager->initSSLContext( + &clientCred, params, SSLManagerInterface::ConnectionDirection::kOutgoing); + ASSERT_OK(clientStatus); + // Verify TLS 1.3 is disabled (in disabled protocols) + ASSERT_TRUE(clientCred.pTlsParameters->grbitDisabledProtocols & SP_PROT_TLS1_3_CLIENT); + // Verify TLS 1.2 is still enabled + ASSERT_FALSE(clientCred.pTlsParameters->grbitDisabledProtocols & SP_PROT_TLS1_2_CLIENT); +} + +// Test that disabling all TLS protocols (including TLS 1.3) throws an error +TEST(SSLManager, WindowsDisableAllTLSProtocolsFails) { + SSLParams params; + params.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + params.sslPEMKeyFile = "jstests/libs/server.pem"; + params.sslCAFile = "jstests/libs/ca.pem"; + params.sslDisabledProtocols = {SSLParams::Protocols::TLS1_0, + SSLParams::Protocols::TLS1_1, + SSLParams::Protocols::TLS1_2, + SSLParams::Protocols::TLS1_3}; + + ASSERT_THROWS_CODE_AND_WHAT(SSLManagerInterface::create(params, true /* isSSLServer */), + DBException, + ErrorCodes::InvalidSSLConfiguration, + "All supported TLS protocols have been disabled."); +} + +// Tests for the TLS 1.3 post-handshake record boundary logic added for SERVER-79980. +// +// decryptBuffer parses the 5-byte TLS record header to locate trailing bytes when +// DecryptMessage returns 0x80090317 without populating SECBUFFER_EXTRA. The record +// header layout is: ContentType(1) | LegacyVersion(2) | PayloadLength(2), so the +// total record size is 5 + big-endian(bytes[3..4]). + +TEST(SSLManager, WindowsTLS13RecordHeaderParsing) { + auto tlsRecordTotalSize = [](const uint8_t* buf, size_t bufLen) -> uint32_t { + if (bufLen < 5) + return 0; + return 5u + ((static_cast(buf[3]) << 8) | buf[4]); + }; + + // Buffer shorter than one header — no parse possible. + { + const uint8_t buf[4] = {}; + ASSERT_EQ(tlsRecordTotalSize(buf, sizeof(buf)), 0u); + } + + // Zero-payload record: 5-byte header only. + { + const uint8_t buf[5] = {23, 3, 3, 0, 0}; + ASSERT_EQ(tlsRecordTotalSize(buf, sizeof(buf)), 5u); + } + + // Max TLS record payload (16384 = 0x4000). + { + const uint8_t hdr[5] = {23, 3, 3, 0x40, 0x00}; + ASSERT_EQ(tlsRecordTotalSize(hdr, sizeof(hdr)), 5u + 16384u); + } + + // NST record (250-byte payload) immediately followed by KMIP response (168-byte payload). + // This is the exact scenario fixed in SERVER-79980: both records arrive in one recv(), + // DecryptMessage returns 0x80090317 for the NST without setting SECBUFFER_EXTRA, and + // the fallback must locate the KMIP response start via the TLS header. + { + constexpr uint32_t kNstPayload = 250; + constexpr uint32_t kAppPayload = 168; + constexpr uint32_t kNstTotal = 5 + kNstPayload; + constexpr uint32_t kAppTotal = 5 + kAppPayload; + + uint8_t buf[kNstTotal + kAppTotal] = {}; + buf[3] = static_cast(kNstPayload >> 8); + buf[4] = static_cast(kNstPayload & 0xFF); + + const uint32_t recordSize = tlsRecordTotalSize(buf, sizeof(buf)); + ASSERT_EQ(recordSize, kNstTotal); + ASSERT_LT(recordSize, static_cast(sizeof(buf))); + + const uint32_t extraBytes = static_cast(sizeof(buf)) - recordSize; + ASSERT_EQ(extraBytes, kAppTotal); + } +} + +TEST(SSLManager, WindowsReusableBufferOps) { + using asio::ssl::detail::ReusableBuffer; + + ReusableBuffer buf(64); + ASSERT_TRUE(buf.empty()); + ASSERT_EQ(buf.size(), 0u); + + const uint8_t src[] = {1, 2, 3, 4, 5}; + buf.append(src, sizeof(src)); + ASSERT_EQ(buf.size(), 5u); + ASSERT_FALSE(buf.empty()); + + // Partial read leaves buffer non-empty. + uint8_t out[8] = {}; + std::size_t nRead = 0; + buf.readInto(out, 3, nRead); + ASSERT_EQ(nRead, 3u); + ASSERT_EQ(out[0], 1); + ASSERT_EQ(out[2], 3); + ASSERT_FALSE(buf.empty()); + + // Over-read drains the buffer. + buf.readInto(out, sizeof(out), nRead); + ASSERT_EQ(nRead, 2u); + ASSERT_EQ(out[0], 4); + ASSERT_EQ(out[1], 5); + ASSERT_TRUE(buf.empty()); + + // swap transfers ownership. + ReusableBuffer a(16), b(16); + const uint8_t aData[] = {10, 20}; + a.append(aData, sizeof(aData)); + ASSERT_EQ(a.size(), 2u); + ASSERT_TRUE(b.empty()); + + a.swap(b); + ASSERT_TRUE(a.empty()); + ASSERT_EQ(b.size(), 2u); + + // reset empties the buffer. + b.reset(); + ASSERT_TRUE(b.empty()); +} + +#endif // MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS + #ifdef MONGO_CONFIG_SSL TEST(SSLManager, CheckCertificateInTransientManager) { @@ -1117,18 +1309,52 @@ public: serverConn->sslSocket->handshake(asio::ssl::stream_base::server); } catch (const DBException& ex) { serverStatus = ex.toStatus().withContext("Server handshake failed"); + } catch (const std::exception& ex) { + serverStatus = Status(ErrorCodes::SSLHandshakeFailed, ex.what()) + .withContext("Server handshake failed"); } }); + + bool clientHandshakeThrewStdException = false; try { clientConn->sslSocket->handshake(asio::ssl::stream_base::client); } catch (const DBException& ex) { clientStatus = ex.toStatus().withContext("Client handshake failed"); + } catch (const std::exception& ex) { + clientStatus = Status(ErrorCodes::SSLHandshakeFailed, + str::stream() << "Client handshake failed: " << ex.what()); + clientHandshakeThrewStdException = true; + } + + // If the client handshake threw a std::exception (not DBException), it means the + // ASIO handshake failed at the protocol level. In this case, close both sockets to + // unblock the server thread, which may still be blocked in its handshake call. + // This is necessary on Windows where the server doesn't automatically fail when the + // client's handshake throws at the ASIO level. + if (clientHandshakeThrewStdException) { + asio::error_code ec; + if (serverConn && serverConn->sslSocket) { + serverConn->sslSocket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, + ec); + serverConn->sslSocket->lowest_layer().close(ec); + } + if (clientConn && clientConn->sslSocket) { + clientConn->sslSocket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, + ec); + clientConn->sslSocket->lowest_layer().close(ec); + } } serverThread.join(); - // rethrow any handshake errors with context - uassertStatusOK(serverStatus); + // Rethrow any handshake errors with context. When we've intentionally closed the socket + // to unblock the server (clientHandshakeThrewStdException is true), the server may report + // a "WSACancelBlockingCall" error on Windows. This is expected behavior from closing the + // socket, so we should prioritize the client error which represents the actual failure. + // Only throw the server error if we didn't intentionally close the socket. + if (!clientHandshakeThrewStdException) { + uassertStatusOK(serverStatus); + } uassertStatusOK(clientStatus); } @@ -1789,6 +2015,491 @@ TEST(SSLManager, revocationWithCRLsIntermediateTests) { #endif // MONGO_CONFIG_SSL_PROVIDER != MONGO_CONFIG_SSL_PROVIDER_APPLE +// Helper function to configure SSLParams for TLS 1.3 only mode. +// Disables TLS 1.0, TLS 1.1, and TLS 1.2. +void enableOnlyTLS13(SSLParams& params) { + params.sslDisabledProtocols = { + SSLParams::Protocols::TLS1_0, + SSLParams::Protocols::TLS1_1, + SSLParams::Protocols::TLS1_2, + }; +} + +// TLS 1.3 mutation tests. +// These tests re-run the handshake tests with TLS 1.3 only mode enabled on both ingress and egress. +#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL || \ + MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS + +TEST(SSLManager, basicEgressValidationTestsTLS13Only) { + constexpr auto validCaFile = caFile; + constexpr auto badCaFile = trustedCaFile; + + std::vector testCases = { + {"", "", false}, + {validCaFile, "", true}, + {badCaFile, "", false}, + {badCaFile, validCaFile, false}, + {validCaFile, badCaFile, true}, + }; + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = serverKeyFile; + serverParams.sslAllowInvalidHostnames = true; + enableOnlyTLS13(serverParams); + + for (auto& test : testCases) { + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslPEMKeyFile = clientKeyFile; + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslCAFile = test.cafile; + clientParams.sslClusterCAFile = test.clusterCaFile; + enableOnlyTLS13(clientParams); + + for (auto weak : {false, true}) { + test.allowInvalidCerts = weak; + clientParams.sslAllowInvalidCertificates = weak; + + LOGV2(9476800, "Running TLS1.3-only test case", "test"_attr = test); + + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, true /*expectIngressPass*/, test.pass || weak); + } + } +} + +TEST(SSLManager, basicIngressValidationTestsTLS13Only) { + constexpr auto validCaFile = caFile; + constexpr auto badCaFile = trustedCaFile; + + std::vector testCases = { + {"", "", false}, + {validCaFile, "", true}, + {badCaFile, "", false}, + {badCaFile, validCaFile, true}, + {validCaFile, badCaFile, false}, + }; + + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslCAFile = caFile; + clientParams.sslPEMKeyFile = clientKeyFile; + clientParams.sslAllowInvalidHostnames = true; + enableOnlyTLS13(clientParams); + + for (auto& test : testCases) { + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslPEMKeyFile = serverKeyFile; + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = test.cafile; + serverParams.sslClusterCAFile = test.clusterCaFile; + enableOnlyTLS13(serverParams); + + for (auto weak : {false, true}) { + test.allowInvalidCerts = weak; + serverParams.sslAllowInvalidCertificates = weak; + + LOGV2(9476801, "Running TLS1.3-only test case", "test"_attr = test); + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, test.pass || weak, true /*expectEgressPass*/); + } + } +} + +TEST(SSLManager, keyFileUsageTestsTLS13Only) { + struct TestCase { + std::string clientPemKeyFile; + std::string clientClusterFile; + std::string serverPemKeyFile; + std::string serverClusterFile; + bool clientPass; + bool serverPass; + void serialize(BSONObjBuilder* bob) const { + bob->append("clientPemKeyFile", clientPemKeyFile); + bob->append("clientClusterFile", clientClusterFile); + bob->append("serverPemKeyFile", serverPemKeyFile); + bob->append("serverClusterFile", serverClusterFile); + bob->append("clientPass", clientPass); + bob->append("serverPass", serverPass); + } + }; + + std::vector testCases = { + {clientKeyFile, "", serverKeyFile, "", true, true}, + {trustedClientKeyFile, "", serverKeyFile, "", true, false}, + {clientKeyFile, "", trustedServerKeyFile, "", false, true}, + {trustedClientKeyFile, "", trustedServerKeyFile, "", false, false}, + {trustedClientKeyFile, clientKeyFile, serverKeyFile, "", true, true}, + {clientKeyFile, trustedClientKeyFile, serverKeyFile, "", true, false}, + {clientKeyFile, "", trustedServerKeyFile, serverKeyFile, false, true}, + {clientKeyFile, "", serverKeyFile, trustedServerKeyFile, true, true}, + }; + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslCAFile = caFile; + serverParams.sslAllowInvalidHostnames = true; + enableOnlyTLS13(serverParams); + + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslCAFile = caFile; + clientParams.sslAllowInvalidHostnames = true; + enableOnlyTLS13(clientParams); + + for (auto& test : testCases) { + LOGV2(9476802, "Running TLS1.3-only test case", "test"_attr = test); + + serverParams.sslPEMKeyFile = test.serverPemKeyFile; + serverParams.sslClusterFile = test.serverClusterFile; + + clientParams.sslPEMKeyFile = test.clientPemKeyFile; + clientParams.sslClusterFile = test.clientClusterFile; + + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, test.serverPass, test.clientPass); + } +} + +TEST(SSLManager, noCertificatePresentedByPeerTestsTLS13Only) { + SSLParams clientParams, serverParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslCAFile = caFile; + clientParams.sslPEMKeyFile = clientKeyFile; + clientParams.sslAllowInvalidHostnames = true; + clientParams.tlsWithholdClientCertificate = true; + enableOnlyTLS13(clientParams); + + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = serverKeyFile; + serverParams.sslAllowInvalidHostnames = true; + enableOnlyTLS13(serverParams); + + for (auto weak : {false, true}) { + LOGV2(9476803, + "Running TLS1.3-only with sslWeakCertificateValidation=weak", + "weak"_attr = weak); + + serverParams.sslWeakCertificateValidation = weak; + + SSLTestFixture tf( + serverParams, clientParams, true /*ingressIsServer*/, true /*egressIsServer*/); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, weak /*expectIngressPass*/, true /*expectEgressPass*/); + } +} + +TEST(SSLManager, transientSSLParamsOverrideGlobalParamsTestsTLS13Only) { + SSLParams clientParams, serverParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslCAFile = caFile; + clientParams.sslPEMKeyFile = clientKeyFile; + clientParams.sslAllowInvalidHostnames = true; + enableOnlyTLS13(clientParams); + + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslCAFile = trustedCaFile; + serverParams.sslPEMKeyFile = trustedServerKeyFile; + serverParams.sslAllowInvalidHostnames = true; + enableOnlyTLS13(serverParams); + + struct TestCase { + std::string caFile; + std::string keyFile; + bool clientPass; + bool serverPass; + void serialize(BSONObjBuilder* bob) const { + bob->append("caFile", caFile); + bob->append("keyFile", keyFile); + bob->append("clientPass", clientPass); + bob->append("serverPass", serverPass); + } + }; + std::vector testCases{ + {trustedCaFile, trustedClientKeyFile, true, true}, + {caFile, trustedClientKeyFile, false, true}, + {trustedCaFile, clientKeyFile, true, false}, + {trustedCaFile, "", true, false}, + }; + + // First, test that validation fails on both sides without the transient params. + SSLTestFixture tf( + serverParams, clientParams, true /*ingressIsServer*/, false /*egressIsServer*/); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, false, false); + + for (auto& test : testCases) { + LOGV2(9476804, "Running TLS1.3-only test case", "test"_attr = test); + + TLSCredentials creds; + creds.tlsAllowInvalidHostnames = true; + creds.tlsCAFile = test.caFile; + creds.tlsPEMKeyFile = test.keyFile; + creds.tlsDisabledProtocols = { + SSLParams::Protocols::TLS1_0, + SSLParams::Protocols::TLS1_1, + SSLParams::Protocols::TLS1_2, + }; + TransientSSLParams transientParams(creds); + + SSLTestFixture tf(serverParams, clientParams, true, false, transientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, test.serverPass, test.clientPass); + } +} + +TEST(SSLManager, intermediateCATestsTLS13Only) { + struct TestCase { + std::string clientCAFile; + std::string serverKeyFile; + bool clientPass; + void serialize(BSONObjBuilder* bob) const { + bob->append("clientCAFile", clientCAFile); + bob->append("serverKeyFile", serverKeyFile); + bob->append("clientPass", clientPass); + } + }; + + const std::string intermediateALeafWithIssuerCertKeyFile = combinePEMFiles( + {{intermediateALeafKeyFile, true /*includePrivKey*/}, {intermediateACaFile}}); + const std::string intermediateALeafWithAllIssuerCertsKeyFile = combinePEMFiles( + {{intermediateALeafWithIssuerCertKeyFile, true /*includePrivKey*/}, {caFile}}); + const std::string intermediateAWithRootCaFile = + combinePEMFiles({{intermediateACaFile}, {caFile}}); + const std::string intermediateBLeafWithIssuerCertKeyFile = combinePEMFiles( + {{intermediateBLeafKeyFile, true /*includePrivKey*/}, {intermediateBCaFile}}); + + std::vector testCases = { + {intermediateACaFile, intermediateALeafKeyFile, false}, + {intermediateACaFile, serverKeyFile, false}, + {intermediateACaFile, intermediateALeafWithIssuerCertKeyFile, false}, + {intermediateACaFile, intermediateALeafWithAllIssuerCertsKeyFile, false}, + {intermediateAWithRootCaFile, intermediateALeafKeyFile, true}, + {intermediateAWithRootCaFile, serverKeyFile, true}, + {intermediateAWithRootCaFile, intermediateBLeafWithIssuerCertKeyFile, true}, + {intermediateAWithRootCaFile, intermediateBLeafKeyFile, false}, + {caFile, intermediateALeafKeyFile, false}, + {caFile, intermediateBLeafKeyFile, false}, + {caFile, intermediateALeafWithIssuerCertKeyFile, true}, + {caFile, intermediateBLeafWithIssuerCertKeyFile, true}, + }; + + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslPEMKeyFile = trustedClientKeyFile; + enableOnlyTLS13(clientParams); + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = trustedCaFile; + enableOnlyTLS13(serverParams); + + for (auto& test : testCases) { + clientParams.sslCAFile = test.clientCAFile; + serverParams.sslPEMKeyFile = test.serverKeyFile; + + LOGV2(9476805, "Running TLS1.3-only test case", "test"_attr = test); + + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, true /*expectIngressPass*/, test.clientPass); + } +} + +TEST(SSLManager, expiredCRLTestTLS13Only) { + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslCAFile = caFile; + clientParams.sslPEMKeyFile = clientKeyFile; + clientParams.sslCRLFile = expiredCRL; + enableOnlyTLS13(clientParams); + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = serverKeyFile; + serverParams.sslCRLFile = expiredCRL; + enableOnlyTLS13(serverParams); + + 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); +} + +TEST(SSLManager, multipleCRLsFromSameIssuerTestsTLS13Only) { + const auto expiredCRLWithNonExpiredCRL = combinePEMFiles({{clientRevokedCRL}, {expiredCRL}}); + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = serverKeyFile; + serverParams.sslCRLFile = expiredCRLWithNonExpiredCRL; + enableOnlyTLS13(serverParams); + + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslCAFile = caFile; + enableOnlyTLS13(clientParams); + +#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS + ASSERT_THROWS_CODE_AND_WHAT( + SSLManagerInterface::create(serverParams, true), + DBException, + ErrorCodes::InvalidSSLConfiguration, + "CertAddCRLContextToStore Failed: The object or property already exists."); +#else + { + clientParams.sslPEMKeyFile = clientKeyFile; + LOGV2(9476806, "Running TLS1.3-only with client key file", "keyfile"_attr = clientKeyFile); + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, true /*expectIngressPass*/, true); + } + { + clientParams.sslPEMKeyFile = revokedClientKeyFile; + LOGV2(9476807, + "Running TLS1.3-only with client key file", + "keyfile"_attr = revokedClientKeyFile); + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); + checkValidationResults(result, false, true); + ASSERT_NE(result.ingress.getStatus().reason().find("revoked"), std::string::npos); + } +#endif +} + +TEST(SSLManager, basicCRLRevocationTestsTLS13Only) { + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslCAFile = trustedCaFile; + clientParams.sslPEMKeyFile = revokedClientKeyFile; + enableOnlyTLS13(clientParams); + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = trustedServerKeyFile; + enableOnlyTLS13(serverParams); + + { + serverParams.sslCRLFile = emptyCRL; + LOGV2(9476808, + "Running TLS1.3-only 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(9476809, + "Running TLS1.3-only 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); + } +} + +TEST(SSLManager, noCRLFoundTestsTLS13Only) { + SSLParams clientParams; + clientParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + clientParams.sslAllowInvalidHostnames = true; + clientParams.sslCAFile = caFile; + clientParams.sslPEMKeyFile = clientKeyFile; + clientParams.sslCRLFile = trustedEmptyCRL; // CRL issued by trusted-ca.pem, not ca.pem + enableOnlyTLS13(clientParams); + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = serverKeyFile; + serverParams.sslCRLFile = trustedEmptyCRL; + enableOnlyTLS13(serverParams); + + SSLTestFixture tf(serverParams, clientParams); + tf.doHandshake(); + auto result = tf.runIngressEgressValidation(); +#if MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS + checkValidationResults(result, true, true); +#else + checkValidationResults(result, false, false); + constexpr const char* expectedError = "unable to get certificate CRL"; + ASSERT_NE(result.ingress.getStatus().reason().find(expectedError), std::string::npos); + ASSERT_NE(result.egress.getStatus().reason().find(expectedError), std::string::npos); +#endif +} + +TEST(SSLManager, revocationWithCRLsIntermediateTestsTLS13Only) { + const std::string intermediateBLeafWithIssuerCertKeyFile = combinePEMFiles( + {{intermediateBLeafKeyFile, true /*includePrivKey*/}, {intermediateBCaFile}}); + 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; + enableOnlyTLS13(clientParams); + + SSLParams serverParams; + serverParams.sslMode.store(::mongo::sslGlobalParams.SSLMode_requireSSL); + serverParams.sslAllowInvalidHostnames = true; + serverParams.sslCAFile = caFile; + serverParams.sslPEMKeyFile = intermediateBLeafWithIssuerCertKeyFile; + enableOnlyTLS13(serverParams); + + 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 // MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_OPENSSL || + // MONGO_CONFIG_SSL_PROVIDER == MONGO_CONFIG_SSL_PROVIDER_WINDOWS + #endif // MONGO_CONFIG_SSL } // namespace } // namespace mongo diff --git a/src/mongo/util/net/ssl_manager_windows.cpp b/src/mongo/util/net/ssl_manager_windows.cpp index 7fc6847512f..ce1249ffd62 100644 --- a/src/mongo/util/net/ssl_manager_windows.cpp +++ b/src/mongo/util/net/ssl_manager_windows.cpp @@ -35,7 +35,6 @@ #include "mongo/db/connection_health_metrics_parameter_gen.h" #include "mongo/db/server_options.h" #include "mongo/logv2/log.h" -#include "mongo/platform/atomic_word.h" #include "mongo/transport/ssl_connection_context.h" #include "mongo/util/debug_util.h" #include "mongo/util/exit.h" @@ -53,8 +52,8 @@ #include "mongo/util/net/ssl_util.h" #include "mongo/util/str.h" #include "mongo/util/text.h" -#include "mongo/util/uuid.h" +#include #include #include #include @@ -62,6 +61,7 @@ #include #include +#include #include #include @@ -78,6 +78,10 @@ namespace { // This failpoint is a no-op on Windows. MONGO_FAIL_POINT_DEFINE(disableStapling); +// Bitmask representing all TLS protocol bits supported by SChannel. +constexpr uint32_t kAllTLSProtocols = + SP_PROT_TLS1_0 | SP_PROT_TLS1_1 | SP_PROT_TLS1_2 | SP_PROT_TLS1_3; + /** * Free a Certificate Context. */ @@ -166,7 +170,7 @@ private: }; /** - * Free a HCRYPTPROV Handle + * Free a HCRYPTPROV Handle (legacy CAPI, used when acquiring keys from the Windows cert store). */ struct CryptProviderFree { void operator()(HCRYPTPROV const h) noexcept { @@ -179,17 +183,33 @@ struct CryptProviderFree { using UniqueCryptProvider = AutoHandle; /** - * Free a HCRYPTKEY Handle + * Free an NCrypt handle (provider or key). */ -struct CryptKeyFree { - void operator()(HCRYPTKEY const h) noexcept { +struct NcryptFree { + void operator()(NCRYPT_HANDLE const h) noexcept { if (h) { - ::CryptDestroyKey(h); + ::NCryptFreeObject(h); } } }; -using UniqueCryptKey = AutoHandle; +using UniqueNcryptProvider = AutoHandle; +using UniqueNcryptKey = AutoHandle; + +/** + * Permanently delete a named NCrypt key container from persistent storage. + * NCryptDeleteKey both removes the on-disk container and releases the in-memory handle, so + * NCryptFreeObject must NOT be called separately. + */ +struct NcryptKeyDeleter { + void operator()(NCRYPT_KEY_HANDLE const h) noexcept { + if (h) { + ::NCryptDeleteKey(h, 0); + } + } +}; + +using UniqueNcryptKeyWithDeletion = AutoHandle; /** * Free a CERTSTORE Handle @@ -221,11 +241,49 @@ struct CertChainEngineFree { using UniqueCertChainEngine = AutoHandle; /** - * The lifetime of a private key of a certificate loaded from a PEM is bound to the CryptContext's - * lifetime - * so we treat the certificate and cryptcontext as a pair. + * A certificate loaded from a PEM file with its CNG private key stored in a named key container. + * The certificate context's CERT_KEY_PROV_INFO_PROP_ID references the container by name and + * provider; Schannel opens the container by name at handshake time rather than enumerating all + * containers in the KSP. When this struct is destroyed, NCryptDeleteKey removes the named + * container from persistent storage, cleaning up the ephemeral key material. */ -using UniqueCertificateWithPrivateKey = std::tuple; +struct CertificateWithKey { + UniqueCertificate cert; + UniqueNcryptKeyWithDeletion key; + + CertificateWithKey() = default; + CertificateWithKey(UniqueCertificate c, UniqueNcryptKeyWithDeletion k) + : cert(std::move(c)), key(std::move(k)) {} + + // AutoHandle's move assignment deliberately skips freeing the old handle (it is designed + // for "give away ownership" patterns). For certificate rotation we need the old named key + // container to be deleted, so we save the old handle, transfer the new one, then delete. + CertificateWithKey& operator=(CertificateWithKey&& other) noexcept { + if (this != &other) { + cert = std::move(other.cert); + NCRYPT_KEY_HANDLE oldHandle = static_cast(key); + key = std::move(other.key); + if (oldHandle) { + NcryptKeyDeleter()(oldHandle); + } + } + return *this; + } + CertificateWithKey(CertificateWithKey&&) = default; + CertificateWithKey(const CertificateWithKey&) = delete; + CertificateWithKey& operator=(const CertificateWithKey&) = delete; + + explicit operator bool() const { + return static_cast(cert); + } + PCCERT_CONTEXT get() const { + return cert.get(); + } + const CERT_CONTEXT* operator->() const { + return cert.get(); + } +}; +using UniqueCertificateWithPrivateKey = CertificateWithKey; StatusWith> parsePeerRoles(PCCERT_CONTEXT cert) { @@ -249,13 +307,13 @@ StatusWith> parsePeerRoles(PCCERT_CONTEXT cert) { */ class SSLConnectionWindows : public SSLConnectionInterface { public: - SCHANNEL_CRED* _cred; + SCH_CREDENTIALS* _cred; Socket* socket; asio::ssl::detail::engine _engine; std::vector _tempBuffer; - SSLConnectionWindows(SCHANNEL_CRED* cred, Socket* sock, const char* initialBytes, int len); + SSLConnectionWindows(SCH_CREDENTIALS* cred, Socket* sock, const char* initialBytes, int len); void* getConnection() final { return _engine.native_handle(); @@ -271,11 +329,13 @@ public: const boost::optional& transientSSLParams, bool isServer); + ~SSLManagerWindows() override; + /** * Initializes an OpenSSL context according to the provided settings. Only settings which are * acceptable on non-blocking connections are set. */ - Status initSSLContext(SCHANNEL_CRED* cred, + Status initSSLContext(SCH_CREDENTIALS* cred, const SSLParams& params, ConnectionDirection direction) final; @@ -296,7 +356,7 @@ public: const HostAndPort& hostForLogging, const ExecutorPtr& reactor) final; - Status stapleOCSPResponse(SCHANNEL_CRED* cred, bool asyncOCSPStaple) final; + Status stapleOCSPResponse(SCH_CREDENTIALS* cred, bool asyncOCSPStaple) final; const SSLConfiguration& getSSLConfiguration() const final { return _sslConfiguration; @@ -343,8 +403,10 @@ private: // If set, this parameters are used to create new transient SSL connection. const boost::optional _transientSSLParams; - SCHANNEL_CRED _clientCred; - SCHANNEL_CRED _serverCred; + TLS_PARAMETERS _clientTLSCred; + SCH_CREDENTIALS _clientCred; + TLS_PARAMETERS _serverTLSCred; + SCH_CREDENTIALS _serverCred; UniqueCertificateWithPrivateKey _pemCertificate; UniqueCertificateWithPrivateKey _clusterPEMCertificate; @@ -370,6 +432,15 @@ private: // Weak pointer to verify that this manager is still owned by this context. // Will be used if stapling is implemented. synchronized_value> _ownedByContext; + + // Thumbprints of intermediate CA certs we added to the Windows system "CA" store. + // Each entry is the CERT_SHA1_HASH_PROP_ID value: a SHA1 hash of the raw DER-encoded cert + // bytes, computed by Windows CryptoAPI as a content fingerprint. This is independent of + // the certificate's own signature algorithm (which may be SHA256, etc.). + // Schannel TLS 1.3 (SCH_CREDENTIALS) only searches system stores when building the + // Certificate message chain, so we install intermediate CAs there and remove them on + // destruction. + std::vector> _addedIntermediateCAThumbprints; }; GlobalInitializerRegisterer sslManagerInitializer( @@ -388,7 +459,7 @@ GlobalInitializerRegisterer sslManagerInitializer( {"EndStartupOptionHandling"}, {}); -SSLConnectionWindows::SSLConnectionWindows(SCHANNEL_CRED* cred, +SSLConnectionWindows::SSLConnectionWindows(SCH_CREDENTIALS* cred, Socket* sock, const char* initialBytes, int len) @@ -433,6 +504,10 @@ SSLManagerWindows::SSLManagerWindows(const SSLParams& params, _allowInvalidHostnames(params.sslAllowInvalidHostnames), _suppressNoCertificateWarning(params.suppressNoTLSPeerCertificateWarning), _transientSSLParams(transientSSLParams) { + memset(&_clientTLSCred, 0, sizeof(_clientTLSCred)); + memset(&_serverTLSCred, 0, sizeof(_serverTLSCred)); + _clientCred.pTlsParameters = &_clientTLSCred; + _serverCred.pTlsParameters = &_serverTLSCred; if (MONGO_unlikely(getSSLManagerMode() == SSLManagerMode::TransientWithOverride)) { uassert(ErrorCodes::InvalidSSLConfiguration, @@ -496,6 +571,25 @@ SSLManagerWindows::SSLManagerWindows(const SSLParams& params, uassertStatusOK(_initChainEngines(&_clientEngine)); } +SSLManagerWindows::~SSLManagerWindows() { + if (_addedIntermediateCAThumbprints.empty()) { + return; + } + HCERTSTORE hSystemCAStore = CertOpenSystemStoreW(NULL, L"CA"); + if (!hSystemCAStore) { + return; + } + for (auto& thumbprint : _addedIntermediateCAThumbprints) { + CRYPT_HASH_BLOB hashBlob = {20, const_cast(thumbprint.data())}; + PCCERT_CONTEXT pCert = CertFindCertificateInStore( + hSystemCAStore, X509_ASN_ENCODING, 0, CERT_FIND_SHA1_HASH, &hashBlob, NULL); + if (pCert) { + CertDeleteCertificateFromStore(pCert); // consumes pCert + } + } + CertCloseStore(hSystemCAStore, 0); +} + StatusWith initChainEngine(CERT_CHAIN_ENGINE_CONFIG* chainEngineConfig, HCERTSTORE certStore, DWORD flags) { @@ -567,10 +661,31 @@ int SSLManagerWindows::SSL_read(SSLConnectionInterface* connInterface, void* buf conn->socket->handleRecvError(ret, num); } + LOGV2_DEBUG( + 7998036, 0, "TLS SSL_read: recv returned encrypted data", "bytes"_attr = ret); + conn->_engine.put_input(asio::const_buffer(conn->_tempBuffer.data(), ret)); continue; } + case asio::ssl::detail::engine::want_output: + case asio::ssl::detail::engine::want_output_and_retry: { + // TLS 1.3 post-handshake response (e.g. KeyUpdate acknowledgement): + // send the queued output, then retry the read if needed. + asio::mutable_buffer outBuf = conn->_engine.get_output( + asio::mutable_buffer(conn->_tempBuffer.data(), conn->_tempBuffer.size())); + int ret = send(conn->socket->rawFD(), + reinterpret_cast(outBuf.data()), + outBuf.size(), + portSendFlags); + if (ret == SOCKET_ERROR) { + conn->socket->handleSendError(ret, ""); + } + if (want == asio::ssl::detail::engine::want_output_and_retry) { + continue; + } + return bytes_transferred; + } case asio::ssl::detail::engine::want_nothing: { // ASIO wants nothing, return to caller with anything transfered. return bytes_transferred; @@ -752,6 +867,51 @@ StatusWith> readCAPEMBuffer(StringData buffer) { return {std::move(certs)}; } +// Returns true if pCert has CA:TRUE in its Basic Constraints extension. +static bool isCACertificate(PCCERT_CONTEXT pCert) { + // Check Basic Constraints v2 (most common for modern certs) + PCERT_EXTENSION pExt = CertFindExtension( + szOID_BASIC_CONSTRAINTS2, pCert->pCertInfo->cExtension, pCert->pCertInfo->rgExtension); + if (pExt) { + CERT_BASIC_CONSTRAINTS2_INFO* pInfo = nullptr; + DWORD cbInfo = 0; + if (CryptDecodeObjectEx(X509_ASN_ENCODING, + szOID_BASIC_CONSTRAINTS2, + pExt->Value.pbData, + pExt->Value.cbData, + CRYPT_DECODE_ALLOC_FLAG, + nullptr, + &pInfo, + &cbInfo)) { + bool isCA = (pInfo->fCA != FALSE); + LocalFree(pInfo); + return isCA; + } + } + + // Fallback: Basic Constraints v1 (older certs) + pExt = CertFindExtension( + szOID_BASIC_CONSTRAINTS, pCert->pCertInfo->cExtension, pCert->pCertInfo->rgExtension); + if (pExt) { + CERT_BASIC_CONSTRAINTS_INFO* pInfo = nullptr; + DWORD cbInfo = 0; + if (CryptDecodeObjectEx(X509_ASN_ENCODING, + szOID_BASIC_CONSTRAINTS, + pExt->Value.pbData, + pExt->Value.cbData, + CRYPT_DECODE_ALLOC_FLAG, + nullptr, + &pInfo, + &cbInfo)) { + bool isCA = (pInfo->SubjectType.cbData > 0 && + (pInfo->SubjectType.pbData[0] & CERT_CA_SUBJECT_FLAG)); + LocalFree(pInfo); + return isCA; + } + } + return false; +} + Status addCertificatesToStore(HCERTSTORE certStore, std::vector& certificates) { for (auto& cert : certificates) { if (!CertAddCertificateContextToStore(certStore, cert.get(), CERT_STORE_ADD_NEW, NULL)) { @@ -816,40 +976,19 @@ StatusWith readCertPEMFile(StringData fileName, auto certBuf = swCert.getValue(); + // Use CertCreateCertificateContext rather than CertAddEncodedCertificateToStore. + // The latter routes through CertAddCertificateContextToStore, which allocates a property-table + // slot for CERT_NCRYPT_KEY_HANDLE_TRANSFER_PROP_ID (propId 99) without initialising it, + // leaving pbData = NULL with cbData != 0. CertCreateCertificateContext never copies + // properties, so the table is clean and the property can be set safely. PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING, certBuf.data(), certBuf.size()); - if (!cert) { auto ec = lastSystemError(); return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "CertCreateCertificateContext failed to decode cert: " - << errorMessage(ec)); + str::stream() << "CertCreateCertificateContext failed: " << errorMessage(ec)); } - UniqueCertificate tempCertHolder(cert); - - HCERTSTORE store = CertOpenStore( - CERT_STORE_PROV_MEMORY, 0, NULL, CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG, NULL); - if (!store) { - auto ec = lastSystemError(); - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() - << "CertOpenStore failed to create memory store: " << errorMessage(ec)); - } - - UniqueCertStore storeHolder(store); - - // Add the newly created certificate to the memory store, this makes a copy - if (!CertAddCertificateContextToStore(store, cert, CERT_STORE_ADD_NEW, NULL)) { - auto ec = lastSystemError(); - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "CertAddCertificateContextToStore Memory Failed " - << errorMessage(ec)); - } - - // Get the certificate from the store so we attach the private key to the cert in the store - cert = CertEnumCertificatesInStore(store, NULL); - UniqueCertificate certHolder(cert); std::vector privateKey; @@ -917,103 +1056,113 @@ StatusWith readCertPEMFile(StringData fileName, } - HCRYPTPROV hProv; - std::wstring wstr; + // Generate a container name that is unique per import to avoid cross-process and + // intra-process contention on the MS_KEY_STORAGE_PROVIDER. + // + // Why uniqueness matters: + // If we used a deterministic name (e.g. the cert thumbprint), concurrent processes + // loading the same certificate (common in Evergreen CI where many test tasks run in + // parallel on the same host) would all try to NCryptImportKey into the same named + // container. NCryptImportKey with NCRYPT_OVERWRITE_KEY_FLAG blocks until it can + // acquire an exclusive lock on the container — if another process has the container + // open, the call hangs indefinitely, causing the "HIT EVERGREEN TIMEOUT" failures. + // + // Container name format: "mongod__" where counter is a process-global + // atomic that increments on every call. This guarantees uniqueness within a process + // (across certificate rotations) and across processes (different PIDs), with no locking. + // + // Why NCRYPT_OVERWRITE_KEY_FLAG is safe here: + // PIDs are unique among *running* processes, so no two live processes share a + // container name. The only scenario in which a "mongod__" container + // already exists is when a previous process with the same recycled PID was + // force-killed (e.g. by the Evergreen hang-analyser) before NCryptDeleteKey ran, + // leaving a stale orphan on disk. Because that process is dead, no handle is open + // on the container, so NCRYPT_OVERWRITE_KEY_FLAG overwrites it instantly without + // blocking. Without this flag, NCryptImportKey would return NTE_EXISTS + // (0x8009000F) and the server would fail to start. + static std::atomic keyContainerCounter{0}; + wchar_t containerName[64] = {}; + _snwprintf_s(containerName, + static_cast(std::size(containerName)), + _TRUNCATE, + L"mongod_%lu_%llu", + static_cast(GetCurrentProcessId()), + static_cast(keyContainerCounter.fetch_add(1))); - // Create the right Crypto context depending on whether we running in a server or outside. - // See https://msdn.microsoft.com/en-us/library/windows/desktop/aa375195(v=vs.85).aspx - if (isSSLServer) { - // Generate a unique name for each key container - // Use the the log file if possible - if (!serverGlobalParams.logpath.empty()) { - static AtomicWord counter{0}; - std::string keyContainerName = str::stream() - << serverGlobalParams.logpath << counter.fetchAndAdd(1); - wstr = toNativeString(keyContainerName.c_str()); - } else { - auto us = UUID::gen().toString(); - wstr = toNativeString(us.c_str()); - } + LOGV2_DEBUG(7998007, + 2, + "Importing private key into CNG key storage provider", + "containerName"_attr = toUtf8String(containerName)); - // Use a new key container for the key. We cannot use the default container since the - // default - // container is shared across processes owned by the same user. - // Note: Server side Schannel requires CRYPT_VERIFYCONTEXT off - if (!CryptAcquireContextW(&hProv, - wstr.c_str(), - MS_ENHANCED_PROV, - PROV_RSA_FULL, - CRYPT_NEWKEYSET | CRYPT_SILENT)) { - auto ec = lastSystemError(); - if (ec == systemError(NTE_EXISTS)) { - if (!CryptAcquireContextW( - &hProv, wstr.c_str(), MS_ENHANCED_PROV, PROV_RSA_FULL, CRYPT_SILENT)) { - auto ec = lastSystemError(); - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() - << "CryptAcquireContextW failed " << errorMessage(ec)); - } - - } else { - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "CryptAcquireContextW failed " << errorMessage(ec)); - } - } - } else { - // Use a transient key container for the key - if (!CryptAcquireContextW(&hProv, - NULL, - MS_ENHANCED_PROV, - PROV_RSA_FULL, - CRYPT_VERIFYCONTEXT | CRYPT_SILENT)) { - auto ec = lastSystemError(); - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "CryptAcquireContextW failed " << errorMessage(ec)); - } - } - UniqueCryptProvider cryptProvider(hProv); - - HCRYPTKEY hkey; - if (!CryptImportKey(hProv, privateKey.data(), privateKey.size(), 0, 0, &hkey)) { - auto ec = lastSystemError(); + // Open the Microsoft Software Key Storage Provider (CNG). SCH_CREDENTIALS (version 5), + // required for TLS 1.3, needs a CNG key rather than a legacy CAPI key. + NCRYPT_PROV_HANDLE hProvider = 0; + SECURITY_STATUS ss = NCryptOpenStorageProvider(&hProvider, MS_KEY_STORAGE_PROVIDER, 0); + if (ss != ERROR_SUCCESS) { return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() << "CryptImportKey failed " << errorMessage(ec)); + str::stream() << "NCryptOpenStorageProvider failed with error: " << ss); } - UniqueCryptKey keyHolder(hkey); + UniqueNcryptProvider providerGuard(hProvider); - if (isSSLServer) { - // Server-side SChannel requires a different way of attaching the private key to the - // certificate - CRYPT_KEY_PROV_INFO keyProvInfo; - memset(&keyProvInfo, 0, sizeof(keyProvInfo)); - keyProvInfo.pwszContainerName = const_cast(wstr.c_str()); - keyProvInfo.pwszProvName = const_cast(MS_ENHANCED_PROV); - keyProvInfo.dwFlags = CERT_SET_KEY_PROV_HANDLE_PROP_ID | CERT_SET_KEY_CONTEXT_PROP_ID; - keyProvInfo.dwProvType = PROV_RSA_FULL; - keyProvInfo.dwKeySpec = AT_KEYEXCHANGE; + // Import the private key into a named container. + // + // Use CERT_KEY_PROV_INFO_PROP_ID (propId 2) to associate the key with the certificate rather + // than CERT_NCRYPT_KEY_HANDLE_PROP_ID (propId 78) or CERT_NCRYPT_KEY_HANDLE_TRANSFER_PROP_ID + // (propId 99). Setting propId 78 or 99 triggers an internal CertGetCertificateContextProperty + // call that, if the handle is not yet cached, enumerates all key containers in the KSP to find + // a matching public key — an unnecessary and potentially slow operation. Setting propId 2 + // stores only a name/provider blob; when Schannel needs the private key for the TLS handshake + // it resolves propId 78 from propId 2 by opening the named container directly (NCryptOpenKey + // by name), which is both simpler and more predictable. + // + // NCRYPT_OVERWRITE_KEY_FLAG is safe here because each container name is PID-scoped: + // no two *running* processes share a name, so the flag only ever overwrites orphaned + // containers left by previously force-killed processes (see comment above). + NCryptBuffer nameBuffer = {}; + nameBuffer.cbBuffer = static_cast((wcslen(containerName) + 1) * sizeof(wchar_t)); + nameBuffer.BufferType = NCRYPTBUFFER_PKCS_KEY_NAME; + nameBuffer.pvBuffer = containerName; + NCryptBufferDesc paramList = {}; + paramList.ulVersion = NCRYPTBUFFER_VERSION; + paramList.cBuffers = 1; + paramList.pBuffers = &nameBuffer; - if (!CertSetCertificateContextProperty( - certHolder.get(), CERT_KEY_PROV_INFO_PROP_ID, 0, &keyProvInfo)) { - auto ec = lastSystemError(); - return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() - << "CertSetCertificateContextProperty Failed " << errorMessage(ec)); - } + NCRYPT_KEY_HANDLE hKey = 0; + ss = NCryptImportKey(hProvider, + NULL, + LEGACY_RSAPRIVATE_BLOB, + ¶mList, + &hKey, + privateKey.data(), + static_cast(privateKey.size()), + NCRYPT_SILENT_FLAG | NCRYPT_OVERWRITE_KEY_FLAG); + if (ss != ERROR_SUCCESS) { + return Status(ErrorCodes::InvalidSSLConfiguration, + str::stream() << "NCryptImportKey failed with error: " << ss); } + // keyGuard calls NCryptDeleteKey on destruction, removing the named container from disk. + UniqueNcryptKeyWithDeletion keyGuard(hKey); - // NOTE: This is used to set the certificate for client side SChannel + // Point the cert context at the named container via CERT_KEY_PROV_INFO_PROP_ID. + // This is a plain blob write; it never dereferences a key handle or enumerates the KSP. + CRYPT_KEY_PROV_INFO provInfo = {}; + provInfo.pwszContainerName = containerName; + provInfo.pwszProvName = const_cast(MS_KEY_STORAGE_PROVIDER); + provInfo.dwProvType = 0; // 0 = CNG provider, not legacy CAPI + provInfo.dwKeySpec = AT_KEYEXCHANGE; if (!CertSetCertificateContextProperty( - cert, CERT_KEY_PROV_HANDLE_PROP_ID, 0, (const void*)hProv)) { + certHolder.get(), CERT_KEY_PROV_INFO_PROP_ID, 0, &provInfo)) { auto ec = lastSystemError(); return Status(ErrorCodes::InvalidSSLConfiguration, - str::stream() - << "CertSetCertificateContextProperty failed " << errorMessage(ec)); + str::stream() << "CertSetCertificateContextProperty " + "(CERT_KEY_PROV_INFO_PROP_ID) failed: " + << errorMessage(ec)); } - // Add the extra certificates into the same certificate store as the certificate + // Add the extra certificates into the same certificate store as the certificate. uassertStatusOK(addCertificatesToStore(certHolder->hCertStore, extraCertificates)); - return UniqueCertificateWithPrivateKey{std::move(certHolder), std::move(cryptProvider)}; + return CertificateWithKey{std::move(certHolder), std::move(keyGuard)}; } Status readCAPEMFile(HCERTSTORE certStore, StringData fileName) { @@ -1297,13 +1446,13 @@ Status SSLManagerWindows::_loadCertificates(const SSLParams& params) { _clusterPEMCertificate = std::move(swCertificate.getValue()); } - if (std::get<0>(_pemCertificate)) { - _clientCertificates[0] = std::get<0>(_pemCertificate).get(); - _serverCertificates[0] = std::get<0>(_pemCertificate).get(); + if (_pemCertificate) { + _clientCertificates[0] = _pemCertificate.get(); + _serverCertificates[0] = _pemCertificate.get(); } - if (std::get<0>(_clusterPEMCertificate)) { - _clientCertificates[0] = std::get<0>(_clusterPEMCertificate).get(); + if (_clusterPEMCertificate) { + _clientCertificates[0] = _clusterPEMCertificate.get(); } // If the user has specified --setParameter tlsUseSystemCA=true, then no params.sslCAFile nor @@ -1317,9 +1466,8 @@ Status SSLManagerWindows::_loadCertificates(const SSLParams& params) { // Dump the CA cert chain into the memory store for the client cert. This ensures Windows // can build a complete chain to send to the remote side. - if (std::get<0>(_pemCertificate)) { - auto status = - readCAPEMFile(std::get<0>(_pemCertificate).get()->hCertStore, sslConfig.cafile); + if (_pemCertificate) { + auto status = readCAPEMFile(_pemCertificate.get()->hCertStore, sslConfig.cafile); if (!status.isOK()) { return status; } @@ -1341,9 +1489,8 @@ Status SSLManagerWindows::_loadCertificates(const SSLParams& params) { // Dump the CA cert chain into the memory store for the cluster cert. This ensures Windows // can build a complete chain to send to the remote side. - if (std::get<0>(_clusterPEMCertificate)) { - auto status = readCAPEMFile(std::get<0>(_clusterPEMCertificate).get()->hCertStore, - sslConfig.cafile); + if (_clusterPEMCertificate) { + auto status = readCAPEMFile(_clusterPEMCertificate.get()->hCertStore, sslConfig.cafile); if (!status.isOK()) { return status; } @@ -1353,6 +1500,123 @@ Status SSLManagerWindows::_loadCertificates(const SSLParams& params) { } _serverEngine.hasCRL = !crlfile.empty(); + // Schannel TLS 1.3 (SCH_CREDENTIALS) only searches Windows system stores when building the + // Certificate message chain — it ignores the cert's hCertStore even with + // SCH_CRED_MEMORY_STORE_CERT set. Install any intermediate CA certs (non-self-signed) into the + // user-level system "CA" store so Schannel can find them. We track the SHA1 thumbprints so we + // can remove the certs in the destructor. + auto addIntermediateCAsToSystemStore = [&](HCERTSTORE sourceCertStore) { + if (!sourceCertStore) { + return; + } + HCERTSTORE hSystemCAStore = CertOpenSystemStoreW(NULL, L"CA"); + if (!hSystemCAStore) { + LOGV2_DEBUG( + 7998018, 2, "addIntermediateCAsToSystemStore: failed to open user CA system store"); + return; + } + DWORD addedCount = 0; + PCCERT_CONTEXT pCert = NULL; + while ((pCert = CertEnumCertificatesInStore(sourceCertStore, pCert)) != NULL) { + // Skip self-signed (root) certs — only install intermediate CAs. + if (CertCompareCertificateName( + X509_ASN_ENCODING, &pCert->pCertInfo->Issuer, &pCert->pCertInfo->Subject)) { + continue; + } + // Skip leaf certs (certs without CA:TRUE in Basic Constraints). + if (!isCACertificate(pCert)) { + continue; + } + char subjectBuf[256] = {}; + CertNameToStrA(X509_ASN_ENCODING, + &pCert->pCertInfo->Subject, + CERT_X500_NAME_STR, + subjectBuf, + sizeof(subjectBuf)); + PCCERT_CONTEXT pNewCert = NULL; + BOOL added = CertAddCertificateContextToStore( + hSystemCAStore, pCert, CERT_STORE_ADD_NEW, &pNewCert); + LOGV2_DEBUG(7998011, + 2, + "addIntermediateCAsToSystemStore: intermediate CA cert", + "subject"_attr = subjectBuf, + "addedToSystemStore"_attr = (bool)added); + if (added && pNewCert) { + ++addedCount; + std::array sha1{}; + DWORD cbHash = 20; + if (CertGetCertificateContextProperty( + pNewCert, CERT_SHA1_HASH_PROP_ID, sha1.data(), &cbHash)) { + _addedIntermediateCAThumbprints.push_back(sha1); + } + CertFreeCertificateContext(pNewCert); + } + } + LOGV2_DEBUG(7998019, + 2, + "addIntermediateCAsToSystemStore: complete", + "addedCount"_attr = addedCount); + CertCloseStore(hSystemCAStore, 0); + }; + if (_pemCertificate) { + addIntermediateCAsToSystemStore(_pemCertificate.get()->hCertStore); + } + if (_clusterPEMCertificate) { + addIntermediateCAsToSystemStore(_clusterPEMCertificate.get()->hCertStore); + } + + // The hCertStore path above only picks up certs explicitly added via addCertificatesToStore. + // Read ALL certs from each PEM key file directly so that any intermediate CA embedded in the + // key file (after the leaf cert) is also installed into the system "CA" store. Schannel TLS + // 1.3 needs the intermediate CA in the system store to include it in the Certificate message. + auto addIntermediateCAFromPEMFile = [&](StringData pemFile) { + if (pemFile.empty()) { + return; + } + auto swBuf = ssl_util::readPEMFile(pemFile); + if (!swBuf.isOK()) { + LOGV2_DEBUG(7998020, + 2, + "addIntermediateCAFromPEMFile: failed to read PEM file", + "file"_attr = pemFile, + "error"_attr = swBuf.getStatus().reason()); + return; + } + auto swCerts = readCAPEMBuffer(swBuf.getValue()); + if (!swCerts.isOK()) { + LOGV2_DEBUG(7998021, + 2, + "addIntermediateCAFromPEMFile: failed to parse PEM certs", + "file"_attr = pemFile, + "error"_attr = swCerts.getStatus().reason()); + return; + } + auto& certs = swCerts.getValue(); + LOGV2_DEBUG(7998022, + 2, + "addIntermediateCAFromPEMFile: parsed certs from key file", + "file"_attr = pemFile, + "count"_attr = certs.size()); + if (certs.empty()) { + return; + } + HCERTSTORE hTempStore = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, NULL, 0, NULL); + if (!hTempStore) { + return; + } + for (auto& cert : certs) { + CertAddCertificateContextToStore( + hTempStore, cert.get(), CERT_STORE_ADD_REPLACE_EXISTING, NULL); + } + addIntermediateCAsToSystemStore(hTempStore); + CertCloseStore(hTempStore, 0); + }; + addIntermediateCAFromPEMFile(sslConfig.clientPEM); + addIntermediateCAFromPEMFile(sslConfig.cafile); + if (managerMode != SSLManagerMode::TransientWithOverride) { + addIntermediateCAFromPEMFile(params.sslClusterFile); + } + if (hasCertificateSelector(*certificateSelector)) { auto swCert = loadAndValidateCertificateSelector(*certificateSelector); if (!swCert.isOK()) { @@ -1392,15 +1656,17 @@ Status SSLManagerWindows::_loadCertificates(const SSLParams& params) { return Status::OK(); } -Status SSLManagerWindows::initSSLContext(SCHANNEL_CRED* cred, +Status SSLManagerWindows::initSSLContext(SCH_CREDENTIALS* cred, const SSLParams& params, ConnectionDirection direction) { - memset(cred, 0, sizeof(*cred)); - cred->dwVersion = SCHANNEL_CRED_VERSION; - cred->dwFlags = SCH_USE_STRONG_CRYPTO; // Use strong crypto; - - uint32_t supportedProtocols = 0; + auto* tlsParams = cred->pTlsParameters; + *tlsParams = {}; + // SCH_USE_STRONG_CRYPTO is a legacy flag intended for SCHANNEL_CRED (version 4) and must not + // be set on SCH_CREDENTIALS (version 5). On Windows Server 2022 and later it causes + // AcquireCredentialsHandle to fail with SEC_E_NO_LSA. Protocol/cipher strength is already + // controlled via TLS_PARAMETERS.grbitDisabledProtocols below. + *cred = {.dwVersion = SCH_CREDENTIALS_VERSION, .pTlsParameters = tlsParams}; const auto [disabledProtocols, cipherConfig] = [&]() -> std::pair*, const std::string&> { @@ -1413,9 +1679,6 @@ Status SSLManagerWindows::initSSLContext(SCHANNEL_CRED* cred, }(); if (direction == ConnectionDirection::kIncoming) { - supportedProtocols = SP_PROT_TLS1_SERVER | SP_PROT_TLS1_0_SERVER | SP_PROT_TLS1_1_SERVER | - SP_PROT_TLS1_2_SERVER; - cred->hRootStore = _serverEngine.CAstore; cred->dwFlags = cred->dwFlags // flags | SCH_CRED_REVOCATION_CHECK_CHAIN // Check certificate revocation @@ -1426,9 +1689,6 @@ Status SSLManagerWindows::initSSLContext(SCHANNEL_CRED* cred, | SCH_CRED_DISABLE_RECONNECTS; // Do not support reconnects } else { - supportedProtocols = SP_PROT_TLS1_CLIENT | SP_PROT_TLS1_0_CLIENT | SP_PROT_TLS1_1_CLIENT | - SP_PROT_TLS1_2_CLIENT; - cred->hRootStore = _clientEngine.CAstore; cred->dwFlags = cred->dwFlags // Flags | SCH_CRED_REVOCATION_CHECK_CHAIN // Check certificate revocation @@ -1440,19 +1700,22 @@ Status SSLManagerWindows::initSSLContext(SCHANNEL_CRED* cred, } // Set the supported TLS protocols. Allow --sslDisabledProtocols to disable selected ciphers. + uint32_t disabledProtocolsFlag = 0; for (const SSLParams::Protocols& protocol : *disabledProtocols) { if (protocol == SSLParams::Protocols::TLS1_0) { - supportedProtocols &= ~(SP_PROT_TLS1_0_CLIENT | SP_PROT_TLS1_0_SERVER); + disabledProtocolsFlag |= SP_PROT_TLS1_0; } else if (protocol == SSLParams::Protocols::TLS1_1) { - supportedProtocols &= ~(SP_PROT_TLS1_1_CLIENT | SP_PROT_TLS1_1_SERVER); + disabledProtocolsFlag |= SP_PROT_TLS1_1; } else if (protocol == SSLParams::Protocols::TLS1_2) { - supportedProtocols &= ~(SP_PROT_TLS1_2_CLIENT | SP_PROT_TLS1_2_SERVER); + disabledProtocolsFlag |= SP_PROT_TLS1_2; + } else if (protocol == SSLParams::Protocols::TLS1_3) { + disabledProtocolsFlag |= SP_PROT_TLS1_3; } - // SERVER-98279: support tls 1.3 for windows & apple } - cred->grbitEnabledProtocols = supportedProtocols; - if (supportedProtocols == 0) { + cred->cTlsParameters = 1; + cred->pTlsParameters->grbitDisabledProtocols = disabledProtocolsFlag; + if (cred->pTlsParameters->grbitDisabledProtocols == kAllTLSProtocols) { return {ErrorCodes::InvalidSSLConfiguration, "All supported TLS protocols have been disabled."}; } @@ -1506,12 +1769,22 @@ void SSLManagerWindows::_handshake(SSLConnectionWindows* conn, bool client) { client ? SSLManagerInterface::ConnectionDirection::kOutgoing : SSLManagerInterface::ConnectionDirection::kIncoming)); + LOGV2_DEBUG(7998009, 2, "TLS handshake starting", "role"_attr = (client ? "client" : "server")); + + int iteration = 0; while (true) { asio::error_code ec; asio::ssl::detail::engine::want want = conn->_engine.handshake(client ? asio::ssl::stream_base::handshake_type::client : asio::ssl::stream_base::handshake_type::server, ec); + LOGV2_DEBUG(7998010, + 2, + "TLS handshake iteration", + "role"_attr = (client ? "client" : "server"), + "iteration"_attr = iteration++, + "want"_attr = static_cast(want), + "ecMessage"_attr = (ec ? ec.message() : std::string{})); if (ec) { throwSocketError(SocketErrorKind::RECV_ERROR, ec.message()); } @@ -1793,12 +2066,62 @@ Status validatePeerCertificate(const std::string& remoteHost, certChainPara.dwUrlRetrievalTimeout = gTLSOCSPVerifyTimeoutSecs * 1000; + // Build a flat memory store combining all candidate intermediate CA certs from: + // (a) the certs from the peer's TLS Certificate message (cert->hCertStore), + // (b) the current-user "CA" store (populated with intermediate CAs at startup), and + // (c) the local-machine "CA" store. + // This is required because CertGetCertificateChain with hExclusiveRoot set does NOT + // search system CA stores for intermediate certificates — it only looks in the + // hAdditionalStore argument and hExclusiveRoot itself. + // + // We use a flat MEMORY store (not a collection store) because CertGetCertificateChain + // does not enumerate sibling stores when the hAdditionalStore argument is a collection + // store — it only searches the collection store's own in-memory contents. + auto copyStoreToFlat = [](HCERTSTORE dst, HCERTSTORE src) -> DWORD { + DWORD count = 0; + for (PCCERT_CONTEXT pCert = nullptr; + (pCert = CertEnumCertificatesInStore(src, pCert)) != nullptr;) { + if (CertAddCertificateContextToStore(dst, pCert, CERT_STORE_ADD_USE_EXISTING, nullptr)) + ++count; + } + return count; + }; + UniqueCertStore additionalStoreHolder; + HCERTSTORE hAdditionalStore = cert->hCertStore; + if (HCERTSTORE hFlatStore = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, NULL, 0, NULL)) { + additionalStoreHolder = hFlatStore; + DWORD fromPeer = copyStoreToFlat(hFlatStore, cert->hCertStore); + DWORD fromUserCA = 0; + if (HCERTSTORE hUserCA = CertOpenSystemStoreW(NULL, L"CA")) { + fromUserCA = copyStoreToFlat(hFlatStore, hUserCA); + CertCloseStore(hUserCA, 0); + } + DWORD fromMachineCA = 0; + if (HCERTSTORE hMachineCA = + CertOpenStore(CERT_STORE_PROV_SYSTEM, + 0, + NULL, + CERT_SYSTEM_STORE_LOCAL_MACHINE | CERT_STORE_READONLY_FLAG, + L"CA")) { + fromMachineCA = copyStoreToFlat(hFlatStore, hMachineCA); + CertCloseStore(hMachineCA, 0); + } + hAdditionalStore = hFlatStore; + LOGV2_DEBUG(7998017, + 2, + "validatePeerCertificate: flat intermediate-CA store populated", + "fromPeerTLSMessage"_attr = fromPeer, + "fromUserCAStore"_attr = fromUserCA, + "fromMachineCAStore"_attr = fromMachineCA, + "total"_attr = fromPeer + fromUserCA + fromMachineCA); + } + auto before = Date_t::now(); PCCERT_CHAIN_CONTEXT chainContext; if (!CertGetCertificateChain(certChainEngine, cert, NULL, - cert->hCertStore, + hAdditionalStore, &certChainPara, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT, NULL, @@ -1816,6 +2139,15 @@ Status validatePeerCertificate(const std::string& remoteHost, UniqueCertChain certChainHolder(chainContext); + LOGV2_DEBUG(7998013, + 2, + "CertGetCertificateChain result", + "trustErrorStatus"_attr = unsignedHex(chainContext->TrustStatus.dwErrorStatus), + "trustInfoStatus"_attr = unsignedHex(chainContext->TrustStatus.dwInfoStatus), + "chainCount"_attr = chainContext->cChain, + "chainLength"_attr = + (chainContext->cChain > 0 ? chainContext->rgpChain[0]->cElement : 0)); + SSL_EXTRA_CERT_CHAIN_POLICY_PARA sslCertChainPolicy; memset(&sslCertChainPolicy, 0, sizeof(sslCertChainPolicy)); sslCertChainPolicy.cbSize = sizeof(sslCertChainPolicy); @@ -2006,7 +2338,6 @@ StatusWith mapTLSVersion(PCtxtHandle ssl) { << "QueryContextAttributes for connection info failed with" << ss); } - // SERVER-98279: support tls 1.3 for windows & apple switch (connInfo.dwProtocol) { case SP_PROT_TLS1_CLIENT: case SP_PROT_TLS1_SERVER: @@ -2017,12 +2348,15 @@ StatusWith mapTLSVersion(PCtxtHandle ssl) { case SP_PROT_TLS1_2_CLIENT: case SP_PROT_TLS1_2_SERVER: return TLSVersion::kTLS12; + case SP_PROT_TLS1_3_CLIENT: + case SP_PROT_TLS1_3_SERVER: + return TLSVersion::kTLS13; default: return TLSVersion::kUnknown; } } -Status SSLManagerWindows::stapleOCSPResponse(SCHANNEL_CRED* cred, bool asyncOCSPStaple) { +Status SSLManagerWindows::stapleOCSPResponse(SCH_CREDENTIALS* cred, bool asyncOCSPStaple) { return Status::OK(); }