node/lib/internal/tls/secure-context.js
Adam Majer 9cde7a033e
crypto: don't disable TLS 1.3 without suites
In the manual page, there is a statement that ciphersuites contain
explicit default settings - all TLS 1.3 ciphersuites enabled.
In node, we assume that an empty setting mean no ciphersuites and
we disable TLS 1.3. A correct approach to disabling TLS 1.3 is to
disable TLS 1.3 and by not override the default ciphersuits
with an empty string.

So, only override OpenSSL's TLS 1.3 ciphersuites with an explicit
list of ciphers. If none are acceptable, the correct approach is
to disable TLS 1.3 instead elsewhere.

Fixes: https://github.com/nodejs/node/issues/43419

PR-URL: https://github.com/nodejs/node/pull/43427
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: James M Snell <jasnell@gmail.com>
2022-06-27 09:47:13 +01:00

319 lines
9.0 KiB
JavaScript

'use strict';
const {
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypeForEach,
ArrayPrototypeJoin,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;
const {
codes: {
ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');
const {
kEmptyObject,
} = require('internal/util');
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');
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 !== undefined && 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 = kEmptyObject, 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];
const pem = (
val?.pem !== undefined ? val.pem : val);
const pass = (
val?.passphrase !== undefined ? val.passphrase : passphrase);
setKey(context, pem, pass, name);
}
} else {
setKey(context, key, passphrase, name);
}
}
if (sigalgs !== undefined && sigalgs !== null) {
validateString(sigalgs, `${name}.sigalgs`);
if (sigalgs === '')
throw new ERR_INVALID_ARG_VALUE(`${name}.sigalgs`, sigalgs);
context.setSigalgs(sigalgs);
}
if (privateKeyIdentifier !== undefined && privateKeyIdentifier !== null) {
if (privateKeyEngine === undefined || privateKeyEngine === null) {
// 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', 'null', 'undefined'],
privateKeyIdentifier);
} else {
throw new ERR_INVALID_ARG_TYPE(`${name}.privateKeyEngine`,
['string', 'null', 'undefined'],
privateKeyEngine);
}
}
if (ciphers !== undefined && 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`);
if (cipherSuites !== '')
context.setCipherSuites(cipherSuites);
context.setCiphers(cipherList);
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 && dhparam !== null) {
validateKeyOrCertOption(`${name}.dhparam`, dhparam);
const warning = context.setDHParam(dhparam);
if (warning)
process.emitWarning(warning, 'SecurityWarning');
}
if (crl !== undefined && crl !== null) {
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 && sessionIdContext !== null) {
validateString(sessionIdContext, `${name}.sessionIdContext`);
context.setSessionIdContext(sessionIdContext);
}
if (pfx !== undefined && pfx !== null) {
if (ArrayIsArray(pfx)) {
ArrayPrototypeForEach(pfx, (val) => {
const raw = val.buf ? val.buf : val;
const pass = val.passphrase || passphrase;
if (pass !== undefined && pass !== null) {
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 (typeof clientCertEngine === 'string') {
if (typeof context.setClientCertEngine !== 'function')
throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED();
else
context.setClientCertEngine(clientCertEngine);
} else if (clientCertEngine !== undefined && clientCertEngine !== null) {
throw new ERR_INVALID_ARG_TYPE(`${name}.clientCertEngine`,
['string', 'null', 'undefined'],
clientCertEngine);
}
if (ticketKeys !== undefined && ticketKeys !== null) {
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 && sessionTimeout !== null) {
validateInt32(sessionTimeout, `${name}.sessionTimeout`);
context.setSessionTimeout(sessionTimeout);
}
}
module.exports = {
configSecureContext,
};