Skip to content

Commit 6963e8a

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 d4e160c commit 6963e8a

File tree

6 files changed

+150
-0
lines changed

6 files changed

+150
-0
lines changed

doc/api/cli.md

+11
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,18 @@ asynchronous when outputting to a TTY on platforms which support async stdio.
315315
Setting this will void any guarantee that stdio will not be interleaved or
316316
dropped at program exit. **Use of this mode is not recommended.**
317317

318+
### `NODE_EXTRA_CA_CERTS=file`
318319

320+
When set, the well known "root" CAs (like VeriSign) will be extended with the
321+
extra certificates in `file`. The file should consist of one or more trusted
322+
certificates in PEM format. A message will be emitted (once) with
323+
[`process.emitWarning()`][emit_warning] if the file is missing or
324+
misformatted, but any errors are otherwise ignored.
325+
326+
Note that neither the well known nor extra certificates are used when the `ca`
327+
options property is explicitly specified for a TLS or HTTPS client or server.
328+
329+
[emit_warning]: process.html#process_process_emitwarning_warning_name_ctor
319330
[Buffer]: buffer.html#buffer_buffer
320331
[debugger]: debugger.html
321332
[REPL]: repl.html

src/node.cc

+2
Original file line numberDiff line numberDiff line change
@@ -4535,6 +4535,8 @@ int Start(int argc, char** argv) {
45354535
Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);
45364536

45374537
#if HAVE_OPENSSL
4538+
if (const char* extra = secure_getenv("NODE_EXTRA_CA_CERTS"))
4539+
crypto::UseExtraCaCerts(extra);
45384540
#ifdef NODE_FIPS_MODE
45394541
// In the case of FIPS builds we should make sure
45404542
// the random source is properly initialized first.

src/node_crypto.cc

+47
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
std::vector<X509*>* root_certs_vector;
125127

@@ -789,6 +791,39 @@ void SecureContext::AddCRL(const FunctionCallbackInfo<Value>& args) {
789791
}
790792

791793

794+
void UseExtraCaCerts(const std::string& file) {
795+
extra_root_certs_file = file;
796+
}
797+
798+
799+
static unsigned long AddCertsFromFile( // NOLINT(runtime/int)
800+
X509_STORE* store,
801+
const char* file) {
802+
ERR_clear_error();
803+
MarkPopErrorOnReturn mark_pop_error_on_return;
804+
805+
BIO* bio = BIO_new_file(file, "r");
806+
if (!bio) {
807+
return ERR_get_error();
808+
}
809+
810+
while (X509* x509 =
811+
PEM_read_bio_X509(bio, nullptr, CryptoPemCallback, nullptr)) {
812+
X509_STORE_add_cert(store, x509);
813+
X509_free(x509);
814+
}
815+
BIO_free_all(bio);
816+
817+
unsigned long err = ERR_peek_error(); // NOLINT(runtime/int)
818+
// Ignore error if its EOF/no start line found.
819+
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
820+
ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
821+
return 0;
822+
}
823+
824+
return err;
825+
}
826+
792827
void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
793828
SecureContext* sc;
794829
ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
@@ -797,6 +832,18 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
797832

798833
if (!root_cert_store) {
799834
root_cert_store = NewRootCertStore();
835+
836+
if (!extra_root_certs_file.empty()) {
837+
unsigned long err = AddCertsFromFile( // NOLINT(runtime/int)
838+
root_cert_store,
839+
extra_root_certs_file.c_str());
840+
if (err) {
841+
ProcessEmitWarning(sc->env(),
842+
"Ignoring extra certs from `%s`, load failed: %s\n",
843+
extra_root_certs_file.c_str(),
844+
ERR_error_string(err, nullptr));
845+
}
846+
}
800847
}
801848

802849
// Increment reference count so global store is not deleted along with CTX.

src/node_crypto.h

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ extern int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx);
6565

6666
extern X509_STORE* root_cert_store;
6767

68+
extern void UseExtraCaCerts(const std::string& file);
69+
6870
// Forward declaration
6971
class Connection;
7072

+43
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+
});
+45
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)