diff --git a/doc/api/tls.md b/doc/api/tls.md index 8ceb1350e11289..813953e9fee79b 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -2308,21 +2308,35 @@ const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...']; tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]); ``` -## `tls.getCACertificates([type])` +## `tls.getCACertificates([options])` - -* `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`: +changes: + - version: + - REPLACEME + pr-url: https://github.com/nodejs/node/pull/59349 + description: Added optional `options.type` parameter to `getCACertificates()`. +--> + +* `options` {string|Object|undefined} + Optional. If a string, it is treated as the `type` of certificates to return. + If an object, it may contain: + * `type` {string} The type of CA certificates to return. One of `"default"`, `"system"`, `"bundled"`, or `"extra"`. + **Default:** `"default"`. + * `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"`. * `"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, @@ -2494,7 +2508,7 @@ added: v0.11.3 [`tls.connect()`]: #tlsconnectoptions-callback [`tls.createSecureContext()`]: #tlscreatesecurecontextoptions [`tls.createServer()`]: #tlscreateserveroptions-secureconnectionlistener -[`tls.getCACertificates()`]: #tlsgetcacertificatestype +[`tls.getCACertificates()`]: #tlsgetcacertificatesoptions [`tls.getCiphers()`]: #tlsgetciphers [`tls.rootCertificates`]: #tlsrootcertificates [`x509.checkHost()`]: crypto.md#x509checkhostname-options @@ -2503,3 +2517,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 diff --git a/lib/tls.js b/lib/tls.js index 3ad4bf77086f4d..372eba6bcea083 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -60,7 +60,11 @@ const { Buffer } = require('buffer'); const { canonicalizeIP } = internalBinding('cares_wrap'); const tlsCommon = require('internal/tls/common'); const tlsWrap = require('internal/tls/wrap'); -const { validateString } = require('internal/validators'); +const { + validateOneOf, + validateString, +} = require('internal/validators'); +const { X509Certificate } = require('crypto'); // Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations // every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more @@ -164,8 +168,7 @@ function cacheDefaultCACertificates() { return defaultCACertificates; } -// TODO(joyeecheung): support X509Certificate output? -function getCACertificates(type = 'default') { +function getCACertificatesAsStrings(type = 'default') { validateString(type, 'type'); switch (type) { @@ -181,6 +184,44 @@ function getCACertificates(type = 'default') { throw new ERR_INVALID_ARG_VALUE('type', type); } } + +function getCACertificates(options = undefined) { + if (typeof options === 'string' || options === undefined) { + return getCACertificatesAsStrings(options); + } + + if (typeof options === 'object' && options !== null) { + const { + type = 'default', + format = 'pem', + } = options; + + validateString(type, 'type'); + validateOneOf(format, 'format', ['pem', 'der', 'x509', 'string', 'buffer']); + + const certs = getCACertificatesAsStrings(type); + + if (format === 'x509') { + return certs.map((cert) => new X509Certificate(cert)); + } + + if (format === 'pem' || format === 'string') { + return certs; + } + + const buffers = certs.map((cert) => { + const base64 = cert.replace(/(?:\s|-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----)+/g, ''); + return Buffer.from(base64, 'base64'); + }); + + if (format === 'der' || format === 'buffer') { + return buffers; + } + } + + throw new ERR_INVALID_ARG_TYPE('options', ['string', 'object'], options); +} + exports.getCACertificates = getCACertificates; function setDefaultCACertificates(certs) { diff --git a/test/parallel/test-tls-get-ca-certificates-default.js b/test/parallel/test-tls-get-ca-certificates-default.js index 29fb2a29a8cb33..91298c072149ca 100644 --- a/test/parallel/test-tls-get-ca-certificates-default.js +++ b/test/parallel/test-tls-get-ca-certificates-default.js @@ -13,8 +13,5 @@ const { assertIsCAArray } = require('../common/tls'); const certs = tls.getCACertificates(); assertIsCAArray(certs); -const certs2 = tls.getCACertificates('default'); -assert.strictEqual(certs, certs2); - // It's cached on subsequent accesses. assert.strictEqual(certs, tls.getCACertificates('default')); diff --git a/test/parallel/test-tls-get-ca-certificates-x509-option.js b/test/parallel/test-tls-get-ca-certificates-x509-option.js new file mode 100644 index 00000000000000..0f819df3f41f2f --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-x509-option.js @@ -0,0 +1,66 @@ +'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 certs = tls.getCACertificates({ type: 'default', format: 'x509' }); + assert.ok(Array.isArray(certs)); + assert.ok(certs.length > 0); + for (const cert of certs) { + assert.ok(cert instanceof X509Certificate); + } +} + +{ + const certs = tls.getCACertificates({ type: 'default', format: 'buffer' }); + assert.ok(Array.isArray(certs)); + assert.ok(certs.length > 0); + for (const cert of certs) { + assert.ok(Buffer.isBuffer(cert)); + } +} + +{ + const certs = tls.getCACertificates({ type: 'default' }); + assert.ok(Array.isArray(certs)); + assert.ok(certs.length > 0); + for (const cert of certs) { + assert.strictEqual(typeof cert, 'string'); + assert.ok(cert.includes('-----BEGIN CERTIFICATE-----')); + } +} + +{ + assert.throws(() => { + tls.getCACertificates({ type: 'default', format: 'invalid' }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: /must be one of/ + }); +} + +{ + const certs = tls.getCACertificates({ 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({ type: 'invalid', format: 'buffer' }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "The argument 'type' is invalid. Received 'invalid'" + }); +}