Skip to content

crypto: add tls.setDefaultCACertificates() #58822

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
48 changes: 48 additions & 0 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -51,6 +52,8 @@ const {
getBundledRootCertificates,
getExtraCACertificates,
getSystemCACertificates,
resetRootCertStore,
getUserRootCertificates,
getSSLCiphers,
} = internalBinding('crypto');
const { Buffer } = require('buffer');
Expand Down Expand Up @@ -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')) {
Expand Down Expand Up @@ -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) {
Expand Down
196 changes: 182 additions & 14 deletions src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
#include <wincrypt.h>
#endif

#include <set>

namespace node {

using ncrypto::BignumPointer;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand All @@ -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);
}
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading