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: https://github.com/nodejs/node/pull/9139
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Fedor Indutny <fedor.indutny@gmail.com>
This commit is contained in:
Sam Roberts 2016-10-17 11:56:58 -07:00
parent 213134f66d
commit fd644f51f8
6 changed files with 150 additions and 0 deletions

View File

@ -315,7 +315,18 @@ asynchronous when outputting to a TTY on platforms which support async stdio.
Setting this will void any guarantee that stdio will not be interleaved or Setting this will void any guarantee that stdio will not be interleaved or
dropped at program exit. **Use of this mode is not recommended.** dropped at program exit. **Use of this mode is not recommended.**
### `NODE_EXTRA_CA_CERTS=file`
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 emitted (once) with
[`process.emitWarning()`][emit_warning] 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 [Buffer]: buffer.html#buffer_buffer
[debugger]: debugger.html [debugger]: debugger.html
[REPL]: repl.html [REPL]: repl.html

View File

@ -4536,6 +4536,8 @@ int Start(int argc, char** argv) {
Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv); Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);
#if HAVE_OPENSSL #if HAVE_OPENSSL
if (const char* extra = secure_getenv("NODE_EXTRA_CA_CERTS"))
crypto::UseExtraCaCerts(extra);
#ifdef NODE_FIPS_MODE #ifdef NODE_FIPS_MODE
// In the case of FIPS builds we should make sure // In the case of FIPS builds we should make sure
// the random source is properly initialized first. // the random source is properly initialized first.

View File

@ -120,6 +120,8 @@ const char* const root_certs[] = {
#include "node_root_certs.h" // NOLINT(build/include_order) #include "node_root_certs.h" // NOLINT(build/include_order)
}; };
std::string extra_root_certs_file; // NOLINT(runtime/string)
X509_STORE* root_cert_store; X509_STORE* root_cert_store;
std::vector<X509*>* root_certs_vector; std::vector<X509*>* root_certs_vector;
@ -789,6 +791,39 @@ 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) { void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
SecureContext* sc; SecureContext* sc;
ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
@ -797,6 +832,18 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
if (!root_cert_store) { if (!root_cert_store) {
root_cert_store = NewRootCertStore(); root_cert_store = NewRootCertStore();
if (!extra_root_certs_file.empty()) {
unsigned long err = AddCertsFromFile( // NOLINT(runtime/int)
root_cert_store,
extra_root_certs_file.c_str());
if (err) {
ProcessEmitWarning(sc->env(),
"Ignoring extra certs from `%s`, load failed: %s\n",
extra_root_certs_file.c_str(),
ERR_error_string(err, nullptr));
}
}
} }
// Increment reference count so global store is not deleted along with CTX. // Increment reference count so global store is not deleted along with CTX.

View File

@ -65,6 +65,8 @@ extern int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx);
extern X509_STORE* root_cert_store; extern X509_STORE* root_cert_store;
extern void UseExtraCaCerts(const std::string& file);
// Forward declaration // Forward declaration
class Connection; class Connection;

View File

@ -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;
});

View File

@ -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');
}));
}));