From 1595328b44b4bb7008cc2ed6e209dbf66a769fb6 Mon Sep 17 00:00:00 2001 From: Sam Roberts Date: Mon, 17 Oct 2016 11:56:58 -0700 Subject: [PATCH] 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 Reviewed-By: Fedor Indutny --- doc/api/cli.md | 15 +++++++ src/node.cc | 2 + src/node_crypto.cc | 46 ++++++++++++++++++++++ src/node_crypto.h | 2 + test/parallel/test-tls-env-bad-extra-ca.js | 43 ++++++++++++++++++++ test/parallel/test-tls-env-extra-ca.js | 45 +++++++++++++++++++++ 6 files changed, 153 insertions(+) create mode 100644 test/parallel/test-tls-env-bad-extra-ca.js create mode 100644 test/parallel/test-tls-env-extra-ca.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 357f3c33a8..b64cf55e84 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -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` + + +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 diff --git a/src/node.cc b/src/node.cc index 05930e8bac..3b0837b1c1 100644 --- a/src/node.cc +++ b/src/node.cc @@ -4400,6 +4400,8 @@ int Start(int argc, char** argv) { Init(&argc, const_cast(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); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 0d3053625f..e58b0bc25b 100644 --- a/src/node_crypto.cc +++ b/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; // Just to generate static methods @@ -753,6 +755,38 @@ void SecureContext::AddCRL(const FunctionCallbackInfo& 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& args) { SecureContext* sc; @@ -782,6 +816,18 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo& 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; diff --git a/src/node_crypto.h b/src/node_crypto.h index 1f9271d0e6..9a4d936f30 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -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; diff --git a/test/parallel/test-tls-env-bad-extra-ca.js b/test/parallel/test-tls-env-bad-extra-ca.js new file mode 100644 index 0000000000..1862366e01 --- /dev/null +++ b/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; + }); diff --git a/test/parallel/test-tls-env-extra-ca.js b/test/parallel/test-tls-env-extra-ca.js new file mode 100644 index 0000000000..12e3272bd4 --- /dev/null +++ b/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'); + })); +}));