Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

@joyeecheung joyeecheung Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to document that the certificates queried this way will diverge from what --use-openssl-ca actually uses to verify the certificates: --use-system-ca differs in that it trusts all certificates from the directories without the hash lookup, which is also e.g. what go's client does, but --use-openssl-ca actually filters https://docs.openssl.org/1.1.1/man3/X509_LOOKUP_hash_dir/#hashed-directory-method

I am somewhat skeptical whether this should be implemented without a hash lookup, though. Another workaround is to accept a second parameter that indicates the ceritifcate/subject name we are filtering for. I think we will also need a 'openssl' type that takes this filtering argument.

@Archkon Archkon Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that hash lookup semantics are specific to the OpenSSL CA store. Adding an extra argument to this method for that case may blur the existing API semantics, since getCACertificates() currently behaves like an enumeration API. Would a separate API ( like tls.lookupCACertificates )for OpenSSL-style lookup be a cleaner design?

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
Expand Down
16 changes: 15 additions & 1 deletion lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const {
const {
getBundledRootCertificates,
getExtraCACertificates,
getOpenSSLCACertificates,
getSystemCACertificates,
resetRootCertStore,
getUserRootCertificates,
Expand Down Expand Up @@ -146,6 +147,13 @@ function cacheSystemCACertificates() {
return systemCACertificates;
}

let opensslCACertificates;
function cacheOpenSSLCACertificates() {
opensslCACertificates ||= ObjectFreeze(getOpenSSLCACertificates());

return opensslCACertificates;
}

let defaultCACertificates;
let hasResetDefaultCACertificates = false;

Expand All @@ -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]);
Expand Down Expand Up @@ -231,6 +244,7 @@ if (isBuildingSnapshot()) {
// Bundled certificates are immutable so they are spared.
extraCACertificates = undefined;
systemCACertificates = undefined;
opensslCACertificates = undefined;
if (hasResetDefaultCACertificates) {
defaultCACertificates = undefined;
}
Expand Down
49 changes: 48 additions & 1 deletion src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,33 @@ static void LoadCertsFromDir(std::vector<X509*>* certs,
}
}

static void LoadCertsFromOpenSSLDirs(std::vector<X509*>* 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<X509*>* system_store_certs) {
Expand All @@ -867,7 +894,7 @@ void GetOpenSSLSystemCertificates(std::vector<X509*>* system_store_certs) {
}

if (!cert_dir.empty()) {
LoadCertsFromDir(system_store_certs, cert_dir.c_str());
LoadCertsFromOpenSSLDirs(system_store_certs, cert_dir);
Comment thread
Archkon marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -1334,6 +1361,23 @@ void GetSystemCACertificates(const FunctionCallbackInfo<Value>& args) {
}
}

void GetOpenSSLCACertificates(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
std::vector<X509*> certs;
GetOpenSSLSystemCertificates(&certs);
auto cleanup = OnScopeLeave([&certs]() {
for (X509* cert : certs) {
X509_free(cert);
}
});

Local<Array> results;
if (X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size())
.ToLocal(&results)) {
args.GetReturnValue().Set(results);
}
}

void GetExtraCACertificates(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (extra_root_certs_file.empty()) {
Expand Down Expand Up @@ -1438,6 +1482,8 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
GetBundledRootCertificates);
SetMethodNoSideEffect(
context, target, "getSystemCACertificates", GetSystemCACertificates);
SetMethodNoSideEffect(
context, target, "getOpenSSLCACertificates", GetOpenSSLCACertificates);
SetMethodNoSideEffect(
context, target, "getExtraCACertificates", GetExtraCACertificates);
SetMethod(context, target, "resetRootCertStore", ResetRootCertStore);
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions test/fixtures/tls-check-openssl-ca-certificates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

const assert = require('assert');
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 defaultCerts = tls.getCACertificates('default');

for (const certFile of expectedCertFiles) {
const expected = fs.readFileSync(certFile, 'utf8');
assert(includesCert(defaultCerts, expected));
}
assert.strictEqual(defaultCerts, tls.getCACertificates());
assert.strictEqual(defaultCerts, tls.getCACertificates('default'));
43 changes: 43 additions & 0 deletions test/parallel/test-tls-get-ca-certificates-openssl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

// This tests that tls.getCACertificates('default') includes certificates from
// OpenSSL's default certificate file and directories when --use-openssl-ca is
// enabled.

const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');

const fs = require('fs');
const path = require('path');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const firstCertDir = tmpdir.resolve('openssl-certs-1');
const secondCertDir = tmpdir.resolve('openssl-certs-2');
fs.mkdirSync(firstCertDir);
fs.mkdirSync(secondCertDir);

const expectedCertFiles = [
fixtures.path('keys', 'ca1-cert.pem'),
fixtures.path('keys', 'ca2-cert.pem'),
fixtures.path('keys', 'ca3-cert.pem'),
];
fs.copyFileSync(expectedCertFiles[1], path.join(firstCertDir, 'ca2-cert.pem'));
fs.copyFileSync(expectedCertFiles[2], path.join(secondCertDir, 'ca3-cert.pem'));

spawnSyncAndExitWithoutError(
process.execPath,
['--use-openssl-ca', fixtures.path('tls-check-openssl-ca-certificates.js')],
{
env: {
...process.env,
EXPECTED_CERT_FILES: JSON.stringify(expectedCertFiles),
NODE_EXTRA_CA_CERTS: undefined,
SSL_CERT_FILE: expectedCertFiles[0],
SSL_CERT_DIR: [firstCertDir, secondCertDir].join(path.delimiter),
},
},
);
1 change: 1 addition & 0 deletions typings/internalBinding/crypto.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
Loading