SERVER-79980 Windows TLS 1.3 support (#52302)
Co-authored-by: Zack Winter <zack.winter@mongodb.com> GitOrigin-RevId: 130b44f51346450505473d5744b280587ff5563c
This commit is contained in:
parent
45ed8d2e32
commit
278e845819
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"}}),
|
||||
);
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"]);
|
||||
}
|
||||
|
||||
203
jstests/ssl/tls13_windows.js
Normal file
203
jstests/ssl/tls13_windows.js
Normal file
@ -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.");
|
||||
@ -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(
|
||||
|
||||
@ -96,13 +96,14 @@
|
||||
|
||||
#define CERT_CHAIN_PARA_HAS_EXTRA_FIELDS
|
||||
|
||||
#include <schannel.h>
|
||||
|
||||
#undef WIN32_NO_STATUS
|
||||
|
||||
// Obtain a definition for the ntstatus type.
|
||||
#include <winternl.h>
|
||||
|
||||
#define SCHANNEL_USE_BLACKLISTS
|
||||
#include <schannel.h>
|
||||
|
||||
// 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).
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -12,3 +12,6 @@ filters:
|
||||
- "*openssl*":
|
||||
approvers:
|
||||
- 10gen/server-security
|
||||
- "README-windowstls.md":
|
||||
approvers:
|
||||
- 10gen/server-security
|
||||
|
||||
261
src/mongo/util/net/README-windowstls.md
Normal file
261
src/mongo/util/net/README-windowstls.md
Normal file
@ -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<SECURITY_STATUS>(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.
|
||||
@ -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_.
|
||||
|
||||
|
||||
@ -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_;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 <algorithm>
|
||||
@ -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<int>(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<SecBuffer, 3> 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<const uint8_t*>(_pInBuffer->data());
|
||||
const ULONG preCallInputLen = static_cast<ULONG>(_pInBuffer->size());
|
||||
|
||||
LOGV2_DEBUG(7998035,
|
||||
0,
|
||||
"TLS decryptBuffer: calling DecryptMessage",
|
||||
"inputBytes"_attr = preCallInputLen);
|
||||
|
||||
std::array<SecBuffer, 4> 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<int32_t>(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<SECURITY_STATUS>(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<const uint8_t*>(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<uint32_t>(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<int32_t>(ss),
|
||||
"messageLength"_attr = messageLength);
|
||||
ec = asio::error_code(ss, asio::error::get_ssl_category());
|
||||
return ssl_want::want_nothing;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -42,7 +42,9 @@ struct stream_core {
|
||||
|
||||
template <typename Executor>
|
||||
#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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 <openssl/err.h>
|
||||
#include <openssl/ssl.h>
|
||||
#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;
|
||||
|
||||
@ -1448,7 +1448,7 @@ StatusWith<std::pair<::SSLProtocol, ::SSLProtocol>> 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<TLSVersion> 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 <fstream>
|
||||
|
||||
@ -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<SSLManagerInterface> 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<SSLManagerInterface> 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<uint32_t>(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<uint8_t>(kNstPayload >> 8);
|
||||
buf[4] = static_cast<uint8_t>(kNstPayload & 0xFF);
|
||||
|
||||
const uint32_t recordSize = tlsRecordTotalSize(buf, sizeof(buf));
|
||||
ASSERT_EQ(recordSize, kNstTotal);
|
||||
ASSERT_LT(recordSize, static_cast<uint32_t>(sizeof(buf)));
|
||||
|
||||
const uint32_t extraBytes = static_cast<uint32_t>(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<CertValidationTestCase> 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<CertValidationTestCase> 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<TestCase> 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<TestCase> 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<TestCase> 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
|
||||
|
||||
@ -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 <atomic>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@ -62,6 +61,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <asio.hpp>
|
||||
#include <ncrypt.h>
|
||||
#include <winhttp.h>
|
||||
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
@ -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<HCRYPTPROV, CryptProviderFree>;
|
||||
|
||||
/**
|
||||
* 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<HCRYPTKEY, CryptKeyFree>;
|
||||
using UniqueNcryptProvider = AutoHandle<NCRYPT_PROV_HANDLE, NcryptFree>;
|
||||
using UniqueNcryptKey = AutoHandle<NCRYPT_KEY_HANDLE, NcryptFree>;
|
||||
|
||||
/**
|
||||
* 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<NCRYPT_KEY_HANDLE, NcryptKeyDeleter>;
|
||||
|
||||
/**
|
||||
* Free a CERTSTORE Handle
|
||||
@ -221,11 +241,49 @@ struct CertChainEngineFree {
|
||||
using UniqueCertChainEngine = AutoHandle<HCERTCHAINENGINE, CertChainEngineFree>;
|
||||
|
||||
/**
|
||||
* 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<UniqueCertificate, UniqueCryptProvider>;
|
||||
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<NCRYPT_KEY_HANDLE>(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<bool>(cert);
|
||||
}
|
||||
PCCERT_CONTEXT get() const {
|
||||
return cert.get();
|
||||
}
|
||||
const CERT_CONTEXT* operator->() const {
|
||||
return cert.get();
|
||||
}
|
||||
};
|
||||
using UniqueCertificateWithPrivateKey = CertificateWithKey;
|
||||
|
||||
|
||||
StatusWith<stdx::unordered_set<RoleName>> parsePeerRoles(PCCERT_CONTEXT cert) {
|
||||
@ -249,13 +307,13 @@ StatusWith<stdx::unordered_set<RoleName>> parsePeerRoles(PCCERT_CONTEXT cert) {
|
||||
*/
|
||||
class SSLConnectionWindows : public SSLConnectionInterface {
|
||||
public:
|
||||
SCHANNEL_CRED* _cred;
|
||||
SCH_CREDENTIALS* _cred;
|
||||
Socket* socket;
|
||||
asio::ssl::detail::engine _engine;
|
||||
|
||||
std::vector<char> _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>& 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> _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<std::weak_ptr<const SSLConnectionContext>> _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<std::array<BYTE, 20>> _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<BYTE*>(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<UniqueCertChainEngine> 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<const char*>(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<std::vector<UniqueCertificate>> 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<UniqueCertificate>& certificates) {
|
||||
for (auto& cert : certificates) {
|
||||
if (!CertAddCertificateContextToStore(certStore, cert.get(), CERT_STORE_ADD_NEW, NULL)) {
|
||||
@ -816,40 +976,19 @@ StatusWith<UniqueCertificateWithPrivateKey> 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<uint8_t> privateKey;
|
||||
@ -917,103 +1056,113 @@ StatusWith<UniqueCertificateWithPrivateKey> 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_<PID>_<counter>" 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_<PID>_<N>" 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<uint64_t> keyContainerCounter{0};
|
||||
wchar_t containerName[64] = {};
|
||||
_snwprintf_s(containerName,
|
||||
static_cast<int>(std::size(containerName)),
|
||||
_TRUNCATE,
|
||||
L"mongod_%lu_%llu",
|
||||
static_cast<unsigned long>(GetCurrentProcessId()),
|
||||
static_cast<unsigned long long>(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<int> 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<wchar_t*>(wstr.c_str());
|
||||
keyProvInfo.pwszProvName = const_cast<wchar_t*>(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<ULONG>((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<ULONG>(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<LPWSTR>(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<BYTE, 20> 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::vector<SSLParams::Protocols>*, 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<int>(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<TLSVersion> 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<TLSVersion> 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();
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user