Skip to content

Commit

Permalink
src,crypto: refactoring of crypto_context, SecureContext
Browse files Browse the repository at this point in the history
Cleaup and improvement of crypto_context and SecureContext.

Signed-off-by: James M Snell <[email protected]>

PR-URL: #35665
Reviewed-By: Alba Mendez <[email protected]>
Reviewed-By: Tobias Nießen <[email protected]>
Reviewed-By: Rich Trott <[email protected]>
Reviewed-By: Franziska Hinkelmann <[email protected]>
  • Loading branch information
jasnell committed Nov 11, 2020
1 parent a8b8b8b commit 35274cb
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 287 deletions.
274 changes: 172 additions & 102 deletions lib/_tls_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@

const {
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypeJoin,
ObjectCreate,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;

const { parseCertString } = require('internal/tls');
Expand All @@ -44,8 +48,15 @@ const {
TLS1_3_VERSION,
} = internalBinding('constants').crypto;

// Lazily loaded from internal/crypto/util.
let toBuf = null;
const {
validateString,
validateInteger,
validateInt32,
} = require('internal/validators');

const {
toBuf
} = require('internal/crypto/util');

function toV(which, v, def) {
if (v == null) v = def;
Expand Down Expand Up @@ -75,7 +86,10 @@ function SecureContext(secureProtocol, secureOptions, minVersion, maxVersion) {
toV('minimum', minVersion, tls.DEFAULT_MIN_VERSION),
toV('maximum', maxVersion, tls.DEFAULT_MAX_VERSION));

if (secureOptions) this.context.setOptions(secureOptions);
if (secureOptions) {
validateInteger(secureOptions, 'secureOptions');
this.context.setOptions(secureOptions);
}
}

function validateKeyOrCertOption(name, value) {
Expand All @@ -90,80 +104,136 @@ function validateKeyOrCertOption(name, 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) {
for (const cert of certs) {
validateKeyOrCertOption('ca', cert);
context.addCACert(cert);
}
}

function setCerts(context, ...certs) {
for (const cert of certs) {
validateKeyOrCertOption('cert', cert);
context.setCert(cert);
}
}

exports.createSecureContext = function createSecureContext(options) {
if (!options) options = {};

let secureOptions = options.secureOptions;
if (options.honorCipherOrder)
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;

if (honorCipherOrder)
secureOptions |= SSL_OP_CIPHER_SERVER_PREFERENCE;

const c = new SecureContext(options.secureProtocol, secureOptions,
options.minVersion, options.maxVersion);
const c = new SecureContext(secureProtocol, secureOptions,
minVersion, maxVersion);

// Add CA before the cert to be able to load cert's issuer in C++ code.
const { ca } = options;
// 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)) {
for (const val of ca) {
validateKeyOrCertOption('ca', val);
c.context.addCACert(val);
}
} else {
validateKeyOrCertOption('ca', ca);
c.context.addCACert(ca);
}
if (ArrayIsArray(ca))
addCACerts(c.context, ...ca);
else
addCACerts(c.context, ca);
} else {
c.context.addRootCerts();
}

const { cert } = options;
if (cert) {
if (ArrayIsArray(cert)) {
for (const val of cert) {
validateKeyOrCertOption('cert', val);
c.context.setCert(val);
}
} else {
validateKeyOrCertOption('cert', cert);
c.context.setCert(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.
const key = options.key;
const passphrase = options.passphrase;
if (key) {
if (ArrayIsArray(key)) {
for (const val of key) {
// eslint-disable-next-line eqeqeq
const pem = (val != undefined && val.pem !== undefined ? val.pem : val);
validateKeyOrCertOption('key', pem);
c.context.setKey(pem, val.passphrase || passphrase);
setKey(c.context, pem, val.passphrase || passphrase);
}
} else {
validateKeyOrCertOption('key', key);
c.context.setKey(key, passphrase);
setKey(c.context, key, passphrase);
}
}

const sigalgs = options.sigalgs;
if (sigalgs !== undefined) {
if (typeof sigalgs !== 'string') {
if (typeof sigalgs !== 'string')
throw new ERR_INVALID_ARG_TYPE('options.sigalgs', 'string', sigalgs);
}

if (sigalgs === '') {
if (sigalgs === '')
throw new ERR_INVALID_ARG_VALUE('options.sigalgs', sigalgs);
}

c.context.setSigalgs(sigalgs);
}

const { privateKeyIdentifier, privateKeyEngine } = options;
if (privateKeyIdentifier !== undefined) {
if (privateKeyEngine === undefined) {
// Engine is required when privateKeyIdentifier is present
Expand Down Expand Up @@ -193,113 +263,113 @@ exports.createSecureContext = function createSecureContext(options) {
}
}

if (options.ciphers && typeof options.ciphers !== 'string') {
throw new ERR_INVALID_ARG_TYPE(
'options.ciphers', 'string', options.ciphers);
}
if (ciphers !== undefined)
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 ciphers = (options.ciphers || tls.DEFAULT_CIPHERS).split(':');
const cipherList = ciphers.filter((_) => !_.match(/^TLS_/) &&
_.length > 0).join(':');
const cipherSuites = ciphers.filter((_) => _.match(/^TLS_/)).join(':');

if (cipherSuites === '' && cipherList === '') {
// Specifying empty cipher suites for both TLS1.2 and TLS1.3 is invalid, its
// not possible to handshake with no suites.
throw new ERR_INVALID_ARG_VALUE('options.ciphers', ciphers);
}
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)
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)
if (cipherList === '' &&
c.context.getMinProto() < TLS1_3_VERSION &&
c.context.getMaxProto() > TLS1_2_VERSION) {
c.context.setMinProto(TLS1_3_VERSION);
}

if (options.ecdhCurve === undefined)
c.context.setECDHCurve(tls.DEFAULT_ECDH_CURVE);
else if (options.ecdhCurve)
c.context.setECDHCurve(options.ecdhCurve);
validateString(ecdhCurve, 'options.ecdhCurve');
c.context.setECDHCurve(ecdhCurve);

if (options.dhparam) {
const warning = c.context.setDHParam(options.dhparam);
if (dhparam !== undefined) {
validateKeyOrCertOption('dhparam', dhparam);
const warning = c.context.setDHParam(dhparam);
if (warning)
process.emitWarning(warning, 'SecurityWarning');
}

if (options.crl) {
if (ArrayIsArray(options.crl)) {
for (const crl of options.crl) {
c.context.addCRL(crl);
if (crl !== undefined) {
if (ArrayIsArray(crl)) {
for (const val of crl) {
validateKeyOrCertOption('crl', val);
c.context.addCRL(val);
}
} else {
c.context.addCRL(options.crl);
validateKeyOrCertOption('crl', crl);
c.context.addCRL(crl);
}
}

if (options.sessionIdContext) {
c.context.setSessionIdContext(options.sessionIdContext);
if (sessionIdContext !== undefined) {
validateString(sessionIdContext, 'options.sessionIdContext');
c.context.setSessionIdContext(sessionIdContext);
}

if (options.pfx) {
if (!toBuf)
toBuf = require('internal/crypto/util').toBuf;

if (ArrayIsArray(options.pfx)) {
for (const pfx of options.pfx) {
const raw = pfx.buf ? pfx.buf : pfx;
const buf = toBuf(raw);
const passphrase = pfx.passphrase || options.passphrase;
if (passphrase) {
c.context.loadPKCS12(buf, toBuf(passphrase));
if (pfx !== undefined) {
if (ArrayIsArray(pfx)) {
for (const val of pfx) {
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(buf);
c.context.loadPKCS12(toBuf(raw));
}
}
} else if (passphrase) {
c.context.loadPKCS12(toBuf(pfx), toBuf(passphrase));
} else {
const buf = toBuf(options.pfx);
const passphrase = options.passphrase;
if (passphrase) {
c.context.loadPKCS12(buf, toBuf(passphrase));
} else {
c.context.loadPKCS12(buf);
}
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 (options.singleUse) {
if (singleUse) {
c.singleUse = true;
c.context.setFreeListLength(0);
}

if (typeof options.clientCertEngine === 'string') {
if (c.context.setClientCertEngine)
c.context.setClientCertEngine(options.clientCertEngine);
else
if (clientCertEngine !== undefined) {
if (typeof c.context.setClientCertEngine !== 'function')
throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED();
} else if (options.clientCertEngine != null) {
throw new ERR_INVALID_ARG_TYPE('options.clientCertEngine',
['string', 'null', 'undefined'],
options.clientCertEngine);
if (typeof clientCertEngine !== 'string') {
throw new ERR_INVALID_ARG_TYPE('options.clientCertEngine',
['string', 'null', 'undefined'],
clientCertEngine);
}
c.context.setClientCertEngine(clientCertEngine);
}

if (options.ticketKeys) {
c.context.setTicketKeys(options.ticketKeys);
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.byteLenth,
'must be exactly 48 bytes');
}
c.context.setTicketKeys(ticketKeys);
}

if (options.sessionTimeout) {
c.context.setSessionTimeout(options.sessionTimeout);
if (sessionTimeout !== undefined) {
validateInt32(sessionTimeout, 'options.sessionTimeout');
c.context.setSessionTimeout(sessionTimeout);
}

return c;
Expand Down
Loading

0 comments on commit 35274cb

Please sign in to comment.