diff --git a/doc/api/tls.md b/doc/api/tls.md index c8e12eaacf62..e69e6cf348f2 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -2417,19 +2417,34 @@ const additionalCerts = ['-----BEGIN CERTIFICATE-----\n...']; tls.setDefaultCACertificates([...currentCerts, ...additionalCerts]); ``` -## `tls.getCACertificates([type])` +## `tls.getCACertificates([type][, options])` -* `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`: @@ -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 @@ -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 diff --git a/lib/tls.js b/lib/tls.js index a1d978c50f27..a0a37881a281 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -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('crypto'); const { namespace: { @@ -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) { @@ -203,6 +203,39 @@ function getCACertificates(type = 'default') { throw new ERR_INVALID_ARG_VALUE('type', type); } } + +function getCACertificates(type = 'default', options) { + validateString(type, 'type'); + + 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) { diff --git a/test/parallel/test-tls-get-ca-certificates-bundled.js b/test/parallel/test-tls-get-ca-certificates-bundled.js index 5dbe254bcabd..9c6668a0e99b 100644 --- a/test/parallel/test-tls-get-ca-certificates-bundled.js +++ b/test/parallel/test-tls-get-ca-certificates-bundled.js @@ -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' })); diff --git a/test/parallel/test-tls-get-ca-certificates-default.js b/test/parallel/test-tls-get-ca-certificates-default.js index 29fb2a29a8cb..0937a33f87aa 100644 --- a/test/parallel/test-tls-get-ca-certificates-default.js +++ b/test/parallel/test-tls-get-ca-certificates-default.js @@ -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' })); diff --git a/test/parallel/test-tls-get-ca-certificates-system.js b/test/parallel/test-tls-get-ca-certificates-system.js index 0dfed80af92c..36d9b27853ef 100644 --- a/test/parallel/test-tls-get-ca-certificates-system.js +++ b/test/parallel/test-tls-get-ca-certificates-system.js @@ -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' })); 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 000000000000..421c66de3a7d --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-x509-option.js @@ -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/ + }); +}