From eb20447407d428dbb239ca0765ba6273975ed663 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 6 Apr 2021 13:29:09 -0700 Subject: [PATCH] tls: extract out SecureContext configuration In preparation for re-introducing quic, refactor out a reusable piece of the SecureContext configuration that will also be used by the quic impl. Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/38116 Reviewed-By: Anna Henningsen Reviewed-By: Matteo Collina --- lib/_tls_common.js | 323 +++++--------------------------------------- lib/internal/tls.js | 309 +++++++++++++++++++++++++++++++++++++++++- lib/tls.js | 2 +- 3 files changed, 342 insertions(+), 292 deletions(-) diff --git a/lib/_tls_common.js b/lib/_tls_common.js index 5b13d8c79bc..5ca6d65181d 100644 --- a/lib/_tls_common.js +++ b/lib/_tls_common.js @@ -21,45 +21,39 @@ 'use strict'; +const tls = require('tls'); + const { - ArrayIsArray, - ArrayPrototypeFilter, - ArrayPrototypeForEach, - ArrayPrototypeJoin, ArrayPrototypePush, ObjectCreate, StringPrototypeReplace, - StringPrototypeSplit, - StringPrototypeStartsWith, } = primordials; -const { parseCertString } = require('internal/tls'); -const { isArrayBufferView } = require('internal/util/types'); -const tls = require('tls'); const { - ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - ERR_TLS_INVALID_PROTOCOL_VERSION, - ERR_TLS_PROTOCOL_VERSION_CONFLICT, -} = require('internal/errors').codes; + codes: { + ERR_TLS_INVALID_PROTOCOL_VERSION, + ERR_TLS_PROTOCOL_VERSION_CONFLICT, + }, +} = require('internal/errors'); + const { - SSL_OP_CIPHER_SERVER_PREFERENCE, - TLS1_VERSION, - TLS1_1_VERSION, - TLS1_2_VERSION, - TLS1_3_VERSION, -} = internalBinding('constants').crypto; + crypto: { + SSL_OP_CIPHER_SERVER_PREFERENCE, + TLS1_VERSION, + TLS1_1_VERSION, + TLS1_2_VERSION, + TLS1_3_VERSION, + }, +} = internalBinding('constants'); const { - validateString, validateInteger, - validateInt32, } = require('internal/validators'); const { - toBuf -} = require('internal/crypto/util'); + configSecureContext, + parseCertString, +} = require('internal/tls'); function toV(which, v, def) { if (v == null) v = def; @@ -70,7 +64,10 @@ function toV(which, v, def) { throw new ERR_TLS_INVALID_PROTOCOL_VERSION(v, which); } -const { SecureContext: NativeSecureContext } = internalBinding('crypto'); +const { + SecureContext: NativeSecureContext, +} = internalBinding('crypto'); + function SecureContext(secureProtocol, secureOptions, minVersion, maxVersion) { if (!(this instanceof SecureContext)) { return new SecureContext(secureProtocol, secureOptions, minVersion, @@ -95,93 +92,14 @@ function SecureContext(secureProtocol, secureOptions, minVersion, maxVersion) { } } -function validateKeyOrCertOption(name, value) { - if (typeof value !== 'string' && !isArrayBufferView(value)) { - throw new ERR_INVALID_ARG_TYPE( - `options.${name}`, - ['string', 'Buffer', 'TypedArray', 'DataView'], - value - ); - } -} - -exports.SecureContext = SecureContext; - -function setKey(context, key, passphrase) { - validateKeyOrCertOption('key', key); - if (passphrase != null) - validateString(passphrase, 'options.passphrase'); - context.setKey(key, passphrase); -} - -function processCiphers(ciphers) { - ciphers = StringPrototypeSplit(ciphers || tls.DEFAULT_CIPHERS, ':'); - - const cipherList = - ArrayPrototypeJoin( - ArrayPrototypeFilter( - ciphers, - (cipher) => { - return cipher.length > 0 && - !StringPrototypeStartsWith(cipher, 'TLS_'); - }), ':'); - - const cipherSuites = - ArrayPrototypeJoin( - ArrayPrototypeFilter( - ciphers, - (cipher) => { - return cipher.length > 0 && - StringPrototypeStartsWith(cipher, 'TLS_'); - }), ':'); - - // Specifying empty cipher suites for both TLS1.2 and TLS1.3 is invalid, its - // not possible to handshake with no suites. - if (cipherSuites === '' && cipherList === '') - throw new ERR_INVALID_ARG_VALUE('options.ciphers', ciphers); - - return { cipherList, cipherSuites }; -} - -function addCACerts(context, certs) { - ArrayPrototypeForEach(certs, (cert) => { - validateKeyOrCertOption('ca', cert); - context.addCACert(cert); - }); -} - -function setCerts(context, certs) { - ArrayPrototypeForEach(certs, (cert) => { - validateKeyOrCertOption('cert', cert); - context.setCert(cert); - }); -} - -exports.createSecureContext = function createSecureContext(options) { +function createSecureContext(options) { if (!options) options = {}; const { - ca, - cert, - ciphers, - clientCertEngine, - crl, - dhparam, - ecdhCurve = tls.DEFAULT_ECDH_CURVE, honorCipherOrder, - key, minVersion, maxVersion, - passphrase, - pfx, - privateKeyIdentifier, - privateKeyEngine, secureProtocol, - sessionIdContext, - sessionTimeout, - sigalgs, - singleUse, - ticketKeys, } = options; let { secureOptions } = options; @@ -192,196 +110,15 @@ exports.createSecureContext = function createSecureContext(options) { const c = new SecureContext(secureProtocol, secureOptions, minVersion, maxVersion); - // Add CA before the cert to be able to load cert's issuer in C++ code. - // NOTE(@jasnell): ca, cert, and key are permitted to be falsy, so do not - // change the checks to !== undefined checks. - if (ca) { - if (ArrayIsArray(ca)) - addCACerts(c.context, ca); - else - addCACerts(c.context, [ca]); - } else { - c.context.addRootCerts(); - } - - if (cert) { - if (ArrayIsArray(cert)) - setCerts(c.context, cert); - else - setCerts(c.context, [cert]); - } - - // Set the key after the cert. - // `ssl_set_pkey` returns `0` when the key does not match the cert, but - // `ssl_set_cert` returns `1` and nullifies the key in the SSL structure - // which leads to the crash later on. - if (key) { - if (ArrayIsArray(key)) { - for (let i = 0; i < key.length; ++i) { - const val = key[i]; - // eslint-disable-next-line eqeqeq - const pem = (val != undefined && val.pem !== undefined ? val.pem : val); - setKey(c.context, pem, val.passphrase || passphrase); - } - } else { - setKey(c.context, key, passphrase); - } - } - - if (sigalgs !== undefined) { - validateString(sigalgs, 'options.sigalgs'); - - if (sigalgs === '') - throw new ERR_INVALID_ARG_VALUE('options.sigalgs', sigalgs); - - c.context.setSigalgs(sigalgs); - } - - if (privateKeyIdentifier !== undefined) { - if (privateKeyEngine === undefined) { - // Engine is required when privateKeyIdentifier is present - throw new ERR_INVALID_ARG_VALUE('options.privateKeyEngine', - privateKeyEngine); - } - if (key) { - // Both data key and engine key can't be set at the same time - throw new ERR_INVALID_ARG_VALUE('options.privateKeyIdentifier', - privateKeyIdentifier); - } - - if (typeof privateKeyIdentifier === 'string' && - typeof privateKeyEngine === 'string') { - if (c.context.setEngineKey) - c.context.setEngineKey(privateKeyIdentifier, privateKeyEngine); - else - throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED(); - } else if (typeof privateKeyIdentifier !== 'string') { - throw new ERR_INVALID_ARG_TYPE('options.privateKeyIdentifier', - ['string', 'undefined'], - privateKeyIdentifier); - } else { - throw new ERR_INVALID_ARG_TYPE('options.privateKeyEngine', - ['string', 'undefined'], - privateKeyEngine); - } - } - - if (ciphers != null) - validateString(ciphers, 'options.ciphers'); - - // Work around an OpenSSL API quirk. cipherList is for TLSv1.2 and below, - // cipherSuites is for TLSv1.3 (and presumably any later versions). TLSv1.3 - // cipher suites all have a standard name format beginning with TLS_, so split - // the ciphers and pass them to the appropriate API. - const { cipherList, cipherSuites } = processCiphers(ciphers); - - c.context.setCipherSuites(cipherSuites); - c.context.setCiphers(cipherList); - - if (cipherSuites === '' && - c.context.getMaxProto() > TLS1_2_VERSION && - c.context.getMinProto() < TLS1_3_VERSION) { - c.context.setMaxProto(TLS1_2_VERSION); - } - - if (cipherList === '' && - c.context.getMinProto() < TLS1_3_VERSION && - c.context.getMaxProto() > TLS1_2_VERSION) { - c.context.setMinProto(TLS1_3_VERSION); - } - - validateString(ecdhCurve, 'options.ecdhCurve'); - c.context.setECDHCurve(ecdhCurve); - - if (dhparam !== undefined) { - validateKeyOrCertOption('dhparam', dhparam); - const warning = c.context.setDHParam(dhparam); - if (warning) - process.emitWarning(warning, 'SecurityWarning'); - } - - if (crl !== undefined) { - if (ArrayIsArray(crl)) { - for (const val of crl) { - validateKeyOrCertOption('crl', val); - c.context.addCRL(val); - } - } else { - validateKeyOrCertOption('crl', crl); - c.context.addCRL(crl); - } - } - - if (sessionIdContext !== undefined) { - validateString(sessionIdContext, 'options.sessionIdContext'); - c.context.setSessionIdContext(sessionIdContext); - } - - if (pfx !== undefined) { - if (ArrayIsArray(pfx)) { - ArrayPrototypeForEach(pfx, (val) => { - const raw = val.buf ? val.buf : val; - const pass = val.passphrase || passphrase; - if (pass !== undefined) { - c.context.loadPKCS12(toBuf(raw), toBuf(pass)); - } else { - c.context.loadPKCS12(toBuf(raw)); - } - }); - } else if (passphrase) { - c.context.loadPKCS12(toBuf(pfx), toBuf(passphrase)); - } else { - c.context.loadPKCS12(toBuf(pfx)); - } - } - - // Do not keep read/write buffers in free list for OpenSSL < 1.1.0. (For - // OpenSSL 1.1.0, buffers are malloced and freed without the use of a - // freelist.) - if (singleUse) { - c.singleUse = true; - c.context.setFreeListLength(0); - } - - if (clientCertEngine !== undefined) { - if (typeof c.context.setClientCertEngine !== 'function') - throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED(); - if (typeof clientCertEngine !== 'string') { - throw new ERR_INVALID_ARG_TYPE('options.clientCertEngine', - ['string', 'null', 'undefined'], - clientCertEngine); - } - c.context.setClientCertEngine(clientCertEngine); - } - - if (ticketKeys !== undefined) { - if (!isArrayBufferView(ticketKeys)) { - throw new ERR_INVALID_ARG_TYPE( - 'options.ticketKeys', - ['Buffer', 'TypedArray', 'DataView'], - ticketKeys); - } - if (ticketKeys.byteLength !== 48) { - throw new ERR_INVALID_ARG_VALUE( - 'options.ticketKeys', - ticketKeys.byteLength, - 'must be exactly 48 bytes'); - } - c.context.setTicketKeys(ticketKeys); - } - - if (sessionTimeout !== undefined) { - validateInt32(sessionTimeout, 'options.sessionTimeout'); - c.context.setSessionTimeout(sessionTimeout); - } + configSecureContext(c.context, options); return c; -}; +} // Translate some fields from the handle's C-friendly format into more idiomatic // javascript object representations before passing them back to the user. Can // be used on any cert object, but changing the name would be semver-major. -exports.translatePeerCertificate = function translatePeerCertificate(c) { +function translatePeerCertificate(c) { if (!c) return null; @@ -404,4 +141,10 @@ exports.translatePeerCertificate = function translatePeerCertificate(c) { }); } return c; +} + +module.exports = { + SecureContext, + createSecureContext, + translatePeerCertificate, }; diff --git a/lib/internal/tls.js b/lib/internal/tls.js index 57366839c17..0ebecb57c88 100644 --- a/lib/internal/tls.js +++ b/lib/internal/tls.js @@ -2,14 +2,46 @@ const { ArrayIsArray, + ArrayPrototypeFilter, ArrayPrototypeForEach, + ArrayPrototypeJoin, ArrayPrototypePush, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeSplit, + StringPrototypeStartsWith, ObjectCreate, } = primordials; +const { + codes: { + ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + }, +} = require('internal/errors'); + +const { + isArrayBufferView, +} = require('internal/util/types'); + +const { + validateInt32, + validateObject, + validateString, +} = require('internal/validators'); + +const { + toBuf, +} = require('internal/crypto/util'); + +const { + crypto: { + TLS1_2_VERSION, + TLS1_3_VERSION, + }, +} = internalBinding('constants'); + // Example: // C=US\nST=CA\nL=SF\nO=Joyent\nOU=Node.js\nCN=ca1\nemailAddress=ry@clouds.org function parseCertString(s) { @@ -32,6 +64,281 @@ function parseCertString(s) { return out; } +function getDefaultEcdhCurve() { + // We do it this way because DEFAULT_ECDH_CURVE can be + // changed by users, so we need to grab the current + // value, but we want the evaluation to be lazy. + return require('tls').DEFAULT_ECDH_CURVE || 'auto'; +} + +function getDefaultCiphers() { + // We do it this way because DEFAULT_CIPHERS can be + // changed by users, so we need to grab the current + // value, but we want the evaluation to be lazy. + return require('tls').DEFAULT_CIPHERS; +} + +function addCACerts(context, certs, name) { + ArrayPrototypeForEach(certs, (cert) => { + validateKeyOrCertOption(name, cert); + context.addCACert(cert); + }); +} + +function setCerts(context, certs, name) { + ArrayPrototypeForEach(certs, (cert) => { + validateKeyOrCertOption(name, cert); + context.setCert(cert); + }); +} + +function validateKeyOrCertOption(name, value) { + if (typeof value !== 'string' && !isArrayBufferView(value)) { + throw new ERR_INVALID_ARG_TYPE( + name, + [ + 'string', + 'Buffer', + 'TypedArray', + 'DataView', + ], + value + ); + } +} + +function setKey(context, key, passphrase, name) { + validateKeyOrCertOption(`${name}.key`, key); + if (passphrase != null) + validateString(passphrase, `${name}.passphrase`); + context.setKey(key, passphrase); +} + +function processCiphers(ciphers, name) { + ciphers = StringPrototypeSplit(ciphers || getDefaultCiphers(), ':'); + + const cipherList = + ArrayPrototypeJoin( + ArrayPrototypeFilter( + ciphers, + (cipher) => { + return cipher.length > 0 && + !StringPrototypeStartsWith(cipher, 'TLS_'); + }), ':'); + + const cipherSuites = + ArrayPrototypeJoin( + ArrayPrototypeFilter( + ciphers, + (cipher) => { + return cipher.length > 0 && + StringPrototypeStartsWith(cipher, 'TLS_'); + }), ':'); + + // Specifying empty cipher suites for both TLS1.2 and TLS1.3 is invalid, its + // not possible to handshake with no suites. + if (cipherSuites === '' && cipherList === '') + throw new ERR_INVALID_ARG_VALUE(name, ciphers); + + return { cipherList, cipherSuites }; +} + +function configSecureContext(context, options = {}, name = 'options') { + validateObject(options, name); + + const { + ca, + cert, + ciphers = getDefaultCiphers(), + clientCertEngine, + crl, + dhparam, + ecdhCurve = getDefaultEcdhCurve(), + key, + passphrase, + pfx, + privateKeyIdentifier, + privateKeyEngine, + sessionIdContext, + sessionTimeout, + sigalgs, + ticketKeys, + } = options; + + // Add CA before the cert to be able to load cert's issuer in C++ code. + // NOTE(@jasnell): ca, cert, and key are permitted to be falsy, so do not + // change the checks to !== undefined checks. + if (ca) { + addCACerts(context, ArrayIsArray(ca) ? ca : [ca], `${name}.ca`); + } else { + context.addRootCerts(); + } + + if (cert) { + setCerts(context, ArrayIsArray(cert) ? cert : [cert], `${name}.cert`); + } + + // Set the key after the cert. + // `ssl_set_pkey` returns `0` when the key does not match the cert, but + // `ssl_set_cert` returns `1` and nullifies the key in the SSL structure + // which leads to the crash later on. + if (key) { + if (ArrayIsArray(key)) { + for (let i = 0; i < key.length; ++i) { + const val = key[i]; + // eslint-disable-next-line eqeqeq + const pem = (val != undefined && val.pem !== undefined ? val.pem : val); + setKey(context, pem, val.passphrase || passphrase, name); + } + } else { + setKey(context, key, passphrase, name); + } + } + + if (sigalgs !== undefined) { + validateString(sigalgs, `${name}.sigalgs`); + + if (sigalgs === '') + throw new ERR_INVALID_ARG_VALUE(`${name}.sigalgs`, sigalgs); + + context.setSigalgs(sigalgs); + } + + if (privateKeyIdentifier !== undefined) { + if (privateKeyEngine === undefined) { + // Engine is required when privateKeyIdentifier is present + throw new ERR_INVALID_ARG_VALUE(`${name}.privateKeyEngine`, + privateKeyEngine); + } + if (key) { + // Both data key and engine key can't be set at the same time + throw new ERR_INVALID_ARG_VALUE(`${name}.privateKeyIdentifier`, + privateKeyIdentifier); + } + + if (typeof privateKeyIdentifier === 'string' && + typeof privateKeyEngine === 'string') { + if (context.setEngineKey) + context.setEngineKey(privateKeyIdentifier, privateKeyEngine); + else + throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED(); + } else if (typeof privateKeyIdentifier !== 'string') { + throw new ERR_INVALID_ARG_TYPE(`${name}.privateKeyIdentifier`, + ['string', 'undefined'], + privateKeyIdentifier); + } else { + throw new ERR_INVALID_ARG_TYPE(`${name}.privateKeyEngine`, + ['string', 'undefined'], + privateKeyEngine); + } + } + + if (ciphers != null) + validateString(ciphers, `${name}.ciphers`); + + // Work around an OpenSSL API quirk. cipherList is for TLSv1.2 and below, + // cipherSuites is for TLSv1.3 (and presumably any later versions). TLSv1.3 + // cipher suites all have a standard name format beginning with TLS_, so split + // the ciphers and pass them to the appropriate API. + const { + cipherList, + cipherSuites, + } = processCiphers(ciphers, `${name}.ciphers`); + + context.setCipherSuites(cipherSuites); + context.setCiphers(cipherList); + + if (cipherSuites === '' && + context.getMaxProto() > TLS1_2_VERSION && + context.getMinProto() < TLS1_3_VERSION) { + context.setMaxProto(TLS1_2_VERSION); + } + + if (cipherList === '' && + context.getMinProto() < TLS1_3_VERSION && + context.getMaxProto() > TLS1_2_VERSION) { + context.setMinProto(TLS1_3_VERSION); + } + + validateString(ecdhCurve, `${name}.ecdhCurve`); + context.setECDHCurve(ecdhCurve); + + if (dhparam !== undefined) { + validateKeyOrCertOption(`${name}.dhparam`, dhparam); + const warning = context.setDHParam(dhparam); + if (warning) + process.emitWarning(warning, 'SecurityWarning'); + } + + if (crl !== undefined) { + if (ArrayIsArray(crl)) { + for (const val of crl) { + validateKeyOrCertOption(`${name}.crl`, val); + context.addCRL(val); + } + } else { + validateKeyOrCertOption(`${name}.crl`, crl); + context.addCRL(crl); + } + } + + if (sessionIdContext !== undefined) { + validateString(sessionIdContext, `${name}.sessionIdContext`); + context.setSessionIdContext(sessionIdContext); + } + + if (pfx !== undefined) { + if (ArrayIsArray(pfx)) { + ArrayPrototypeForEach(pfx, (val) => { + const raw = val.buf ? val.buf : val; + const pass = val.passphrase || passphrase; + if (pass !== undefined) { + context.loadPKCS12(toBuf(raw), toBuf(pass)); + } else { + context.loadPKCS12(toBuf(raw)); + } + }); + } else if (passphrase) { + context.loadPKCS12(toBuf(pfx), toBuf(passphrase)); + } else { + context.loadPKCS12(toBuf(pfx)); + } + } + + if (clientCertEngine !== undefined) { + if (typeof context.setClientCertEngine !== 'function') + throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED(); + if (typeof clientCertEngine !== 'string') { + throw new ERR_INVALID_ARG_TYPE(`${name}.clientCertEngine`, + ['string', 'null', 'undefined'], + clientCertEngine); + } + context.setClientCertEngine(clientCertEngine); + } + + if (ticketKeys !== undefined) { + if (!isArrayBufferView(ticketKeys)) { + throw new ERR_INVALID_ARG_TYPE( + `${name}.ticketKeys`, + ['Buffer', 'TypedArray', 'DataView'], + ticketKeys); + } + if (ticketKeys.byteLength !== 48) { + throw new ERR_INVALID_ARG_VALUE( + `${name}.ticketKeys`, + ticketKeys.byteLength, + 'must be exactly 48 bytes'); + } + context.setTicketKeys(ticketKeys); + } + + if (sessionTimeout !== undefined) { + validateInt32(sessionTimeout, `${name}.sessionTimeout`); + context.setSessionTimeout(sessionTimeout); + } +} + module.exports = { - parseCertString + configSecureContext, + parseCertString, }; diff --git a/lib/tls.js b/lib/tls.js index eae34a42391..2282fd33008 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -49,8 +49,8 @@ const { ERR_OUT_OF_RANGE } = require('internal/errors').codes; const internalUtil = require('internal/util'); -const internalTLS = require('internal/tls'); internalUtil.assertCrypto(); +const internalTLS = require('internal/tls'); const { isArrayBufferView } = require('internal/util/types'); const net = require('net');