Skip to content

Commit

Permalink
tls: extract out SecureContext configuration
Browse files Browse the repository at this point in the history
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 <jasnell@gmail.com>

PR-URL: nodejs#38116
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
  • Loading branch information
jasnell committed Apr 12, 2021
1 parent 5823fc7 commit eb20447
Show file tree
Hide file tree
Showing 3 changed files with 342 additions and 292 deletions.
323 changes: 33 additions & 290 deletions lib/_tls_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -404,4 +141,10 @@ exports.translatePeerCertificate = function translatePeerCertificate(c) {
});
}
return c;
}

module.exports = {
SecureContext,
createSecureContext,
translatePeerCertificate,
};
Loading

0 comments on commit eb20447

Please sign in to comment.