Skip to content

Commit cda1dab

Browse files
joyeecheungaduh95
authored andcommitted
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: #58822 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
1 parent ff6be2e commit cda1dab

21 files changed

+1128
-14
lines changed

doc/api/tls.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2342,6 +2342,54 @@ openssl pkcs12 -certpbe AES-256-CBC -export -out client-cert.pem \
23422342
The server can be tested by connecting to it using the example client from
23432343
[`tls.connect()`][].
23442344

2345+
## `tls.setDefaultCACertificates(certs)`
2346+
2347+
<!-- YAML
2348+
added: REPLACEME
2349+
-->
2350+
2351+
* `certs` {string\[]|ArrayBufferView\[]} An array of CA certificates in PEM format.
2352+
2353+
Sets the default CA certificates used by Node.js TLS clients. If the provided
2354+
certificates are parsed successfully, they will become the default CA
2355+
certificate list returned by [`tls.getCACertificates()`][] and used
2356+
by subsequent TLS connections that don't specify their own CA certificates.
2357+
The certificates will be deduplicated before being set as the default.
2358+
2359+
This function only affects the current Node.js thread. Previous
2360+
sessions cached by the HTTPS agent won't be affected by this change, so
2361+
this method should be called before any unwanted cachable TLS connections are
2362+
made.
2363+
2364+
To use system CA certificates as the default:
2365+
2366+
```cjs
2367+
const tls = require('node:tls');
2368+
tls.setDefaultCACertificates(tls.getCACertificates('system'));
2369+
```
2370+
2371+
```mjs
2372+
import tls from 'node:tls';
2373+
tls.setDefaultCACertificates(tls.getCACertificates('system'));
2374+
```
2375+
2376+
This function completely replaces the default CA certificate list. To add additional
2377+
certificates to the existing defaults, get the current certificates and append to them:
2378+
2379+
```cjs
2380+
const tls = require('node:tls');
2381+
const currentCerts = tls.getCACertificates('default');
2382+
const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...'];
2383+
tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]);
2384+
```
2385+
2386+
```mjs
2387+
import tls from 'node:tls';
2388+
const currentCerts = tls.getCACertificates('default');
2389+
const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...'];
2390+
tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]);
2391+
```
2392+
23452393
## `tls.getCACertificates([type])`
23462394

23472395
<!-- YAML

lib/tls.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const {
3737
ERR_TLS_CERT_ALTNAME_INVALID,
3838
ERR_OUT_OF_RANGE,
3939
ERR_INVALID_ARG_VALUE,
40+
ERR_INVALID_ARG_TYPE,
4041
} = require('internal/errors').codes;
4142
const internalUtil = require('internal/util');
4243
internalUtil.assertCrypto();
@@ -51,6 +52,8 @@ const {
5152
getBundledRootCertificates,
5253
getExtraCACertificates,
5354
getSystemCACertificates,
55+
resetRootCertStore,
56+
getUserRootCertificates,
5457
getSSLCiphers,
5558
} = internalBinding('crypto');
5659
const { Buffer } = require('buffer');
@@ -123,8 +126,17 @@ function cacheSystemCACertificates() {
123126
}
124127

125128
let defaultCACertificates;
129+
let hasResetDefaultCACertificates = false;
130+
126131
function cacheDefaultCACertificates() {
127132
if (defaultCACertificates) { return defaultCACertificates; }
133+
134+
if (hasResetDefaultCACertificates) {
135+
defaultCACertificates = getUserRootCertificates();
136+
ObjectFreeze(defaultCACertificates);
137+
return defaultCACertificates;
138+
}
139+
128140
defaultCACertificates = [];
129141

130142
if (!getOptionValue('--use-openssl-ca')) {
@@ -172,6 +184,26 @@ function getCACertificates(type = 'default') {
172184
}
173185
exports.getCACertificates = getCACertificates;
174186

187+
function setDefaultCACertificates(certs) {
188+
if (!ArrayIsArray(certs)) {
189+
throw new ERR_INVALID_ARG_TYPE('certs', 'Array', certs);
190+
}
191+
192+
// Verify that all elements in the array are strings
193+
for (let i = 0; i < certs.length; i++) {
194+
if (typeof certs[i] !== 'string' && !isArrayBufferView(certs[i])) {
195+
throw new ERR_INVALID_ARG_TYPE(
196+
`certs[${i}]`, ['string', 'ArrayBufferView'], certs[i]);
197+
}
198+
}
199+
200+
resetRootCertStore(certs);
201+
defaultCACertificates = undefined; // Reset the cached default certificates
202+
hasResetDefaultCACertificates = true;
203+
}
204+
205+
exports.setDefaultCACertificates = setDefaultCACertificates;
206+
175207
// Convert protocols array into valid OpenSSL protocols list
176208
// ("\x06spdy/2\x08http/1.1\x08http/1.0")
177209
function convertProtocols(protocols) {

src/crypto/crypto_context.cc

Lines changed: 182 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
#include <wincrypt.h>
2828
#endif
2929

30+
#include <set>
31+
3032
namespace node {
3133

3234
using ncrypto::BignumPointer;
@@ -81,10 +83,28 @@ static std::atomic<bool> has_cached_bundled_root_certs{false};
8183
static std::atomic<bool> has_cached_system_root_certs{false};
8284
static std::atomic<bool> has_cached_extra_root_certs{false};
8385

86+
// Used for sets of X509.
87+
struct X509Less {
88+
bool operator()(const X509* lhs, const X509* rhs) const noexcept {
89+
return X509_cmp(const_cast<X509*>(lhs), const_cast<X509*>(rhs)) < 0;
90+
}
91+
};
92+
using X509Set = std::set<X509*, X509Less>;
93+
94+
// Per-thread root cert store. See NewRootCertStore() on what it contains.
95+
static thread_local X509_STORE* root_cert_store = nullptr;
96+
// If the user calls tls.setDefaultCACertificates() this will be used
97+
// to hold the user-provided certificates, the root_cert_store and any new
98+
// copy generated by NewRootCertStore() will then contain the certificates
99+
// from this set.
100+
static thread_local std::unique_ptr<X509Set> root_certs_from_users;
101+
84102
X509_STORE* GetOrCreateRootCertStore() {
85-
// Guaranteed thread-safe by standard, just don't use -fno-threadsafe-statics.
86-
static X509_STORE* store = NewRootCertStore();
87-
return store;
103+
if (root_cert_store != nullptr) {
104+
return root_cert_store;
105+
}
106+
root_cert_store = NewRootCertStore();
107+
return root_cert_store;
88108
}
89109

90110
// Takes a string or buffer and loads it into a BIO.
@@ -225,14 +245,11 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
225245
issuer);
226246
}
227247

228-
static unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
248+
static unsigned long LoadCertsFromBIO( // NOLINT(runtime/int)
229249
std::vector<X509*>* certs,
230-
const char* file) {
250+
BIOPointer bio) {
231251
MarkPopErrorOnReturn mark_pop_error_on_return;
232252

233-
auto bio = BIOPointer::NewFile(file, "r");
234-
if (!bio) return ERR_get_error();
235-
236253
while (X509* x509 = PEM_read_bio_X509(
237254
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
238255
certs->push_back(x509);
@@ -248,6 +265,17 @@ static unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
248265
}
249266
}
250267

268+
static unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
269+
std::vector<X509*>* certs,
270+
const char* file) {
271+
MarkPopErrorOnReturn mark_pop_error_on_return;
272+
273+
auto bio = BIOPointer::NewFile(file, "r");
274+
if (!bio) return ERR_get_error();
275+
276+
return LoadCertsFromBIO(certs, std::move(bio));
277+
}
278+
251279
// Indicates the trust status of a certificate.
252280
enum class TrustStatus {
253281
// Trust status is unknown / uninitialized.
@@ -829,11 +857,24 @@ static std::vector<X509*>& GetExtraCACertificates() {
829857
// NODE_EXTRA_CA_CERTS are cached after first load. Certificates
830858
// from --use-system-ca are not cached and always reloaded from
831859
// disk.
860+
// 8. If users have reset the root cert store by calling
861+
// tls.setDefaultCACertificates(), the store will be populated with
862+
// the certificates provided by users.
832863
// TODO(joyeecheung): maybe these rules need a bit of consolidation?
833864
X509_STORE* NewRootCertStore() {
834865
X509_STORE* store = X509_STORE_new();
835866
CHECK_NOT_NULL(store);
836867

868+
// If the root cert store is already reset by users through
869+
// tls.setDefaultCACertificates(), just create a copy from the
870+
// user-provided certificates.
871+
if (root_certs_from_users != nullptr) {
872+
for (X509* cert : *root_certs_from_users) {
873+
CHECK_EQ(1, X509_STORE_add_cert(store, cert));
874+
}
875+
return store;
876+
}
877+
837878
#ifdef NODE_OPENSSL_SYSTEM_CERT_PATH
838879
if constexpr (sizeof(NODE_OPENSSL_SYSTEM_CERT_PATH) > 1) {
839880
ERR_set_mark();
@@ -901,14 +942,57 @@ void GetBundledRootCertificates(const FunctionCallbackInfo<Value>& args) {
901942
Array::New(env->isolate(), result, arraysize(root_certs)));
902943
}
903944

945+
bool ArrayOfStringsToX509s(Local<Context> context,
946+
Local<Array> cert_array,
947+
std::vector<X509*>* certs) {
948+
ClearErrorOnReturn clear_error_on_return;
949+
Isolate* isolate = context->GetIsolate();
950+
Environment* env = Environment::GetCurrent(context);
951+
uint32_t array_length = cert_array->Length();
952+
953+
std::vector<v8::Global<Value>> cert_items;
954+
if (FromV8Array(context, cert_array, &cert_items).IsNothing()) {
955+
return false;
956+
}
957+
958+
for (uint32_t i = 0; i < array_length; i++) {
959+
Local<Value> cert_val = cert_items[i].Get(isolate);
960+
// Parse the PEM certificate.
961+
BIOPointer bio(LoadBIO(env, cert_val));
962+
if (!bio) {
963+
ThrowCryptoError(env, ERR_get_error(), "Failed to load certificate data");
964+
return false;
965+
}
966+
967+
// Read all certificates from this PEM string
968+
size_t start = certs->size();
969+
auto err = LoadCertsFromBIO(certs, std::move(bio));
970+
if (err != 0) {
971+
size_t end = certs->size();
972+
// Clean up any certificates we've already parsed upon failure.
973+
for (size_t j = start; j < end; ++j) {
974+
X509_free((*certs)[j]);
975+
}
976+
ThrowCryptoError(env, err, "Failed to parse certificate");
977+
return false;
978+
}
979+
}
980+
981+
return true;
982+
}
983+
984+
template <typename It>
904985
MaybeLocal<Array> X509sToArrayOfStrings(Environment* env,
905-
const std::vector<X509*>& certs) {
986+
It first,
987+
It last,
988+
size_t size) {
906989
ClearErrorOnReturn clear_error_on_return;
907990
EscapableHandleScope scope(env->isolate());
908991

909-
LocalVector<Value> result(env->isolate(), certs.size());
910-
for (size_t i = 0; i < certs.size(); ++i) {
911-
X509View view(certs[i]);
992+
LocalVector<Value> result(env->isolate(), size);
993+
size_t i = 0;
994+
for (It cur = first; cur != last; ++cur, ++i) {
995+
X509View view(*cur);
912996
auto pem_bio = view.toPEM();
913997
if (!pem_bio) {
914998
ThrowCryptoError(env, ERR_get_error(), "X509 to PEM conversion");
@@ -933,10 +1017,87 @@ MaybeLocal<Array> X509sToArrayOfStrings(Environment* env,
9331017
return scope.Escape(Array::New(env->isolate(), result.data(), result.size()));
9341018
}
9351019

1020+
void GetUserRootCertificates(const FunctionCallbackInfo<Value>& args) {
1021+
Environment* env = Environment::GetCurrent(args);
1022+
CHECK_NOT_NULL(root_certs_from_users);
1023+
Local<Array> results;
1024+
if (X509sToArrayOfStrings(env,
1025+
root_certs_from_users->begin(),
1026+
root_certs_from_users->end(),
1027+
root_certs_from_users->size())
1028+
.ToLocal(&results)) {
1029+
args.GetReturnValue().Set(results);
1030+
}
1031+
}
1032+
1033+
void ResetRootCertStore(const FunctionCallbackInfo<Value>& args) {
1034+
Local<Context> context = args.GetIsolate()->GetCurrentContext();
1035+
CHECK(args[0]->IsArray());
1036+
Local<Array> cert_array = args[0].As<Array>();
1037+
1038+
if (cert_array->Length() == 0) {
1039+
// If the array is empty, just clear the user certs and reset the store.
1040+
if (root_cert_store != nullptr) {
1041+
X509_STORE_free(root_cert_store);
1042+
root_cert_store = nullptr;
1043+
}
1044+
1045+
// Free any existing certificates in the old set.
1046+
if (root_certs_from_users != nullptr) {
1047+
for (X509* cert : *root_certs_from_users) {
1048+
X509_free(cert);
1049+
}
1050+
}
1051+
root_certs_from_users = std::make_unique<X509Set>();
1052+
return;
1053+
}
1054+
1055+
// Parse certificates from the array
1056+
std::unique_ptr<std::vector<X509*>> certs =
1057+
std::make_unique<std::vector<X509*>>();
1058+
if (!ArrayOfStringsToX509s(context, cert_array, certs.get())) {
1059+
// Error already thrown by ArrayOfStringsToX509s
1060+
return;
1061+
}
1062+
1063+
if (certs->empty()) {
1064+
Environment* env = Environment::GetCurrent(context);
1065+
return THROW_ERR_CRYPTO_OPERATION_FAILED(
1066+
env, "No valid certificates found in the provided array");
1067+
}
1068+
1069+
auto new_set = std::make_unique<X509Set>();
1070+
for (X509* cert : *certs) {
1071+
auto [it, inserted] = new_set->insert(cert);
1072+
if (!inserted) { // Free duplicate certificates from the vector.
1073+
X509_free(cert);
1074+
}
1075+
}
1076+
1077+
// Free any existing certificates in the old set.
1078+
if (root_certs_from_users != nullptr) {
1079+
for (X509* cert : *root_certs_from_users) {
1080+
X509_free(cert);
1081+
}
1082+
}
1083+
std::swap(root_certs_from_users, new_set);
1084+
1085+
// Reset the global root cert store and create a new one with the
1086+
// certificates.
1087+
if (root_cert_store != nullptr) {
1088+
X509_STORE_free(root_cert_store);
1089+
}
1090+
1091+
// TODO(joyeecheung): we can probably just reset it to nullptr
1092+
// and let the next call to NewRootCertStore() create a new one.
1093+
root_cert_store = NewRootCertStore();
1094+
}
1095+
9361096
void GetSystemCACertificates(const FunctionCallbackInfo<Value>& args) {
9371097
Environment* env = Environment::GetCurrent(args);
9381098
Local<Array> results;
939-
if (X509sToArrayOfStrings(env, GetSystemStoreCACertificates())
1099+
std::vector<X509*>& certs = GetSystemStoreCACertificates();
1100+
if (X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size())
9401101
.ToLocal(&results)) {
9411102
args.GetReturnValue().Set(results);
9421103
}
@@ -948,7 +1109,9 @@ void GetExtraCACertificates(const FunctionCallbackInfo<Value>& args) {
9481109
return args.GetReturnValue().Set(Array::New(env->isolate()));
9491110
}
9501111
Local<Array> results;
951-
if (X509sToArrayOfStrings(env, GetExtraCACertificates()).ToLocal(&results)) {
1112+
std::vector<X509*>& certs = GetExtraCACertificates();
1113+
if (X509sToArrayOfStrings(env, certs.begin(), certs.end(), certs.size())
1114+
.ToLocal(&results)) {
9521115
args.GetReturnValue().Set(results);
9531116
}
9541117
}
@@ -1044,6 +1207,9 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
10441207
context, target, "getSystemCACertificates", GetSystemCACertificates);
10451208
SetMethodNoSideEffect(
10461209
context, target, "getExtraCACertificates", GetExtraCACertificates);
1210+
SetMethod(context, target, "resetRootCertStore", ResetRootCertStore);
1211+
SetMethodNoSideEffect(
1212+
context, target, "getUserRootCertificates", GetUserRootCertificates);
10471213
}
10481214

10491215
void SecureContext::RegisterExternalReferences(
@@ -1086,6 +1252,8 @@ void SecureContext::RegisterExternalReferences(
10861252
registry->Register(GetBundledRootCertificates);
10871253
registry->Register(GetSystemCACertificates);
10881254
registry->Register(GetExtraCACertificates);
1255+
registry->Register(ResetRootCertStore);
1256+
registry->Register(GetUserRootCertificates);
10891257
}
10901258

10911259
SecureContext* SecureContext::Create(Environment* env) {

0 commit comments

Comments
 (0)