Skip to content

Commit 314bada

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 #20432, #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: #32010 Refs: #40524 Refs: #23354
1 parent 03553c5 commit 314bada

7 files changed

+231
-132
lines changed

src/crypto/crypto_context.cc

Lines changed: 67 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "node.h"
99
#include "node_buffer.h"
1010
#include "node_options.h"
11+
#include "node_process-inl.h"
1112
#include "util.h"
1213
#include "v8.h"
1314

@@ -49,7 +50,7 @@ static const char system_cert_path[] = NODE_OPENSSL_SYSTEM_CERT_PATH;
4950

5051
static X509_STORE* root_cert_store;
5152

52-
static bool extra_root_certs_loaded = false;
53+
static std::string extra_root_certs_file; // NOLINT(runtime/string)
5354

5455
// Takes a string or buffer and loads it into a BIO.
5556
// Caller responsible for BIO_free_all-ing the returned object.
@@ -188,28 +189,69 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
188189
issuer);
189190
}
190191

192+
unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
193+
std::vector<X509*>* certs,
194+
const char* file) {
195+
ClearErrorOnReturn clear_error_on_return;
196+
197+
BIOPointer bio(BIO_new_file(file, "r"));
198+
if (!bio)
199+
return ERR_get_error();
200+
201+
while (X509* x509 =
202+
PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr)) {
203+
certs->push_back(x509);
204+
}
205+
206+
unsigned long err = ERR_peek_error(); // NOLINT(runtime/int)
207+
// Ignore error if its EOF/no start line found.
208+
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
209+
ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
210+
return 0;
211+
}
212+
213+
return err;
214+
}
215+
191216
} // namespace
192217

193-
X509_STORE* NewRootCertStore() {
218+
X509_STORE* NewRootCertStore(Environment* env) {
194219
static std::vector<X509*> root_certs_vector;
195-
static Mutex root_certs_vector_mutex;
220+
static bool root_certs_vector_loaded = false;
221+
static Mutex root_certs_vector_mutex;
196222
Mutex::ScopedLock lock(root_certs_vector_mutex);
197223

198-
if (root_certs_vector.empty() &&
199-
per_process::cli_options->ssl_openssl_cert_store == false) {
200-
for (size_t i = 0; i < arraysize(root_certs); i++) {
201-
X509* x509 =
202-
PEM_read_bio_X509(NodeBIO::NewFixed(root_certs[i],
203-
strlen(root_certs[i])).get(),
204-
nullptr, // no re-use of X509 structure
205-
NoPasswordCallback,
206-
nullptr); // no callback data
224+
if (!root_certs_vector_loaded) {
225+
if (per_process::cli_options->ssl_openssl_cert_store == false) {
226+
for (size_t i = 0; i < arraysize(root_certs); i++) {
227+
X509* x509 =
228+
PEM_read_bio_X509(NodeBIO::NewFixed(root_certs[i],
229+
strlen(root_certs[i])).get(),
230+
nullptr, // no re-use of X509 structure
231+
NoPasswordCallback,
232+
nullptr); // no callback data
207233

208-
// Parse errors from the built-in roots are fatal.
209-
CHECK_NOT_NULL(x509);
234+
// Parse errors from the built-in roots are fatal.
235+
CHECK_NOT_NULL(x509);
210236

211-
root_certs_vector.push_back(x509);
237+
root_certs_vector.push_back(x509);
238+
}
212239
}
240+
241+
if (!extra_root_certs_file.empty()) {
242+
unsigned long err = LoadCertsFromFile( // NOLINT(runtime/int)
243+
&root_certs_vector,
244+
extra_root_certs_file.c_str());
245+
if (err) {
246+
ProcessEmitWarning(
247+
env,
248+
"Ignoring extra certs from `%s`, load failed: %s\n",
249+
extra_root_certs_file.c_str(),
250+
ERR_error_string(err, nullptr));
251+
}
252+
}
253+
254+
root_certs_vector_loaded = true;
213255
}
214256

215257
X509_STORE* store = X509_STORE_new();
@@ -222,11 +264,10 @@ X509_STORE* NewRootCertStore() {
222264
Mutex::ScopedLock cli_lock(node::per_process::cli_options_mutex);
223265
if (per_process::cli_options->ssl_openssl_cert_store) {
224266
X509_STORE_set_default_paths(store);
225-
} else {
226-
for (X509* cert : root_certs_vector) {
227-
X509_up_ref(cert);
228-
X509_STORE_add_cert(store, cert);
229-
}
267+
}
268+
269+
for (X509* cert : root_certs_vector) {
270+
X509_STORE_add_cert(store, cert);
230271
}
231272

232273
return store;
@@ -333,11 +374,6 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
333374

334375
SetMethodNoSideEffect(
335376
context, target, "getRootCertificates", GetRootCertificates);
336-
// Exposed for testing purposes only.
337-
SetMethodNoSideEffect(context,
338-
target,
339-
"isExtraRootCertsFileLoaded",
340-
IsExtraRootCertsFileLoaded);
341377
}
342378

343379
void SecureContext::RegisterExternalReferences(
@@ -377,7 +413,6 @@ void SecureContext::RegisterExternalReferences(
377413
registry->Register(CtxGetter);
378414

379415
registry->Register(GetRootCertificates);
380-
registry->Register(IsExtraRootCertsFileLoaded);
381416
}
382417

383418
SecureContext* SecureContext::Create(Environment* env) {
@@ -702,7 +737,7 @@ void SecureContext::AddCACert(const FunctionCallbackInfo<Value>& args) {
702737
while (X509* x509 = PEM_read_bio_X509_AUX(
703738
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
704739
if (cert_store == root_cert_store) {
705-
cert_store = NewRootCertStore();
740+
cert_store = NewRootCertStore(env);
706741
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
707742
}
708743
X509_STORE_add_cert(cert_store, x509);
@@ -733,7 +768,7 @@ void SecureContext::AddCRL(const FunctionCallbackInfo<Value>& args) {
733768

734769
X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get());
735770
if (cert_store == root_cert_store) {
736-
cert_store = NewRootCertStore();
771+
cert_store = NewRootCertStore(env);
737772
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
738773
}
739774

@@ -748,7 +783,8 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
748783
ClearErrorOnReturn clear_error_on_return;
749784

750785
if (root_cert_store == nullptr) {
751-
root_cert_store = NewRootCertStore();
786+
Environment* env = Environment::GetCurrent(args);
787+
root_cert_store = NewRootCertStore(env);
752788
}
753789

754790
// Increment reference count so global store is not deleted along with CTX.
@@ -1027,7 +1063,7 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
10271063
X509* ca = sk_X509_value(extra_certs.get(), i);
10281064

10291065
if (cert_store == root_cert_store) {
1030-
cert_store = NewRootCertStore();
1066+
cert_store = NewRootCertStore(env);
10311067
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
10321068
}
10331069
X509_STORE_add_cert(cert_store, ca);
@@ -1297,61 +1333,9 @@ void SecureContext::GetCertificate(const FunctionCallbackInfo<Value>& args) {
12971333
args.GetReturnValue().Set(buff);
12981334
}
12991335

1300-
namespace {
1301-
unsigned long AddCertsFromFile( // NOLINT(runtime/int)
1302-
X509_STORE* store,
1303-
const char* file) {
1304-
ERR_clear_error();
1305-
MarkPopErrorOnReturn mark_pop_error_on_return;
1306-
1307-
BIOPointer bio(BIO_new_file(file, "r"));
1308-
if (!bio)
1309-
return ERR_get_error();
1310-
1311-
while (X509* x509 =
1312-
PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr)) {
1313-
X509_STORE_add_cert(store, x509);
1314-
X509_free(x509);
1315-
}
1316-
1317-
unsigned long err = ERR_peek_error(); // NOLINT(runtime/int)
1318-
// Ignore error if its EOF/no start line found.
1319-
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
1320-
ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
1321-
return 0;
1322-
}
1323-
1324-
return err;
1325-
}
1326-
} // namespace
1327-
13281336
// UseExtraCaCerts is called only once at the start of the Node.js process.
13291337
void UseExtraCaCerts(const std::string& file) {
1330-
ClearErrorOnReturn clear_error_on_return;
1331-
1332-
if (root_cert_store == nullptr) {
1333-
root_cert_store = NewRootCertStore();
1334-
1335-
if (!file.empty()) {
1336-
unsigned long err = AddCertsFromFile( // NOLINT(runtime/int)
1337-
root_cert_store,
1338-
file.c_str());
1339-
if (err) {
1340-
fprintf(stderr,
1341-
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
1342-
file.c_str(),
1343-
ERR_error_string(err, nullptr));
1344-
} else {
1345-
extra_root_certs_loaded = true;
1346-
}
1347-
}
1348-
}
1349-
}
1350-
1351-
// Exposed to JavaScript strictly for testing purposes.
1352-
void IsExtraRootCertsFileLoaded(
1353-
const FunctionCallbackInfo<Value>& args) {
1354-
return args.GetReturnValue().Set(extra_root_certs_loaded);
1338+
extra_root_certs_file = file;
13551339
}
13561340

13571341
} // namespace crypto

src/crypto/crypto_context.h

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ constexpr int kMaxSupportedVersion = TLS1_3_VERSION;
1818
void GetRootCertificates(
1919
const v8::FunctionCallbackInfo<v8::Value>& args);
2020

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

2623
BIOPointer LoadBIO(Environment* env, v8::Local<v8::Value> v);
2724

test/cctest/test_node_crypto.cc

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@
66
#include "node_options.h"
77
#include "openssl/err.h"
88
#include "gtest/gtest.h"
9+
#include "node_test_fixture.h"
10+
11+
class NodeCryptoTest : public EnvironmentTestFixture {};
912

1013
/*
1114
* This test verifies that a call to NewRootCertDir with the build time
1215
* configuration option --openssl-system-ca-path set to an missing file, will
1316
* not leave any OpenSSL errors on the OpenSSL error stack.
1417
* See https://github.com/nodejs/node/issues/35456 for details.
1518
*/
16-
TEST(NodeCrypto, NewRootCertStore) {
19+
TEST_F(NodeCryptoTest, NewRootCertStore) {
20+
const v8::HandleScope handle_scope(isolate_);
21+
Argv argv;
22+
23+
Env test_env{handle_scope, argv};
24+
25+
node::Environment* env = *test_env;
26+
node::LoadEnvironment(env, "");
27+
1728
node::per_process::cli_options->ssl_openssl_cert_store = true;
18-
X509_STORE* store = node::crypto::NewRootCertStore();
29+
X509_STORE* store = node::crypto::NewRootCertStore(env);
1930
ASSERT_TRUE(store);
2031
ASSERT_EQ(ERR_peek_error(), 0UL) << "NewRootCertStore should not have left "
2132
"any errors on the OpenSSL error stack\n";

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

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const assert = require('assert');
9+
const tls = require('tls');
10+
const fixtures = require('../common/fixtures');
11+
12+
const { fork } = require('child_process');
13+
14+
if (process.env.CHILD) {
15+
const copts = {
16+
port: process.env.PORT,
17+
checkServerIdentity: common.mustCall()
18+
};
19+
20+
// New secure contexts have the well-known root CAs.
21+
copts.secureContext = tls.createSecureContext();
22+
23+
// Explicit calls to addCACert() add to the root certificates,
24+
// instead of replacing, so connection still succeeds.
25+
copts.secureContext.context.addCACert(
26+
fixtures.readKey('ca1-cert.pem')
27+
);
28+
29+
const client = tls.connect(copts, common.mustCall(() => {
30+
client.end('hi');
31+
}));
32+
33+
return;
34+
}
35+
36+
const options = {
37+
key: fixtures.readKey('agent3-key.pem'),
38+
cert: fixtures.readKey('agent3-cert.pem')
39+
};
40+
41+
const server = tls.createServer(options, common.mustCall((socket) => {
42+
socket.end('bye');
43+
server.close();
44+
})).listen(0, common.mustCall(() => {
45+
const env = Object.assign({}, process.env, {
46+
CHILD: 'yes',
47+
PORT: server.address().port,
48+
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'ca2-cert.pem')
49+
});
50+
51+
fork(__filename, { env }).on('exit', common.mustCall((status) => {
52+
// Client did not succeed in connecting
53+
assert.strictEqual(status, 0);
54+
}));
55+
}));

0 commit comments

Comments
 (0)