Browse Source

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>
v6
Sam Roberts 8 years ago
parent
commit
fd644f51f8
  1. 11
      doc/api/cli.md
  2. 2
      src/node.cc
  3. 47
      src/node_crypto.cc
  4. 2
      src/node_crypto.h
  5. 43
      test/parallel/test-tls-env-bad-extra-ca.js
  6. 45
      test/parallel/test-tls-env-extra-ca.js

11
doc/api/cli.md

@ -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
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
[debugger]: debugger.html
[REPL]: repl.html

2
src/node.cc

@ -4536,6 +4536,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);
#ifdef NODE_FIPS_MODE
// In the case of FIPS builds we should make sure
// the random source is properly initialized first.

47
src/node_crypto.cc

@ -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;
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) {
SecureContext* sc;
ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
@ -797,6 +832,18 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
if (!root_cert_store) {
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.

2
src/node_crypto.h

@ -65,6 +65,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;

43
test/parallel/test-tls-env-bad-extra-ca.js

@ -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
test/parallel/test-tls-env-extra-ca.js

@ -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');
}));
}));
Loading…
Cancel
Save