Skip to content

Commit bbd77e1

Browse files
committed
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.
1 parent 049664b commit bbd77e1

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
@@ -2260,6 +2260,54 @@ openssl pkcs12 -certpbe AES-256-CBC -export -out client-cert.pem \
22602260
The server can be tested by connecting to it using the example client from
22612261
[`tls.connect()`][].
22622262

2263+
## `tls.setDefaultCACertificates(certs)`
2264+
2265+
<!-- YAML
2266+
added: REPLACEME
2267+
-->
2268+
2269+
* `certs` {string\[]|ArrayBufferView\[]} An array of CA certificates in PEM format.
2270+
2271+
Sets the default CA certificates used by Node.js TLS clients. If the provided
2272+
certificates are parsed successfully, they will become the default CA
2273+
certificate list returned by [`tls.getCACertificates()`][] and used
2274+
by subsequent TLS connections that don't specify their own CA certificates.
2275+
The certificates will be deduplicated before being set as the default.
2276+
2277+
This function only affects the current Node.js thread. Previous
2278+
sessions cached by the HTTPS agent won't be affected by this change, so
2279+
this method should be called before any unwanted cachable TLS connections are
2280+
made.
2281+
2282+
To use system CA certificates as the default:
2283+
2284+
```cjs
2285+
const tls = require('node:tls');
2286+
tls.setDefaultCACertificates(tls.getCACertificates('system'));
2287+
```
2288+
2289+
```mjs
2290+
import tls from 'node:tls';
2291+
tls.setDefaultCACertificates(tls.getCACertificates('system'));
2292+
```
2293+
2294+
This function completely replaces the default CA certificate list. To add additional
2295+
certificates to the existing defaults, get the current certificates and append to them:
2296+
2297+
```cjs
2298+
const tls = require('node:tls');
2299+
const currentCerts = tls.getCACertificates('default');
2300+
const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...'];
2301+
tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]);
2302+
```
2303+
2304+
```mjs
2305+
import tls from 'node:tls';
2306+
const currentCerts = tls.getCACertificates('default');
2307+
const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...'];
2308+
tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]);
2309+
```
2310+
22632311
## `tls.getCACertificates([type])`
22642312

22652313
<!-- 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');
@@ -122,8 +125,17 @@ function cacheSystemCACertificates() {
122125
}
123126

124127
let defaultCACertificates;
128+
let hasResetDefaultCACertificates = false;
129+
125130
function cacheDefaultCACertificates() {
126131
if (defaultCACertificates) { return defaultCACertificates; }
132+
133+
if (hasResetDefaultCACertificates) {
134+
defaultCACertificates = getUserRootCertificates();
135+
ObjectFreeze(defaultCACertificates);
136+
return defaultCACertificates;
137+
}
138+
127139
defaultCACertificates = [];
128140

129141
if (!getOptionValue('--use-openssl-ca')) {
@@ -171,6 +183,26 @@ function getCACertificates(type = 'default') {
171183
}
172184
exports.getCACertificates = getCACertificates;
173185

186+
function setDefaultCACertificates(certs) {
187+
if (!ArrayIsArray(certs)) {
188+
throw new ERR_INVALID_ARG_TYPE('certs', 'Array', certs);
189+
}
190+
191+
// Verify that all elements in the array are strings
192+
for (let i = 0; i < certs.length; i++) {
193+
if (typeof certs[i] !== 'string' && !isArrayBufferView(certs[i])) {
194+
throw new ERR_INVALID_ARG_TYPE(
195+
`certs[${i}]`, ['string', 'ArrayBufferView'], certs[i]);
196+
}
197+
}
198+
199+
resetRootCertStore(certs);
200+
defaultCACertificates = undefined; // Reset the cached default certificates
201+
hasResetDefaultCACertificates = true;
202+
}
203+
204+
exports.setDefaultCACertificates = setDefaultCACertificates;
205+
174206
// Convert protocols array into valid OpenSSL protocols list
175207
// ("\x06spdy/2\x08http/1.1\x08http/1.0")
176208
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;
@@ -83,10 +85,28 @@ static std::atomic<bool> has_cached_bundled_root_certs{false};
8385
static std::atomic<bool> has_cached_system_root_certs{false};
8486
static std::atomic<bool> has_cached_extra_root_certs{false};
8587

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

92112
// Takes a string or buffer and loads it into a BIO.
@@ -227,14 +247,11 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
227247
issuer);
228248
}
229249

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

235-
auto bio = BIOPointer::NewFile(file, "r");
236-
if (!bio) return ERR_get_error();
237-
238255
while (X509* x509 = PEM_read_bio_X509(
239256
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
240257
certs->push_back(x509);
@@ -250,6 +267,17 @@ static unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
250267
}
251268
}
252269

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

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

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

911-
LocalVector<Value> result(env->isolate(), certs.size());
912-
for (size_t i = 0; i < certs.size(); ++i) {
913-
X509View view(certs[i]);
994+
LocalVector<Value> result(env->isolate(), size);
995+
size_t i = 0;
996+
for (It cur = first; cur != last; ++cur, ++i) {
997+
X509View view(*cur);
914998
auto pem_bio = view.toPEM();
915999
if (!pem_bio) {
9161000
ThrowCryptoError(env, ERR_get_error(), "X509 to PEM conversion");
@@ -935,10 +1019,87 @@ MaybeLocal<Array> X509sToArrayOfStrings(Environment* env,
9351019
return scope.Escape(Array::New(env->isolate(), result.data(), result.size()));
9361020
}
9371021

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

10511217
void SecureContext::RegisterExternalReferences(
@@ -1088,6 +1254,8 @@ void SecureContext::RegisterExternalReferences(
10881254
registry->Register(GetBundledRootCertificates);
10891255
registry->Register(GetSystemCACertificates);
10901256
registry->Register(GetExtraCACertificates);
1257+
registry->Register(ResetRootCertStore);
1258+
registry->Register(GetUserRootCertificates);
10911259
}
10921260

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

0 commit comments

Comments
 (0)