From bf1d0d434b5a6842a2d8b22a7c5840add6280494 Mon Sep 17 00:00:00 2001 From: Archkon <180910180+Archkon@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:12:55 +0800 Subject: [PATCH 1/2] tls: include OpenSSL CAs in default CA list When --use-openssl-ca is enabled, TLS clients use OpenSSL's default certificate locations, but tls.getCACertificates('default') did not include those certificates. Expose the enumerable OpenSSL default CA certificates through the crypto binding and include them in the default CA list returned by tls.getCACertificates('default'). Also add regression coverage using SSL_CERT_FILE to avoid depending on the host system CA store. Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> --- doc/api/tls.md | 6 ++- lib/tls.js | 16 +++++- src/crypto/crypto_context.cc | 49 ++++++++++++++++++- .../tls-check-openssl-ca-certificates.js | 13 +++++ .../test-tls-get-ca-certificates-openssl.js | 23 +++++++++ typings/internalBinding/crypto.d.ts | 1 + 6 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/tls-check-openssl-ca-certificates.js create mode 100644 test/parallel/test-tls-get-ca-certificates-openssl.js diff --git a/doc/api/tls.md b/doc/api/tls.md index 95335e6f275099..5e45857ff75818 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -2370,8 +2370,10 @@ added: Returns an array containing the CA certificates from various sources, depending on `type`: * `"default"`: return the CA certificates that will be used by the Node.js TLS clients by default. - * When [`--use-bundled-ca`][] is enabled (default), or [`--use-openssl-ca`][] is not enabled, - this would include CA certificates from the bundled Mozilla CA store. + * When [`--use-openssl-ca`][] is enabled, this would include CA certificates loaded + from OpenSSL's default certificate file and directory. + * When [`--use-bundled-ca`][] is enabled (default), this would include CA certificates + from the bundled Mozilla CA store. * When [`--use-system-ca`][] is enabled, this would also include certificates from the system's trusted store. * When [`NODE_EXTRA_CA_CERTS`][] is used, this would also include certificates loaded from the specified diff --git a/lib/tls.js b/lib/tls.js index 283e5f5bc7870a..7d429823714cd1 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -43,6 +43,7 @@ const { const { getBundledRootCertificates, getExtraCACertificates, + getOpenSSLCACertificates, getSystemCACertificates, resetRootCertStore, getUserRootCertificates, @@ -146,6 +147,13 @@ function cacheSystemCACertificates() { return systemCACertificates; } +let opensslCACertificates; +function cacheOpenSSLCACertificates() { + opensslCACertificates ||= ObjectFreeze(getOpenSSLCACertificates()); + + return opensslCACertificates; +} + let defaultCACertificates; let hasResetDefaultCACertificates = false; @@ -160,7 +168,12 @@ function cacheDefaultCACertificates() { defaultCACertificates = []; - if (!getOptionValue('--use-openssl-ca')) { + if (getOptionValue('--use-openssl-ca')) { + const openssl = cacheOpenSSLCACertificates(); + for (let i = 0; i < openssl.length; ++i) { + ArrayPrototypePush(defaultCACertificates, openssl[i]); + } + } else { const bundled = cacheBundledRootCertificates(); for (let i = 0; i < bundled.length; ++i) { ArrayPrototypePush(defaultCACertificates, bundled[i]); @@ -231,6 +244,7 @@ if (isBuildingSnapshot()) { // Bundled certificates are immutable so they are spared. extraCACertificates = undefined; systemCACertificates = undefined; + opensslCACertificates = undefined; if (hasResetDefaultCACertificates) { defaultCACertificates = undefined; } diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 01d8a17d8e2f53..452fb0a8e2e102 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -843,6 +843,33 @@ static void LoadCertsFromDir(std::vector* certs, } } +static void LoadCertsFromOpenSSLDirs(std::vector* certs, + std::string_view cert_dirs) { +#ifdef _WIN32 + static constexpr char kOpenSSLDirSeparator = ';'; +#elif defined(__VMS) + static constexpr char kOpenSSLDirSeparator = ','; +#else + static constexpr char kOpenSSLDirSeparator = ':'; +#endif + + size_t start = 0; + while (start <= cert_dirs.size()) { + size_t end = cert_dirs.find(kOpenSSLDirSeparator, start); + if (end == std::string_view::npos) { + end = cert_dirs.size(); + } + if (end > start) { + std::string cert_dir(cert_dirs.substr(start, end - start)); + LoadCertsFromDir(certs, cert_dir); + } + if (end == cert_dirs.size()) { + break; + } + start = end + 1; + } +} + // Loads CA certificates from the default certificate paths respected by // OpenSSL. void GetOpenSSLSystemCertificates(std::vector* system_store_certs) { @@ -867,7 +894,7 @@ void GetOpenSSLSystemCertificates(std::vector* system_store_certs) { } if (!cert_dir.empty()) { - LoadCertsFromDir(system_store_certs, cert_dir.c_str()); + LoadCertsFromOpenSSLDirs(system_store_certs, cert_dir); } } @@ -1334,6 +1361,23 @@ void GetSystemCACertificates(const FunctionCallbackInfo& args) { } } +void GetOpenSSLCACertificates(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + std::vector certs; + GetOpenSSLSystemCertificates(&certs); + auto cleanup = OnScopeLeave([&certs]() { + for (X509* cert : certs) { + X509_free(cert); + } + }); + + Local results; + if (X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size()) + .ToLocal(&results)) { + args.GetReturnValue().Set(results); + } +} + void GetExtraCACertificates(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); if (extra_root_certs_file.empty()) { @@ -1438,6 +1482,8 @@ void SecureContext::Initialize(Environment* env, Local target) { GetBundledRootCertificates); SetMethodNoSideEffect( context, target, "getSystemCACertificates", GetSystemCACertificates); + SetMethodNoSideEffect( + context, target, "getOpenSSLCACertificates", GetOpenSSLCACertificates); SetMethodNoSideEffect( context, target, "getExtraCACertificates", GetExtraCACertificates); SetMethod(context, target, "resetRootCertStore", ResetRootCertStore); @@ -1493,6 +1539,7 @@ void SecureContext::RegisterExternalReferences( registry->Register(GetBundledRootCertificates); registry->Register(GetSystemCACertificates); + registry->Register(GetOpenSSLCACertificates); registry->Register(GetExtraCACertificates); registry->Register(ResetRootCertStore); registry->Register(GetUserRootCertificates); diff --git a/test/fixtures/tls-check-openssl-ca-certificates.js b/test/fixtures/tls-check-openssl-ca-certificates.js new file mode 100644 index 00000000000000..6bf72c551f8b49 --- /dev/null +++ b/test/fixtures/tls-check-openssl-ca-certificates.js @@ -0,0 +1,13 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const tls = require('tls'); +const { includesCert } = require('../common/tls'); + +const expected = fs.readFileSync(process.env.SSL_CERT_FILE, 'utf8'); +const defaultCerts = tls.getCACertificates('default'); + +assert(includesCert(defaultCerts, expected)); +assert.strictEqual(defaultCerts, tls.getCACertificates()); +assert.strictEqual(defaultCerts, tls.getCACertificates('default')); diff --git a/test/parallel/test-tls-get-ca-certificates-openssl.js b/test/parallel/test-tls-get-ca-certificates-openssl.js new file mode 100644 index 00000000000000..166940c77749f0 --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-openssl.js @@ -0,0 +1,23 @@ +'use strict'; + +// This tests that tls.getCACertificates('default') includes certificates from +// OpenSSL's default certificate file when --use-openssl-ca is enabled. + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); +const fixtures = require('../common/fixtures'); + +spawnSyncAndExitWithoutError( + process.execPath, + ['--use-openssl-ca', fixtures.path('tls-check-openssl-ca-certificates.js')], + { + env: { + ...process.env, + NODE_EXTRA_CA_CERTS: undefined, + SSL_CERT_FILE: fixtures.path('keys', 'ca1-cert.pem'), + SSL_CERT_DIR: '', + }, + }, +); diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index d91c5018ba688a..52e36759ed077b 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -933,6 +933,7 @@ export interface CryptoBinding { getFipsCrypto(): 0 | 1; getHashes(): string[]; getKeyObjectSlots(key: object): InternalCryptoBinding.KeyObjectSlots; + getOpenSSLCACertificates(): string[]; getOpenSSLSecLevelCrypto(): number | undefined; getSSLCiphers(): string[]; getSystemCACertificates(): string[]; From 13c253d242384d61a68da0cd5ba4e64e01d4ede3 Mon Sep 17 00:00:00 2001 From: Archkon <180910180+Archkon@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:34:49 +0800 Subject: [PATCH 2/2] test: add coverage for multiple SSL_CERT_DIR entries Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> --- .../tls-check-openssl-ca-certificates.js | 8 ++++-- .../test-tls-get-ca-certificates-openssl.js | 26 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/test/fixtures/tls-check-openssl-ca-certificates.js b/test/fixtures/tls-check-openssl-ca-certificates.js index 6bf72c551f8b49..96c5482ed17444 100644 --- a/test/fixtures/tls-check-openssl-ca-certificates.js +++ b/test/fixtures/tls-check-openssl-ca-certificates.js @@ -5,9 +5,13 @@ const fs = require('fs'); const tls = require('tls'); const { includesCert } = require('../common/tls'); -const expected = fs.readFileSync(process.env.SSL_CERT_FILE, 'utf8'); +const expectedCertFiles = process.env.EXPECTED_CERT_FILES ? + JSON.parse(process.env.EXPECTED_CERT_FILES) : [process.env.SSL_CERT_FILE]; const defaultCerts = tls.getCACertificates('default'); -assert(includesCert(defaultCerts, expected)); +for (const certFile of expectedCertFiles) { + const expected = fs.readFileSync(certFile, 'utf8'); + assert(includesCert(defaultCerts, expected)); +} assert.strictEqual(defaultCerts, tls.getCACertificates()); assert.strictEqual(defaultCerts, tls.getCACertificates('default')); diff --git a/test/parallel/test-tls-get-ca-certificates-openssl.js b/test/parallel/test-tls-get-ca-certificates-openssl.js index 166940c77749f0..0d5fa80a648aad 100644 --- a/test/parallel/test-tls-get-ca-certificates-openssl.js +++ b/test/parallel/test-tls-get-ca-certificates-openssl.js @@ -1,13 +1,32 @@ 'use strict'; // This tests that tls.getCACertificates('default') includes certificates from -// OpenSSL's default certificate file when --use-openssl-ca is enabled. +// OpenSSL's default certificate file and directories when --use-openssl-ca is +// enabled. const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const fs = require('fs'); +const path = require('path'); const { spawnSyncAndExitWithoutError } = require('../common/child_process'); const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const firstCertDir = tmpdir.resolve('openssl-certs-1'); +const secondCertDir = tmpdir.resolve('openssl-certs-2'); +fs.mkdirSync(firstCertDir); +fs.mkdirSync(secondCertDir); + +const expectedCertFiles = [ + fixtures.path('keys', 'ca1-cert.pem'), + fixtures.path('keys', 'ca2-cert.pem'), + fixtures.path('keys', 'ca3-cert.pem'), +]; +fs.copyFileSync(expectedCertFiles[1], path.join(firstCertDir, 'ca2-cert.pem')); +fs.copyFileSync(expectedCertFiles[2], path.join(secondCertDir, 'ca3-cert.pem')); spawnSyncAndExitWithoutError( process.execPath, @@ -15,9 +34,10 @@ spawnSyncAndExitWithoutError( { env: { ...process.env, + EXPECTED_CERT_FILES: JSON.stringify(expectedCertFiles), NODE_EXTRA_CA_CERTS: undefined, - SSL_CERT_FILE: fixtures.path('keys', 'ca1-cert.pem'), - SSL_CERT_DIR: '', + SSL_CERT_FILE: expectedCertFiles[0], + SSL_CERT_DIR: [firstCertDir, secondCertDir].join(path.delimiter), }, }, );