From 79190ee653225894add95a44508ef80eb7a8d16b 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/3] 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 b962d648a636ad8814eb87537c4a1cc3d4e104a9 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/3] tls: add OpenSSL CA lookup to getCACertificates Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> --- doc/api/tls.md | 18 +- lib/tls.js | 66 ++++-- src/crypto/crypto_context.cc | 198 +++++++++++++++++- .../tls-check-openssl-ca-certificates.js | 73 ++++++- .../test-tls-get-ca-certificates-openssl.js | 50 ++++- ...-get-ca-certificates-system-openssl-dir.js | 67 ++++++ typings/internalBinding/crypto.d.ts | 3 +- 7 files changed, 445 insertions(+), 30 deletions(-) create mode 100644 test/parallel/test-tls-get-ca-certificates-system-openssl-dir.js diff --git a/doc/api/tls.md b/doc/api/tls.md index 5e45857ff75818..d1854e50eb1326 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -2353,7 +2353,7 @@ const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...']; tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]); ``` -## `tls.getCACertificates([type])` +## `tls.getCACertificates([type[, subjectNameOrCert]])` * `type` {string|undefined} The type of CA certificates that will be returned. Valid values - are `"default"`, `"system"`, `"bundled"` and `"extra"`. + are `"default"`, `"system"`, `"bundled"`, `"extra"` and `"openssl"`. **Default:** `"default"`. +* `subjectNameOrCert` {string|ArrayBufferView|X509Certificate|undefined} + The subject name or certificate used to filter OpenSSL CA certificates. + Required when `type` is `"openssl"`. * Returns: {string\[]} An array of PEM-encoded certificates. The array may contain duplicates if the same certificate is repeatedly stored in multiple sources. 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-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-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-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 @@ -2385,6 +2386,11 @@ Returns an array containing the CA certificates from various sources, depending as [`tls.rootCertificates`][]. * `"extra"`: return the CA certificates loaded from [`NODE_EXTRA_CA_CERTS`][]. It's an empty array if [`NODE_EXTRA_CA_CERTS`][] is not set. +* `"openssl"`: return CA certificates looked up from OpenSSL's default CA + store using `subjectNameOrCert`. If `subjectNameOrCert` is a certificate, + its issuer name is used for the lookup. If it is a subject name string, the + subject name is used directly. This follows OpenSSL's hashed-directory lookup + behavior for certificates loaded from the default certificate directory. ## `tls.getCiphers()` diff --git a/lib/tls.js b/lib/tls.js index 7d429823714cd1..5043e667afe260 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -29,6 +29,7 @@ const { JSONParse, ObjectDefineProperty, ObjectFreeze, + SafeMap, StringFromCharCode, } = primordials; @@ -43,7 +44,8 @@ const { const { getBundledRootCertificates, getExtraCACertificates, - getOpenSSLCACertificates, + // getOpenSSLCACertificates, + lookupOpenSSLCACertificates, getSystemCACertificates, resetRootCertStore, getUserRootCertificates, @@ -72,6 +74,8 @@ const tlsCommon = require('internal/tls/common'); const tlsWrap = require('internal/tls/wrap'); const { domainToASCII } = require('internal/url'); const { validateString } = require('internal/validators'); +const { isX509Certificate } = require('internal/crypto/x509'); +const { kHandle } = require('internal/crypto/util'); const { namespace: { @@ -147,11 +151,30 @@ function cacheSystemCACertificates() { return systemCACertificates; } -let opensslCACertificates; -function cacheOpenSSLCACertificates() { - opensslCACertificates ||= ObjectFreeze(getOpenSSLCACertificates()); +// let opensslCACertificates; +// function cacheOpenSSLCACertificates() { +// opensslCACertificates ||= ObjectFreeze(getOpenSSLCACertificates()); - return opensslCACertificates; +// return opensslCACertificates; +// } + +let opensslCACertificateLookup; +function cacheOpenSSLCertificateLookup(subjectNameOrCert) { + opensslCACertificateLookup ??= new SafeMap(); + + const key = isArrayBufferView(subjectNameOrCert) ? + `buffer:${Buffer.from( + subjectNameOrCert.buffer, + subjectNameOrCert.byteOffset, + subjectNameOrCert.byteLength).toString('base64')}` : + `subject:${subjectNameOrCert}`; + + const cached = opensslCACertificateLookup.get(key); + if (cached !== undefined) return cached; + + const result = ObjectFreeze(lookupOpenSSLCACertificates(subjectNameOrCert)); + opensslCACertificateLookup.set(key, result); + return result; } let defaultCACertificates; @@ -168,12 +191,7 @@ function cacheDefaultCACertificates() { defaultCACertificates = []; - if (getOptionValue('--use-openssl-ca')) { - const openssl = cacheOpenSSLCACertificates(); - for (let i = 0; i < openssl.length; ++i) { - ArrayPrototypePush(defaultCACertificates, openssl[i]); - } - } else { + if (!getOptionValue('--use-openssl-ca')) { const bundled = cacheBundledRootCertificates(); for (let i = 0; i < bundled.length; ++i) { ArrayPrototypePush(defaultCACertificates, bundled[i]); @@ -199,8 +217,25 @@ function cacheDefaultCACertificates() { return defaultCACertificates; } +function validateOpenSSLCACertificateFilter(subjectNameOrCert) { + if (subjectNameOrCert !== null && + (typeof subjectNameOrCert === 'object' || + typeof subjectNameOrCert === 'function') && + isX509Certificate(subjectNameOrCert)) { + return subjectNameOrCert[kHandle].pem(); + } + if (typeof subjectNameOrCert !== 'string' && + !isArrayBufferView(subjectNameOrCert)) { + throw new ERR_INVALID_ARG_TYPE( + 'subjectNameOrCert', + ['string', 'ArrayBufferView', 'X509Certificate'], + subjectNameOrCert); + } + return subjectNameOrCert; +} + // TODO(joyeecheung): support X509Certificate output? -function getCACertificates(type = 'default') { +function getCACertificates(type = 'default', subjectNameOrCert) { validateString(type, 'type'); switch (type) { @@ -212,6 +247,10 @@ function getCACertificates(type = 'default') { return cacheSystemCACertificates(); case 'extra': return cacheExtraCACertificates(); + case 'openssl': + subjectNameOrCert = + validateOpenSSLCACertificateFilter(subjectNameOrCert); + return cacheOpenSSLCertificateLookup(subjectNameOrCert); default: throw new ERR_INVALID_ARG_VALUE('type', type); } @@ -244,7 +283,8 @@ if (isBuildingSnapshot()) { // Bundled certificates are immutable so they are spared. extraCACertificates = undefined; systemCACertificates = undefined; - opensslCACertificates = undefined; + // opensslCACertificates = undefined; + opensslCACertificateLookup = undefined; if (hasResetDefaultCACertificates) { defaultCACertificates = undefined; } diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 452fb0a8e2e102..6a0bb47961011a 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -30,6 +30,7 @@ #include #endif +#include #include namespace node { @@ -78,6 +79,13 @@ using v8::String; using v8::Value; namespace crypto { +using X509NamePointer = + std::unique_ptr; +using X509StorePointer = + std::unique_ptr; +using X509StoreCtxPointer = + std::unique_ptr; + static const char* const root_certs[] = { #include "node_root_certs.h" // NOLINT(build/include_order) }; @@ -308,6 +316,64 @@ static unsigned long LoadCertsFromFile( // NOLINT(runtime/int) return LoadCertsFromBIO(certs, std::move(bio)); } +static std::string TrimX509NameComponent(std::string_view value) { + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + while (!value.empty() && + (value.back() == ' ' || value.back() == '\t' || + value.back() == '\r')) { + value.remove_suffix(1); + } + return std::string(value); +} + +// Maybe parser for X509Certificate.subject-style strings. This does not +// fully preserve ASN.1 string types, multi-value RDNs, or escaped separators, so +// certificate input is preferred when exact OpenSSL lookup semantics matter. +static X509NamePointer ParseX509SubjectName(std::string_view subject) { + MarkPopErrorOnReturn mark_pop_error_on_return; + X509NamePointer name(X509_NAME_new(), X509_NAME_free); + if (!name) return {nullptr, X509_NAME_free}; + + size_t start = 0; + while (start <= subject.size()) { + size_t end = subject.find('\n', start); + if (end == std::string_view::npos) { + end = subject.size(); + } + + std::string_view line = subject.substr(start, end - start); + if (!line.empty()) { + size_t separator = line.find('='); + if (separator == std::string_view::npos || separator == 0) { + return {nullptr, X509_NAME_free}; + } + + std::string key = TrimX509NameComponent(line.substr(0, separator)); + std::string value = TrimX509NameComponent(line.substr(separator + 1)); + if (key.empty() || value.size() > INT_MAX || + X509_NAME_add_entry_by_txt( + name.get(), + key.c_str(), + MBSTRING_UTF8, + reinterpret_cast(value.data()), + static_cast(value.size()), + -1, + 0) != 1) { + return {nullptr, X509_NAME_free}; + } + } + + if (end == subject.size()) { + break; + } + start = end + 1; + } + + return name; +} + // Indicates the trust status of a certificate. enum class TrustStatus { // Trust status is unknown / uninitialized. @@ -1361,6 +1427,7 @@ void GetSystemCACertificates(const FunctionCallbackInfo& args) { } } +/* void GetOpenSSLCACertificates(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); std::vector certs; @@ -1377,6 +1444,126 @@ void GetOpenSSLCACertificates(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(results); } } +*/ + +static X509StorePointer NewOpenSSLDefaultCertificateStore(Environment* env) { + X509StorePointer store(X509_STORE_new(), X509_STORE_free); + if (!store) { + ThrowCryptoError(env, ERR_get_error(), "X509_STORE_new"); + return {nullptr, X509_STORE_free}; + } + + if (X509_STORE_set_default_paths(store.get()) != 1) { + ThrowCryptoError(env, ERR_get_error(), "X509_STORE_set_default_paths"); + return {nullptr, X509_STORE_free}; + } + + return store; +} + +static MaybeLocal LookupOpenSSLCACertificatesBySubject( + Environment* env, + const X509_NAME* subject) { + X509StorePointer store = NewOpenSSLDefaultCertificateStore(env); + if (!store) return MaybeLocal(); + + X509StoreCtxPointer store_ctx(X509_STORE_CTX_new(), X509_STORE_CTX_free); + if (!store_ctx) { + ThrowCryptoError(env, ERR_get_error(), "X509_STORE_CTX_new"); + return MaybeLocal(); + } + + if (X509_STORE_CTX_init(store_ctx.get(), store.get(), nullptr, nullptr) != + 1) { + ThrowCryptoError(env, ERR_get_error(), "X509_STORE_CTX_init"); + return MaybeLocal(); + } + + STACK_OF(X509)* stack = X509_STORE_CTX_get1_certs(store_ctx.get(), subject); + if (stack == nullptr) { + ThrowCryptoError(env, ERR_get_error(), "X509_STORE_CTX_get1_certs"); + return MaybeLocal(); + } + auto cleanup_stack = + OnScopeLeave([stack]() { sk_X509_pop_free(stack, X509_free); }); + + std::vector certs; + int count = sk_X509_num(stack); + certs.reserve(count); + for (int i = 0; i < count; ++i) { + certs.push_back(sk_X509_value(stack, i)); + } + + return X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size()); +} + +static MaybeLocal LookupOpenSSLCACertificatesByIssuer( + Environment* env, + X509* cert) { + X509StorePointer store = NewOpenSSLDefaultCertificateStore(env); + if (!store) return MaybeLocal(); + + X509StoreCtxPointer store_ctx(X509_STORE_CTX_new(), X509_STORE_CTX_free); + if (!store_ctx) { + ThrowCryptoError(env, ERR_get_error(), "X509_STORE_CTX_new"); + return MaybeLocal(); + } + + if (X509_STORE_CTX_init(store_ctx.get(), store.get(), cert, nullptr) != 1) { + ThrowCryptoError(env, ERR_get_error(), "X509_STORE_CTX_init"); + return MaybeLocal(); + } + + X509* issuer = nullptr; + int ret = X509_STORE_CTX_get1_issuer(&issuer, store_ctx.get(), cert); + if (ret < 0) { + ThrowCryptoError(env, ERR_get_error(), "X509_STORE_CTX_get1_issuer"); + return MaybeLocal(); + } + + if (ret == 0) { + return Array::New(env->isolate()); + } + + auto cleanup_issuer = OnScopeLeave([issuer]() { X509_free(issuer); }); + std::vector certs{issuer}; + return X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size()); +} + +void LookupOpenSSLCACertificates(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString() || args[0]->IsArrayBufferView()); + + ByteSource input = ByteSource::FromStringOrBuffer(env, args[0]); + auto parsed_cert = X509Pointer::Parse(ncrypto::Buffer{ + .data = input.data(), + .len = input.size(), + }); + Local results; + if (parsed_cert.value) { + if (LookupOpenSSLCACertificatesByIssuer(env, parsed_cert.value.get()) + .ToLocal(&results)) { + args.GetReturnValue().Set(results); + } + return; + } + + if (!args[0]->IsString()) { + return ThrowCryptoError( + env, parsed_cert.error.value_or(0), "Failed to parse certificate"); + } + + std::string subject(input.data(), input.size()); + X509NamePointer subject_name = ParseX509SubjectName(subject); + if (!subject_name) { + return THROW_ERR_INVALID_ARG_VALUE(env, "Invalid subject name"); + } + + if (LookupOpenSSLCACertificatesBySubject(env, subject_name.get()) + .ToLocal(&results)) { + args.GetReturnValue().Set(results); + } +} void GetExtraCACertificates(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1482,8 +1669,12 @@ void SecureContext::Initialize(Environment* env, Local target) { GetBundledRootCertificates); SetMethodNoSideEffect( context, target, "getSystemCACertificates", GetSystemCACertificates); - SetMethodNoSideEffect( - context, target, "getOpenSSLCACertificates", GetOpenSSLCACertificates); + // SetMethodNoSideEffect( + // context, target, "getOpenSSLCACertificates", GetOpenSSLCACertificates); + SetMethodNoSideEffect(context, + target, + "lookupOpenSSLCACertificates", + LookupOpenSSLCACertificates); SetMethodNoSideEffect( context, target, "getExtraCACertificates", GetExtraCACertificates); SetMethod(context, target, "resetRootCertStore", ResetRootCertStore); @@ -1539,7 +1730,8 @@ void SecureContext::RegisterExternalReferences( registry->Register(GetBundledRootCertificates); registry->Register(GetSystemCACertificates); - registry->Register(GetOpenSSLCACertificates); + // registry->Register(GetOpenSSLCACertificates); + registry->Register(LookupOpenSSLCACertificates); 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 index 6bf72c551f8b49..467f774eba4436 100644 --- a/test/fixtures/tls-check-openssl-ca-certificates.js +++ b/test/fixtures/tls-check-openssl-ca-certificates.js @@ -1,13 +1,80 @@ 'use strict'; const assert = require('assert'); +const { X509Certificate } = require('crypto'); 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)); +assert.deepStrictEqual(defaultCerts, []); assert.strictEqual(defaultCerts, tls.getCACertificates()); assert.strictEqual(defaultCerts, tls.getCACertificates('default')); + +const expectedCerts = expectedCertFiles.map((certFile) => { + const cert = fs.readFileSync(certFile, 'utf8'); + const x509 = new X509Certificate(cert); + return { cert, x509 }; +}); +const certsBySubject = new Map( + expectedCerts.map(({ cert, x509 }) => [x509.subject, cert]), +); + +for (const { cert, x509 } of expectedCerts) { + const opensslCertsBySubject = tls.getCACertificates('openssl', x509.subject); + assert(includesCert(opensslCertsBySubject, cert)); + + const opensslCertsByCert = tls.getCACertificates('openssl', x509); + assert(includesCert(opensslCertsByCert, certsBySubject.get(x509.issuer))); +} + +{ + const { x509 } = expectedCerts[1]; + x509.toString = () => ({}); + assert(includesCert( + tls.getCACertificates('openssl', x509), + certsBySubject.get(x509.issuer))); +} + +{ + const first = expectedCerts[1]; + const second = expectedCerts[0]; + const mutableCert = Buffer.alloc( + Math.max(Buffer.byteLength(first.cert), Buffer.byteLength(second.cert)), + '\n'); + + mutableCert.write(first.cert); + assert(includesCert( + tls.getCACertificates('openssl', mutableCert), + certsBySubject.get(first.x509.issuer))); + + mutableCert.fill('\n'); + mutableCert.write(second.cert); + const lookupAfterMutation = tls.getCACertificates('openssl', mutableCert); + assert(includesCert( + lookupAfterMutation, + certsBySubject.get(second.x509.issuer))); + assert(!includesCert( + lookupAfterMutation, + certsBySubject.get(first.x509.issuer))); +} + +if (process.env.UNEXPECTED_CERT_FILE) { + const unexpected = fs.readFileSync(process.env.UNEXPECTED_CERT_FILE, 'utf8'); + const unexpectedX509 = new X509Certificate(unexpected); + assert.deepStrictEqual( + tls.getCACertificates('openssl', unexpectedX509.subject), + []); + assert(includesCert( + tls.getCACertificates('openssl', unexpectedX509), + certsBySubject.get(unexpectedX509.issuer))); +} + +assert.throws(() => tls.getCACertificates('openssl'), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws(() => tls.getCACertificates('openssl', 'BAD=x'), { + code: 'ERR_INVALID_ARG_VALUE', +}); diff --git a/test/parallel/test-tls-get-ca-certificates-openssl.js b/test/parallel/test-tls-get-ca-certificates-openssl.js index 166940c77749f0..803c73c095a275 100644 --- a/test/parallel/test-tls-get-ca-certificates-openssl.js +++ b/test/parallel/test-tls-get-ca-certificates-openssl.js @@ -1,13 +1,53 @@ 'use strict'; -// This tests that tls.getCACertificates('default') includes certificates from -// OpenSSL's default certificate file when --use-openssl-ca is enabled. +// This tests that tls.getCACertificates('openssl', subjectNameOrCert) looks up +// certificates from OpenSSL's default certificate file and hashed directories. const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); const { spawnSyncAndExitWithoutError } = require('../common/child_process'); +const { opensslCli } = require('../common/crypto'); const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +if (!opensslCli) common.skip('missing openssl command'); + +tmpdir.refresh(); + +const firstCertDir = tmpdir.resolve('openssl-certs-1'); +const secondCertDir = tmpdir.resolve('openssl-certs-2'); +fs.mkdirSync(firstCertDir); +fs.mkdirSync(secondCertDir); + +function getSubjectHash(certFile) { + const child = spawnSync( + opensslCli, + ['x509', '-hash', '-noout', '-in', certFile], + { encoding: 'utf8' }, + ); + assert.strictEqual(child.status, 0, child.stderr); + return child.stdout.trim().split(/\r?\n/)[0]; +} + +function copyHashDirCertificate(certFile, certDir) { + const subjectHash = getSubjectHash(certFile); + fs.copyFileSync(certFile, path.join(certDir, `${subjectHash}.0`)); +} + +const expectedCertFiles = [ + fixtures.path('keys', 'ca1-cert.pem'), + fixtures.path('keys', 'ca2-cert.pem'), + fixtures.path('keys', 'ca3-cert.pem'), +]; +const unexpectedCertFile = fixtures.path('keys', 'ca4-cert.pem'); +copyHashDirCertificate(expectedCertFiles[1], firstCertDir); +copyHashDirCertificate(expectedCertFiles[2], secondCertDir); +fs.copyFileSync(unexpectedCertFile, path.join(secondCertDir, 'ca4-cert.pem')); spawnSyncAndExitWithoutError( process.execPath, @@ -15,9 +55,11 @@ spawnSyncAndExitWithoutError( { env: { ...process.env, + EXPECTED_CERT_FILES: JSON.stringify(expectedCertFiles), + UNEXPECTED_CERT_FILE: unexpectedCertFile, 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), }, }, ); diff --git a/test/parallel/test-tls-get-ca-certificates-system-openssl-dir.js b/test/parallel/test-tls-get-ca-certificates-system-openssl-dir.js new file mode 100644 index 00000000000000..7894c6ad849d68 --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-system-openssl-dir.js @@ -0,0 +1,67 @@ +'use strict'; + +// This tests that OpenSSL-style system certificate directories are split +// before loading system CA certificates. + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +if (common.isWindows || common.isMacOS) { + common.skip('OpenSSL system certificate directories are not used'); +} + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); +const fixtures = require('../common/fixtures'); +const { includesCert } = require('../common/tls'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const emptyCertFile = tmpdir.resolve('empty-cert-file.pem'); +const firstCertDir = tmpdir.resolve('system-certs-1'); +const secondCertDir = tmpdir.resolve('system-certs-2'); +fs.writeFileSync(emptyCertFile, ''); +fs.mkdirSync(firstCertDir); +fs.mkdirSync(secondCertDir); + +const expectedCertFiles = [ + fixtures.path('keys', 'ca1-cert.pem'), + fixtures.path('keys', 'ca2-cert.pem'), +]; +const expectedCerts = expectedCertFiles.map((certFile) => + fs.readFileSync(certFile, 'utf8')); + +fs.copyFileSync(expectedCertFiles[0], path.join(firstCertDir, 'ca1-cert.pem')); +fs.copyFileSync(expectedCertFiles[1], path.join(secondCertDir, 'ca2-cert.pem')); + +const env = { + ...process.env, + NODE_EXTRA_CA_CERTS: undefined, + SSL_CERT_FILE: emptyCertFile, + SSL_CERT_DIR: [firstCertDir, secondCertDir].join(path.delimiter), +}; + +function getCACertificates(type, execArgv = []) { + const certsJSON = tmpdir.resolve(`${type}.json`); + spawnSyncAndExitWithoutError(process.execPath, [ + ...execArgv, + fixtures.path('tls-get-ca-certificates.js'), + ], { + env: { + ...env, + CA_TYPE: type, + CA_OUT: certsJSON, + }, + }); + + return JSON.parse(fs.readFileSync(certsJSON, 'utf8')); +} + +const systemCerts = getCACertificates('system'); +const defaultCerts = getCACertificates('default', ['--use-system-ca']); +for (const cert of expectedCerts) { + assert(includesCert(systemCerts, cert)); + assert(includesCert(defaultCerts, cert)); +} diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index 52e36759ed077b..7bf51d19a0deb5 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -933,7 +933,8 @@ export interface CryptoBinding { getFipsCrypto(): 0 | 1; getHashes(): string[]; getKeyObjectSlots(key: object): InternalCryptoBinding.KeyObjectSlots; - getOpenSSLCACertificates(): string[]; + // getOpenSSLCACertificates(): string[]; + lookupOpenSSLCACertificates(subjectNameOrCert: InternalCryptoBinding.ByteSource): string[]; getOpenSSLSecLevelCrypto(): number | undefined; getSSLCiphers(): string[]; getSystemCACertificates(): string[]; From e73c4adb2c5296458d2727f8db276e2d22526874 Mon Sep 17 00:00:00 2001 From: Archkon <180910180+Archkon@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:29:53 +0800 Subject: [PATCH 3/3] tls: support code compatibility using OpenSSL default CAfile Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> --- lib/tls.js | 19 ++++++++++-- src/crypto/crypto_context.cc | 30 +++++++++++++++++++ .../tls-check-openssl-ca-certificates.js | 16 +++++++++- .../test-tls-get-ca-certificates-openssl.js | 10 ++++++- 4 files changed, 70 insertions(+), 5 deletions(-) diff --git a/lib/tls.js b/lib/tls.js index 5043e667afe260..10d182401b3e5a 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -45,6 +45,7 @@ const { getBundledRootCertificates, getExtraCACertificates, // getOpenSSLCACertificates, + getOpenSSLDefaultCACertificates, lookupOpenSSLCACertificates, getSystemCACertificates, resetRootCertStore, @@ -158,6 +159,14 @@ function cacheSystemCACertificates() { // return opensslCACertificates; // } +let opensslDefaultCACertificates; +function cacheOpenSSLDefaultCACertificates() { + opensslDefaultCACertificates ||= ObjectFreeze( + getOpenSSLDefaultCACertificates()); + + return opensslDefaultCACertificates; +} + let opensslCACertificateLookup; function cacheOpenSSLCertificateLookup(subjectNameOrCert) { opensslCACertificateLookup ??= new SafeMap(); @@ -191,7 +200,12 @@ function cacheDefaultCACertificates() { defaultCACertificates = []; - if (!getOptionValue('--use-openssl-ca')) { + if (getOptionValue('--use-openssl-ca')) { + const openssl = cacheOpenSSLDefaultCACertificates(); + 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]); @@ -199,7 +213,6 @@ function cacheDefaultCACertificates() { if (getOptionValue('--use-system-ca')) { const system = cacheSystemCACertificates(); for (let i = 0; i < system.length; ++i) { - ArrayPrototypePush(defaultCACertificates, system[i]); } } @@ -208,7 +221,6 @@ function cacheDefaultCACertificates() { if (process.env.NODE_EXTRA_CA_CERTS) { const extra = cacheExtraCACertificates(); for (let i = 0; i < extra.length; ++i) { - ArrayPrototypePush(defaultCACertificates, extra[i]); } } @@ -284,6 +296,7 @@ if (isBuildingSnapshot()) { extraCACertificates = undefined; systemCACertificates = undefined; // opensslCACertificates = undefined; + opensslDefaultCACertificates = undefined; opensslCACertificateLookup = undefined; if (hasResetDefaultCACertificates) { defaultCACertificates = undefined; diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 6a0bb47961011a..6121774e2f6276 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -1446,6 +1446,31 @@ void GetOpenSSLCACertificates(const FunctionCallbackInfo& args) { } */ +void GetOpenSSLDefaultCACertificates(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + std::vector certs; + std::string cert_file; + // SSL_CERT_DIR uses subject-hash lookups, so there is no exact list without + // a lookup key. Only the default CA file can be enumerated here. + if (!credentials::SafeGetenv(X509_get_default_cert_file_env(), &cert_file)) { + cert_file = X509_get_default_cert_file(); + } + if (!cert_file.empty()) { + LoadCertsFromFile(&certs, cert_file.c_str()); + } + 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); + } +} + static X509StorePointer NewOpenSSLDefaultCertificateStore(Environment* env) { X509StorePointer store(X509_STORE_new(), X509_STORE_free); if (!store) { @@ -1671,6 +1696,10 @@ void SecureContext::Initialize(Environment* env, Local target) { context, target, "getSystemCACertificates", GetSystemCACertificates); // SetMethodNoSideEffect( // context, target, "getOpenSSLCACertificates", GetOpenSSLCACertificates); + SetMethodNoSideEffect(context, + target, + "getOpenSSLDefaultCACertificates", + GetOpenSSLDefaultCACertificates); SetMethodNoSideEffect(context, target, "lookupOpenSSLCACertificates", @@ -1731,6 +1760,7 @@ void SecureContext::RegisterExternalReferences( registry->Register(GetBundledRootCertificates); registry->Register(GetSystemCACertificates); // registry->Register(GetOpenSSLCACertificates); + registry->Register(GetOpenSSLDefaultCACertificates); registry->Register(LookupOpenSSLCACertificates); registry->Register(GetExtraCACertificates); registry->Register(ResetRootCertStore); diff --git a/test/fixtures/tls-check-openssl-ca-certificates.js b/test/fixtures/tls-check-openssl-ca-certificates.js index 467f774eba4436..be426a49fe26e9 100644 --- a/test/fixtures/tls-check-openssl-ca-certificates.js +++ b/test/fixtures/tls-check-openssl-ca-certificates.js @@ -8,10 +8,24 @@ const { includesCert } = require('../common/tls'); const expectedCertFiles = process.env.EXPECTED_CERT_FILES ? JSON.parse(process.env.EXPECTED_CERT_FILES) : [process.env.SSL_CERT_FILE]; +const expectedDefaultCertFiles = process.env.EXPECTED_DEFAULT_CERT_FILES ? + JSON.parse(process.env.EXPECTED_DEFAULT_CERT_FILES) : []; +const unexpectedDefaultCertFiles = process.env.UNEXPECTED_DEFAULT_CERT_FILES ? + JSON.parse(process.env.UNEXPECTED_DEFAULT_CERT_FILES) : []; const defaultCerts = tls.getCACertificates('default'); -assert.deepStrictEqual(defaultCerts, []); assert.strictEqual(defaultCerts, tls.getCACertificates()); assert.strictEqual(defaultCerts, tls.getCACertificates('default')); +assert.strictEqual(defaultCerts.length, expectedDefaultCertFiles.length); + +for (const certFile of expectedDefaultCertFiles) { + const cert = fs.readFileSync(certFile, 'utf8'); + assert(includesCert(defaultCerts, cert)); +} + +for (const certFile of unexpectedDefaultCertFiles) { + const cert = fs.readFileSync(certFile, 'utf8'); + assert(!includesCert(defaultCerts, cert)); +} const expectedCerts = expectedCertFiles.map((certFile) => { const cert = fs.readFileSync(certFile, 'utf8'); diff --git a/test/parallel/test-tls-get-ca-certificates-openssl.js b/test/parallel/test-tls-get-ca-certificates-openssl.js index 803c73c095a275..45febcb8ad9399 100644 --- a/test/parallel/test-tls-get-ca-certificates-openssl.js +++ b/test/parallel/test-tls-get-ca-certificates-openssl.js @@ -1,7 +1,9 @@ 'use strict'; // This tests that tls.getCACertificates('openssl', subjectNameOrCert) looks up -// certificates from OpenSSL's default certificate file and hashed directories. +// certificates from OpenSSL's default certificate file and hashed directories, +// while tls.getCACertificates('default') only enumerates the default certificate +// file without scanning hashed directories. const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -56,6 +58,12 @@ spawnSyncAndExitWithoutError( env: { ...process.env, EXPECTED_CERT_FILES: JSON.stringify(expectedCertFiles), + EXPECTED_DEFAULT_CERT_FILES: JSON.stringify([expectedCertFiles[0]]), + UNEXPECTED_DEFAULT_CERT_FILES: JSON.stringify([ + expectedCertFiles[1], + expectedCertFiles[2], + unexpectedCertFile, + ]), UNEXPECTED_CERT_FILE: unexpectedCertFile, NODE_EXTRA_CA_CERTS: undefined, SSL_CERT_FILE: expectedCertFiles[0],