From 802bc8556f744824763970bf1b2502cd94d179b0 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] tls: add OpenSSL CA lookup to getCACertificates When OpenSSL is the effective default CA store, TLS clients load issuers from OpenSSL's default certificate file and hashed directories. However, tls.getCACertificates('default') did not expose the same store, so its result did not reflect the certificates available to TLS verification. OpenSSL certificate directories are indexed by subject hash and are resolved for a specific issuer. They cannot be represented reliably by enumerating every file in those directories. Add a cert argument and use X509_STORE_CTX_get1_issuer() so getCACertificates() follows OpenSSL's issuer lookup behavior. Add the "openssl" type to query only OpenSSL's default store. When OpenSSL is the effective default, make "default" query Node.js' complete default store so NODE_EXTRA_CA_CERTS is considered as well. Require a certificate for these lookups because there is no issuer key otherwise. Accept PEM strings, PEM or DER ArrayBufferViews, and X509Certificate objects. Detect the effective OpenSSL store so build-time --openssl-use-def-ca-store configurations behave the same as --use-openssl-ca. Also split path-delimited OpenSSL certificate directories when loading system CA certificates. Add regression coverage for certificate files, hashed directories, extra CAs, supported input types, invalid input, default-store overrides ,and OpenSSL-backed build configurations. Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com> --- doc/api/tls.md | 25 +++- lib/tls.js | 94 ++++++++++--- src/crypto/crypto_context.cc | 127 ++++++++++++++++- test/fixtures/list-certs.js | 13 +- .../tls-check-openssl-ca-certificates.js | 131 ++++++++++++++++++ .../test-tls-get-ca-certificates-default.js | 23 ++- .../test-tls-get-ca-certificates-openssl.js | 81 +++++++++++ ...-get-ca-certificates-system-openssl-dir.js | 67 +++++++++ ...st-tls-off-thread-cert-loading-disabled.js | 4 +- typings/internalBinding/crypto.d.ts | 2 + 10 files changed, 535 insertions(+), 32 deletions(-) create mode 100644 test/fixtures/tls-check-openssl-ca-certificates.js create mode 100644 test/parallel/test-tls-get-ca-certificates-openssl.js 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 95335e6f275099..834656547cad1a 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[, cert]])` * `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"`. +* `cert` {string|ArrayBufferView|X509Certificate|undefined} The certificate used + to look up its issuer in the selected CA store. Strings must contain a + PEM-encoded certificate. An `ArrayBufferView` may contain a PEM or DER encoded + certificate. + Required when `type` is `"openssl"`, or when `type` is `"default"` and + [`--use-openssl-ca`][] is enabled. * Returns: {string\[]} An array of PEM-encoded certificates. The array may contain duplicates if the same certificate is repeatedly stored in multiple sources. @@ -2372,10 +2378,15 @@ Returns an array containing the CA certificates from various sources, depending * `"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 OpenSSL's default CA store is selected, either at build time or through + [`--use-openssl-ca`][], `cert` is required. OpenSSL's default certificate + locations form the base of the default CA store. Certificate directories + use subject-hash lookup and cannot be enumerated without a certificate + lookup key. Calling this method without `cert` throws. * 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 - file. + * When [`NODE_EXTRA_CA_CERTS`][] is used, certificates from the specified + file are included in the default CA store. * `"system"`: return the CA certificates that are loaded from the system's trusted store, according to rules set by [`--use-system-ca`][]. This can be used to get the certificates from the system when [`--use-system-ca`][] is not enabled. @@ -2383,6 +2394,10 @@ 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 the issuer name of `cert`. This follows OpenSSL's + hashed-directory lookup behavior for certificates loaded from the default + certificate directory. ## `tls.getCiphers()` @@ -2562,7 +2577,7 @@ added: v0.11.3 [`tls.connect()`]: #tlsconnectoptions-callback [`tls.createSecureContext()`]: #tlscreatesecurecontextoptions [`tls.createServer()`]: #tlscreateserveroptions-secureconnectionlistener -[`tls.getCACertificates()`]: #tlsgetcacertificatestype +[`tls.getCACertificates()`]: #tlsgetcacertificatestype-cert [`tls.getCiphers()`]: #tlsgetciphers [`tls.rootCertificates`]: #tlsrootcertificates [`x509.checkHost()`]: crypto.md#x509checkhostname-options diff --git a/lib/tls.js b/lib/tls.js index 296f6189da1728..06fe0b59213e48 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -29,6 +29,7 @@ const { JSONParse, ObjectDefineProperty, ObjectFreeze, + SafeMap, StringFromCharCode, } = primordials; @@ -38,11 +39,14 @@ const { ERR_OUT_OF_RANGE, ERR_INVALID_ARG_VALUE, ERR_INVALID_ARG_TYPE, + ERR_MISSING_ARGS, } = require('internal/errors').codes; const { getBundledRootCertificates, getExtraCACertificates, + lookupDefaultCACertificates, + lookupOpenSSLCACertificates, getSystemCACertificates, resetRootCertStore, getUserRootCertificates, @@ -71,6 +75,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: { @@ -145,11 +151,30 @@ function cacheSystemCACertificates() { return systemCACertificates; } +let opensslCACertificateLookup; +function cacheOpenSSLCertificateLookup(cert) { + opensslCACertificateLookup ??= new SafeMap(); + + const key = isArrayBufferView(cert) ? + `buffer:${Buffer.from( + cert.buffer, + cert.byteOffset, + cert.byteLength).toString('base64')}` : + `cert:${cert}`; + + const cached = opensslCACertificateLookup.get(key); + if (cached !== undefined) return cached; + + const result = ObjectFreeze(lookupOpenSSLCACertificates(cert)); + opensslCACertificateLookup.set(key, result); + return result; +} let defaultCACertificates; +let defaultCACertificateLookup; let hasResetDefaultCACertificates = false; -function cacheDefaultCACertificates() { +function cacheDefaultCACertificates(cert) { if (defaultCACertificates) { return defaultCACertificates; } if (hasResetDefaultCACertificates) { @@ -158,26 +183,44 @@ function cacheDefaultCACertificates() { return defaultCACertificates; } - defaultCACertificates = []; - - if (!getOptionValue('--use-openssl-ca')) { - const bundled = cacheBundledRootCertificates(); - for (let i = 0; i < bundled.length; ++i) { - ArrayPrototypePush(defaultCACertificates, bundled[i]); + if (getOptionValue('[ssl_openssl_cert_store]')) { + if (cert === undefined) { + throw new ERR_MISSING_ARGS('cert'); } - if (getOptionValue('--use-system-ca')) { - const system = cacheSystemCACertificates(); - for (let i = 0; i < system.length; ++i) { + cert = validateOpenSSLCACertificate(cert); + + const key = isArrayBufferView(cert) ? + `buffer:${Buffer.from( + cert.buffer, + cert.byteOffset, + cert.byteLength).toString('base64')}` : + `cert:${cert}`; + + defaultCACertificateLookup ??= new SafeMap(); + const cached = defaultCACertificateLookup.get(key); + if (cached !== undefined) return cached; + + const result = ObjectFreeze(lookupDefaultCACertificates(cert)); + defaultCACertificateLookup.set(key, result); + return result; + } - ArrayPrototypePush(defaultCACertificates, system[i]); - } + defaultCACertificates = []; + + const bundled = cacheBundledRootCertificates(); + for (let i = 0; i < bundled.length; ++i) { + ArrayPrototypePush(defaultCACertificates, bundled[i]); + } + if (getOptionValue('--use-system-ca')) { + const system = cacheSystemCACertificates(); + for (let i = 0; i < system.length; ++i) { + ArrayPrototypePush(defaultCACertificates, system[i]); } } if (process.env.NODE_EXTRA_CA_CERTS) { const extra = cacheExtraCACertificates(); for (let i = 0; i < extra.length; ++i) { - ArrayPrototypePush(defaultCACertificates, extra[i]); } } @@ -186,19 +229,35 @@ function cacheDefaultCACertificates() { return defaultCACertificates; } +function validateOpenSSLCACertificate(cert) { + if (cert !== null && + (typeof cert === 'object' || typeof cert === 'function') && + isX509Certificate(cert)) { + return cert[kHandle].pem(); + } + if (typeof cert !== 'string' && !isArrayBufferView(cert)) { + throw new ERR_INVALID_ARG_TYPE( + 'cert', ['string', 'ArrayBufferView', 'X509Certificate'], cert); + } + return cert; +} + // TODO(joyeecheung): support X509Certificate output? -function getCACertificates(type = 'default') { +function getCACertificates(type = 'default', cert) { validateString(type, 'type'); switch (type) { case 'default': - return cacheDefaultCACertificates(); + return cacheDefaultCACertificates(cert); case 'bundled': return cacheBundledRootCertificates(); case 'system': return cacheSystemCACertificates(); case 'extra': return cacheExtraCACertificates(); + case 'openssl': + cert = validateOpenSSLCACertificate(cert); + return cacheOpenSSLCertificateLookup(cert); default: throw new ERR_INVALID_ARG_VALUE('type', type); } @@ -210,7 +269,7 @@ function setDefaultCACertificates(certs) { throw new ERR_INVALID_ARG_TYPE('certs', 'Array', certs); } - // Verify that all elements in the array are strings + // Verify that all elements are strings or ArrayBufferViews. for (let i = 0; i < certs.length; i++) { if (typeof certs[i] !== 'string' && !isArrayBufferView(certs[i])) { throw new ERR_INVALID_ARG_TYPE( @@ -220,6 +279,7 @@ function setDefaultCACertificates(certs) { resetRootCertStore(certs); defaultCACertificates = undefined; // Reset the cached default certificates + defaultCACertificateLookup = undefined; hasResetDefaultCACertificates = true; } @@ -231,6 +291,8 @@ if (isBuildingSnapshot()) { // Bundled certificates are immutable so they are spared. extraCACertificates = undefined; systemCACertificates = undefined; + opensslCACertificateLookup = undefined; + defaultCACertificateLookup = undefined; if (hasResetDefaultCACertificates) { defaultCACertificates = undefined; } diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 5447de596f98ec..30188357cb649f 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -27,6 +27,7 @@ #include #endif +#include #include namespace node { @@ -74,6 +75,11 @@ using v8::String; using v8::Value; namespace crypto { +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) }; @@ -839,6 +845,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) { @@ -863,7 +896,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); } } @@ -1330,6 +1363,88 @@ void GetSystemCACertificates(const FunctionCallbackInfo& args) { } } +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 LookupCACertificatesByIssuer(Environment* env, + X509_STORE* store, + X509* cert) { + ClearErrorOnReturn clear_error_on_return; + 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, 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()); +} + +static void LookupCACertificates(const FunctionCallbackInfo& args, + X509StorePointer store) { + 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 (LookupCACertificatesByIssuer(env, store.get(), parsed_cert.value.get()) + .ToLocal(&results)) { + args.GetReturnValue().Set(results); + } + return; + } + + return THROW_ERR_INVALID_ARG_VALUE( + env, "The \"cert\" argument must be a valid X.509 certificate"); +} + +void LookupDefaultCACertificates(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + X509StorePointer store(NewRootCertStore(env), X509_STORE_free); + LookupCACertificates(args, std::move(store)); +} + +void LookupOpenSSLCACertificates(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + X509StorePointer store = NewOpenSSLDefaultCertificateStore(env); + if (!store) return; + LookupCACertificates(args, std::move(store)); +} + void GetExtraCACertificates(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); if (extra_root_certs_file.empty()) { @@ -1434,6 +1549,14 @@ void SecureContext::Initialize(Environment* env, Local target) { GetBundledRootCertificates); SetMethodNoSideEffect( context, target, "getSystemCACertificates", GetSystemCACertificates); + SetMethodNoSideEffect(context, + target, + "lookupDefaultCACertificates", + LookupDefaultCACertificates); + SetMethodNoSideEffect(context, + target, + "lookupOpenSSLCACertificates", + LookupOpenSSLCACertificates); SetMethodNoSideEffect( context, target, "getExtraCACertificates", GetExtraCACertificates); SetMethod(context, target, "resetRootCertStore", ResetRootCertStore); @@ -1489,6 +1612,8 @@ void SecureContext::RegisterExternalReferences( registry->Register(GetBundledRootCertificates); registry->Register(GetSystemCACertificates); + registry->Register(LookupDefaultCACertificates); + registry->Register(LookupOpenSSLCACertificates); registry->Register(GetExtraCACertificates); registry->Register(ResetRootCertStore); registry->Register(GetUserRootCertificates); diff --git a/test/fixtures/list-certs.js b/test/fixtures/list-certs.js index 7c9538df242115..3ece138b78ffd4 100644 --- a/test/fixtures/list-certs.js +++ b/test/fixtures/list-certs.js @@ -13,7 +13,14 @@ const tls = require('tls'); const { includesCert, extractMetadata } = require('../common/tls'); const CERTS_TYPE = process.env.CERTS_TYPE || 'default'; -const actualCerts = tls.getCACertificates(CERTS_TYPE); -for (const cert of expectedCerts) { - assert(includesCert(actualCerts, cert), 'Expected certificate not found: ' + JSON.stringify(extractMetadata(cert))); +const EXPECTED_ERROR_CODE = process.env.EXPECTED_ERROR_CODE; +if (EXPECTED_ERROR_CODE) { + assert.throws(() => tls.getCACertificates(CERTS_TYPE), { + code: EXPECTED_ERROR_CODE, + }); +} else { + const actualCerts = tls.getCACertificates(CERTS_TYPE); + for (const cert of expectedCerts) { + assert(includesCert(actualCerts, cert), 'Expected certificate not found: ' + JSON.stringify(extractMetadata(cert))); + } } 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..f2b357b258ff04 --- /dev/null +++ b/test/fixtures/tls-check-openssl-ca-certificates.js @@ -0,0 +1,131 @@ +'use strict'; + +const assert = require('assert'); +const { X509Certificate } = require('crypto'); +const fs = require('fs'); +const tls = require('tls'); +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 lookupCertFiles = process.env.LOOKUP_CERT_FILES ? + JSON.parse(process.env.LOOKUP_CERT_FILES) : expectedCertFiles; + +assert.throws(() => tls.getCACertificates(), { + code: 'ERR_MISSING_ARGS', +}); +assert.throws(() => tls.getCACertificates('default'), { + code: 'ERR_MISSING_ARGS', +}); + +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]), +); +const lookupCerts = lookupCertFiles.map((certFile) => { + const cert = fs.readFileSync(certFile, 'utf8'); + const x509 = new X509Certificate(cert); + return { cert, x509 }; +}); + +for (const { cert, x509 } of lookupCerts) { + const issuerCert = certsBySubject.get(x509.issuer); + assert(issuerCert); + for (const certInput of [x509, cert, Buffer.from(cert)]) { + const opensslCertsByCert = + tls.getCACertificates('openssl', certInput); + assert(includesCert(opensslCertsByCert, issuerCert)); + + const defaultCertsByCert = + tls.getCACertificates('default', certInput); + assert(includesCert(defaultCertsByCert, issuerCert)); + } + + const defaultCertsByCert = tls.getCACertificates('default', x509); + assert.strictEqual( + defaultCertsByCert, + tls.getCACertificates('default', x509)); + assert.deepStrictEqual( + defaultCertsByCert, + tls.getCACertificates('default', cert)); +} + +assert.throws(() => tls.getCACertificates('default'), { + code: 'ERR_MISSING_ARGS', +}); + +{ + 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(includesCert( + tls.getCACertificates('openssl', unexpectedX509), + certsBySubject.get(unexpectedX509.issuer))); +} + +if (process.env.UNHASHED_ISSUER_CERT_FILE) { + const unhashIssuer = + fs.readFileSync(process.env.UNHASHED_ISSUER_CERT_FILE, 'utf8'); + const unhashIssuerX509 = new X509Certificate(unhashIssuer); + assert.deepStrictEqual( + tls.getCACertificates('openssl', unhashIssuerX509), + []); + + const defaultCerts = + tls.getCACertificates('default', unhashIssuerX509); + assert(includesCert( + defaultCerts, + fs.readFileSync(process.env.UNEXPECTED_CERT_FILE, 'utf8'))); + assert(!includesCert(defaultCerts, expectedCerts[1].cert)); +} + +assert.throws(() => tls.getCACertificates('openssl'), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws(() => tls.getCACertificates('openssl', {}), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws( + () => tls.getCACertificates('openssl', 'BAD=x'), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws( + () => tls.getCACertificates('openssl', Buffer.alloc(0)), + { code: 'ERR_INVALID_ARG_VALUE' }); + +tls.setDefaultCACertificates([]); +assert.deepStrictEqual(tls.getCACertificates('default'), []); diff --git a/test/parallel/test-tls-get-ca-certificates-default.js b/test/parallel/test-tls-get-ca-certificates-default.js index 29fb2a29a8cb33..350106a3113a68 100644 --- a/test/parallel/test-tls-get-ca-certificates-default.js +++ b/test/parallel/test-tls-get-ca-certificates-default.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals 'use strict'; // This tests that tls.getCACertificates() returns the default @@ -8,13 +9,23 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const tls = require('tls'); +const { getOptionValue } = require('internal/options'); const { assertIsCAArray } = require('../common/tls'); -const certs = tls.getCACertificates(); -assertIsCAArray(certs); +if (getOptionValue('[ssl_openssl_cert_store]')) { + assert.throws(() => tls.getCACertificates(), { + code: 'ERR_MISSING_ARGS', + }); + assert.throws(() => tls.getCACertificates('default'), { + code: 'ERR_MISSING_ARGS', + }); +} else { + const certs = tls.getCACertificates(); + assertIsCAArray(certs); -const certs2 = tls.getCACertificates('default'); -assert.strictEqual(certs, certs2); + const certs2 = tls.getCACertificates('default'); + assert.strictEqual(certs, certs2); -// It's cached on subsequent accesses. -assert.strictEqual(certs, tls.getCACertificates('default')); + // It's cached on subsequent accesses. + assert.strictEqual(certs, 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..04268ec22de18f --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-openssl.js @@ -0,0 +1,81 @@ +'use strict'; + +// This tests that tls.getCACertificates('openssl', cert) looks up +// certificates from OpenSSL's default certificate file and hashed directories +// with certificate inputs. + +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 lookupCertFiles = [ + fixtures.path('keys', 'ca1-cert.pem'), + fixtures.path('keys', 'ca2-cert.pem'), + fixtures.path('keys', 'agent6-cert.pem'), +]; +const unexpectedCertFile = fixtures.path('keys', 'ca4-cert.pem'); +const unhashIssuerCertFile = fixtures.path('keys', 'agent10-cert.pem'); +const extraCertFile = tmpdir.resolve('extra-certs.pem'); +copyHashDirCertificate(expectedCertFiles[1], firstCertDir); +copyHashDirCertificate(expectedCertFiles[2], secondCertDir); +fs.copyFileSync(unexpectedCertFile, path.join(secondCertDir, 'ca4-cert.pem')); +fs.writeFileSync(extraCertFile, [ + fs.readFileSync(unexpectedCertFile, 'utf8'), + fs.readFileSync(expectedCertFiles[1], 'utf8'), +].join('\n')); + +spawnSyncAndExitWithoutError( + process.execPath, + ['--use-openssl-ca', fixtures.path('tls-check-openssl-ca-certificates.js')], + { + env: { + ...process.env, + EXPECTED_CERT_FILES: JSON.stringify(expectedCertFiles), + LOOKUP_CERT_FILES: JSON.stringify(lookupCertFiles), + UNEXPECTED_CERT_FILE: unexpectedCertFile, + UNHASHED_ISSUER_CERT_FILE: unhashIssuerCertFile, + NODE_EXTRA_CA_CERTS: extraCertFile, + // Nix-patched OpenSSL gives this precedence over SSL_CERT_FILE. + NIX_SSL_CERT_FILE: undefined, + 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/test/parallel/test-tls-off-thread-cert-loading-disabled.js b/test/parallel/test-tls-off-thread-cert-loading-disabled.js index c2e466fcae1db5..26ef82b1820c16 100644 --- a/test/parallel/test-tls-off-thread-cert-loading-disabled.js +++ b/test/parallel/test-tls-off-thread-cert-loading-disabled.js @@ -1,5 +1,6 @@ 'use strict'; -// This tests that when --use-openssl-ca is specified, no off-thread cert loading happens. +// This tests that when --use-openssl-ca is specified, no off-thread cert +// loading happens and default CA lookup without a certificate throws. const common = require('../common'); if (!common.hasCrypto) { @@ -18,6 +19,7 @@ spawnSyncAndAssert( NODE_DEBUG_NATIVE: 'crypto', NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), CERTS_TYPE: 'default', + EXPECTED_ERROR_CODE: 'ERR_MISSING_ARGS', } }, { diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index d91c5018ba688a..4289f45f616017 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -933,6 +933,8 @@ export interface CryptoBinding { getFipsCrypto(): 0 | 1; getHashes(): string[]; getKeyObjectSlots(key: object): InternalCryptoBinding.KeyObjectSlots; + lookupDefaultCACertificates(cert: InternalCryptoBinding.ByteSource): string[]; + lookupOpenSSLCACertificates(cert: InternalCryptoBinding.ByteSource): string[]; getOpenSSLSecLevelCrypto(): number | undefined; getSSLCiphers(): string[]; getSystemCACertificates(): string[];