crypto: add tls.setDefaultCACertificates()

This API allows dynamically configuring CA certificates that
will be used by the Node.js TLS clients by default.

Once called, the provided certificates will become the default CA
certificate list returned by `tls.getCACertificates('default')` and
used by TLS connections that don't specify their own CA certificates.

This function only affects the current Node.js thread.

PR-URL: https://github.com/nodejs/node/pull/58822
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
This commit is contained in:
Joyee Cheung 2025-07-18 21:57:53 +02:00 committed by GitHub
parent a22c9c4f42
commit edd66d0130
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1128 additions and 14 deletions

View file

@ -2260,6 +2260,54 @@ openssl pkcs12 -certpbe AES-256-CBC -export -out client-cert.pem \
The server can be tested by connecting to it using the example client from
[`tls.connect()`][].
## `tls.setDefaultCACertificates(certs)`
<!-- YAML
added: REPLACEME
-->
* `certs` {string\[]|ArrayBufferView\[]} An array of CA certificates in PEM format.
Sets the default CA certificates used by Node.js TLS clients. If the provided
certificates are parsed successfully, they will become the default CA
certificate list returned by [`tls.getCACertificates()`][] and used
by subsequent TLS connections that don't specify their own CA certificates.
The certificates will be deduplicated before being set as the default.
This function only affects the current Node.js thread. Previous
sessions cached by the HTTPS agent won't be affected by this change, so
this method should be called before any unwanted cachable TLS connections are
made.
To use system CA certificates as the default:
```cjs
const tls = require('node:tls');
tls.setDefaultCACertificates(tls.getCACertificates('system'));
```
```mjs
import tls from 'node:tls';
tls.setDefaultCACertificates(tls.getCACertificates('system'));
```
This function completely replaces the default CA certificate list. To add additional
certificates to the existing defaults, get the current certificates and append to them:
```cjs
const tls = require('node:tls');
const currentCerts = tls.getCACertificates('default');
const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...'];
tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]);
```
```mjs
import tls from 'node:tls';
const currentCerts = tls.getCACertificates('default');
const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...'];
tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]);
```
## `tls.getCACertificates([type])`
<!-- YAML

View file

@ -37,6 +37,7 @@ const {
ERR_TLS_CERT_ALTNAME_INVALID,
ERR_OUT_OF_RANGE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_ARG_TYPE,
} = require('internal/errors').codes;
const internalUtil = require('internal/util');
internalUtil.assertCrypto();
@ -51,6 +52,8 @@ const {
getBundledRootCertificates,
getExtraCACertificates,
getSystemCACertificates,
resetRootCertStore,
getUserRootCertificates,
getSSLCiphers,
} = internalBinding('crypto');
const { Buffer } = require('buffer');
@ -122,8 +125,17 @@ function cacheSystemCACertificates() {
}
let defaultCACertificates;
let hasResetDefaultCACertificates = false;
function cacheDefaultCACertificates() {
if (defaultCACertificates) { return defaultCACertificates; }
if (hasResetDefaultCACertificates) {
defaultCACertificates = getUserRootCertificates();
ObjectFreeze(defaultCACertificates);
return defaultCACertificates;
}
defaultCACertificates = [];
if (!getOptionValue('--use-openssl-ca')) {
@ -171,6 +183,26 @@ function getCACertificates(type = 'default') {
}
exports.getCACertificates = getCACertificates;
function setDefaultCACertificates(certs) {
if (!ArrayIsArray(certs)) {
throw new ERR_INVALID_ARG_TYPE('certs', 'Array', certs);
}
// Verify that all elements in the array are strings
for (let i = 0; i < certs.length; i++) {
if (typeof certs[i] !== 'string' && !isArrayBufferView(certs[i])) {
throw new ERR_INVALID_ARG_TYPE(
`certs[${i}]`, ['string', 'ArrayBufferView'], certs[i]);
}
}
resetRootCertStore(certs);
defaultCACertificates = undefined; // Reset the cached default certificates
hasResetDefaultCACertificates = true;
}
exports.setDefaultCACertificates = setDefaultCACertificates;
// Convert protocols array into valid OpenSSL protocols list
// ("\x06spdy/2\x08http/1.1\x08http/1.0")
function convertProtocols(protocols) {

View file

@ -27,6 +27,8 @@
#include <wincrypt.h>
#endif
#include <set>
namespace node {
using ncrypto::BignumPointer;
@ -83,10 +85,28 @@ static std::atomic<bool> has_cached_bundled_root_certs{false};
static std::atomic<bool> has_cached_system_root_certs{false};
static std::atomic<bool> has_cached_extra_root_certs{false};
// Used for sets of X509.
struct X509Less {
bool operator()(const X509* lhs, const X509* rhs) const noexcept {
return X509_cmp(const_cast<X509*>(lhs), const_cast<X509*>(rhs)) < 0;
}
};
using X509Set = std::set<X509*, X509Less>;
// Per-thread root cert store. See NewRootCertStore() on what it contains.
static thread_local X509_STORE* root_cert_store = nullptr;
// If the user calls tls.setDefaultCACertificates() this will be used
// to hold the user-provided certificates, the root_cert_store and any new
// copy generated by NewRootCertStore() will then contain the certificates
// from this set.
static thread_local std::unique_ptr<X509Set> root_certs_from_users;
X509_STORE* GetOrCreateRootCertStore() {
// Guaranteed thread-safe by standard, just don't use -fno-threadsafe-statics.
static X509_STORE* store = NewRootCertStore();
return store;
if (root_cert_store != nullptr) {
return root_cert_store;
}
root_cert_store = NewRootCertStore();
return root_cert_store;
}
// Takes a string or buffer and loads it into a BIO.
@ -227,14 +247,11 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
issuer);
}
static unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
static unsigned long LoadCertsFromBIO( // NOLINT(runtime/int)
std::vector<X509*>* certs,
const char* file) {
BIOPointer bio) {
MarkPopErrorOnReturn mark_pop_error_on_return;
auto bio = BIOPointer::NewFile(file, "r");
if (!bio) return ERR_get_error();
while (X509* x509 = PEM_read_bio_X509(
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
certs->push_back(x509);
@ -250,6 +267,17 @@ static unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
}
}
static unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
std::vector<X509*>* certs,
const char* file) {
MarkPopErrorOnReturn mark_pop_error_on_return;
auto bio = BIOPointer::NewFile(file, "r");
if (!bio) return ERR_get_error();
return LoadCertsFromBIO(certs, std::move(bio));
}
// Indicates the trust status of a certificate.
enum class TrustStatus {
// Trust status is unknown / uninitialized.
@ -831,11 +859,24 @@ static std::vector<X509*>& GetExtraCACertificates() {
// NODE_EXTRA_CA_CERTS are cached after first load. Certificates
// from --use-system-ca are not cached and always reloaded from
// disk.
// 8. If users have reset the root cert store by calling
// tls.setDefaultCACertificates(), the store will be populated with
// the certificates provided by users.
// TODO(joyeecheung): maybe these rules need a bit of consolidation?
X509_STORE* NewRootCertStore() {
X509_STORE* store = X509_STORE_new();
CHECK_NOT_NULL(store);
// If the root cert store is already reset by users through
// tls.setDefaultCACertificates(), just create a copy from the
// user-provided certificates.
if (root_certs_from_users != nullptr) {
for (X509* cert : *root_certs_from_users) {
CHECK_EQ(1, X509_STORE_add_cert(store, cert));
}
return store;
}
#ifdef NODE_OPENSSL_SYSTEM_CERT_PATH
if constexpr (sizeof(NODE_OPENSSL_SYSTEM_CERT_PATH) > 1) {
ERR_set_mark();
@ -903,14 +944,57 @@ void GetBundledRootCertificates(const FunctionCallbackInfo<Value>& args) {
Array::New(env->isolate(), result, arraysize(root_certs)));
}
bool ArrayOfStringsToX509s(Local<Context> context,
Local<Array> cert_array,
std::vector<X509*>* certs) {
ClearErrorOnReturn clear_error_on_return;
Isolate* isolate = context->GetIsolate();
Environment* env = Environment::GetCurrent(context);
uint32_t array_length = cert_array->Length();
std::vector<v8::Global<Value>> cert_items;
if (FromV8Array(context, cert_array, &cert_items).IsNothing()) {
return false;
}
for (uint32_t i = 0; i < array_length; i++) {
Local<Value> cert_val = cert_items[i].Get(isolate);
// Parse the PEM certificate.
BIOPointer bio(LoadBIO(env, cert_val));
if (!bio) {
ThrowCryptoError(env, ERR_get_error(), "Failed to load certificate data");
return false;
}
// Read all certificates from this PEM string
size_t start = certs->size();
auto err = LoadCertsFromBIO(certs, std::move(bio));
if (err != 0) {
size_t end = certs->size();
// Clean up any certificates we've already parsed upon failure.
for (size_t j = start; j < end; ++j) {
X509_free((*certs)[j]);
}
ThrowCryptoError(env, err, "Failed to parse certificate");
return false;
}
}
return true;
}
template <typename It>
MaybeLocal<Array> X509sToArrayOfStrings(Environment* env,
const std::vector<X509*>& certs) {
It first,
It last,
size_t size) {
ClearErrorOnReturn clear_error_on_return;
EscapableHandleScope scope(env->isolate());
LocalVector<Value> result(env->isolate(), certs.size());
for (size_t i = 0; i < certs.size(); ++i) {
X509View view(certs[i]);
LocalVector<Value> result(env->isolate(), size);
size_t i = 0;
for (It cur = first; cur != last; ++cur, ++i) {
X509View view(*cur);
auto pem_bio = view.toPEM();
if (!pem_bio) {
ThrowCryptoError(env, ERR_get_error(), "X509 to PEM conversion");
@ -935,10 +1019,87 @@ MaybeLocal<Array> X509sToArrayOfStrings(Environment* env,
return scope.Escape(Array::New(env->isolate(), result.data(), result.size()));
}
void GetUserRootCertificates(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_NOT_NULL(root_certs_from_users);
Local<Array> results;
if (X509sToArrayOfStrings(env,
root_certs_from_users->begin(),
root_certs_from_users->end(),
root_certs_from_users->size())
.ToLocal(&results)) {
args.GetReturnValue().Set(results);
}
}
void ResetRootCertStore(const FunctionCallbackInfo<Value>& args) {
Local<Context> context = args.GetIsolate()->GetCurrentContext();
CHECK(args[0]->IsArray());
Local<Array> cert_array = args[0].As<Array>();
if (cert_array->Length() == 0) {
// If the array is empty, just clear the user certs and reset the store.
if (root_cert_store != nullptr) {
X509_STORE_free(root_cert_store);
root_cert_store = nullptr;
}
// Free any existing certificates in the old set.
if (root_certs_from_users != nullptr) {
for (X509* cert : *root_certs_from_users) {
X509_free(cert);
}
}
root_certs_from_users = std::make_unique<X509Set>();
return;
}
// Parse certificates from the array
std::unique_ptr<std::vector<X509*>> certs =
std::make_unique<std::vector<X509*>>();
if (!ArrayOfStringsToX509s(context, cert_array, certs.get())) {
// Error already thrown by ArrayOfStringsToX509s
return;
}
if (certs->empty()) {
Environment* env = Environment::GetCurrent(context);
return THROW_ERR_CRYPTO_OPERATION_FAILED(
env, "No valid certificates found in the provided array");
}
auto new_set = std::make_unique<X509Set>();
for (X509* cert : *certs) {
auto [it, inserted] = new_set->insert(cert);
if (!inserted) { // Free duplicate certificates from the vector.
X509_free(cert);
}
}
// Free any existing certificates in the old set.
if (root_certs_from_users != nullptr) {
for (X509* cert : *root_certs_from_users) {
X509_free(cert);
}
}
std::swap(root_certs_from_users, new_set);
// Reset the global root cert store and create a new one with the
// certificates.
if (root_cert_store != nullptr) {
X509_STORE_free(root_cert_store);
}
// TODO(joyeecheung): we can probably just reset it to nullptr
// and let the next call to NewRootCertStore() create a new one.
root_cert_store = NewRootCertStore();
}
void GetSystemCACertificates(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Array> results;
if (X509sToArrayOfStrings(env, GetSystemStoreCACertificates())
std::vector<X509*>& certs = GetSystemStoreCACertificates();
if (X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size())
.ToLocal(&results)) {
args.GetReturnValue().Set(results);
}
@ -950,7 +1111,9 @@ void GetExtraCACertificates(const FunctionCallbackInfo<Value>& args) {
return args.GetReturnValue().Set(Array::New(env->isolate()));
}
Local<Array> results;
if (X509sToArrayOfStrings(env, GetExtraCACertificates()).ToLocal(&results)) {
std::vector<X509*>& certs = GetExtraCACertificates();
if (X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size())
.ToLocal(&results)) {
args.GetReturnValue().Set(results);
}
}
@ -1046,6 +1209,9 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
context, target, "getSystemCACertificates", GetSystemCACertificates);
SetMethodNoSideEffect(
context, target, "getExtraCACertificates", GetExtraCACertificates);
SetMethod(context, target, "resetRootCertStore", ResetRootCertStore);
SetMethodNoSideEffect(
context, target, "getUserRootCertificates", GetUserRootCertificates);
}
void SecureContext::RegisterExternalReferences(
@ -1088,6 +1254,8 @@ void SecureContext::RegisterExternalReferences(
registry->Register(GetBundledRootCertificates);
registry->Register(GetSystemCACertificates);
registry->Register(GetExtraCACertificates);
registry->Register(ResetRootCertStore);
registry->Register(GetUserRootCertificates);
}
SecureContext* SecureContext::Create(Environment* env) {

View file

@ -186,4 +186,36 @@ exports.assertIsCAArray = function assertIsCAArray(certs) {
}
};
function extractMetadata(cert) {
const x509 = new crypto.X509Certificate(cert);
return {
serialNumber: x509.serialNumber,
issuer: x509.issuer,
subject: x509.subject,
};
}
// To compare two certificates, we can just compare serialNumber, issuer,
// and subject like X509_comp(). We can't just compare two strings because
// the line endings or order of the fields may differ after PEM serdes by
// OpenSSL.
exports.assertEqualCerts = function assertEqualCerts(a, b) {
const setA = new Set(a.map(extractMetadata));
const setB = new Set(b.map(extractMetadata));
assert.deepStrictEqual(setA, setB);
};
exports.includesCert = function includesCert(certs, cert) {
const metadata = extractMetadata(cert);
for (const c of certs) {
const cMetadata = extractMetadata(c);
if (cMetadata.serialNumber === metadata.serialNumber &&
cMetadata.issuer === metadata.issuer &&
cMetadata.subject === metadata.subject) {
return true;
}
}
return false;
};
exports.TestTLSSocket = TestTLSSocket;

View file

@ -0,0 +1,6 @@
exports.cjs = function(key) {
return require(key);
};
exports.esm = function(key) {
return import(key);
};

50
test/fixtures/tls-extra-ca-override.js vendored Normal file
View file

@ -0,0 +1,50 @@
'use strict';
// Test script for overidding NODE_EXTRA_CA_CERTS with tls.setDefaultCACertificates().
const tls = require('tls');
const assert = require('assert');
const { assertEqualCerts, includesCert } = require('../common/tls');
// Assert that NODE_EXTRA_CA_CERTS is set
assert(process.env.NODE_EXTRA_CA_CERTS, 'NODE_EXTRA_CA_CERTS environment variable should be set');
// Get initial state with extra CA
const initialDefaults = tls.getCACertificates('default');
const systemCerts = tls.getCACertificates('system');
const bundledCerts = tls.getCACertificates('bundled');
const extraCerts = tls.getCACertificates('extra');
// For this test to work the extra certs must not be in bundled certs
assert.notStrictEqual(bundledCerts.length, 0);
for (const cert of extraCerts) {
assert(!includesCert(bundledCerts, cert));
}
// Test setting it to initial defaults.
tls.setDefaultCACertificates(initialDefaults);
assertEqualCerts(tls.getCACertificates('default'), initialDefaults);
assertEqualCerts(tls.getCACertificates('default'), initialDefaults);
// Test setting it to the bundled certificates.
tls.setDefaultCACertificates(bundledCerts);
assertEqualCerts(tls.getCACertificates('default'), bundledCerts);
assertEqualCerts(tls.getCACertificates('default'), bundledCerts);
// Test setting it to just the extra certificates.
tls.setDefaultCACertificates(extraCerts);
assertEqualCerts(tls.getCACertificates('default'), extraCerts);
assertEqualCerts(tls.getCACertificates('default'), extraCerts);
// Test setting it to an empty array.
tls.setDefaultCACertificates([]);
assert.deepStrictEqual(tls.getCACertificates('default'), []);
// Test bundled and extra certs are unaffected
assertEqualCerts(tls.getCACertificates('bundled'), bundledCerts);
assertEqualCerts(tls.getCACertificates('extra'), extraCerts);
if (systemCerts.length > 0) {
// Test system certs are unaffected.
assertEqualCerts(tls.getCACertificates('system'), systemCerts);
}

View file

@ -0,0 +1,54 @@
// Flags: --no-use-system-ca
// This tests appending certificates to existing defaults should work correctly
// with fetch.
import * as common from '../common/index.mjs';
import { once } from 'node:events';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';
if (!common.hasCrypto) common.skip('missing crypto');
const { includesCert } = await import('../common/tls.js');
const { default: https } = await import('node:https');
const { default: tls } = await import('node:tls');
const bundledCerts = tls.getCACertificates('bundled');
const fixtureCert = fixtures.readKey('fake-startcom-root-cert.pem');
if (includesCert(bundledCerts, fixtureCert)) {
common.skip('fake-startcom-root-cert is already in bundled certificates, skipping test');
}
// Test HTTPS connection fails with bundled CA, succeeds after adding custom CA
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, common.mustCall((req, res) => {
res.writeHead(200);
res.end('hello world');
}, 1));
server.listen(0);
await once(server, 'listening');
const url = `https://localhost:${server.address().port}/hello-world`;
// First attempt should fail without custom CA.
await assert.rejects(
fetch(url),
(err) => {
assert.strictEqual(err.cause.code, 'UNABLE_TO_VERIFY_LEAF_SIGNATURE');
return true;
},
);
// Now enable custom CA certificate.
tls.setDefaultCACertificates([fixtureCert]);
// Second attempt should succeed.
const response = await fetch(url);
assert.strictEqual(response.status, 200);
const text = await response.text();
assert.strictEqual(text, 'hello world');
server.close();

View file

@ -0,0 +1,71 @@
'use strict';
// This tests appending certificates to existing defaults should work correctly
// with https.request().
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const https = require('https');
const tls = require('tls');
const fixtures = require('../common/fixtures');
const { includesCert } = require('../common/tls');
const bundledCerts = tls.getCACertificates('bundled');
const fixtureCert = fixtures.readKey('fake-startcom-root-cert.pem');
if (includesCert(bundledCerts, fixtureCert)) {
common.skip('fake-startcom-root-cert is already in bundled certificates, skipping test');
}
// Test HTTPS connection fails with bundled CA, succeeds after adding custom CA
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, (req, res) => {
res.writeHead(200);
res.end('success');
});
server.listen(0, common.mustCall(() => {
const port = server.address().port;
// Set to bundled CA certificates - connection should fail
tls.setDefaultCACertificates(bundledCerts);
const req1 = https.request({
hostname: 'localhost',
port: port,
path: '/',
method: 'GET'
}, common.mustNotCall('Should not succeed with bundled CA only'));
req1.on('error', common.mustCall((err) => {
console.log(err);
// Should fail with certificate verification error
assert.strictEqual(err.code, 'UNABLE_TO_VERIFY_LEAF_SIGNATURE');
// Now add the fake-startcom-root-cert to bundled certs - connection should succeed
tls.setDefaultCACertificates([...bundledCerts, fixtureCert]);
const req2 = https.request({
hostname: 'localhost',
port: port,
path: '/',
method: 'GET'
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', common.mustCall(() => {
assert.strictEqual(data, 'success');
server.close();
}));
}));
req2.on('error', common.mustNotCall('Should not error with correct CA added'));
req2.end();
}));
req1.end();
}));

View file

@ -0,0 +1,39 @@
// Flags: --no-use-system-ca
'use strict';
// This tests tls.setDefaultCACertificates() support ArrayBufferView.
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const tls = require('tls');
const fixtures = require('../common/fixtures');
const { assertEqualCerts } = require('../common/tls');
const fixtureCert = fixtures.readKey('fake-startcom-root-cert.pem');
// Should accept Buffer.
tls.setDefaultCACertificates([Buffer.from(fixtureCert)]);
const result = tls.getCACertificates('default');
assertEqualCerts(result, [fixtureCert]);
// Reset it to empty.
tls.setDefaultCACertificates([]);
assertEqualCerts(tls.getCACertificates('default'), []);
// Should accept Uint8Array.
const encoder = new TextEncoder();
const uint8Cert = encoder.encode(fixtureCert);
tls.setDefaultCACertificates([uint8Cert]);
const uint8Result = tls.getCACertificates('default');
assertEqualCerts(uint8Result, [fixtureCert]);
// Reset it to empty.
tls.setDefaultCACertificates([]);
assertEqualCerts(tls.getCACertificates('default'), []);
// Should accept DataView.
const dataViewCert = new DataView(uint8Cert.buffer, uint8Cert.byteOffset, uint8Cert.byteLength);
tls.setDefaultCACertificates([dataViewCert]);
const dataViewResult = tls.getCACertificates('default');
assertEqualCerts(dataViewResult, [fixtureCert]);

View file

@ -0,0 +1,58 @@
'use strict';
// This tests the basic functionality of tls.setDefaultCACertificates().
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const tls = require('tls');
const fixtures = require('../common/fixtures');
const { assertEqualCerts } = require('../common/tls');
const originalBundled = tls.getCACertificates('bundled');
const originalSystem = tls.getCACertificates('system');
const fixtureCert = fixtures.readKey('fake-startcom-root-cert.pem');
function testSetCertificates(certs) {
// Test setting it can be verified with tls.getCACertificates().
tls.setDefaultCACertificates(certs);
const result = tls.getCACertificates('default');
assertEqualCerts(result, certs);
// Verify that other certificate types are unchanged
const newBundled = tls.getCACertificates('bundled');
const newSystem = tls.getCACertificates('system');
assertEqualCerts(newBundled, originalBundled);
assertEqualCerts(newSystem, originalSystem);
// Test implicit defaults.
const implicitDefaults = tls.getCACertificates();
assertEqualCerts(implicitDefaults, certs);
// Test cached results.
const cachedResult = tls.getCACertificates('default');
assertEqualCerts(cachedResult, certs);
const cachedImplicitDefaults = tls.getCACertificates();
assertEqualCerts(cachedImplicitDefaults, certs);
}
// Test setting with fixture certificate.
testSetCertificates([fixtureCert]);
// Test setting with empty array.
testSetCertificates([]);
// Test setting with bundled certificates
testSetCertificates(originalBundled);
// Test combining bundled and extra certificates.
testSetCertificates([...originalBundled, fixtureCert]);
// Test setting with a subset of bundled certificates
if (originalBundled.length >= 3) {
testSetCertificates(originalBundled.slice(0, 3));
}
// Test duplicate certificates
tls.setDefaultCACertificates([fixtureCert, fixtureCert, fixtureCert]);
assertEqualCerts(tls.getCACertificates('default'), [fixtureCert]);

View file

@ -0,0 +1,41 @@
'use strict';
// This tests input validation of tls.setDefaultCACertificates().
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const tls = require('tls');
const { assertEqualCerts } = require('../common/tls');
const defaultCerts = tls.getCACertificates('default');
const fixtureCert = fixtures.readKey('fake-startcom-root-cert.pem');
for (const invalid of [null, undefined, 'string', 42, {}, true]) {
// Test input validation - should throw when not passed an array
assert.throws(() => tls.setDefaultCACertificates(invalid), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "certs" argument must be an instance of Array/
});
// Verify that default certificates remain unchanged after error.
assertEqualCerts(tls.getCACertificates('default'), defaultCerts);
}
for (const invalid of [null, undefined, 42, {}, true]) {
// Test input validation - should throw when passed an array with invalid elements
assert.throws(() => tls.setDefaultCACertificates([invalid]), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "certs\[0\]" argument must be of type string or an instance of ArrayBufferView/
});
// Verify that default certificates remain unchanged after error.
assertEqualCerts(tls.getCACertificates('default'), defaultCerts);
assert.throws(() => tls.setDefaultCACertificates([fixtureCert, invalid]), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "certs\[1\]" argument must be of type string or an instance of ArrayBufferView/
});
// Verify that default certificates remain unchanged after error.
assertEqualCerts(tls.getCACertificates('default'), defaultCerts);
}

View file

@ -0,0 +1,19 @@
'use strict';
// This tests that tls.setDefaultCACertificates() properly overrides certificates
// added through NODE_EXTRA_CA_CERTS environment variable.
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
spawnSyncAndExitWithoutError(process.execPath, [
fixtures.path('tls-extra-ca-override.js'),
], {
env: {
...process.env,
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem')
}
});

View file

@ -0,0 +1,46 @@
'use strict';
// This tests mixed input types for tls.setDefaultCACertificates().
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const tls = require('tls');
const { assertEqualCerts } = require('../common/tls');
const bundledCerts = tls.getCACertificates('bundled');
if (bundledCerts.length < 4) {
common.skip('Not enough bundled CA certificates available');
}
const encoder = new TextEncoder();
// Test mixed array with string and Buffer.
{
tls.setDefaultCACertificates([bundledCerts[0], Buffer.from(bundledCerts[1], 'utf8')]);
const result = tls.getCACertificates('default');
assertEqualCerts(result, [bundledCerts[0], bundledCerts[1]]);
}
// Test mixed array with string and Uint8Array.
{
tls.setDefaultCACertificates([bundledCerts[1], encoder.encode(bundledCerts[2])]);
const result = tls.getCACertificates('default');
assertEqualCerts(result, [bundledCerts[1], bundledCerts[2]]);
}
// Test mixed array with string and DataView.
{
const uint8Cert = encoder.encode(bundledCerts[3]);
const dataViewCert = new DataView(uint8Cert.buffer, uint8Cert.byteOffset, uint8Cert.byteLength);
tls.setDefaultCACertificates([bundledCerts[1], dataViewCert]);
const result = tls.getCACertificates('default');
assertEqualCerts(result, [bundledCerts[1], bundledCerts[3]]);
}
// Test mixed array with Buffer and Uint8Array.
{
tls.setDefaultCACertificates([Buffer.from(bundledCerts[0], 'utf8'), encoder.encode(bundledCerts[2])]);
const result = tls.getCACertificates('default');
assertEqualCerts(result, [bundledCerts[0], bundledCerts[2]]);
}

View file

@ -0,0 +1,53 @@
'use strict';
// This tests that per-connection ca option overrides bundled default CA certificates.
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const https = require('https');
const tls = require('tls');
const fixtures = require('../common/fixtures');
const { includesCert } = require('../common/tls');
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, common.mustCall((req, res) => {
res.writeHead(200);
res.end('override works');
}, 1));
server.listen(0, common.mustCall(() => {
const port = server.address().port;
const bundledCerts = tls.getCACertificates('bundled');
const fakeStartcomCert = fixtures.readKey('fake-startcom-root-cert.pem');
// Set default CA to bundled certs (which don't include fake-startcom-root-cert)
tls.setDefaultCACertificates(bundledCerts);
// Verify that fake-startcom-root-cert is not in default
const defaultCerts = tls.getCACertificates('default');
assert(!includesCert(defaultCerts, fakeStartcomCert));
// Connection with per-connection ca should succeed despite wrong default
const req = https.request({
hostname: 'localhost',
port: port,
path: '/',
method: 'GET',
ca: [fakeStartcomCert] // This should override the bundled defaults
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', common.mustCall(() => {
assert.strictEqual(data, 'override works');
server.close();
}));
}));
req.on('error', common.mustNotCall('Should not error with per-connection ca option'));
req.end();
}));

View file

@ -0,0 +1,51 @@
'use strict';
// This tests that per-connection ca option overrides empty default CA certificates
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const https = require('https');
const tls = require('tls');
const fixtures = require('../common/fixtures');
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, common.mustCall((req, res) => {
res.writeHead(200);
res.end('per-connection ca works');
}, 1));
server.listen(0, common.mustCall(() => {
const port = server.address().port;
const fakeStartcomCert = fixtures.readKey('fake-startcom-root-cert.pem');
// Set default CA to empty array - connections should normally fail
tls.setDefaultCACertificates([]);
// Verify that default CA is empty
const defaultCerts = tls.getCACertificates('default');
assert.deepStrictEqual(defaultCerts, []);
// Connection with per-connection ca option should succeed despite empty default
const req = https.request({
hostname: 'localhost',
port: port,
path: '/',
method: 'GET',
ca: [fakeStartcomCert] // This should override the empty default
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', common.mustCall(() => {
assert.strictEqual(data, 'per-connection ca works');
server.close();
}));
}));
req.on('error', common.mustNotCall('Should not error with per-connection ca option'));
req.end();
}));

View file

@ -0,0 +1,43 @@
'use strict';
// This tests error recovery and fallback behavior for tls.setDefaultCACertificates()
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const tls = require('tls');
const fixtures = require('../common/fixtures');
const { assertEqualCerts } = require('../common/tls');
const fixtureCert = fixtures.readKey('fake-startcom-root-cert.pem');
// Test recovery from errors when setting default CA certificates.
function testRecovery(expectedCerts) {
{
const invalidCert = 'not a valid certificate';
assert.throws(() => tls.setDefaultCACertificates([invalidCert]), {
code: 'ERR_CRYPTO_OPERATION_FAILED',
message: /No valid certificates found in the provided array/
});
assertEqualCerts(tls.getCACertificates('default'), expectedCerts);
}
// Test with mixed valid and invalid certificate formats.
{
const invalidCert = '-----BEGIN CERTIFICATE-----\nvalid cert content\n-----END CERTIFICATE-----';
assert.throws(() => tls.setDefaultCACertificates([fixtureCert, invalidCert]), {
code: 'ERR_OSSL_PEM_ASN1_LIB',
});
assertEqualCerts(tls.getCACertificates('default'), expectedCerts);
}
}
const originalDefaultCerts = tls.getCACertificates('default');
testRecovery(originalDefaultCerts);
// Check that recovery still works after replacing the default certificates.
const subset = tls.getCACertificates('bundled').slice(0, 3);
tls.setDefaultCACertificates(subset);
assertEqualCerts(tls.getCACertificates('default'), subset);
testRecovery(subset);

View file

@ -0,0 +1,47 @@
// Flags: --no-use-system-ca
// This tests appending certificates to existing defaults should work correctly
// with fetch.
import * as common from '../common/index.mjs';
import { once } from 'node:events';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';
if (!common.hasCrypto) common.skip('missing crypto');
const { default: https } = await import('node:https');
const { default: tls } = await import('node:tls');
// Test HTTPS connection fails with bundled CA, succeeds after adding custom CA.
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, common.mustCall((req, res) => {
res.writeHead(200);
res.end('hello world');
}, 1));
server.listen(0);
await once(server, 'listening');
const fixturesCert = fixtures.readKey('fake-startcom-root-cert.pem');
tls.setDefaultCACertificates([fixturesCert]);
// First, verify connection works with custom CA.
const response1 = await fetch(`https://localhost:${server.address().port}/custom-ca-test`);
assert.strictEqual(response1.status, 200);
const text1 = await response1.text();
assert.strictEqual(text1, 'hello world');
// Now set empty CA store - connection should fail.
tls.setDefaultCACertificates([]);
// Use IP address to skip session cache.
await assert.rejects(
fetch(`https://127.0.0.1:${server.address().port}/empty-ca-test`),
(err) => {
assert.strictEqual(err.cause.code, 'UNABLE_TO_VERIFY_LEAF_SIGNATURE');
return true;
},
);
server.close();

View file

@ -0,0 +1,62 @@
'use strict';
// This tests that tls.setDefaultCACertificates() affects actual HTTPS connections
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const https = require('https');
const tls = require('tls');
const fixtures = require('../common/fixtures');
// Test HTTPS connection succeeds with proper CA, fails after removing it
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, common.mustCall((req, res) => {
res.writeHead(200);
res.end('hello world');
}, 1));
server.listen(0, common.mustCall(() => {
const port = server.address().port;
// First, set the correct CA certificate - connection should succeed.
tls.setDefaultCACertificates([fixtures.readKey('fake-startcom-root-cert.pem')]);
const req1 = https.request({
hostname: 'localhost',
port: port,
path: '/',
method: 'GET'
}, common.mustCall((res) => {
assert.strictEqual(res.statusCode, 200);
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', common.mustCall(() => {
assert.strictEqual(data, 'hello world');
// Now set empty CA store - connection should fail.
tls.setDefaultCACertificates([]);
const req2 = https.request({
hostname: '127.0.0.1', // Use a different hostname to skip session cache.
port: port,
path: '/',
method: 'GET'
}, common.mustNotCall('Should not succeed with empty CA'));
req2.on('error', common.mustCall((err) => {
// Should fail with certificate verification error.
assert.strictEqual(err.code, 'UNABLE_TO_VERIFY_LEAF_SIGNATURE');
server.close();
}));
req2.end();
}));
}));
req1.on('error', common.mustNotCall('Should not error with correct CA'));
req1.end();
}));

View file

@ -0,0 +1,49 @@
// Flags: --no-use-system-ca
// This tests that tls.setDefaultCACertificates() can be used to remove
// system CA certificates from the default CA store.
// To run this test, install the certificates as described in README.md
import * as common from '../common/index.mjs';
import assert from 'node:assert/strict';
import fixtures from '../common/fixtures.js';
import { once } from 'events';
if (!common.hasCrypto) {
common.skip('requires crypto');
}
const { default: https } = await import('node:https');
const { default: tls } = await import('node:tls');
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, common.mustCall((req, res) => {
res.writeHead(200);
res.end('hello world');
}, 1));
server.listen(0);
await once(server, 'listening');
const url = `https://localhost:${server.address().port}/hello-world`;
// First attempt should fail without system certificates.
await assert.rejects(
fetch(url),
(err) => {
assert.strictEqual(err.cause.code, 'UNABLE_TO_VERIFY_LEAF_SIGNATURE');
return true;
},
);
// Now enable system CA certificates
tls.setDefaultCACertificates(tls.getCACertificates('system'));
// Second attempt should succeed.
const response = await fetch(url);
assert.strictEqual(response.status, 200);
const text = await response.text();
assert.strictEqual(text, 'hello world');
server.close();

View file

@ -0,0 +1,87 @@
// Flags: --use-system-ca
// This tests that tls.setDefaultCACertificates() can be used to dynamically
// enable system CA certificates for HTTPS connections.
// To run this test, install the certificates as described in README.md
import * as common from '../common/index.mjs';
import assert from 'node:assert/strict';
import fixtures from '../common/fixtures.js';
import { once } from 'events';
import { includesCert, assertEqualCerts } from '../common/tls.js';
if (!common.hasCrypto) {
common.skip('requires crypto');
}
const { default: https } = await import('node:https');
const { default: tls } = await import('node:tls');
// Verify that system CA includes the fake-startcom-root-cert.
const systemCerts = tls.getCACertificates('system');
const fixturesCert = fixtures.readKey('fake-startcom-root-cert.pem');
if (!includesCert(systemCerts, fixturesCert)) {
common.skip('fake-startcom-root-cert.pem not found in system CA store. ' +
'Please follow setup instructions in test/system-ca/README.md');
}
const bundledCerts = tls.getCACertificates('bundled');
if (includesCert(bundledCerts, fixturesCert)) {
common.skip('fake-startcom-root-cert.pem should not be in bundled CA store');
}
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, common.mustCall((req, res) => {
const path = req.url;
switch (path) {
case '/system-ca-test':
res.writeHead(200);
res.end('system ca works');
break;
case '/bundled-ca-test':
res.writeHead(200);
res.end('bundled ca works');
break;
default:
assert(false, `Unexpected path: ${path}`);
}
}, 1));
server.listen(0);
await once(server, 'listening');
const url = `https://localhost:${server.address().port}`;
// First, verify connection works with system CA (including fake-startcom-root-cert)
const response1 = await fetch(`${url}/system-ca-test`);
assert.strictEqual(response1.status, 200);
const text1 = await response1.text();
assert.strictEqual(text1, 'system ca works');
// Now override with bundled certs (which do not include fake-startcom-root-cert)
tls.setDefaultCACertificates(bundledCerts);
// Connection should now fail because fake-startcom-root-cert is no longer in the CA store.
// Use IP address to skip session cache.
await assert.rejects(
fetch(`https://127.0.0.1:${server.address().port}/bundled-ca-test`),
(err) => {
assert.strictEqual(err.cause.code, 'SELF_SIGNED_CERT_IN_CHAIN');
return true;
},
);
// Verify that system CA type still returns original system certs
const stillSystemCerts = tls.getCACertificates('system');
assertEqualCerts(stillSystemCerts, systemCerts);
assert(includesCert(stillSystemCerts, fixturesCert));
// Verify that default CA now returns bundled certs
const currentDefaults = tls.getCACertificates('default');
assertEqualCerts(currentDefaults, bundledCerts);
assert(!includesCert(currentDefaults, fixturesCert));
server.close();

View file

@ -0,0 +1,58 @@
// Flags: --use-system-ca
// This tests various combinations of CA certificates with
// tls.setDefaultCACertificates().
'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const tls = require('tls');
const { assertEqualCerts } = require('../common/tls');
const fixtures = require('../common/fixtures');
const fixtureCert = fixtures.readKey('fake-startcom-root-cert.pem');
const originalBundled = tls.getCACertificates('bundled');
const originalSystem = tls.getCACertificates('system');
function testSetCertificates(certs) {
// Test setting it can be verified with tls.getCACertificates().
tls.setDefaultCACertificates(certs);
const result = tls.getCACertificates('default');
assertEqualCerts(result, certs);
// Verify that other certificate types are unchanged
const newBundled = tls.getCACertificates('bundled');
const newSystem = tls.getCACertificates('system');
assertEqualCerts(newBundled, originalBundled);
assertEqualCerts(newSystem, originalSystem);
// Test implicit defaults.
const implicitDefaults = tls.getCACertificates();
assertEqualCerts(implicitDefaults, certs);
// Test cached results.
const cachedResult = tls.getCACertificates('default');
assertEqualCerts(cachedResult, certs);
const cachedImplicitDefaults = tls.getCACertificates();
assertEqualCerts(cachedImplicitDefaults, certs);
// Test system CA certificates are not affected.
const systemCerts = tls.getCACertificates('system');
assertEqualCerts(systemCerts, originalSystem);
}
// Test setting with fixture certificate.
testSetCertificates([fixtureCert]);
// Test setting with empty array.
testSetCertificates([]);
// Test setting with bundled certificates
testSetCertificates(originalBundled);
// Test setting with a subset of bundled certificates
if (originalBundled.length >= 3) {
testSetCertificates(originalBundled.slice(0, 3));
}