Skip to content
Open
26 changes: 21 additions & 5 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -2417,19 +2417,34 @@ const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...'];
tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]);
```

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

<!-- YAML
added:
- v23.10.0
- v22.15.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59349
description: Added the `options` argument with `format` option.
-->

* `type` {string|undefined} The type of CA certificates that will be returned. Valid values
* `type` {string} 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.

* `options` {Object}
* `format` {string} The format of returned certificates. One of `"pem"`, `"der"`, or `"x509"`.
**Default:** `"pem"`.
* `"pem"` (alias: `"string"`): Returns an array of PEM-encoded certificate strings.
* `"der"` (alias: `"buffer"`): Returns an array of certificate data as `Buffer` objects in DER format.
* `"x509"`: Returns an array of [`X509Certificate`][x509certificate] instances.

* Returns: {Array}
An array of certificate data in the specified format:
* PEM strings when `format` is `"pem"` (or `"string"`).
* `Buffer` objects containing DER data when `format` is `"der"` (or `"buffer"`).
* [`X509Certificate`][x509certificate] instances when `format` is `"x509"`.

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

Expand Down Expand Up @@ -2630,7 +2645,7 @@ added: v0.11.3
[`tls.connect()`]: #tlsconnectoptions-callback
[`tls.createSecureContext()`]: #tlscreatesecurecontextoptions
[`tls.createServer()`]: #tlscreateserveroptions-secureconnectionlistener
[`tls.getCACertificates()`]: #tlsgetcacertificatestype
[`tls.getCACertificates()`]: #tlsgetcacertificatestype-options
[`tls.getCiphers()`]: #tlsgetciphers
[`tls.rootCertificates`]: #tlsrootcertificates
[`x509.checkHost()`]: crypto.md#x509checkhostname-options
Expand All @@ -2639,3 +2654,4 @@ added: v0.11.3
[cipher list format]: https://www.openssl.org/docs/man1.1.1/man1/ciphers.html#CIPHER-LIST-FORMAT
[forward secrecy]: https://en.wikipedia.org/wiki/Perfect_forward_secrecy
[perfect forward secrecy]: #perfect-forward-secrecy
[x509certificate]: crypto.md#class-x509certificate
37 changes: 34 additions & 3 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ const { canonicalizeIP } = internalBinding('cares_wrap');
const tlsCommon = require('internal/tls/common');
const tlsWrap = require('internal/tls/wrap');
const { domainToASCII } = require('internal/url');
const { validateArray, validateString } = require('internal/validators');
const { validateArray, validateOneOf, validateString } = require('internal/validators');
const { X509Certificate } = require('internal/crypto/x509');

const {
namespace: {
Expand Down Expand Up @@ -186,8 +187,7 @@ function cacheDefaultCACertificates() {
return defaultCACertificates;
}

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

switch (type) {
Expand All @@ -203,6 +203,37 @@ function getCACertificates(type = 'default') {
throw new ERR_INVALID_ARG_VALUE('type', type);
}
}

function getCACertificates(type = 'default', options) {
let format = 'pem';

if (options != null) {
if (typeof options !== 'object') {
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
}
({ format = 'pem' } = options);
}

validateOneOf(format, 'format', ['pem', 'der', 'x509', 'string', 'buffer']);

const certs = getCACertificatesAsStrings(type);

if (format === 'pem' || format === 'string') {
return certs;
}

if (format === 'x509') {
return certs.map((cert) => new X509Certificate(cert));
}

const buffers = certs.map((cert) => {
const base64 = cert.replace(/(?:\s|-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----)+/g, '');
return Buffer.from(base64, 'base64');
});

return buffers;
}

exports.getCACertificates = getCACertificates;

function setDefaultCACertificates(certs) {
Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-tls-get-ca-certificates-bundled.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ assert.strictEqual(certs, tls.rootCertificates);

// It's cached on subsequent accesses.
assert.strictEqual(certs, tls.getCACertificates('bundled'));

// The options form with format: 'pem' returns the same result.
assert.deepStrictEqual(certs, tls.getCACertificates('bundled', { format: 'pem' }));
3 changes: 3 additions & 0 deletions test/parallel/test-tls-get-ca-certificates-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ assert.strictEqual(certs, certs2);

// It's cached on subsequent accesses.
assert.strictEqual(certs, tls.getCACertificates('default'));

// The options form with format: 'pem' returns the same result.
assert.deepStrictEqual(certs, tls.getCACertificates('default', { format: 'pem' }));
3 changes: 3 additions & 0 deletions test/parallel/test-tls-get-ca-certificates-system.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ assert.deepStrictEqual(defaultSet.intersection(systemSet), systemSet);

// It's cached on subsequent accesses.
assert.strictEqual(systemCerts, tls.getCACertificates('system'));

// The options form with format: 'pem' returns the same result.
assert.deepStrictEqual(systemCerts, tls.getCACertificates('system', { format: 'pem' }));
72 changes: 72 additions & 0 deletions test/parallel/test-tls-get-ca-certificates-x509-option.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const tls = require('tls');
const { X509Certificate } = require('crypto');
const tlsCommon = require('../common/tls');

const expectedPems = tls.getCACertificates('default');

{
const certs = tls.getCACertificates('default', { format: 'x509' });
assert.strictEqual(certs.length, expectedPems.length);

const certsRaw = certs.map((c) => c.raw);
tlsCommon.assertEqualCerts(certsRaw, expectedPems);

for (const cert of certs) {
assert.ok(cert instanceof X509Certificate);
}
}

{
const certs = tls.getCACertificates('default', { format: 'buffer' });
assert.strictEqual(certs.length, expectedPems.length);
tlsCommon.assertEqualCerts(certs, expectedPems);

for (const cert of certs) {
assert.ok(Buffer.isBuffer(cert));
}
}

{
const certs = tls.getCACertificates('default');
assert.strictEqual(certs.length, expectedPems.length);
for (const cert of certs) {
assert.strictEqual(typeof cert, 'string');
assert.ok(cert.includes('-----BEGIN CERTIFICATE-----'));
}
}

{
assert.throws(() => {
tls.getCACertificates('default', { format: 'invalid' });
}, {
name: 'TypeError',
code: 'ERR_INVALID_ARG_VALUE',
message: /must be one of/
});
}

{
const certs = tls.getCACertificates(undefined, { format: 'buffer' });
assert.ok(Array.isArray(certs));
assert.ok(certs.length > 0);
for (const cert of certs) {
assert.ok(Buffer.isBuffer(cert));
}
}

{
assert.throws(() => {
tls.getCACertificates('invalid', { format: 'buffer' });
}, {
name: 'TypeError',
code: 'ERR_INVALID_ARG_VALUE',
message: /The argument 'type' is invalid/
});
}
Loading