diff --git a/doc/api/tls.md b/doc/api/tls.md index 95335e6f275099..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. @@ -2383,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 283e5f5bc7870a..10d182401b3e5a 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -29,6 +29,7 @@ const { JSONParse, ObjectDefineProperty, ObjectFreeze, + SafeMap, StringFromCharCode, } = primordials; @@ -43,6 +44,9 @@ const { const { getBundledRootCertificates, getExtraCACertificates, + // getOpenSSLCACertificates, + getOpenSSLDefaultCACertificates, + 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: { @@ -146,6 +152,40 @@ function cacheSystemCACertificates() { return systemCACertificates; } +// let opensslCACertificates; +// function cacheOpenSSLCACertificates() { +// opensslCACertificates ||= ObjectFreeze(getOpenSSLCACertificates()); + +// return opensslCACertificates; +// } + +let opensslDefaultCACertificates; +function cacheOpenSSLDefaultCACertificates() { + opensslDefaultCACertificates ||= ObjectFreeze( + getOpenSSLDefaultCACertificates()); + + return opensslDefaultCACertificates; +} + +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; let hasResetDefaultCACertificates = false; @@ -160,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]); @@ -168,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]); } } @@ -177,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]); } } @@ -186,8 +229,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) { @@ -199,6 +259,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); } @@ -231,6 +295,9 @@ if (isBuildingSnapshot()) { // Bundled certificates are immutable so they are spared. 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 01d8a17d8e2f53..6121774e2f6276 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. @@ -843,6 +909,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 +960,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 +1427,169 @@ 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 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) { + 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); if (extra_root_certs_file.empty()) { @@ -1438,6 +1694,16 @@ void SecureContext::Initialize(Environment* env, Local target) { GetBundledRootCertificates); SetMethodNoSideEffect( context, target, "getSystemCACertificates", GetSystemCACertificates); + // SetMethodNoSideEffect( + // context, target, "getOpenSSLCACertificates", GetOpenSSLCACertificates); + SetMethodNoSideEffect(context, + target, + "getOpenSSLDefaultCACertificates", + GetOpenSSLDefaultCACertificates); + SetMethodNoSideEffect(context, + target, + "lookupOpenSSLCACertificates", + LookupOpenSSLCACertificates); SetMethodNoSideEffect( context, target, "getExtraCACertificates", GetExtraCACertificates); SetMethod(context, target, "resetRootCertStore", ResetRootCertStore); @@ -1493,6 +1759,9 @@ void SecureContext::RegisterExternalReferences( registry->Register(GetBundledRootCertificates); registry->Register(GetSystemCACertificates); + // registry->Register(GetOpenSSLCACertificates); + registry->Register(GetOpenSSLDefaultCACertificates); + 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 new file mode 100644 index 00000000000000..be426a49fe26e9 --- /dev/null +++ b/test/fixtures/tls-check-openssl-ca-certificates.js @@ -0,0 +1,94 @@ +'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 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.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'); + 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 new file mode 100644 index 00000000000000..45febcb8ad9399 --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-openssl.js @@ -0,0 +1,73 @@ +'use strict'; + +// This tests that tls.getCACertificates('openssl', subjectNameOrCert) looks up +// 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'); + +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, + ['--use-openssl-ca', fixtures.path('tls-check-openssl-ca-certificates.js')], + { + 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], + 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 d91c5018ba688a..7bf51d19a0deb5 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; + // getOpenSSLCACertificates(): string[]; + lookupOpenSSLCACertificates(subjectNameOrCert: InternalCryptoBinding.ByteSource): string[]; getOpenSSLSecLevelCrypto(): number | undefined; getSSLCiphers(): string[]; getSystemCACertificates(): string[];