Skip to content

Commit 1595328

Browse files
sam-githubMylesBorins
authored andcommitted
crypto: allow adding extra certs to well-known CAs
In closed environments, self-signed or privately signed certificates are commonly used, and rejected by Node.js since their root CAs are not well-known. Allow extending the set of well-known compiled-in CAs via environment, so they can be set as a matter of policy. PR-URL: #9139 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Fedor Indutny <[email protected]>
1 parent 735119c commit 1595328

File tree

6 files changed

+153
-0
lines changed

6 files changed

+153
-0
lines changed

doc/api/cli.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@ Path to the file used to store the persistent REPL history. The default path is
225225
to an empty string (`""` or `" "`) disables persistent REPL history.
226226

227227

228+
### `NODE_EXTRA_CA_CERTS=file`
229+
<!-- YAML
230+
added: XXX
231+
-->
232+
233+
When set, the well known "root" CAs (like VeriSign) will be extended with the
234+
extra certificates in `file`. The file should consist of one or more trusted
235+
certificates in PEM format. A message will be printed to stderr (once)
236+
if the file is missing or
237+
misformatted, but any errors are otherwise ignored.
238+
239+
Note that neither the well known nor extra certificates are used when the `ca`
240+
options property is explicitly specified for a TLS or HTTPS client or server.
241+
242+
[emit_warning]: process.html#process_process_emitwarning_warning_name_ctor
228243
[Buffer]: buffer.html#buffer_buffer
229244
[debugger]: debugger.html
230245
[REPL]: repl.html

src/node.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4400,6 +4400,8 @@ int Start(int argc, char** argv) {
44004400
Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);
44014401

44024402
#if HAVE_OPENSSL
4403+
if (const char* extra = secure_getenv("NODE_EXTRA_CA_CERTS"))
4404+
crypto::UseExtraCaCerts(extra);
44034405
// V8 on Windows doesn't have a good source of entropy. Seed it from
44044406
// OpenSSL's pool.
44054407
V8::SetEntropySource(crypto::EntropySource);

src/node_crypto.cc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ const char* const root_certs[] = {
120120
#include "node_root_certs.h" // NOLINT(build/include_order)
121121
};
122122

123+
std::string extra_root_certs_file; // NOLINT(runtime/string)
124+
123125
X509_STORE* root_cert_store;
124126

125127
// Just to generate static methods
@@ -753,6 +755,38 @@ void SecureContext::AddCRL(const FunctionCallbackInfo<Value>& args) {
753755
}
754756

755757

758+
void UseExtraCaCerts(const std::string& file) {
759+
extra_root_certs_file = file;
760+
}
761+
762+
763+
static unsigned long AddCertsFromFile( // NOLINT(runtime/int)
764+
X509_STORE* store,
765+
const char* file) {
766+
ERR_clear_error();
767+
MarkPopErrorOnReturn mark_pop_error_on_return;
768+
769+
BIO* bio = BIO_new_file(file, "r");
770+
if (!bio) {
771+
return ERR_get_error();
772+
}
773+
774+
while (X509* x509 =
775+
PEM_read_bio_X509(bio, nullptr, CryptoPemCallback, nullptr)) {
776+
X509_STORE_add_cert(store, x509);
777+
X509_free(x509);
778+
}
779+
BIO_free_all(bio);
780+
781+
unsigned long err = ERR_peek_error(); // NOLINT(runtime/int)
782+
// Ignore error if its EOF/no start line found.
783+
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
784+
ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
785+
return 0;
786+
}
787+
788+
return err;
789+
}
756790

757791
void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
758792
SecureContext* sc;
@@ -782,6 +816,18 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
782816
BIO_free_all(bp);
783817
X509_free(x509);
784818
}
819+
820+
if (!extra_root_certs_file.empty()) {
821+
unsigned long err = AddCertsFromFile( // NOLINT(runtime/int)
822+
root_cert_store,
823+
extra_root_certs_file.c_str());
824+
if (err) {
825+
fprintf(stderr,
826+
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
827+
extra_root_certs_file.c_str(),
828+
ERR_error_string(err, nullptr));
829+
}
830+
}
785831
}
786832

787833
sc->ca_store_ = root_cert_store;

src/node_crypto.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ extern int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx);
6363

6464
extern X509_STORE* root_cert_store;
6565

66+
extern void UseExtraCaCerts(const std::string& file);
67+
6668
// Forward declaration
6769
class Connection;
6870

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Setting NODE_EXTRA_CA_CERTS to non-existent file emits a warning
2+
3+
'use strict';
4+
const common = require('../common');
5+
6+
if (!common.hasCrypto) {
7+
common.skip('missing crypto');
8+
return;
9+
}
10+
11+
const assert = require('assert');
12+
const tls = require('tls');
13+
const fork = require('child_process').fork;
14+
15+
if (process.env.CHILD) {
16+
// This will try to load the extra CA certs, and emit a warning when it fails.
17+
return tls.createServer({});
18+
}
19+
20+
const env = {
21+
CHILD: 'yes',
22+
NODE_EXTRA_CA_CERTS: common.fixturesDir + '/no-such-file-exists',
23+
};
24+
25+
var opts = {
26+
env: env,
27+
silent: true,
28+
};
29+
var stderr = '';
30+
31+
fork(__filename, opts)
32+
.on('exit', common.mustCall(function(status) {
33+
assert.equal(status, 0, 'client did not succeed in connecting');
34+
}))
35+
.on('close', common.mustCall(function() {
36+
assert(stderr.match(new RegExp(
37+
'Warning: Ignoring extra certs from.*no-such-file-exists' +
38+
'.* load failed:.*No such file or directory'
39+
)), stderr);
40+
}))
41+
.stderr.setEncoding('utf8').on('data', function(str) {
42+
stderr += str;
43+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Certs in NODE_EXTRA_CA_CERTS are used for TLS peer validation
2+
3+
'use strict';
4+
const common = require('../common');
5+
6+
if (!common.hasCrypto) {
7+
common.skip('missing crypto');
8+
return;
9+
}
10+
11+
const assert = require('assert');
12+
const tls = require('tls');
13+
const fork = require('child_process').fork;
14+
const fs = require('fs');
15+
16+
if (process.env.CHILD) {
17+
const copts = {
18+
port: process.env.PORT,
19+
checkServerIdentity: function() {},
20+
};
21+
const client = tls.connect(copts, function() {
22+
client.end('hi');
23+
});
24+
return;
25+
}
26+
27+
const options = {
28+
key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'),
29+
cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem'),
30+
};
31+
32+
const server = tls.createServer(options, function(s) {
33+
s.end('bye');
34+
server.close();
35+
}).listen(0, common.mustCall(function() {
36+
const env = {
37+
CHILD: 'yes',
38+
PORT: this.address().port,
39+
NODE_EXTRA_CA_CERTS: common.fixturesDir + '/keys/ca1-cert.pem',
40+
};
41+
42+
fork(__filename, {env: env}).on('exit', common.mustCall(function(status) {
43+
assert.equal(status, 0, 'client did not succeed in connecting');
44+
}));
45+
}));

0 commit comments

Comments
 (0)