Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tls: implement tls.getCACertificates() #57107

Merged
merged 3 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -1985,9 +1985,13 @@ changes:
* `allowPartialTrustChain` {boolean} Treat intermediate (non-self-signed)
certificates in the trust CA certificate list as trusted.
* `ca` {string|string\[]|Buffer|Buffer\[]} Optionally override the trusted CA
certificates. Default is to trust the well-known CAs curated by Mozilla.
Mozilla's CAs are completely replaced when CAs are explicitly specified
using this option. The value can be a string or `Buffer`, or an `Array` of
certificates. If not specified, the CA certificates trusted by default are
the same as the ones returned by [`tls.getCACertificates()`][] using the
`default` type. If specified, the default list would be completely replaced
(instead of being concatenated) by the certificates in the `ca` option.
Users need to concatenate manually if they wish to add additional certificates
instead of completely overriding the default.
The value can be a string or `Buffer`, or an `Array` of
strings and/or `Buffer`s. Any string or `Buffer` can contain multiple PEM
CAs concatenated together. The peer's certificate must be chainable to a CA
trusted by the server for the connection to be authenticated. When using
Expand All @@ -2001,7 +2005,6 @@ changes:
provided.
For PEM encoded certificates, supported types are "TRUSTED CERTIFICATE",
"X509 CERTIFICATE", and "CERTIFICATE".
See also [`tls.rootCertificates`][].
* `cert` {string|string\[]|Buffer|Buffer\[]} Cert chains in PEM format. One
cert chain should be provided per private key. Each cert chain should
consist of the PEM formatted certificate for a provided private `key`,
Expand Down Expand Up @@ -2364,6 +2367,39 @@ openssl pkcs12 -certpbe AES-256-CBC -export -out client-cert.pem \
The server can be tested by connecting to it using the example client from
[`tls.connect()`][].

## `tls.getCACertificates([type])`

<!-- YAML
added: REPLACEME
-->

* `type` {string|undefined} The type of CA certificates that will be returned. Valid values
are `"default"`, `"system"`, `"bundled"` and `"extra"`.
**Default:** `"default"`.
* Returns: {string\[]} An array of PEM-encoded certificates. The array may contain duplicates
if the same certificate is repeatedly stored in multiple sources.

Returns an array containing the CA certificates from various sources, depending on `type`:

* `"default"`: return the CA certificates that will be used by the Node.js TLS clients by default.
* When [`--use-bundled-ca`][] is enabled (default), or [`--use-openssl-ca`][] is not enabled,
this would include CA certificates from the bundled Mozilla CA store.
* When [`--use-system-ca`][] is enabled, this would also include certificates from the system's
trusted store.
* When [`NODE_EXTRA_CA_CERTS`][] is used, this would also include certificates loaded from the specified
file.
* `"system"`: return the CA certificates that are loaded from the system's trusted store, according
to rules set by [`--use-system-ca`][]. This can be used to get the certificates from the system
when [`--use-system-ca`][] is not enabled.
* `"bundled"`: return the CA certificates from the bundled Mozilla CA store. This would be the same
as [`tls.rootCertificates`][].
* `"extra"`: return the CA certificates loaded from [`NODE_EXTRA_CA_CERTS`][]. It's an empty array if
[`NODE_EXTRA_CA_CERTS`][] is not set.

<!-- YAML
added: v0.10.2
-->

## `tls.getCiphers()`

<!-- YAML
Expand Down Expand Up @@ -2400,8 +2436,10 @@ from the bundled Mozilla CA store as supplied by the current Node.js version.
The bundled CA store, as supplied by Node.js, is a snapshot of Mozilla CA store
that is fixed at release time. It is identical on all supported platforms.

On macOS if `--use-system-ca` is passed then trusted certificates
from the user and system keychains are also included.
To get the actual CA certificates used by the current Node.js instance, which
may include certificates loaded from the system store (if `--use-system-ca` is used)
or loaded from a file indicated by `NODE_EXTRA_CA_CERTS`, use
[`tls.getCACertificates()`][].

## `tls.DEFAULT_ECDH_CURVE`

Expand Down Expand Up @@ -2487,7 +2525,11 @@ added:
[`'secureConnection'`]: #event-secureconnection
[`'session'`]: #event-session
[`--tls-cipher-list`]: cli.md#--tls-cipher-listlist
[`--use-bundled-ca`]: cli.md#--use-bundled-ca---use-openssl-ca
[`--use-openssl-ca`]: cli.md#--use-bundled-ca---use-openssl-ca
[`--use-system-ca`]: cli.md#--use-system-ca
[`Duplex`]: stream.md#class-streamduplex
[`NODE_EXTRA_CA_CERTS`]: cli.md#node_extra_ca_certsfile
[`NODE_OPTIONS`]: cli.md#node_optionsoptions
[`SSL_export_keying_material`]: https://www.openssl.org/docs/man1.1.1/man3/SSL_export_keying_material.html
[`SSL_get_version`]: https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html
Expand Down Expand Up @@ -2516,6 +2558,7 @@ added:
[`tls.createSecureContext()`]: #tlscreatesecurecontextoptions
[`tls.createSecurePair()`]: #tlscreatesecurepaircontext-isserver-requestcert-rejectunauthorized-options
[`tls.createServer()`]: #tlscreateserveroptions-secureconnectionlistener
[`tls.getCACertificates()`]: #tlsgetcacertificatestype
[`tls.getCiphers()`]: #tlsgetciphers
[`tls.rootCertificates`]: #tlsrootcertificates
[`x509.checkHost()`]: crypto.md#x509checkhostname-options
Expand Down
88 changes: 79 additions & 9 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
const {
Array,
ArrayIsArray,
// eslint-disable-next-line no-restricted-syntax
ArrayPrototypePush,
JSONParse,
ObjectDefineProperty,
ObjectFreeze,
Expand All @@ -34,6 +36,7 @@ const {
ERR_TLS_CERT_ALTNAME_FORMAT,
ERR_TLS_CERT_ALTNAME_INVALID,
ERR_OUT_OF_RANGE,
ERR_INVALID_ARG_VALUE,
} = require('internal/errors').codes;
const internalUtil = require('internal/util');
internalUtil.assertCrypto();
Expand All @@ -44,12 +47,18 @@ const {

const net = require('net');
const { getOptionValue } = require('internal/options');
const { getRootCertificates, getSSLCiphers } = internalBinding('crypto');
const {
getBundledRootCertificates,
getExtraCACertificates,
getSystemCACertificates,
getSSLCiphers,
} = internalBinding('crypto');
const { Buffer } = require('buffer');
const { canonicalizeIP } = internalBinding('cares_wrap');
const _tls_common = require('_tls_common');
const _tls_wrap = require('_tls_wrap');
const { createSecurePair } = require('internal/tls/secure-pair');
const { validateString } = require('internal/validators');

// Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations
// every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more
Expand Down Expand Up @@ -85,23 +94,84 @@ exports.getCiphers = internalUtil.cachedResult(
() => internalUtil.filterDuplicateStrings(getSSLCiphers(), true),
);

let rootCertificates;
let bundledRootCertificates;
function cacheBundledRootCertificates() {
bundledRootCertificates ||= ObjectFreeze(getBundledRootCertificates());

function cacheRootCertificates() {
rootCertificates = ObjectFreeze(getRootCertificates());
return bundledRootCertificates;
}

ObjectDefineProperty(exports, 'rootCertificates', {
__proto__: null,
configurable: false,
enumerable: true,
get: () => {
// Out-of-line caching to promote inlining the getter.
if (!rootCertificates) cacheRootCertificates();
return rootCertificates;
},
get: cacheBundledRootCertificates,
});

let extraCACertificates;
function cacheExtraCACertificates() {
extraCACertificates ||= ObjectFreeze(getExtraCACertificates());

return extraCACertificates;
}

let systemCACertificates;
function cacheSystemCACertificates() {
systemCACertificates ||= ObjectFreeze(getSystemCACertificates());

return systemCACertificates;
}

let defaultCACertificates;
function cacheDefaultCACertificates() {
if (defaultCACertificates) { return defaultCACertificates; }
defaultCACertificates = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just a nit so feel free to ignore... it would be nice to be able to allocate the array at once rather than using multiple individual array.push(...) calls.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is different in JS - in JS if the array is preallocated, this becomes a holey array. using array.push keeps it compact, which can have perf implications. See https://v8.dev/blog/elements-kinds


if (!getOptionValue('--use-openssl-ca')) {
const bundled = cacheBundledRootCertificates();
for (let i = 0; i < bundled.length; ++i) {
ArrayPrototypePush(defaultCACertificates, bundled[i]);
}
if (getOptionValue('--use-system-ca')) {
const system = cacheSystemCACertificates();
for (let i = 0; i < system.length; ++i) {

ArrayPrototypePush(defaultCACertificates, system[i]);
}
}
}

if (process.env.NODE_EXTRA_CA_CERTS) {
const extra = cacheExtraCACertificates();
for (let i = 0; i < extra.length; ++i) {

ArrayPrototypePush(defaultCACertificates, extra[i]);
}
}

ObjectFreeze(defaultCACertificates);
return defaultCACertificates;
}

// TODO(joyeecheung): support X509Certificate output?
function getCACertificates(type = 'default') {
validateString(type, 'type');

switch (type) {
case 'default':
return cacheDefaultCACertificates();
case 'bundled':
return cacheBundledRootCertificates();
case 'system':
return cacheSystemCACertificates();
case 'extra':
return cacheExtraCACertificates();
default:
throw new ERR_INVALID_ARG_VALUE('type', type);
}
}
exports.getCACertificates = getCACertificates;

// Convert protocols array into valid OpenSSL protocols list
// ("\x06spdy/2\x08http/1.1\x08http/1.0")
function convertProtocols(protocols) {
Expand Down
81 changes: 72 additions & 9 deletions src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ using ncrypto::MarkPopErrorOnReturn;
using ncrypto::SSLPointer;
using ncrypto::StackOfX509;
using ncrypto::X509Pointer;
using ncrypto::X509View;
using v8::Array;
using v8::ArrayBufferView;
using v8::Boolean;
using v8::Context;
using v8::DontDelete;
using v8::EscapableHandleScope;
using v8::Exception;
using v8::External;
using v8::FunctionCallbackInfo;
Expand All @@ -57,7 +59,9 @@ using v8::Integer;
using v8::Isolate;
using v8::JustVoid;
using v8::Local;
using v8::LocalVector;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::PropertyAttribute;
Expand Down Expand Up @@ -657,9 +661,6 @@ static void LoadCertsFromDir(std::vector<X509*>* certs,
return;
}

uv_fs_t stats_req;
auto cleanup_stats =
OnScopeLeave([&stats_req]() { uv_fs_req_cleanup(&stats_req); });
for (;;) {
uv_dirent_t ent;

Expand All @@ -676,12 +677,14 @@ static void LoadCertsFromDir(std::vector<X509*>* certs,
return;
}

uv_fs_t stats_req;
std::string file_path = std::string(cert_dir) + "/" + ent.name;
int stats_r = uv_fs_stat(nullptr, &stats_req, file_path.c_str(), nullptr);
if (stats_r == 0 &&
(static_cast<uv_stat_t*>(stats_req.ptr)->st_mode & S_IFREG)) {
LoadCertsFromFile(certs, file_path.c_str());
}
uv_fs_req_cleanup(&stats_req);
}
}

Expand Down Expand Up @@ -760,7 +763,7 @@ static std::vector<X509*> InitializeSystemStoreCertificates() {
return system_store_certs;
}

static std::vector<X509*>& GetSystemStoreRootCertificates() {
static std::vector<X509*>& GetSystemStoreCACertificates() {
// Use function-local static to guarantee thread safety.
static std::vector<X509*> system_store_certs =
InitializeSystemStoreCertificates();
Expand Down Expand Up @@ -832,7 +835,7 @@ X509_STORE* NewRootCertStore() {
CHECK_EQ(1, X509_STORE_add_cert(store, cert));
}
if (per_process::cli_options->use_system_ca) {
for (X509* cert : GetSystemStoreRootCertificates()) {
for (X509* cert : GetSystemStoreCACertificates()) {
CHECK_EQ(1, X509_STORE_add_cert(store, cert));
}
}
Expand All @@ -854,7 +857,7 @@ void CleanupCachedRootCertificates() {
}
}
if (has_cached_system_root_certs.load()) {
for (X509* cert : GetSystemStoreRootCertificates()) {
for (X509* cert : GetSystemStoreCACertificates()) {
X509_free(cert);
}
}
Expand All @@ -866,7 +869,7 @@ void CleanupCachedRootCertificates() {
}
}

void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {
void GetBundledRootCertificates(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Value> result[arraysize(root_certs)];

Expand All @@ -883,6 +886,58 @@ void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {
Array::New(env->isolate(), result, arraysize(root_certs)));
}

MaybeLocal<Array> X509sToArrayOfStrings(Environment* env,
const std::vector<X509*>& certs) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you're using X509View here anyway, it would be nice to move away from the raw pointers in this std::vector. Can we make this an std::vector<ncrypto::X509View>& ?

Copy link
Member Author

@joyeecheung joyeecheung Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've considered this but it seems a bit too complicated for this PR, because we get the raw pointers from the function-local static storage for thread safety. Passing in std::vector<ncrypto::X509View> means doing another loop to convert them into vectors of X509View first, and then this must be called for all the caller before X509sToArrayOfStrings. It seems like a lot of extra code/slight overhead for no particular gains. Unless ncrypto provides some kind of helper to convert vectors of X509 into vectors of X509View automatically.

ClearErrorOnReturn clear_error_on_return;
EscapableHandleScope scope(env->isolate());

LocalVector<Value> result(env->isolate(), certs.size());
for (size_t i = 0; i < certs.size(); ++i) {
X509View view(certs[i]);
auto pem_bio = view.toPEM();
if (!pem_bio) {
ThrowCryptoError(env, ERR_get_error(), "X509 to PEM conversion");
return MaybeLocal<Array>();
}

char* pem_data = nullptr;
auto pem_size = BIO_get_mem_data(pem_bio.get(), &pem_data);
if (pem_size <= 0 || !pem_data) {
ThrowCryptoError(env, ERR_get_error(), "Reading PEM data");
return MaybeLocal<Array>();
}
// PEM is base64-encoded, so it must be one-byte.
if (!String::NewFromOneByte(env->isolate(),
reinterpret_cast<uint8_t*>(pem_data),
v8::NewStringType::kNormal,
pem_size)
.ToLocal(&result[i])) {
return MaybeLocal<Array>();
}
}
return scope.Escape(Array::New(env->isolate(), result.data(), result.size()));
}

void GetSystemCACertificates(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Array> results;
if (X509sToArrayOfStrings(env, GetSystemStoreCACertificates())
.ToLocal(&results)) {
args.GetReturnValue().Set(results);
}
}

void GetExtraCACertificates(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (extra_root_certs_file.empty()) {
return args.GetReturnValue().Set(Array::New(env->isolate()));
}
Local<Array> results;
if (X509sToArrayOfStrings(env, GetExtraCACertificates()).ToLocal(&results)) {
args.GetReturnValue().Set(results);
}
}

bool SecureContext::HasInstance(Environment* env, const Local<Value>& value) {
return GetConstructorTemplate(env)->HasInstance(value);
}
Expand Down Expand Up @@ -966,8 +1021,14 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
GetConstructorTemplate(env),
SetConstructorFunctionFlag::NONE);

SetMethodNoSideEffect(context,
target,
"getBundledRootCertificates",
GetBundledRootCertificates);
SetMethodNoSideEffect(
context, target, "getSystemCACertificates", GetSystemCACertificates);
SetMethodNoSideEffect(
context, target, "getRootCertificates", GetRootCertificates);
context, target, "getExtraCACertificates", GetExtraCACertificates);
}

void SecureContext::RegisterExternalReferences(
Expand Down Expand Up @@ -1007,7 +1068,9 @@ void SecureContext::RegisterExternalReferences(

registry->Register(CtxGetter);

registry->Register(GetRootCertificates);
registry->Register(GetBundledRootCertificates);
registry->Register(GetSystemCACertificates);
registry->Register(GetExtraCACertificates);
}

SecureContext* SecureContext::Create(Environment* env) {
Expand Down
Loading
Loading