Skip to content

Commit b9e0b7a

Browse files
committed
tls: allow tls.rootCertificates to be set
Modifies the tls.rootCertificates property so that it is settable. PEM-formatted certificates set to the property are loaded into a new Node.js root certificate store that is used for all subsequent TLS requests and peer certificate validation. Allows full programmatic control of a Node.js process' root certificate store. Fixes: nodejs#20432 Refs: nodejs#27079 Changed root_cert_store to a smart pointer Added support for empty root certificates Added comment
1 parent 55a13f8 commit b9e0b7a

File tree

5 files changed

+229
-53
lines changed

5 files changed

+229
-53
lines changed

doc/api/tls.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,6 +1775,20 @@ An immutable array of strings representing the root certificates (in PEM format)
17751775
used for verifying peer certificates. This is the default value of the `ca`
17761776
option to [`tls.createSecureContext()`][].
17771777

1778+
`tls.rootCertificates` can be assigned a new value to change the root
1779+
certificates used used for all subsequent peer certificate verifications.
1780+
Existing secure connections are not affected.
1781+
1782+
The following illustrates adding an extra certificate to the default set of
1783+
well known "root" CAs:
1784+
1785+
```sh
1786+
tls.rootCertificates = [
1787+
...tls.rootCertificates,
1788+
fs.readFileSync('custom-ca.pem')
1789+
];
1790+
```
1791+
17781792
## `tls.DEFAULT_ECDH_CURVE`
17791793
<!-- YAML
17801794
added: v0.11.13

lib/tls.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const {
2929
} = primordials;
3030

3131
const {
32+
ERR_INVALID_ARG_TYPE,
3233
ERR_TLS_CERT_ALTNAME_INVALID,
3334
ERR_OUT_OF_RANGE
3435
} = require('internal/errors').codes;
@@ -40,7 +41,11 @@ const { isArrayBufferView } = require('internal/util/types');
4041
const net = require('net');
4142
const { getOptionValue } = require('internal/options');
4243
const url = require('url');
43-
const { getRootCertificates, getSSLCiphers } = internalBinding('crypto');
44+
const {
45+
getRootCertificates,
46+
setRootCertificates,
47+
getSSLCiphers
48+
} = internalBinding('crypto');
4449
const { Buffer } = require('buffer');
4550
const EventEmitter = require('events');
4651
const { URL } = require('internal/url');
@@ -84,7 +89,7 @@ exports.getCiphers = internalUtil.cachedResult(
8489
() => internalUtil.filterDuplicateStrings(getSSLCiphers(), true)
8590
);
8691

87-
let rootCertificates;
92+
let rootCertificates = null;
8893

8994
function cacheRootCertificates() {
9095
rootCertificates = ObjectFreeze(getRootCertificates());
@@ -98,6 +103,19 @@ ObjectDefineProperty(exports, 'rootCertificates', {
98103
if (!rootCertificates) cacheRootCertificates();
99104
return rootCertificates;
100105
},
106+
set: (value) => {
107+
if (!ArrayIsArray(value))
108+
throw new ERR_INVALID_ARG_TYPE('value', 'Array', value);
109+
for (let i = 0; i < value.length; i++) {
110+
if (typeof value[i] !== 'string')
111+
throw new ERR_INVALID_ARG_TYPE(`value[${i}]`, 'String', value[i]);
112+
}
113+
114+
setRootCertificates(value);
115+
116+
// Clear root certificate cache.
117+
rootCertificates = null;
118+
}
101119
});
102120

103121
// Convert protocols array into valid OpenSSL protocols list

src/node_crypto.cc

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,11 @@ static const char* const root_certs[] = {
111111

112112
static const char system_cert_path[] = NODE_OPENSSL_SYSTEM_CERT_PATH;
113113

114-
static X509_STORE* root_cert_store;
115-
static std::vector<X509*> root_certs_vector;
114+
static X509StorePointer root_cert_store;
115+
116+
static std::vector<X509Pointer> root_certs_vector;
116117
static Mutex root_certs_vector_mutex;
118+
static bool root_certs_vector_loaded = false;
117119

118120
static bool extra_root_certs_loaded = false;
119121

@@ -973,37 +975,39 @@ void SecureContext::SetCert(const FunctionCallbackInfo<Value>& args) {
973975
static void EnsureRootCerts() {
974976
Mutex::ScopedLock lock(root_certs_vector_mutex);
975977

976-
if (root_certs_vector.empty()) {
978+
if (!root_certs_vector_loaded) {
977979
for (size_t i = 0; i < arraysize(root_certs); i++) {
978-
X509* x509 =
980+
X509Pointer x509 = X509Pointer(
979981
PEM_read_bio_X509(NodeBIO::NewFixed(root_certs[i],
980982
strlen(root_certs[i])).get(),
981983
nullptr, // no re-use of X509 structure
982984
NoPasswordCallback,
983-
nullptr); // no callback data
985+
nullptr)); // no callback data
984986

985987
// Parse errors from the built-in roots are fatal.
986988
CHECK_NOT_NULL(x509);
987989

988-
root_certs_vector.push_back(x509);
990+
root_certs_vector.push_back(std::move(x509));
989991
}
992+
993+
root_certs_vector_loaded = true;
990994
}
991995
}
992996

993997

994-
static X509_STORE* NewRootCertStore() {
998+
static X509StorePointer NewRootCertStore() {
995999
EnsureRootCerts();
9961000

997-
X509_STORE* store = X509_STORE_new();
1001+
X509StorePointer store = X509StorePointer(X509_STORE_new());
9981002
if (*system_cert_path != '\0') {
999-
X509_STORE_load_locations(store, system_cert_path, nullptr);
1003+
X509_STORE_load_locations(store.get(), system_cert_path, nullptr);
10001004
}
10011005
if (per_process::cli_options->ssl_openssl_cert_store) {
1002-
X509_STORE_set_default_paths(store);
1006+
X509_STORE_set_default_paths(store.get());
10031007
} else {
10041008
Mutex::ScopedLock lock(root_certs_vector_mutex);
1005-
for (X509* cert : root_certs_vector) {
1006-
X509_STORE_add_cert(store, cert);
1009+
for (X509Pointer& cert : root_certs_vector) {
1010+
X509_STORE_add_cert(store.get(), cert.get());
10071011
}
10081012
}
10091013

@@ -1027,15 +1031,14 @@ void SecureContext::AddCACert(const FunctionCallbackInfo<Value>& args) {
10271031
return;
10281032

10291033
X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get());
1030-
while (X509* x509 = PEM_read_bio_X509_AUX(
1031-
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
1032-
if (cert_store == root_cert_store) {
1033-
cert_store = NewRootCertStore();
1034+
while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509_AUX(
1035+
bio.get(), nullptr, NoPasswordCallback, nullptr))) {
1036+
if (cert_store == root_cert_store.get()) {
1037+
cert_store = NewRootCertStore().release();
10341038
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
10351039
}
1036-
X509_STORE_add_cert(cert_store, x509);
1037-
SSL_CTX_add_client_CA(sc->ctx_.get(), x509);
1038-
X509_free(x509);
1040+
X509_STORE_add_cert(cert_store, x509.get());
1041+
SSL_CTX_add_client_CA(sc->ctx_.get(), x509.get());
10391042
}
10401043
}
10411044

@@ -1063,8 +1066,8 @@ void SecureContext::AddCRL(const FunctionCallbackInfo<Value>& args) {
10631066
return env->ThrowError("Failed to parse CRL");
10641067

10651068
X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get());
1066-
if (cert_store == root_cert_store) {
1067-
cert_store = NewRootCertStore();
1069+
if (cert_store == root_cert_store.get()) {
1070+
cert_store = NewRootCertStore().release();
10681071
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
10691072
}
10701073

@@ -1086,10 +1089,10 @@ static unsigned long AddRootCertsFromFile( // NOLINT(runtime/int)
10861089
// Scope for root_certs_vector lock
10871090
{
10881091
Mutex::ScopedLock lock(root_certs_vector_mutex);
1089-
while (X509* x509 =
1090-
PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr)) {
1091-
X509_STORE_add_cert(root_cert_store, x509);
1092-
root_certs_vector.push_back(x509);
1092+
while (X509Pointer x509 = X509Pointer(
1093+
PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr))) {
1094+
X509_STORE_add_cert(root_cert_store.get(), x509.get());
1095+
root_certs_vector.push_back(std::move(x509));
10931096
}
10941097
}
10951098

@@ -1142,8 +1145,8 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
11421145
}
11431146

11441147
// Increment reference count so global store is not deleted along with CTX.
1145-
X509_STORE_up_ref(root_cert_store);
1146-
SSL_CTX_set_cert_store(sc->ctx_.get(), root_cert_store);
1148+
X509_STORE_up_ref(root_cert_store.get());
1149+
SSL_CTX_set_cert_store(sc->ctx_.get(), root_cert_store.get());
11471150
}
11481151

11491152

@@ -1442,8 +1445,8 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
14421445
for (int i = 0; i < sk_X509_num(extra_certs.get()); i++) {
14431446
X509* ca = sk_X509_value(extra_certs.get(), i);
14441447

1445-
if (cert_store == root_cert_store) {
1446-
cert_store = NewRootCertStore();
1448+
if (cert_store == root_cert_store.get()) {
1449+
cert_store = NewRootCertStore().release();
14471450
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
14481451
}
14491452
X509_STORE_add_cert(cert_store, ca);
@@ -6648,9 +6651,9 @@ void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {
66486651

66496652
result.reserve(root_certs_vector.size());
66506653

6651-
for (X509* cert : root_certs_vector) {
6654+
for (X509Pointer& cert : root_certs_vector) {
66526655
Local<Value> value;
6653-
if (!X509ToPEM(env, cert).ToLocal(&value))
6656+
if (!X509ToPEM(env, cert.get()).ToLocal(&value))
66546657
return;
66556658

66566659
result.push_back(value);
@@ -6662,6 +6665,55 @@ void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {
66626665
}
66636666

66646667

6668+
void SetRootCertificates(const FunctionCallbackInfo<Value>& args) {
6669+
Environment* env = Environment::GetCurrent(args);
6670+
6671+
// Single argument must be an array of PEM-formatted strings.
6672+
CHECK_EQ(args.Length(), 1);
6673+
CHECK(args[0]->IsArray());
6674+
Local<Array> arr = args[0].As<Array>();
6675+
size_t len = arr->Length();
6676+
6677+
ERR_clear_error();
6678+
ClearErrorOnReturn clear_error_on_return;
6679+
6680+
std::vector<X509Pointer> certs;
6681+
certs.reserve(len);
6682+
6683+
for (size_t i = 0; i < len; i++) {
6684+
// All array values must be strings.
6685+
Local<Value> value;
6686+
if (!arr->Get(env->context(), i).ToLocal(&value)) return;
6687+
CHECK(value->IsString());
6688+
6689+
BIOPointer bio(LoadBIO(env, value));
6690+
if (!bio) return ThrowCryptoError(env, ERR_get_error(), "LoadBIO");
6691+
6692+
// Each string can contain multiple PEM-formatted certificates.
6693+
while (X509Pointer x509 = X509Pointer(
6694+
PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr))) {
6695+
certs.push_back(std::move(x509));
6696+
}
6697+
6698+
// Ignore error if its EOF/no start line found.
6699+
unsigned long err = ERR_peek_last_error(); // NOLINT(runtime/int)
6700+
if (ERR_GET_LIB(err) != ERR_LIB_PEM ||
6701+
ERR_GET_REASON(err) != PEM_R_NO_START_LINE) {
6702+
return ThrowCryptoError(env, err, "PEM_read_bio_X509");
6703+
}
6704+
}
6705+
6706+
// Scope for root_certs_vector lock
6707+
{
6708+
Mutex::ScopedLock lock(root_certs_vector_mutex);
6709+
root_certs_vector = std::move(certs);
6710+
}
6711+
6712+
// A new root_cert_store will be built on next use.
6713+
root_cert_store = nullptr;
6714+
}
6715+
6716+
66656717
// Convert the input public key to compressed, uncompressed, or hybrid formats.
66666718
void ConvertKey(const FunctionCallbackInfo<Value>& args) {
66676719
MarkPopErrorOnReturn mark_pop_error_on_return;
@@ -6878,6 +6930,7 @@ void Initialize(Local<Object> target,
68786930
env->SetMethodNoSideEffect(target, "certExportChallenge", ExportChallenge);
68796931
env->SetMethodNoSideEffect(target, "getRootCertificates",
68806932
GetRootCertificates);
6933+
env->SetMethod(target, "setRootCertificates", SetRootCertificates);
68816934
// Exposed for testing purposes only.
68826935
env->SetMethodNoSideEffect(target, "isExtraRootCertsFileLoaded",
68836936
IsExtraRootCertsFileLoaded);

src/node_crypto.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ struct MarkPopErrorOnReturn {
6060

6161
// Define smart pointers for the most commonly used OpenSSL types:
6262
using X509Pointer = DeleteFnPtr<X509, X509_free>;
63+
using X509StorePointer = DeleteFnPtr<X509_STORE, X509_STORE_free>;
6364
using BIOPointer = DeleteFnPtr<BIO, BIO_free_all>;
6465
using SSLCtxPointer = DeleteFnPtr<SSL_CTX, SSL_CTX_free>;
6566
using SSLSessionPointer = DeleteFnPtr<SSL_SESSION, SSL_SESSION_free>;

0 commit comments

Comments
 (0)