Skip to content

Commit 61b1f13

Browse files
committed
crypto: add extra CA certs to all secure contexts
Store loaded NODE_EXTRA_CA_CERTS into root_certs_vector, allowing them to be added to secure contexts when NewRootCertStore() is called. When NODE_EXTRA_CA_CERTS is specified, the root certificates (both bundled and extra) will no longer be preloaded at startup. This improves Node.js startup time and makes the behavior of NODE_EXTRA_CA_CERTS consistent with the default behavior when NODE_EXTRA_CA_CERTS is omitted. The original reason NODE_EXTRA_CA_CERTS were loaded at startup (issues nodejs#20432, nodejs#20434) was to prevent the environment variable from being changed at runtime. This change preserves the runtime consistency without actually having to load the certs at startup. Fixes: nodejs#32010 Refs: nodejs#40524 Refs: nodejs#23354
1 parent 449529d commit 61b1f13

File tree

4 files changed

+141
-117
lines changed

4 files changed

+141
-117
lines changed

src/crypto/crypto_context.cc

+59-71
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ static const char* const root_certs[] = {
5252

5353
static const char system_cert_path[] = NODE_OPENSSL_SYSTEM_CERT_PATH;
5454

55-
static bool extra_root_certs_loaded = false;
55+
static std::string extra_root_certs_file; // NOLINT(runtime/string)
5656

5757
X509_STORE* GetOrCreateRootCertStore() {
5858
// Guaranteed thread-safe by standard, just don't use -fno-threadsafe-statics.
@@ -197,26 +197,66 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
197197
issuer);
198198
}
199199

200+
unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
201+
std::vector<X509*>* certs,
202+
const char* file) {
203+
MarkPopErrorOnReturn mark_pop_error_on_return;
204+
205+
BIOPointer bio(BIO_new_file(file, "r"));
206+
if (!bio) return ERR_get_error();
207+
208+
while (X509* x509 = PEM_read_bio_X509(
209+
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
210+
certs->push_back(x509);
211+
}
212+
213+
unsigned long err = ERR_peek_last_error(); // NOLINT(runtime/int)
214+
// Ignore error if its EOF/no start line found.
215+
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
216+
ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
217+
return 0;
218+
} else {
219+
return err;
220+
}
221+
}
222+
200223
X509_STORE* NewRootCertStore() {
201224
static std::vector<X509*> root_certs_vector;
225+
static bool root_certs_vector_loaded = false;
202226
static Mutex root_certs_vector_mutex;
203227
Mutex::ScopedLock lock(root_certs_vector_mutex);
204228

205-
if (root_certs_vector.empty() &&
206-
per_process::cli_options->ssl_openssl_cert_store == false) {
207-
for (size_t i = 0; i < arraysize(root_certs); i++) {
208-
X509* x509 =
209-
PEM_read_bio_X509(NodeBIO::NewFixed(root_certs[i],
210-
strlen(root_certs[i])).get(),
211-
nullptr, // no re-use of X509 structure
212-
NoPasswordCallback,
213-
nullptr); // no callback data
229+
if (!root_certs_vector_loaded) {
230+
if (per_process::cli_options->ssl_openssl_cert_store == false) {
231+
for (size_t i = 0; i < arraysize(root_certs); i++) {
232+
X509* x509 = PEM_read_bio_X509(
233+
NodeBIO::NewFixed(root_certs[i], strlen(root_certs[i])).get(),
234+
nullptr, // no re-use of X509 structure
235+
NoPasswordCallback,
236+
nullptr); // no callback data
237+
238+
// Parse errors from the built-in roots are fatal.
239+
CHECK_NOT_NULL(x509);
214240

215-
// Parse errors from the built-in roots are fatal.
216-
CHECK_NOT_NULL(x509);
241+
root_certs_vector.push_back(x509);
242+
}
243+
}
217244

218-
root_certs_vector.push_back(x509);
245+
if (!extra_root_certs_file.empty()) {
246+
unsigned long err = LoadCertsFromFile( // NOLINT(runtime/int)
247+
&root_certs_vector,
248+
extra_root_certs_file.c_str());
249+
if (err) {
250+
char buf[256];
251+
ERR_error_string_n(err, buf, sizeof(buf));
252+
fprintf(stderr,
253+
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
254+
extra_root_certs_file.c_str(),
255+
buf);
256+
}
219257
}
258+
259+
root_certs_vector_loaded = true;
220260
}
221261

222262
X509_STORE* store = X509_STORE_new();
@@ -228,12 +268,11 @@ X509_STORE* NewRootCertStore() {
228268

229269
Mutex::ScopedLock cli_lock(node::per_process::cli_options_mutex);
230270
if (per_process::cli_options->ssl_openssl_cert_store) {
231-
X509_STORE_set_default_paths(store);
232-
} else {
233-
for (X509* cert : root_certs_vector) {
234-
X509_up_ref(cert);
235-
X509_STORE_add_cert(store, cert);
236-
}
271+
CHECK_EQ(1, X509_STORE_set_default_paths(store));
272+
}
273+
274+
for (X509* cert : root_certs_vector) {
275+
CHECK_EQ(1, X509_STORE_add_cert(store, cert));
237276
}
238277

239278
return store;
@@ -339,11 +378,6 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
339378

340379
SetMethodNoSideEffect(
341380
context, target, "getRootCertificates", GetRootCertificates);
342-
// Exposed for testing purposes only.
343-
SetMethodNoSideEffect(context,
344-
target,
345-
"isExtraRootCertsFileLoaded",
346-
IsExtraRootCertsFileLoaded);
347381
}
348382

349383
void SecureContext::RegisterExternalReferences(
@@ -383,7 +417,6 @@ void SecureContext::RegisterExternalReferences(
383417
registry->Register(CtxGetter);
384418

385419
registry->Register(GetRootCertificates);
386-
registry->Register(IsExtraRootCertsFileLoaded);
387420
}
388421

389422
SecureContext* SecureContext::Create(Environment* env) {
@@ -1383,54 +1416,9 @@ void SecureContext::GetCertificate(const FunctionCallbackInfo<Value>& args) {
13831416
args.GetReturnValue().Set(buff);
13841417
}
13851418

1386-
namespace {
1387-
unsigned long AddCertsFromFile( // NOLINT(runtime/int)
1388-
X509_STORE* store,
1389-
const char* file) {
1390-
ERR_clear_error();
1391-
MarkPopErrorOnReturn mark_pop_error_on_return;
1392-
1393-
BIOPointer bio(BIO_new_file(file, "r"));
1394-
if (!bio)
1395-
return ERR_get_error();
1396-
1397-
while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509(
1398-
bio.get(), nullptr, NoPasswordCallback, nullptr))) {
1399-
X509_STORE_add_cert(store, x509.get());
1400-
}
1401-
1402-
unsigned long err = ERR_peek_error(); // NOLINT(runtime/int)
1403-
// Ignore error if its EOF/no start line found.
1404-
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
1405-
ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
1406-
return 0;
1407-
}
1408-
1409-
return err;
1410-
}
1411-
} // namespace
1412-
14131419
// UseExtraCaCerts is called only once at the start of the Node.js process.
14141420
void UseExtraCaCerts(const std::string& file) {
1415-
if (file.empty()) return;
1416-
ClearErrorOnReturn clear_error_on_return;
1417-
X509_STORE* store = GetOrCreateRootCertStore();
1418-
if (auto err = AddCertsFromFile(store, file.c_str())) {
1419-
char buf[256];
1420-
ERR_error_string_n(err, buf, sizeof(buf));
1421-
fprintf(stderr,
1422-
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
1423-
file.c_str(),
1424-
buf);
1425-
} else {
1426-
extra_root_certs_loaded = true;
1427-
}
1428-
}
1429-
1430-
// Exposed to JavaScript strictly for testing purposes.
1431-
void IsExtraRootCertsFileLoaded(
1432-
const FunctionCallbackInfo<Value>& args) {
1433-
return args.GetReturnValue().Set(extra_root_certs_loaded);
1421+
extra_root_certs_file = file;
14341422
}
14351423

14361424
} // namespace crypto

src/crypto/crypto_context.h

-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ constexpr int kMaxSupportedVersion = TLS1_3_VERSION;
1919
void GetRootCertificates(
2020
const v8::FunctionCallbackInfo<v8::Value>& args);
2121

22-
void IsExtraRootCertsFileLoaded(
23-
const v8::FunctionCallbackInfo<v8::Value>& args);
24-
2522
X509_STORE* NewRootCertStore();
2623

2724
X509_STORE* GetOrCreateRootCertStore();

test/parallel/test-tls-env-extra-ca-file-load.js

-43
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const assert = require('node:assert');
9+
const tls = require('node:tls');
10+
const { fork } = require('node:child_process');
11+
const fixtures = require('../common/fixtures');
12+
13+
const tests = [
14+
{
15+
get clientOptions() {
16+
const secureContext = tls.createSecureContext();
17+
secureContext.context.addCACert(
18+
fixtures.readKey('ca1-cert.pem')
19+
);
20+
21+
return {
22+
secureContext
23+
};
24+
}
25+
},
26+
{
27+
clientOptions: {
28+
crl: fixtures.readKey('ca2-crl.pem')
29+
}
30+
},
31+
{
32+
clientOptions: {
33+
pfx: fixtures.readKey('agent1.pfx'),
34+
passphrase: 'sample'
35+
}
36+
},
37+
];
38+
39+
if (process.argv[2]) {
40+
const testNumber = parseInt(process.argv[2], 10);
41+
assert(testNumber >= 0 && testNumber < tests.length);
42+
43+
const test = tests[testNumber];
44+
45+
const clientOptions = {
46+
...test.clientOptions,
47+
port: process.argv[3],
48+
checkServerIdentity: common.mustCall()
49+
};
50+
51+
const client = tls.connect(clientOptions, common.mustCall(() => {
52+
client.end('hi');
53+
}));
54+
} else {
55+
const serverOptions = {
56+
key: fixtures.readKey('agent3-key.pem'),
57+
cert: fixtures.readKey('agent3-cert.pem')
58+
};
59+
60+
for (const testNumber in tests) {
61+
const server = tls.createServer(serverOptions, common.mustCall((socket) => {
62+
socket.end('bye');
63+
server.close();
64+
}));
65+
66+
server.listen(0, common.mustCall(() => {
67+
const env = {
68+
...process.env,
69+
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'ca2-cert.pem')
70+
};
71+
72+
const args = [
73+
testNumber,
74+
server.address().port,
75+
];
76+
77+
fork(__filename, args, { env }).on('exit', common.mustCall((status) => {
78+
assert.strictEqual(status, 0);
79+
}));
80+
}));
81+
}
82+
}

0 commit comments

Comments
 (0)