summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorSam Roberts <vieuxtech@gmail.com>2018-11-28 17:58:08 -0800
committerSam Roberts <vieuxtech@gmail.com>2019-03-20 07:48:25 -0700
commit42dbaed4605f44c393a057aad75a31cac1d0e5f5 (patch)
tree096554b95dfb14cef568bfe898018d9bb874305c /lib
parent4306300b5ea8d8c4ff3daf64c7ed5fd64055ec2f (diff)
downloadnode-new-42dbaed4605f44c393a057aad75a31cac1d0e5f5.tar.gz
tls: support TLSv1.3
This introduces TLS1.3 support and makes it the default max protocol, but also supports CLI/NODE_OPTIONS switches to disable it if necessary. TLS1.3 is a major update to the TLS protocol, with many security enhancements. It should be preferred over TLS1.2 whenever possible. TLS1.3 is different enough that even though the OpenSSL APIs are technically API/ABI compatible, that when TLS1.3 is negotiated, the timing of protocol records and of callbacks broke assumptions hard-coded into the 'tls' module. This change introduces no API incompatibilities when TLS1.2 is negotiated. It is the intention that it be backported to current and LTS release lines with the default maximum TLS protocol reset to 'TLSv1.2'. This will allow users of those lines to explicitly enable TLS1.3 if they want. API incompatibilities between TLS1.2 and TLS1.3 are: - Renegotiation is not supported by TLS1.3 protocol, attempts to call `.renegotiate()` will always fail. - Compiling against a system OpenSSL lower than 1.1.1 is no longer supported (OpenSSL-1.1.0 used to be supported with configure flags). - Variations of `conn.write('data'); conn.destroy()` have undefined behaviour according to the streams API. They may or may not send the 'data', and may or may not cause a ERR_STREAM_DESTROYED error to be emitted. This has always been true, but conditions under which the write suceeds is slightly but observably different when TLS1.3 is negotiated vs when TLS1.2 or below is negotiated. - If TLS1.3 is negotiated, and a server calls `conn.end()` in its 'secureConnection' listener without any data being written, the client will not receive session tickets (no 'session' events will be emitted, and `conn.getSession()` will never return a resumable session). - The return value of `conn.getSession()` API may not return a resumable session if called right after the handshake. The effect will be that clients using the legacy `getSession()` API will resume sessions if TLS1.2 is negotiated, but will do full handshakes if TLS1.3 is negotiated. See https://github.com/nodejs/node/pull/25831 for more information. PR-URL: https://github.com/nodejs/node/pull/26209 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rod Vagg <rod@vagg.org>
Diffstat (limited to 'lib')
-rw-r--r--lib/_tls_common.js36
-rw-r--r--lib/_tls_wrap.js88
-rw-r--r--lib/internal/stream_base_commons.js4
-rw-r--r--lib/tls.js16
4 files changed, 125 insertions, 19 deletions
diff --git a/lib/_tls_common.js b/lib/_tls_common.js
index 7ddb0d4757..16e78a62cf 100644
--- a/lib/_tls_common.js
+++ b/lib/_tls_common.js
@@ -27,6 +27,7 @@ const tls = require('tls');
const {
ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED,
ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_OPT_VALUE,
ERR_TLS_INVALID_PROTOCOL_VERSION,
ERR_TLS_PROTOCOL_VERSION_CONFLICT,
} = require('internal/errors').codes;
@@ -35,6 +36,7 @@ const {
TLS1_VERSION,
TLS1_1_VERSION,
TLS1_2_VERSION,
+ TLS1_3_VERSION,
} = internalBinding('constants').crypto;
// Lazily loaded from internal/crypto/util.
@@ -45,6 +47,7 @@ function toV(which, v, def) {
if (v === 'TLSv1') return TLS1_VERSION;
if (v === 'TLSv1.1') return TLS1_1_VERSION;
if (v === 'TLSv1.2') return TLS1_2_VERSION;
+ if (v === 'TLSv1.3') return TLS1_3_VERSION;
throw new ERR_TLS_INVALID_PROTOCOL_VERSION(v, which);
}
@@ -148,10 +151,35 @@ exports.createSecureContext = function createSecureContext(options) {
}
}
- if (options.ciphers)
- c.context.setCiphers(options.ciphers);
- else
- c.context.setCiphers(tls.DEFAULT_CIPHERS);
+ if (options.ciphers && typeof options.ciphers !== 'string') {
+ throw new ERR_INVALID_ARG_TYPE(
+ 'options.ciphers', 'string', 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_/)).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 ERR_INVALID_OPT_VALUE('ciphers', 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);
if (options.ecdhCurve === undefined)
c.context.setECDHCurve(tls.DEFAULT_ECDH_CURVE);
diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js
index 18f0ca5900..e91e9f8aae 100644
--- a/lib/_tls_wrap.js
+++ b/lib/_tls_wrap.js
@@ -65,7 +65,7 @@ let ipServernameWarned = false;
// Server side times how long a handshake is taking to protect against slow
// handshakes being used for DoS.
function onhandshakestart(now) {
- debug('onhandshakestart');
+ debug('server onhandshakestart');
const { lastHandshakeTime } = this;
assert(now >= lastHandshakeTime,
@@ -83,6 +83,9 @@ function onhandshakestart(now) {
this.handshakes++;
const owner = this[owner_symbol];
+
+ assert(owner._tlsOptions.isServer);
+
if (this.handshakes > tls.CLIENT_RENEG_LIMIT) {
owner._emitTLSError(new ERR_TLS_SESSION_ATTACK());
return;
@@ -93,9 +96,10 @@ function onhandshakestart(now) {
}
function onhandshakedone() {
- debug('onhandshakedone');
+ debug('server onhandshakedone');
const owner = this[owner_symbol];
+ assert(owner._tlsOptions.isServer);
// `newSession` callback wasn't called yet
if (owner._newSessionPending) {
@@ -108,10 +112,15 @@ function onhandshakedone() {
function loadSession(hello) {
+ debug('server onclienthello',
+ 'sessionid.len', hello.sessionId.length,
+ 'ticket?', hello.tlsTicket
+ );
const owner = this[owner_symbol];
var once = false;
function onSession(err, session) {
+ debug('server resumeSession callback(err %j, sess? %s)', err, !!session);
if (once)
return owner.destroy(new ERR_MULTIPLE_CALLBACK());
once = true;
@@ -193,6 +202,8 @@ function requestOCSP(socket, info) {
let once = false;
const onOCSP = (err, response) => {
+ debug('server OCSPRequest done', 'handle?', !!socket._handle, 'once?', once,
+ 'response?', !!response, 'err?', err);
if (once)
return socket.destroy(new ERR_MULTIPLE_CALLBACK());
once = true;
@@ -208,6 +219,7 @@ function requestOCSP(socket, info) {
requestOCSPDone(socket);
};
+ debug('server oncertcb emit OCSPRequest');
socket.server.emit('OCSPRequest',
ctx.getCertificate(),
ctx.getIssuer(),
@@ -215,16 +227,17 @@ function requestOCSP(socket, info) {
}
function requestOCSPDone(socket) {
+ debug('server certcb done');
try {
socket._handle.certCbDone();
} catch (e) {
+ debug('server certcb done errored', e);
socket.destroy(e);
}
}
-
function onnewsessionclient(sessionId, session) {
- debug('client onnewsessionclient', sessionId, session);
+ debug('client emit session');
const owner = this[owner_symbol];
owner.emit('session', session);
}
@@ -233,8 +246,9 @@ function onnewsession(sessionId, session) {
debug('onnewsession');
const owner = this[owner_symbol];
- // XXX(sam) no server to emit the event on, but handshake won't continue
- // unless newSessionDone() is called, should it be?
+ // TODO(@sam-github) no server to emit the event on, but handshake won't
+ // continue unless newSessionDone() is called, should it be, or is that
+ // situation unreachable, or only occurring during shutdown?
if (!owner.server)
return;
@@ -263,11 +277,15 @@ function onnewsession(sessionId, session) {
function onocspresponse(resp) {
+ debug('client onocspresponse');
this[owner_symbol].emit('OCSPResponse', resp);
}
function onerror(err) {
const owner = this[owner_symbol];
+ debug('%s onerror %s had? %j',
+ owner._tlsOptions.isServer ? 'server' : 'client', err,
+ owner._hadError);
if (owner._hadError)
return;
@@ -285,7 +303,7 @@ function onerror(err) {
// Ignore server's authorization errors
owner.destroy();
} else {
- // Throw error
+ // Emit error
owner._emitTLSError(err);
}
}
@@ -293,6 +311,11 @@ function onerror(err) {
// Used by both client and server TLSSockets to start data flowing from _handle,
// read(0) causes a StreamBase::ReadStart, via Socket._read.
function initRead(tlsSocket, socket) {
+ debug('%s initRead',
+ tlsSocket._tlsOptions.isServer ? 'server' : 'client',
+ 'handle?', !!tlsSocket._handle,
+ 'buffered?', !!socket && socket.readableLength
+ );
// If we were destroyed already don't bother reading
if (!tlsSocket._handle)
return;
@@ -493,12 +516,17 @@ TLSSocket.prototype._destroySSL = function _destroySSL() {
this.ssl = null;
};
+// Constructor guts, arbitrarily factored out.
TLSSocket.prototype._init = function(socket, wrap) {
var options = this._tlsOptions;
var ssl = this._handle;
-
this.server = options.server;
+ debug('%s _init',
+ options.isServer ? 'server' : 'client',
+ 'handle?', !!ssl
+ );
+
// Clients (!isServer) always request a cert, servers request a client cert
// only on explicit configuration.
const requestCert = !!options.requestCert || !options.isServer;
@@ -529,7 +557,10 @@ TLSSocket.prototype._init = function(socket, wrap) {
}
} else {
ssl.onhandshakestart = noop;
- ssl.onhandshakedone = this._finishInit.bind(this);
+ ssl.onhandshakedone = () => {
+ debug('client onhandshakedone');
+ this._finishInit();
+ };
ssl.onocspresponse = onocspresponse;
if (options.session)
@@ -600,6 +631,11 @@ TLSSocket.prototype.renegotiate = function(options, callback) {
if (callback !== undefined && typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK();
+ debug('%s renegotiate()',
+ this._tlsOptions.isServer ? 'server' : 'client',
+ 'destroyed?', this.destroyed
+ );
+
if (this.destroyed)
return;
@@ -667,9 +703,25 @@ TLSSocket.prototype._releaseControl = function() {
};
TLSSocket.prototype._finishInit = function() {
- debug('secure established');
+ // Guard against getting onhandshakedone() after .destroy().
+ // * 1.2: If destroy() during onocspresponse(), then write of next handshake
+ // record fails, the handshake done info callbacks does not occur, and the
+ // socket closes.
+ // * 1.3: The OCSP response comes in the same record that finishes handshake,
+ // so even after .destroy(), the handshake done info callback occurs
+ // immediately after onocspresponse(). Ignore it.
+ if (!this._handle)
+ return;
+
this.alpnProtocol = this._handle.getALPNNegotiatedProtocol();
this.servername = this._handle.getServername();
+
+ debug('%s _finishInit',
+ this._tlsOptions.isServer ? 'server' : 'client',
+ 'handle?', !!this._handle,
+ 'alpn', this.alpnProtocol,
+ 'servername', this.servername);
+
this._secureEstablished = true;
if (this._tlsOptions.handshakeTimeout > 0)
this.setTimeout(0, this._handleTimeout);
@@ -677,6 +729,12 @@ TLSSocket.prototype._finishInit = function() {
};
TLSSocket.prototype._start = function() {
+ debug('%s _start',
+ this._tlsOptions.isServer ? 'server' : 'client',
+ 'handle?', !!this._handle,
+ 'connecting?', this.connecting,
+ 'requestOCSP?', !!this._tlsOptions.requestOCSP,
+ );
if (this.connecting) {
this.once('connect', this._start);
return;
@@ -686,7 +744,6 @@ TLSSocket.prototype._start = function() {
if (!this._handle)
return;
- debug('start');
if (this._tlsOptions.requestOCSP)
this._handle.requestOCSP();
this._handle.start();
@@ -765,13 +822,16 @@ function onServerSocketSecure() {
}
}
- if (!this.destroyed && this._releaseControl())
+ if (!this.destroyed && this._releaseControl()) {
+ debug('server emit secureConnection');
this._tlsOptions.server.emit('secureConnection', this);
+ }
}
function onSocketTLSError(err) {
if (!this._controlReleased && !this[kErrorEmitted]) {
this[kErrorEmitted] = true;
+ debug('server emit tlsClientError:', err);
this._tlsOptions.server.emit('tlsClientError', err, this);
}
}
@@ -792,6 +852,7 @@ function onSocketClose(err) {
}
function tlsConnectionListener(rawSocket) {
+ debug('net.Server.on(connection): new TLSSocket');
const socket = new TLSSocket(rawSocket, {
secureContext: this._sharedCreds,
isServer: true,
@@ -1180,6 +1241,7 @@ function onConnectSecure() {
const ekeyinfo = this.getEphemeralKeyInfo();
if (ekeyinfo.type === 'DH' && ekeyinfo.size < options.minDHSize) {
const err = new ERR_TLS_DH_PARAM_SIZE(ekeyinfo.size);
+ debug('client emit:', err);
this.emit('error', err);
this.destroy();
return;
@@ -1206,10 +1268,12 @@ function onConnectSecure() {
this.destroy(verifyError);
return;
} else {
+ debug('client emit secureConnect');
this.emit('secureConnect');
}
} else {
this.authorized = true;
+ debug('client emit secureConnect');
this.emit('secureConnect');
}
diff --git a/lib/internal/stream_base_commons.js b/lib/internal/stream_base_commons.js
index 7b5798e82a..67abba0992 100644
--- a/lib/internal/stream_base_commons.js
+++ b/lib/internal/stream_base_commons.js
@@ -30,6 +30,8 @@ const kAfterAsyncWrite = Symbol('kAfterAsyncWrite');
const kHandle = Symbol('kHandle');
const kSession = Symbol('kSession');
+const debug = require('util').debuglog('stream');
+
function handleWriteReq(req, data, encoding) {
const { handle } = req;
@@ -66,6 +68,8 @@ function handleWriteReq(req, data, encoding) {
}
function onWriteComplete(status) {
+ debug('onWriteComplete', status, this.error);
+
const stream = this.handle[owner_symbol];
if (stream.destroyed) {
diff --git a/lib/tls.js b/lib/tls.js
index 645c3e9269..9d2b9add66 100644
--- a/lib/tls.js
+++ b/lib/tls.js
@@ -54,15 +54,25 @@ exports.DEFAULT_CIPHERS =
exports.DEFAULT_ECDH_CURVE = 'auto';
-exports.DEFAULT_MAX_VERSION = 'TLSv1.2';
+exports.DEFAULT_MAX_VERSION = 'TLSv1.3';
-if (getOptionValue('--tls-v1.0'))
+if (getOptionValue('--tls-min-v1.0'))
exports.DEFAULT_MIN_VERSION = 'TLSv1';
-else if (getOptionValue('--tls-v1.1'))
+else if (getOptionValue('--tls-min-v1.1'))
exports.DEFAULT_MIN_VERSION = 'TLSv1.1';
+else if (getOptionValue('--tls-min-v1.3'))
+ exports.DEFAULT_MIN_VERSION = 'TLSv1.3';
else
exports.DEFAULT_MIN_VERSION = 'TLSv1.2';
+if (getOptionValue('--tls-max-v1.3'))
+ exports.DEFAULT_MAX_VERSION = 'TLSv1.3';
+else if (getOptionValue('--tls-max-v1.2'))
+ exports.DEFAULT_MAX_VERSION = 'TLSv1.2';
+else
+ exports.DEFAULT_MAX_VERSION = 'TLSv1.3'; // Will depend on node version.
+
+
exports.getCiphers = internalUtil.cachedResult(
() => internalUtil.filterDuplicateStrings(binding.getSSLCiphers(), true)
);