Skip to content

Commit

Permalink
crypto: allow adding extra certs to well-known CAs
Browse files Browse the repository at this point in the history
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 <info@bnoordhuis.nl>
Reviewed-By: Fedor Indutny <fedor.indutny@gmail.com>
  • Loading branch information
sam-github authored and MylesBorins committed Feb 6, 2017
1 parent 735119c commit 1595328
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 0 deletions.
15 changes: 15 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,21 @@ Path to the file used to store the persistent REPL history. The default path is
to an empty string (`""` or `" "`) disables persistent REPL history.


### `NODE_EXTRA_CA_CERTS=file`
<!-- YAML
added: XXX
-->

When set, the well known "root" CAs (like VeriSign) will be extended with the
extra certificates in `file`. The file should consist of one or more trusted
certificates in PEM format. A message will be printed to stderr (once)
if the file is missing or
misformatted, but any errors are otherwise ignored.

Note that neither the well known nor extra certificates are used when the `ca`
options property is explicitly specified for a TLS or HTTPS client or server.

[emit_warning]: process.html#process_process_emitwarning_warning_name_ctor
[Buffer]: buffer.html#buffer_buffer
[debugger]: debugger.html
[REPL]: repl.html
Expand Down
2 changes: 2 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4400,6 +4400,8 @@ int Start(int argc, char** argv) {
Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);

#if HAVE_OPENSSL
if (const char* extra = secure_getenv("NODE_EXTRA_CA_CERTS"))
crypto::UseExtraCaCerts(extra);
// V8 on Windows doesn't have a good source of entropy. Seed it from
// OpenSSL's pool.
V8::SetEntropySource(crypto::EntropySource);
Expand Down
46 changes: 46 additions & 0 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ const char* const root_certs[] = {
#include "node_root_certs.h" // NOLINT(build/include_order)
};

std::string extra_root_certs_file; // NOLINT(runtime/string)

X509_STORE* root_cert_store;

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


void UseExtraCaCerts(const std::string& file) {
extra_root_certs_file = file;
}


static unsigned long AddCertsFromFile( // NOLINT(runtime/int)
X509_STORE* store,
const char* file) {
ERR_clear_error();
MarkPopErrorOnReturn mark_pop_error_on_return;

BIO* bio = BIO_new_file(file, "r");
if (!bio) {
return ERR_get_error();
}

while (X509* x509 =
PEM_read_bio_X509(bio, nullptr, CryptoPemCallback, nullptr)) {
X509_STORE_add_cert(store, x509);
X509_free(x509);
}
BIO_free_all(bio);

unsigned long err = ERR_peek_error(); // NOLINT(runtime/int)
// Ignore error if its EOF/no start line found.
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
return 0;
}

return err;
}

void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
SecureContext* sc;
Expand Down Expand Up @@ -782,6 +816,18 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
BIO_free_all(bp);
X509_free(x509);
}

if (!extra_root_certs_file.empty()) {
unsigned long err = AddCertsFromFile( // NOLINT(runtime/int)
root_cert_store,
extra_root_certs_file.c_str());
if (err) {
fprintf(stderr,
"Warning: Ignoring extra certs from `%s`, load failed: %s\n",
extra_root_certs_file.c_str(),
ERR_error_string(err, nullptr));
}
}
}

sc->ca_store_ = root_cert_store;
Expand Down
2 changes: 2 additions & 0 deletions src/node_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ extern int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx);

extern X509_STORE* root_cert_store;

extern void UseExtraCaCerts(const std::string& file);

// Forward declaration
class Connection;

Expand Down
43 changes: 43 additions & 0 deletions test/parallel/test-tls-env-bad-extra-ca.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Setting NODE_EXTRA_CA_CERTS to non-existent file emits a warning

'use strict';
const common = require('../common');

if (!common.hasCrypto) {
common.skip('missing crypto');
return;
}

const assert = require('assert');
const tls = require('tls');
const fork = require('child_process').fork;

if (process.env.CHILD) {
// This will try to load the extra CA certs, and emit a warning when it fails.
return tls.createServer({});
}

const env = {
CHILD: 'yes',
NODE_EXTRA_CA_CERTS: common.fixturesDir + '/no-such-file-exists',
};

var opts = {
env: env,
silent: true,
};
var stderr = '';

fork(__filename, opts)
.on('exit', common.mustCall(function(status) {
assert.equal(status, 0, 'client did not succeed in connecting');
}))
.on('close', common.mustCall(function() {
assert(stderr.match(new RegExp(
'Warning: Ignoring extra certs from.*no-such-file-exists' +
'.* load failed:.*No such file or directory'
)), stderr);
}))
.stderr.setEncoding('utf8').on('data', function(str) {
stderr += str;
});
45 changes: 45 additions & 0 deletions test/parallel/test-tls-env-extra-ca.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Certs in NODE_EXTRA_CA_CERTS are used for TLS peer validation

'use strict';
const common = require('../common');

if (!common.hasCrypto) {
common.skip('missing crypto');
return;
}

const assert = require('assert');
const tls = require('tls');
const fork = require('child_process').fork;
const fs = require('fs');

if (process.env.CHILD) {
const copts = {
port: process.env.PORT,
checkServerIdentity: function() {},
};
const client = tls.connect(copts, function() {
client.end('hi');
});
return;
}

const options = {
key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'),
cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem'),
};

const server = tls.createServer(options, function(s) {
s.end('bye');
server.close();
}).listen(0, common.mustCall(function() {
const env = {
CHILD: 'yes',
PORT: this.address().port,
NODE_EXTRA_CA_CERTS: common.fixturesDir + '/keys/ca1-cert.pem',
};

fork(__filename, {env: env}).on('exit', common.mustCall(function(status) {
assert.equal(status, 0, 'client did not succeed in connecting');
}));
}));

0 comments on commit 1595328

Please sign in to comment.