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[];