Skip to content

Commit

Permalink
tls: allow tls.rootCertificates to be set
Browse files Browse the repository at this point in the history
Modifies the tls.rootCertificates property so that it is settable. PEM-formatted certificates set to the property are loaded into a new Node.js root certificate store that is used for all subsequent TLS requests and peer certificate validation.

Allows full programmatic control of a Node.js process' root certificate store.

Fixes: nodejs#20432
Refs: nodejs#27079

Changed root_cert_store to a smart pointer

Added support for empty root certificates

Added comment
  • Loading branch information
ebickle committed Mar 17, 2020
1 parent 55a13f8 commit b9e0b7a
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 53 deletions.
14 changes: 14 additions & 0 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,20 @@ An immutable array of strings representing the root certificates (in PEM format)
used for verifying peer certificates. This is the default value of the `ca`
option to [`tls.createSecureContext()`][].

`tls.rootCertificates` can be assigned a new value to change the root
certificates used used for all subsequent peer certificate verifications.
Existing secure connections are not affected.

The following illustrates adding an extra certificate to the default set of
well known "root" CAs:

```sh
tls.rootCertificates = [
...tls.rootCertificates,
fs.readFileSync('custom-ca.pem')
];
```

## `tls.DEFAULT_ECDH_CURVE`
<!-- YAML
added: v0.11.13
Expand Down
22 changes: 20 additions & 2 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const {
} = primordials;

const {
ERR_INVALID_ARG_TYPE,
ERR_TLS_CERT_ALTNAME_INVALID,
ERR_OUT_OF_RANGE
} = require('internal/errors').codes;
Expand All @@ -40,7 +41,11 @@ const { isArrayBufferView } = require('internal/util/types');
const net = require('net');
const { getOptionValue } = require('internal/options');
const url = require('url');
const { getRootCertificates, getSSLCiphers } = internalBinding('crypto');
const {
getRootCertificates,
setRootCertificates,
getSSLCiphers
} = internalBinding('crypto');
const { Buffer } = require('buffer');
const EventEmitter = require('events');
const { URL } = require('internal/url');
Expand Down Expand Up @@ -84,7 +89,7 @@ exports.getCiphers = internalUtil.cachedResult(
() => internalUtil.filterDuplicateStrings(getSSLCiphers(), true)
);

let rootCertificates;
let rootCertificates = null;

function cacheRootCertificates() {
rootCertificates = ObjectFreeze(getRootCertificates());
Expand All @@ -98,6 +103,19 @@ ObjectDefineProperty(exports, 'rootCertificates', {
if (!rootCertificates) cacheRootCertificates();
return rootCertificates;
},
set: (value) => {
if (!ArrayIsArray(value))
throw new ERR_INVALID_ARG_TYPE('value', 'Array', value);
for (let i = 0; i < value.length; i++) {
if (typeof value[i] !== 'string')
throw new ERR_INVALID_ARG_TYPE(`value[${i}]`, 'String', value[i]);
}

setRootCertificates(value);

// Clear root certificate cache.
rootCertificates = null;
}
});

// Convert protocols array into valid OpenSSL protocols list
Expand Down
115 changes: 84 additions & 31 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,11 @@ static const char* const root_certs[] = {

static const char system_cert_path[] = NODE_OPENSSL_SYSTEM_CERT_PATH;

static X509_STORE* root_cert_store;
static std::vector<X509*> root_certs_vector;
static X509StorePointer root_cert_store;

static std::vector<X509Pointer> root_certs_vector;
static Mutex root_certs_vector_mutex;
static bool root_certs_vector_loaded = false;

static bool extra_root_certs_loaded = false;

Expand Down Expand Up @@ -973,37 +975,39 @@ void SecureContext::SetCert(const FunctionCallbackInfo<Value>& args) {
static void EnsureRootCerts() {
Mutex::ScopedLock lock(root_certs_vector_mutex);

if (root_certs_vector.empty()) {
if (!root_certs_vector_loaded) {
for (size_t i = 0; i < arraysize(root_certs); i++) {
X509* x509 =
X509Pointer x509 = X509Pointer(
PEM_read_bio_X509(NodeBIO::NewFixed(root_certs[i],
strlen(root_certs[i])).get(),
nullptr, // no re-use of X509 structure
NoPasswordCallback,
nullptr); // no callback data
nullptr)); // no callback data

// Parse errors from the built-in roots are fatal.
CHECK_NOT_NULL(x509);

root_certs_vector.push_back(x509);
root_certs_vector.push_back(std::move(x509));
}

root_certs_vector_loaded = true;
}
}


static X509_STORE* NewRootCertStore() {
static X509StorePointer NewRootCertStore() {
EnsureRootCerts();

X509_STORE* store = X509_STORE_new();
X509StorePointer store = X509StorePointer(X509_STORE_new());
if (*system_cert_path != '\0') {
X509_STORE_load_locations(store, system_cert_path, nullptr);
X509_STORE_load_locations(store.get(), system_cert_path, nullptr);
}
if (per_process::cli_options->ssl_openssl_cert_store) {
X509_STORE_set_default_paths(store);
X509_STORE_set_default_paths(store.get());
} else {
Mutex::ScopedLock lock(root_certs_vector_mutex);
for (X509* cert : root_certs_vector) {
X509_STORE_add_cert(store, cert);
for (X509Pointer& cert : root_certs_vector) {
X509_STORE_add_cert(store.get(), cert.get());
}
}

Expand All @@ -1027,15 +1031,14 @@ void SecureContext::AddCACert(const FunctionCallbackInfo<Value>& args) {
return;

X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get());
while (X509* x509 = PEM_read_bio_X509_AUX(
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
if (cert_store == root_cert_store) {
cert_store = NewRootCertStore();
while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509_AUX(
bio.get(), nullptr, NoPasswordCallback, nullptr))) {
if (cert_store == root_cert_store.get()) {
cert_store = NewRootCertStore().release();
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
}
X509_STORE_add_cert(cert_store, x509);
SSL_CTX_add_client_CA(sc->ctx_.get(), x509);
X509_free(x509);
X509_STORE_add_cert(cert_store, x509.get());
SSL_CTX_add_client_CA(sc->ctx_.get(), x509.get());
}
}

Expand Down Expand Up @@ -1063,8 +1066,8 @@ void SecureContext::AddCRL(const FunctionCallbackInfo<Value>& args) {
return env->ThrowError("Failed to parse CRL");

X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get());
if (cert_store == root_cert_store) {
cert_store = NewRootCertStore();
if (cert_store == root_cert_store.get()) {
cert_store = NewRootCertStore().release();
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
}

Expand All @@ -1086,10 +1089,10 @@ static unsigned long AddRootCertsFromFile( // NOLINT(runtime/int)
// Scope for root_certs_vector lock
{
Mutex::ScopedLock lock(root_certs_vector_mutex);
while (X509* x509 =
PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr)) {
X509_STORE_add_cert(root_cert_store, x509);
root_certs_vector.push_back(x509);
while (X509Pointer x509 = X509Pointer(
PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr))) {
X509_STORE_add_cert(root_cert_store.get(), x509.get());
root_certs_vector.push_back(std::move(x509));
}
}

Expand Down Expand Up @@ -1142,8 +1145,8 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
}

// Increment reference count so global store is not deleted along with CTX.
X509_STORE_up_ref(root_cert_store);
SSL_CTX_set_cert_store(sc->ctx_.get(), root_cert_store);
X509_STORE_up_ref(root_cert_store.get());
SSL_CTX_set_cert_store(sc->ctx_.get(), root_cert_store.get());
}


Expand Down Expand Up @@ -1442,8 +1445,8 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
for (int i = 0; i < sk_X509_num(extra_certs.get()); i++) {
X509* ca = sk_X509_value(extra_certs.get(), i);

if (cert_store == root_cert_store) {
cert_store = NewRootCertStore();
if (cert_store == root_cert_store.get()) {
cert_store = NewRootCertStore().release();
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
}
X509_STORE_add_cert(cert_store, ca);
Expand Down Expand Up @@ -6648,9 +6651,9 @@ void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {

result.reserve(root_certs_vector.size());

for (X509* cert : root_certs_vector) {
for (X509Pointer& cert : root_certs_vector) {
Local<Value> value;
if (!X509ToPEM(env, cert).ToLocal(&value))
if (!X509ToPEM(env, cert.get()).ToLocal(&value))
return;

result.push_back(value);
Expand All @@ -6662,6 +6665,55 @@ void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {
}


void SetRootCertificates(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

// Single argument must be an array of PEM-formatted strings.
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsArray());
Local<Array> arr = args[0].As<Array>();
size_t len = arr->Length();

ERR_clear_error();
ClearErrorOnReturn clear_error_on_return;

std::vector<X509Pointer> certs;
certs.reserve(len);

for (size_t i = 0; i < len; i++) {
// All array values must be strings.
Local<Value> value;
if (!arr->Get(env->context(), i).ToLocal(&value)) return;
CHECK(value->IsString());

BIOPointer bio(LoadBIO(env, value));
if (!bio) return ThrowCryptoError(env, ERR_get_error(), "LoadBIO");

// Each string can contain multiple PEM-formatted certificates.
while (X509Pointer x509 = X509Pointer(
PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr))) {
certs.push_back(std::move(x509));
}

// Ignore error if its EOF/no start line found.
unsigned long err = ERR_peek_last_error(); // NOLINT(runtime/int)
if (ERR_GET_LIB(err) != ERR_LIB_PEM ||
ERR_GET_REASON(err) != PEM_R_NO_START_LINE) {
return ThrowCryptoError(env, err, "PEM_read_bio_X509");
}
}

// Scope for root_certs_vector lock
{
Mutex::ScopedLock lock(root_certs_vector_mutex);
root_certs_vector = std::move(certs);
}

// A new root_cert_store will be built on next use.
root_cert_store = nullptr;
}


// Convert the input public key to compressed, uncompressed, or hybrid formats.
void ConvertKey(const FunctionCallbackInfo<Value>& args) {
MarkPopErrorOnReturn mark_pop_error_on_return;
Expand Down Expand Up @@ -6878,6 +6930,7 @@ void Initialize(Local<Object> target,
env->SetMethodNoSideEffect(target, "certExportChallenge", ExportChallenge);
env->SetMethodNoSideEffect(target, "getRootCertificates",
GetRootCertificates);
env->SetMethod(target, "setRootCertificates", SetRootCertificates);
// Exposed for testing purposes only.
env->SetMethodNoSideEffect(target, "isExtraRootCertsFileLoaded",
IsExtraRootCertsFileLoaded);
Expand Down
1 change: 1 addition & 0 deletions src/node_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ struct MarkPopErrorOnReturn {

// Define smart pointers for the most commonly used OpenSSL types:
using X509Pointer = DeleteFnPtr<X509, X509_free>;
using X509StorePointer = DeleteFnPtr<X509_STORE, X509_STORE_free>;
using BIOPointer = DeleteFnPtr<BIO, BIO_free_all>;
using SSLCtxPointer = DeleteFnPtr<SSL_CTX, SSL_CTX_free>;
using SSLSessionPointer = DeleteFnPtr<SSL_SESSION, SSL_SESSION_free>;
Expand Down
Loading

0 comments on commit b9e0b7a

Please sign in to comment.