Skip to content

Commit 9edaff6

Browse files
committed
tls: implement tls.getCACertificates()
To accompany --use-system-ca, this adds a new API that allows querying various kinds of CA certificates. - If the first argument `type` is `"default"` or undefined, it returns the CA certificates that will be used by Node.js TLS clients by default, which includes the Mozilla CA if --use-bundled-ca is enabled or --use-openssl-ca is not enabled, and the system certificates if --use-system-ca is enabled, and the extra certificates if NODE_EXTRA_CA_CERTS is used. - If `type` is `"system"` this returns the system certificates, regardless of whether --use-system-ca is enabeld or not. - If `type` is `"bundled"` this is the same as `tls.rootCertificates` and returns the Mozilla CA certificates. - If `type` is `"extra"` this returns the certificates parsed from the path specified by NODE_EXTRA_CA_CERTS. Drive-by: remove the inaccurate description in `tls.rootCertificates` about including system certificates, since it in fact does not include them, and also it is contradicting the previous description about `tls.rootCertificates` always returning the Mozilla CA store and staying the same across platforms.
1 parent cc7018e commit 9edaff6

15 files changed

+430
-21
lines changed

doc/api/tls.md

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,9 +1985,13 @@ changes:
19851985
* `allowPartialTrustChain` {boolean} Treat intermediate (non-self-signed)
19861986
certificates in the trust CA certificate list as trusted.
19871987
* `ca` {string|string\[]|Buffer|Buffer\[]} Optionally override the trusted CA
1988-
certificates. Default is to trust the well-known CAs curated by Mozilla.
1989-
Mozilla's CAs are completely replaced when CAs are explicitly specified
1990-
using this option. The value can be a string or `Buffer`, or an `Array` of
1988+
certificates. If not specified, the CA certificates trusted by default are
1989+
the same as the ones returned by [`tls.getCACertificates()`][] using the
1990+
`default` type. If specified, the default list would be completely replaced
1991+
(instead of being concatenated) by the certificates in the `ca` option.
1992+
Users need to concatenate manually if they wish to add additional certificates
1993+
instead of completely overriding the default.
1994+
The value can be a string or `Buffer`, or an `Array` of
19911995
strings and/or `Buffer`s. Any string or `Buffer` can contain multiple PEM
19921996
CAs concatenated together. The peer's certificate must be chainable to a CA
19931997
trusted by the server for the connection to be authenticated. When using
@@ -2001,7 +2005,6 @@ changes:
20012005
provided.
20022006
For PEM encoded certificates, supported types are "TRUSTED CERTIFICATE",
20032007
"X509 CERTIFICATE", and "CERTIFICATE".
2004-
See also [`tls.rootCertificates`][].
20052008
* `cert` {string|string\[]|Buffer|Buffer\[]} Cert chains in PEM format. One
20062009
cert chain should be provided per private key. Each cert chain should
20072010
consist of the PEM formatted certificate for a provided private `key`,
@@ -2364,6 +2367,38 @@ openssl pkcs12 -certpbe AES-256-CBC -export -out client-cert.pem \
23642367
The server can be tested by connecting to it using the example client from
23652368
[`tls.connect()`][].
23662369

2370+
## `tls.getCACertificates([type])`
2371+
2372+
<!-- YAML
2373+
added: REPLACEME
2374+
-->
2375+
2376+
* `type` {string|undefined} The type of CA certificates that will be returned. Valid values
2377+
are `"default"`, `"system"`, `"bundled"` and `"extra"`.
2378+
**Default:** `"default"`.
2379+
* Returns: {string\[]}
2380+
2381+
Returns an array containing the CA certificates from various sources, depending on `type`:
2382+
2383+
* `"default"`: return the CA certificates that will be used by the Node.js TLS clients by default.
2384+
* When [`--use-bundled-ca`][] is enabled (default), or [`--use-openssl-ca`][] is not enabled,
2385+
this would include CA certificates from the bundled Mozilla CA store.
2386+
* When [`--use-system-ca`][] is enabled, this would also include certificates from the system's
2387+
trusted store.
2388+
* When [`NODE_EXTRA_CA_CERTS`][] is used, this would also include certificates loaded from the specified
2389+
file.
2390+
* `"system"`: return the CA certificates that are loaded from the system's trusted store, according
2391+
to rules set by [`--use-system-ca`][]. This can be used to get the certificates from the system
2392+
when [`--use-system-ca`][] is not enabled.
2393+
* `"bundled"`: return the CA certificates from the bundled Mozilla CA store. This would be the same
2394+
as [`tls.rootCertificates`][].
2395+
* `"extra"`: return the CA certificates loaded from [`NODE_EXTRA_CA_CERTS`][]. It's an empty array if
2396+
[`NODE_EXTRA_CA_CERTS`][] is not set.
2397+
2398+
<!-- YAML
2399+
added: v0.10.2
2400+
-->
2401+
23672402
## `tls.getCiphers()`
23682403

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

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

24062443
## `tls.DEFAULT_ECDH_CURVE`
24072444

@@ -2487,7 +2524,11 @@ added:
24872524
[`'secureConnection'`]: #event-secureconnection
24882525
[`'session'`]: #event-session
24892526
[`--tls-cipher-list`]: cli.md#--tls-cipher-listlist
2527+
[`--use-bundled-ca`]: cli.md#--use-bundled-ca---use-openssl-ca
2528+
[`--use-openssl-ca`]: cli.md#--use-bundled-ca---use-openssl-ca
2529+
[`--use-system-ca`]: cli.md#--use-system-ca
24902530
[`Duplex`]: stream.md#class-streamduplex
2531+
[`NODE_EXTRA_CA_CERTS`]: cli.md#node_extra_ca_certsfile
24912532
[`NODE_OPTIONS`]: cli.md#node_optionsoptions
24922533
[`SSL_export_keying_material`]: https://www.openssl.org/docs/man1.1.1/man3/SSL_export_keying_material.html
24932534
[`SSL_get_version`]: https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html
@@ -2516,6 +2557,7 @@ added:
25162557
[`tls.createSecureContext()`]: #tlscreatesecurecontextoptions
25172558
[`tls.createSecurePair()`]: #tlscreatesecurepaircontext-isserver-requestcert-rejectunauthorized-options
25182559
[`tls.createServer()`]: #tlscreateserveroptions-secureconnectionlistener
2560+
[`tls.getCACertificates()`]: #tlsgetcacertificates
25192561
[`tls.getCiphers()`]: #tlsgetciphers
25202562
[`tls.rootCertificates`]: #tlsrootcertificates
25212563
[`x509.checkHost()`]: crypto.md#x509checkhostname-options

lib/tls.js

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
const {
2525
Array,
2626
ArrayIsArray,
27+
// eslint-disable-next-line no-restricted-syntax
28+
ArrayPrototypePush,
2729
JSONParse,
2830
ObjectDefineProperty,
2931
ObjectFreeze,
@@ -34,6 +36,7 @@ const {
3436
ERR_TLS_CERT_ALTNAME_FORMAT,
3537
ERR_TLS_CERT_ALTNAME_INVALID,
3638
ERR_OUT_OF_RANGE,
39+
ERR_INVALID_ARG_VALUE,
3740
} = require('internal/errors').codes;
3841
const internalUtil = require('internal/util');
3942
internalUtil.assertCrypto();
@@ -44,12 +47,18 @@ const {
4447

4548
const net = require('net');
4649
const { getOptionValue } = require('internal/options');
47-
const { getRootCertificates, getSSLCiphers } = internalBinding('crypto');
50+
const {
51+
getBundledRootCertificates,
52+
getExtraCACertificates,
53+
getSystemCACertificates,
54+
getSSLCiphers,
55+
} = internalBinding('crypto');
4856
const { Buffer } = require('buffer');
4957
const { canonicalizeIP } = internalBinding('cares_wrap');
5058
const _tls_common = require('_tls_common');
5159
const _tls_wrap = require('_tls_wrap');
5260
const { createSecurePair } = require('internal/tls/secure-pair');
61+
const { validateString } = require('internal/validators');
5362

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

88-
let rootCertificates;
97+
let bundledRootCertificates;
98+
function cacheBundledRootCertificates() {
99+
bundledRootCertificates ||= ObjectFreeze(getBundledRootCertificates());
89100

90-
function cacheRootCertificates() {
91-
rootCertificates = ObjectFreeze(getRootCertificates());
101+
return bundledRootCertificates;
92102
}
93103

94104
ObjectDefineProperty(exports, 'rootCertificates', {
95105
__proto__: null,
96106
configurable: false,
97107
enumerable: true,
98-
get: () => {
99-
// Out-of-line caching to promote inlining the getter.
100-
if (!rootCertificates) cacheRootCertificates();
101-
return rootCertificates;
102-
},
108+
get: cacheBundledRootCertificates,
103109
});
104110

111+
let extraCACertificates;
112+
function cacheExtraCACertificates() {
113+
extraCACertificates ||= ObjectFreeze(getExtraCACertificates());
114+
115+
return extraCACertificates;
116+
}
117+
118+
let systemCACertificates;
119+
function cacheSystemCACertificates() {
120+
systemCACertificates ||= ObjectFreeze(getSystemCACertificates());
121+
122+
return systemCACertificates;
123+
}
124+
125+
let defaultCACertificates;
126+
function cacheDefaultCACertificates() {
127+
if (defaultCACertificates) { return defaultCACertificates; }
128+
defaultCACertificates = [];
129+
130+
if (!getOptionValue('--use-openssl-ca')) {
131+
const bundled = cacheBundledRootCertificates();
132+
for (let i = 0; i < bundled.length; ++i) {
133+
ArrayPrototypePush(defaultCACertificates, bundled[i]);
134+
}
135+
if (getOptionValue('--use-system-ca')) {
136+
const system = cacheSystemCACertificates();
137+
for (let i = 0; i < system.length; ++i) {
138+
139+
ArrayPrototypePush(defaultCACertificates, system[i]);
140+
}
141+
}
142+
}
143+
144+
if (process.env.NODE_EXTRA_CA_CERTS) {
145+
const extra = cacheExtraCACertificates();
146+
for (let i = 0; i < extra.length; ++i) {
147+
148+
ArrayPrototypePush(defaultCACertificates, extra[i]);
149+
}
150+
}
151+
152+
ObjectFreeze(defaultCACertificates);
153+
return defaultCACertificates;
154+
}
155+
156+
// TODO(joyeecheung): support X509Certificate output?
157+
function getCACertificates(type = 'default') {
158+
validateString(type, 'type');
159+
160+
switch (type) {
161+
case 'default':
162+
return cacheDefaultCACertificates();
163+
case 'bundled':
164+
return cacheBundledRootCertificates();
165+
case 'system':
166+
return cacheSystemCACertificates();
167+
case 'extra':
168+
return cacheExtraCACertificates();
169+
default:
170+
throw new ERR_INVALID_ARG_VALUE('type', type);
171+
}
172+
}
173+
exports.getCACertificates = getCACertificates;
174+
105175
// Convert protocols array into valid OpenSSL protocols list
106176
// ("\x06spdy/2\x08http/1.1\x08http/1.0")
107177
function convertProtocols(protocols) {

src/crypto/crypto_context.cc

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ using ncrypto::MarkPopErrorOnReturn;
4242
using ncrypto::SSLPointer;
4343
using ncrypto::StackOfX509;
4444
using ncrypto::X509Pointer;
45+
using ncrypto::X509View;
4546
using v8::Array;
4647
using v8::ArrayBufferView;
4748
using v8::Boolean;
@@ -57,7 +58,9 @@ using v8::Integer;
5758
using v8::Isolate;
5859
using v8::JustVoid;
5960
using v8::Local;
61+
using v8::LocalVector;
6062
using v8::Maybe;
63+
using v8::MaybeLocal;
6164
using v8::Nothing;
6265
using v8::Object;
6366
using v8::PropertyAttribute;
@@ -689,7 +692,7 @@ static std::vector<X509*> InitializeSystemStoreCertificates() {
689692
return system_store_certs;
690693
}
691694

692-
static std::vector<X509*>& GetSystemStoreRootCertificates() {
695+
static std::vector<X509*>& GetSystemStoreCACertificates() {
693696
// Use function-local static to guarantee thread safety.
694697
static std::vector<X509*> system_store_certs =
695698
InitializeSystemStoreCertificates();
@@ -761,7 +764,7 @@ X509_STORE* NewRootCertStore() {
761764
CHECK_EQ(1, X509_STORE_add_cert(store, cert));
762765
}
763766
if (per_process::cli_options->use_system_ca) {
764-
for (X509* cert : GetSystemStoreRootCertificates()) {
767+
for (X509* cert : GetSystemStoreCACertificates()) {
765768
CHECK_EQ(1, X509_STORE_add_cert(store, cert));
766769
}
767770
}
@@ -783,7 +786,7 @@ void CleanupCachedRootCertificates() {
783786
}
784787
}
785788
if (has_cached_system_root_certs.load()) {
786-
for (X509* cert : GetSystemStoreRootCertificates()) {
789+
for (X509* cert : GetSystemStoreCACertificates()) {
787790
X509_free(cert);
788791
}
789792
}
@@ -795,7 +798,7 @@ void CleanupCachedRootCertificates() {
795798
}
796799
}
797800

798-
void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {
801+
void GetBundledRootCertificates(const FunctionCallbackInfo<Value>& args) {
799802
Environment* env = Environment::GetCurrent(args);
800803
Local<Value> result[arraysize(root_certs)];
801804

@@ -812,6 +815,58 @@ void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {
812815
Array::New(env->isolate(), result, arraysize(root_certs)));
813816
}
814817

818+
MaybeLocal<Array> X509sToArrayOfStrings(Environment* env,
819+
const std::vector<X509*>& certs) {
820+
ClearErrorOnReturn clear_error_on_return;
821+
v8::EscapableHandleScope scope(env->isolate());
822+
823+
LocalVector<Value> result(env->isolate(), certs.size());
824+
for (size_t i = 0; i < certs.size(); ++i) {
825+
X509View view(certs[i]);
826+
auto pem_bio = view.toPEM();
827+
if (!pem_bio) {
828+
ThrowCryptoError(env, ERR_get_error(), "X509 to PEM conversion");
829+
return MaybeLocal<Array>();
830+
}
831+
832+
char* pem_data = nullptr;
833+
auto pem_size = BIO_get_mem_data(pem_bio.get(), &pem_data);
834+
if (pem_size <= 0 || !pem_data) {
835+
ThrowCryptoError(env, ERR_get_error(), "Reading PEM data");
836+
return MaybeLocal<Array>();
837+
}
838+
// PEM is base64-encoded, so it must be one-byte.
839+
if (!String::NewFromOneByte(env->isolate(),
840+
reinterpret_cast<uint8_t*>(pem_data),
841+
v8::NewStringType::kNormal,
842+
pem_size)
843+
.ToLocal(&result[i])) {
844+
return MaybeLocal<Array>();
845+
}
846+
}
847+
return scope.Escape(Array::New(env->isolate(), result.data(), result.size()));
848+
}
849+
850+
void GetSystemCACertificates(const FunctionCallbackInfo<Value>& args) {
851+
Environment* env = Environment::GetCurrent(args);
852+
Local<Array> results;
853+
if (X509sToArrayOfStrings(env, GetSystemStoreCACertificates())
854+
.ToLocal(&results)) {
855+
args.GetReturnValue().Set(results);
856+
}
857+
}
858+
859+
void GetExtraCACertificates(const FunctionCallbackInfo<Value>& args) {
860+
Environment* env = Environment::GetCurrent(args);
861+
if (extra_root_certs_file.empty()) {
862+
return args.GetReturnValue().Set(Array::New(env->isolate()));
863+
}
864+
Local<Array> results;
865+
if (X509sToArrayOfStrings(env, GetExtraCACertificates()).ToLocal(&results)) {
866+
args.GetReturnValue().Set(results);
867+
}
868+
}
869+
815870
bool SecureContext::HasInstance(Environment* env, const Local<Value>& value) {
816871
return GetConstructorTemplate(env)->HasInstance(value);
817872
}
@@ -895,8 +950,14 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
895950
GetConstructorTemplate(env),
896951
SetConstructorFunctionFlag::NONE);
897952

953+
SetMethodNoSideEffect(context,
954+
target,
955+
"getBundledRootCertificates",
956+
GetBundledRootCertificates);
957+
SetMethodNoSideEffect(
958+
context, target, "getSystemCACertificates", GetSystemCACertificates);
898959
SetMethodNoSideEffect(
899-
context, target, "getRootCertificates", GetRootCertificates);
960+
context, target, "getExtraCACertificates", GetExtraCACertificates);
900961
}
901962

902963
void SecureContext::RegisterExternalReferences(
@@ -936,7 +997,9 @@ void SecureContext::RegisterExternalReferences(
936997

937998
registry->Register(CtxGetter);
938999

939-
registry->Register(GetRootCertificates);
1000+
registry->Register(GetBundledRootCertificates);
1001+
registry->Register(GetSystemCACertificates);
1002+
registry->Register(GetExtraCACertificates);
9401003
}
9411004

9421005
SecureContext* SecureContext::Create(Environment* env) {

test/common/tls.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
'use strict';
44
const crypto = require('crypto');
55
const net = require('net');
6+
const assert = require('assert');
67

78
exports.ccs = Buffer.from('140303000101', 'hex');
89

@@ -173,4 +174,16 @@ function P_hash(algo, secret, seed, size) {
173174
return result;
174175
}
175176

177+
exports.assertIsCAArray = function assertIsCAArray(certs) {
178+
assert(Array.isArray(certs));
179+
assert(certs.length > 0);
180+
181+
// The certificates looks PEM-encoded.
182+
for (const cert of certs) {
183+
const trimmed = cert.trim();
184+
assert.match(trimmed, /^-----BEGIN CERTIFICATE-----/);
185+
assert.match(trimmed, /-----END CERTIFICATE-----$/);
186+
}
187+
};
188+
176189
exports.TestTLSSocket = TestTLSSocket;

0 commit comments

Comments
 (0)