diff --git a/deps/openssl/openssl/crypto/chacha/build.info b/deps/openssl/openssl/crypto/chacha/build.info index 02f8e518aeca90..e75ca72b67d4fb 100644 --- a/deps/openssl/openssl/crypto/chacha/build.info +++ b/deps/openssl/openssl/crypto/chacha/build.info @@ -9,6 +9,8 @@ GENERATE[chacha-armv4.S]=asm/chacha-armv4.pl $(PERLASM_SCHEME) INCLUDE[chacha-armv4.o]=.. GENERATE[chacha-armv8.S]=asm/chacha-armv8.pl $(PERLASM_SCHEME) INCLUDE[chacha-armv8.o]=.. +GENERATE[chacha-s390x.S]=asm/chacha-s390x.pl $(PERLASM_SCHEME) +INCLUDE[chacha-s390x.o]=.. BEGINRAW[Makefile(unix)] ##### CHACHA assembler implementations diff --git a/deps/openssl/openssl/crypto/poly1305/build.info b/deps/openssl/openssl/crypto/poly1305/build.info index 631b32b8e099ac..b730524afb1393 100644 --- a/deps/openssl/openssl/crypto/poly1305/build.info +++ b/deps/openssl/openssl/crypto/poly1305/build.info @@ -17,6 +17,8 @@ GENERATE[poly1305-armv8.S]=asm/poly1305-armv8.pl $(PERLASM_SCHEME) INCLUDE[poly1305-armv8.o]=.. GENERATE[poly1305-mips.S]=asm/poly1305-mips.pl $(PERLASM_SCHEME) INCLUDE[poly1305-mips.o]=.. +GENERATE[poly1305-s390x.S]=asm/poly1305-s390x.pl $(PERLASM_SCHEME) +INCLUDE[poly1305-s390x.o]=.. BEGINRAW[Makefile(unix)] {- $builddir -}/poly1305-%.S: {- $sourcedir -}/asm/poly1305-%.pl diff --git a/deps/openssl/openssl/crypto/rc4/build.info b/deps/openssl/openssl/crypto/rc4/build.info index 46ee66b61c68a2..913942b5e98003 100644 --- a/deps/openssl/openssl/crypto/rc4/build.info +++ b/deps/openssl/openssl/crypto/rc4/build.info @@ -11,6 +11,8 @@ GENERATE[rc4-md5-x86_64.s]=asm/rc4-md5-x86_64.pl $(PERLASM_SCHEME) GENERATE[rc4-parisc.s]=asm/rc4-parisc.pl $(PERLASM_SCHEME) +GENERATE[rc4-s390x.s]=asm/rc4-s390x.pl $(PERLASM_SCHEME) + BEGINRAW[Makefile] # GNU make "catch all" {- $builddir -}/rc4-%.s: {- $sourcedir -}/asm/rc4-%.pl diff --git a/doc/api/cli.md b/doc/api/cli.md index 59554b1c679327..3d4a1adb91aee2 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -443,6 +443,54 @@ added: v4.0.0 Specify an alternative default TLS cipher list. Requires Node.js to be built with crypto support (default). +### `--tls-max-v1.2` + + +Set [`tls.DEFAULT_MAX_VERSION`][] to 'TLSv1.2'. Use to disable support for +TLSv1.3. + +### `--tls-max-v1.3` + + +Set default [`tls.DEFAULT_MAX_VERSION`][] to 'TLSv1.3'. Use to enable support +for TLSv1.3. + +### `--tls-min-v1.0` + + +Set default [`tls.DEFAULT_MIN_VERSION`][] to 'TLSv1'. Use for compatibility with +old TLS clients or servers. + +### `--tls-min-v1.1` + + +Set default [`tls.DEFAULT_MIN_VERSION`][] to 'TLSv1.1'. Use for compatibility +with old TLS clients or servers. + +### `--tls-min-v1.2` + + +Set default [`minVersion`][] to `'TLSv1.2'`. Use to disable support for TLSv1 +and TLSv1.1 in favour of TLSv1.2, which is more secure. + +### `--tls-min-v1.3` + + +Set default [`tls.DEFAULT_MIN_VERSION`][] to 'TLSv1.3'. Use to disable support +for TLSv1.2, which is not as secure as TLSv1.3. + ### `--trace-deprecation` @@ -136,6 +139,8 @@ threshold is exceeded. The limits are configurable: The default renegotiation limits should not be modified without a full understanding of the implications and risks. +TLSv1.3 does not support renegotiation. + ### Session Resumption Establishing a TLS session can be relatively slow. The process can be sped @@ -176,6 +181,10 @@ as for resumption with session tickets. For debugging, if [`tls.TLSSocket.getTLSTicket()`][] returns a value, the session data contains a ticket, otherwise it contains client-side session state. +With TLSv1.3, be aware that multiple tickets may be sent by the server, +resulting in multiple `'session'` events, see [`'session'`][] for more +information. + Single process servers need no specific implementation to use session tickets. To use session tickets across server restarts or load balancers, servers must all have the same ticket keys. There are three 16-byte keys internally, but the @@ -230,6 +239,9 @@ Node.js is built with a default suite of enabled and disabled TLS ciphers. Currently, the default cipher suite is: ```txt +TLS_AES_256_GCM_SHA384: +TLS_CHACHA20_POLY1305_SHA256: +TLS_AES_128_GCM_SHA256: ECDHE-RSA-AES128-GCM-SHA256: ECDHE-ECDSA-AES128-GCM-SHA256: ECDHE-RSA-AES256-GCM-SHA384: @@ -270,7 +282,19 @@ The default can also be replaced on a per client or server basis using the in [`tls.createServer()`], [`tls.connect()`], and when creating new [`tls.TLSSocket`]s. -Consult [OpenSSL cipher list format documentation][] for details on the format. +The ciphers list can contain a mixture of TLSv1.3 cipher suite names, the ones +that start with `'TLS_'`, and specifications for TLSv1.2 and below cipher +suites. The TLSv1.2 ciphers support a legacy specification format, consult +the OpenSSL [cipher list format][] documentation for details, but those +specifications do *not* apply to TLSv1.3 ciphers. The TLSv1.3 suites can only +be enabled by including their full name in the cipher list. They cannot, for +example, be enabled or disabled by using the legacy TLSv1.2 `'EECDH'` or +`'!EECDH'` specification. + +Despite the relative order of TLSv1.3 and TLSv1.2 cipher suites, the TLSv1.3 +protocol is significantly more secure than TLSv1.2, and will always be chosen +over TLSv1.2 if the handshake indicates it is supported, and if any TLSv1.3 +cipher suites are enabled. The default cipher suite included within Node.js has been carefully selected to reflect current security best practices and risk mitigation. @@ -289,7 +313,18 @@ Old clients that rely on insecure and deprecated RC4 or DES-based ciphers (like Internet Explorer 6) cannot complete the handshaking process with the default configuration. If these clients _must_ be supported, the [TLS recommendations] may offer a compatible cipher suite. For more details -on the format, see the [OpenSSL cipher list format documentation]. +on the format, see the OpenSSL [cipher list format][] documentation. + +There are only 5 TLSv1.3 cipher suites: +- `'TLS_AES_256_GCM_SHA384'` +- `'TLS_CHACHA20_POLY1305_SHA256'` +- `'TLS_AES_128_GCM_SHA256'` +- `'TLS_AES_128_CCM_SHA256'` +- `'TLS_AES_128_CCM_8_SHA256'` + +The first 3 are enabled by default. The last 2 `CCM`-based suites are supported +by TLSv1.3 because they may be more performant on constrained systems, but they +are not enabled by default since they offer less security. ## Class: tls.Server + +* {string} The default value of the `maxVersion` option of + [`tls.createSecureContext()`][]. It can be assigned any of the supported TLS + protocol versions, `TLSv1.3`, `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. + **Default:** `'TLSv1.2'`, unless changed using CLI options. Using + `--tls-max-v1.2` sets the default to `'TLSv1.2`'. Using `--tls-max-v1.3` sets + the default to `'TLSv1.3'`. If multiple of the options are provided, the + highest maximum is used. + +## tls.DEFAULT_MIN_VERSION + + +* {string} The default value of the `minVersion` option of + [`tls.createSecureContext()`][]. It can be assigned any of the supported TLS + protocol versions, `'TLSv1.3'`, `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. + **Default:** `'TLSv1'`, unless changed using CLI options. Using + `--tls-min-v1.0` sets the default to `'TLSv1'`. Using `--tls-min-v1.1` sets + the default to `'TLSv1.1'`. Using `--tls-min-v1.3` sets the default to + `'TLSv1.3'`. If multiple of the options are provided, the lowest minimum is + used. + ## Deprecated APIs ### Class: CryptoStream @@ -1597,6 +1680,8 @@ where `secureSocket` has the same API as `pair.cleartext`. [`server.setTicketKeys()`]: #tls_server_setticketkeys_keys [`socket.setTimeout(timeout)`]: #net_socket_settimeout_timeout_callback [`tls.DEFAULT_ECDH_CURVE`]: #tls_tls_default_ecdh_curve +[`tls.DEFAULT_MAX_VERSION`]: #tls_tls_default_max_version +[`tls.DEFAULT_MIN_VERSION`]: #tls_tls_default_min_version [`tls.Server`]: #tls_class_tls_server [`tls.TLSSocket.getPeerCertificate()`]: #tls_tlssocket_getpeercertificate_detailed [`tls.TLSSocket.getSession()`]: #tls_tlssocket_getsession @@ -1613,16 +1698,16 @@ where `secureSocket` has the same API as `pair.cleartext`. [Forward secrecy]: https://en.wikipedia.org/wiki/Perfect_forward_secrecy [OCSP request]: https://en.wikipedia.org/wiki/OCSP_stapling [OpenSSL Options]: crypto.html#crypto_openssl_options -[OpenSSL cipher list format documentation]: https://www.openssl.org/docs/man1.1.0/apps/ciphers.html#CIPHER-LIST-FORMAT [Perfect Forward Secrecy]: #tls_perfect_forward_secrecy [RFC 2246]: https://www.ietf.org/rfc/rfc2246.txt [RFC 5077]: https://tools.ietf.org/html/rfc5077 [RFC 5929]: https://tools.ietf.org/html/rfc5929 -[SSL_METHODS]: https://www.openssl.org/docs/man1.1.0/ssl/ssl.html#Dealing-with-Protocol-Methods +[SSL_METHODS]: https://www.openssl.org/docs/man1.1.1/man7/ssl.html#Dealing-with-Protocol-Methods [Session Resumption]: #tls_session_resumption [Stream]: stream.html#stream_stream [TLS recommendations]: https://wiki.mozilla.org/Security/Server_Side_TLS [asn1.js]: https://www.npmjs.com/package/asn1.js [certificate object]: #tls_certificate_object +[cipher list format]: https://www.openssl.org/docs/man1.1.1/man1/ciphers.html#CIPHER-LIST-FORMAT [modifying the default cipher suite]: #tls_modifying_the_default_tls_cipher_suite [specific attacks affecting larger AES key sizes]: https://www.schneier.com/blog/archives/2009/07/another_new_aes.html diff --git a/doc/node.1 b/doc/node.1 index 603d72d566c99c..4a3759ad0de031 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -236,6 +236,28 @@ Specify process.title on startup. Specify an alternative default TLS cipher list. Requires Node.js to be built with crypto support. (Default) . +.It Fl -tls-max-v1.2 +Set default maxVersion to 'TLSv1.2'. Use to disable support for TLSv1.3. +. +.It Fl -tls-max-v1.3 +Set default maxVersion to 'TLSv1.3'. Use to enable support for TLSv1.3. +. +.It Fl -tls-min-v1.0 +Set default minVersion to 'TLSv1'. Use for compatibility with old TLS clients +or servers. +. +.It Fl -tls-min-v1.1 +Set default minVersion to 'TLSv1.1'. Use for compatibility with old TLS clients +or servers. +. +.It Fl -tls-min-v1.2 +Set default minVersion to 'TLSv1.2'. Use to disable support for TLSv1 and +TLSv1.1 in favour of TLSv1.2, which is more secure. +. +.It Fl -tls-min-v1.3 +Set default minVersion to 'TLSv1.3'. Use to disable support for TLSv1.2 in +favour of TLSv1.3, which is more secure. +. .It Fl -trace-deprecation Print stack traces for deprecations. . diff --git a/lib/_tls_common.js b/lib/_tls_common.js index 78e67af23d46f0..a0970571be3383 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' && TLS1_3_VERSION) return TLS1_3_VERSION; throw new ERR_TLS_INVALID_PROTOCOL_VERSION(v, which); } @@ -156,10 +159,35 @@ exports.createSecureContext = function createSecureContext(options, context) { } } - 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 98f9fc98fbaa02..047d8391804ea0 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -39,6 +39,7 @@ const { owner_symbol } = require('internal/async_hooks').symbols; const { SecureContext: NativeSecureContext } = internalBinding('crypto'); const { ERR_INVALID_ARG_TYPE, + ERR_INVALID_CALLBACK, ERR_MULTIPLE_CALLBACK, ERR_SOCKET_CLOSED, ERR_TLS_DH_PARAM_SIZE, @@ -62,7 +63,7 @@ const noop = () => {}; // 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, @@ -80,6 +81,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; @@ -90,9 +94,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) { @@ -105,10 +110,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; @@ -190,6 +200,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; @@ -205,6 +217,7 @@ function requestOCSP(socket, info) { requestOCSPDone(socket); }; + debug('server oncertcb emit OCSPRequest'); socket.server.emit('OCSPRequest', ctx.getCertificate(), ctx.getIssuer(), @@ -212,16 +225,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); } @@ -230,8 +244,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; @@ -260,11 +275,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; @@ -282,7 +301,7 @@ function onerror(err) { // Ignore server's authorization errors owner.destroy(); } else { - // Throw error + // Emit error owner._emitTLSError(err); } } @@ -290,6 +309,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; @@ -490,11 +514,17 @@ TLSSocket.prototype._destroySSL = function _destroySSL() { this.ssl = null; }; +// Constructor guts, arbitrarily factored out. TLSSocket.prototype._init = function(socket, wrap) { const options = this._tlsOptions; const 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; @@ -525,7 +555,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) @@ -591,6 +624,16 @@ TLSSocket.prototype._init = function(socket, wrap) { }; TLSSocket.prototype.renegotiate = function(options, callback) { + if (options === null || typeof options !== 'object') + throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); + 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; @@ -658,9 +701,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); @@ -668,6 +727,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; @@ -677,7 +742,6 @@ TLSSocket.prototype._start = function() { if (!this._handle) return; - debug('start'); if (this._tlsOptions.requestOCSP) this._handle.requestOCSP(); this._handle.start(); @@ -756,13 +820,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); } } @@ -783,6 +850,7 @@ function onSocketClose(err) { } function tlsConnectionListener(rawSocket) { + debug('net.Server.on(connection): new TLSSocket'); const socket = new TLSSocket(rawSocket, { secureContext: this._sharedCreds, isServer: true, @@ -1168,6 +1236,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; @@ -1194,10 +1263,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 a6805e39be8390..8f58ff56bb8a73 100644 --- a/lib/internal/stream_base_commons.js +++ b/lib/internal/stream_base_commons.js @@ -19,6 +19,8 @@ const kUpdateTimer = Symbol('kUpdateTimer'); const kAfterAsyncWrite = Symbol('kAfterAsyncWrite'); const kHandle = Symbol('kHandle'); +const debug = require('util').debuglog('stream'); + function handleWriteReq(req, data, encoding) { const { handle } = req; @@ -55,6 +57,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 2be6a15bc5c5e6..c951727e5589c3 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -31,6 +31,7 @@ internalUtil.assertCrypto(); const { isArrayBufferView } = require('internal/util/types'); const net = require('net'); +const { getOptionValue } = require('internal/options'); const url = require('url'); const binding = internalBinding('crypto'); const { Buffer } = require('buffer'); @@ -53,9 +54,24 @@ exports.DEFAULT_CIPHERS = exports.DEFAULT_ECDH_CURVE = 'auto'; -exports.DEFAULT_MAX_VERSION = 'TLSv1.2'; +if (getOptionValue('--tls-min-v1.0')) + exports.DEFAULT_MIN_VERSION = 'TLSv1'; +else if (getOptionValue('--tls-min-v1.1')) + exports.DEFAULT_MIN_VERSION = 'TLSv1.1'; +else if (getOptionValue('--tls-min-v1.2')) + exports.DEFAULT_MIN_VERSION = 'TLSv1.2'; +else if (getOptionValue('--tls-min-v1.3')) + exports.DEFAULT_MIN_VERSION = 'TLSv1.3'; +else + exports.DEFAULT_MIN_VERSION = 'TLSv1'; + +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.2'; // Will depend on node version. -exports.DEFAULT_MIN_VERSION = 'TLSv1'; exports.getCiphers = internalUtil.cachedResult( () => internalUtil.filterDuplicateStrings(binding.getSSLCiphers(), true) diff --git a/src/env.h b/src/env.h index 0516c49f36ddcd..e75e336f9d935b 100644 --- a/src/env.h +++ b/src/env.h @@ -185,6 +185,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(fingerprint_string, "fingerprint") \ V(flags_string, "flags") \ V(fragment_string, "fragment") \ + V(function_string, "function") \ V(get_data_clone_error_string, "_getDataCloneError") \ V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \ V(gid_string, "gid") \ @@ -208,6 +209,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(issuercert_string, "issuerCertificate") \ V(kill_signal_string, "killSignal") \ V(kind_string, "kind") \ + V(library_string, "library") \ V(mac_string, "mac") \ V(main_string, "main") \ V(max_buffer_string, "maxBuffer") \ diff --git a/src/node_constants.cc b/src/node_constants.cc index be27de4ed64430..4593760b2f3d94 100644 --- a/src/node_constants.cc +++ b/src/node_constants.cc @@ -1245,6 +1245,9 @@ void DefineCryptoConstants(Local target) { NODE_DEFINE_CONSTANT(target, TLS1_VERSION); NODE_DEFINE_CONSTANT(target, TLS1_1_VERSION); NODE_DEFINE_CONSTANT(target, TLS1_2_VERSION); +#ifdef TLS1_3_VERSION + NODE_DEFINE_CONSTANT(target, TLS1_3_VERSION); +#endif #endif NODE_DEFINE_CONSTANT(target, INT_MAX); } diff --git a/src/node_constants.h b/src/node_constants.h index 6f73fb4d7d9bfc..af5aa002eb5795 100644 --- a/src/node_constants.h +++ b/src/node_constants.h @@ -41,7 +41,13 @@ #define RSA_PSS_SALTLEN_AUTO -2 #endif -#define DEFAULT_CIPHER_LIST_CORE "ECDHE-RSA-AES128-GCM-SHA256:" \ +// TLSv1.3 suites start with TLS_, and are the OpenSSL defaults, see: +// https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_ciphersuites.html +#define DEFAULT_CIPHER_LIST_CORE \ + "TLS_AES_256_GCM_SHA384:" \ + "TLS_CHACHA20_POLY1305_SHA256:" \ + "TLS_AES_128_GCM_SHA256:" \ + "ECDHE-RSA-AES128-GCM-SHA256:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:" \ "ECDHE-RSA-AES256-GCM-SHA384:" \ "ECDHE-ECDSA-AES256-GCM-SHA384:" \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index cf7e5557102f1d..8cafc808800b0e 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -65,6 +65,8 @@ static const int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL namespace node { namespace crypto { +using node::THROW_ERR_TLS_INVALID_PROTOCOL_METHOD; + using v8::Array; using v8::ArrayBufferView; using v8::Boolean; @@ -349,9 +351,14 @@ void SecureContext::Initialize(Environment* env, Local target) { env->SetProtoMethod(t, "addCACert", AddCACert); env->SetProtoMethod(t, "addCRL", AddCRL); env->SetProtoMethod(t, "addRootCerts", AddRootCerts); + env->SetProtoMethod(t, "setCipherSuites", SetCipherSuites); env->SetProtoMethod(t, "setCiphers", SetCiphers); env->SetProtoMethod(t, "setECDHCurve", SetECDHCurve); env->SetProtoMethod(t, "setDHParam", SetDHParam); + env->SetProtoMethod(t, "setMaxProto", SetMaxProto); + env->SetProtoMethod(t, "setMinProto", SetMinProto); + env->SetProtoMethod(t, "getMaxProto", GetMaxProto); + env->SetProtoMethod(t, "getMinProto", GetMinProto); env->SetProtoMethod(t, "setOptions", SetOptions); env->SetProtoMethod(t, "setSessionIdContext", SetSessionIdContext); env->SetProtoMethod(t, "setSessionTimeout", SetSessionTimeout); @@ -402,6 +409,14 @@ void SecureContext::New(const FunctionCallbackInfo& args) { new SecureContext(env, args.This()); } +// A maxVersion of 0 means "any", but OpenSSL may support TLS versions that +// Node.js doesn't, so pin the max to what we do support. +const int MAX_SUPPORTED_VERSION = +#ifdef TLS1_3_VERSION + TLS1_3_VERSION; +#else + TLS1_2_VERSION; +#endif void SecureContext::Init(const FunctionCallbackInfo& args) { SecureContext* sc; @@ -416,34 +431,53 @@ void SecureContext::Init(const FunctionCallbackInfo& args) { int max_version = args[2].As()->Value(); const SSL_METHOD* method = TLS_method(); + if (max_version == 0) + max_version = MAX_SUPPORTED_VERSION; + if (args[0]->IsString()) { const node::Utf8Value sslmethod(env->isolate(), args[0]); // Note that SSLv2 and SSLv3 are disallowed but SSLv23_method and friends // are still accepted. They are OpenSSL's way of saying that all known - // protocols are supported unless explicitly disabled (which we do below - // for SSLv2 and SSLv3.) + // protocols below TLS 1.3 are supported unless explicitly disabled (which + // we do below for SSLv2 and SSLv3.) if (strcmp(*sslmethod, "SSLv2_method") == 0) { - return env->ThrowError("SSLv2 methods disabled"); + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv2 methods disabled"); + return; } else if (strcmp(*sslmethod, "SSLv2_server_method") == 0) { - return env->ThrowError("SSLv2 methods disabled"); + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv2 methods disabled"); + return; } else if (strcmp(*sslmethod, "SSLv2_client_method") == 0) { - return env->ThrowError("SSLv2 methods disabled"); + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv2 methods disabled"); + return; } else if (strcmp(*sslmethod, "SSLv3_method") == 0) { - return env->ThrowError("SSLv3 methods disabled"); + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv3 methods disabled"); + return; } else if (strcmp(*sslmethod, "SSLv3_server_method") == 0) { - return env->ThrowError("SSLv3 methods disabled"); + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv3 methods disabled"); + return; } else if (strcmp(*sslmethod, "SSLv3_client_method") == 0) { - return env->ThrowError("SSLv3 methods disabled"); + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv3 methods disabled"); + return; } else if (strcmp(*sslmethod, "SSLv23_method") == 0) { - // noop + max_version = TLS1_2_VERSION; } else if (strcmp(*sslmethod, "SSLv23_server_method") == 0) { + max_version = TLS1_2_VERSION; method = TLS_server_method(); } else if (strcmp(*sslmethod, "SSLv23_client_method") == 0) { + max_version = TLS1_2_VERSION; method = TLS_client_method(); } else if (strcmp(*sslmethod, "TLS_method") == 0) { min_version = 0; - max_version = 0; + max_version = MAX_SUPPORTED_VERSION; + } else if (strcmp(*sslmethod, "TLS_server_method") == 0) { + min_version = 0; + max_version = MAX_SUPPORTED_VERSION; + method = TLS_server_method(); + } else if (strcmp(*sslmethod, "TLS_client_method") == 0) { + min_version = 0; + max_version = MAX_SUPPORTED_VERSION; + method = TLS_client_method(); } else if (strcmp(*sslmethod, "TLSv1_method") == 0) { min_version = TLS1_VERSION; max_version = TLS1_VERSION; @@ -478,7 +512,8 @@ void SecureContext::Init(const FunctionCallbackInfo& args) { max_version = TLS1_2_VERSION; method = TLS_client_method(); } else { - return env->ThrowError("Unknown method"); + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "Unknown method"); + return; } } @@ -505,12 +540,6 @@ void SecureContext::Init(const FunctionCallbackInfo& args) { SSL_SESS_CACHE_NO_AUTO_CLEAR); SSL_CTX_set_min_proto_version(sc->ctx_.get(), min_version); - - if (max_version == 0) { - // Selecting some secureProtocol methods allows the TLS version to be "any - // supported", but we don't support TLSv1.3, even if OpenSSL does. - max_version = TLS1_2_VERSION; - } SSL_CTX_set_max_proto_version(sc->ctx_.get(), max_version); // OpenSSL 1.1.0 changed the ticket key size, but the OpenSSL 1.0.x size was // exposed in the public API. To retain compatibility, install a callback @@ -921,42 +950,54 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo& args) { } -void SecureContext::SetCiphers(const FunctionCallbackInfo& args) { +void SecureContext::SetCipherSuites(const FunctionCallbackInfo& args) { + // BoringSSL doesn't allow API config of TLS1.3 cipher suites. +#if defined(TLS1_3_VERSION) && !defined(OPENSSL_IS_BORINGSSL) SecureContext* sc; ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); Environment* env = sc->env(); ClearErrorOnReturn clear_error_on_return; - if (args.Length() != 1) { - return THROW_ERR_MISSING_ARGS(env, "Ciphers argument is mandatory"); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + + const node::Utf8Value ciphers(args.GetIsolate(), args[0]); + if (!SSL_CTX_set_ciphersuites(sc->ctx_.get(), *ciphers)) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + if (!err) { + // This would be an OpenSSL bug if it happened. + return env->ThrowError("Failed to set ciphers"); + } + return ThrowCryptoError(env, err); } +#endif +} - THROW_AND_RETURN_IF_NOT_STRING(env, args[0], "Ciphers"); - // Note: set_ciphersuites() is for TLSv1.3 and was introduced in openssl - // 1.1.1, set_cipher_list() is for TLSv1.2 and earlier. - // - // In openssl 1.1.0, set_cipher_list() would error if it resulted in no - // TLSv1.2 (and earlier) cipher suites, and there is no TLSv1.3 support. - // - // In openssl 1.1.1, set_cipher_list() will not error if it results in no - // TLSv1.2 cipher suites if there are any TLSv1.3 cipher suites, which there - // are by default. There will be an error later, during the handshake, but - // that results in an async error event, rather than a sync error thrown, - // which is a semver-major change for the tls API. - // - // Since we don't currently support TLSv1.3, work around this by removing the - // TLSv1.3 cipher suites, so we get backwards compatible synchronous errors. +void SecureContext::SetCiphers(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + Environment* env = sc->env(); + ClearErrorOnReturn clear_error_on_return; + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + const node::Utf8Value ciphers(args.GetIsolate(), args[0]); - if ( -#if defined(TLS1_3_VERSION) && !defined(OPENSSL_IS_BORINGSSL) - !SSL_CTX_set_ciphersuites(sc->ctx_.get(), "") || -#endif - !SSL_CTX_set_cipher_list(sc->ctx_.get(), *ciphers)) { + if (!SSL_CTX_set_cipher_list(sc->ctx_.get(), *ciphers)) { unsigned long err = ERR_get_error(); // NOLINT(runtime/int) if (!err) { + // This would be an OpenSSL bug if it happened. return env->ThrowError("Failed to set ciphers"); } + + if (strlen(*ciphers) == 0 && ERR_GET_REASON(err) == SSL_R_NO_CIPHER_MATCH) { + // TLS1.2 ciphers were deliberately cleared, so don't consider + // SSL_R_NO_CIPHER_MATCH to be an error (this is how _set_cipher_suites() + // works). If the user actually sets a value (like "no-such-cipher"), then + // that's actually an error. + return; + } return ThrowCryptoError(env, err); } } @@ -1025,6 +1066,56 @@ void SecureContext::SetDHParam(const FunctionCallbackInfo& args) { } +void SecureContext::SetMinProto(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsInt32()); + + int version = args[0].As()->Value(); + + CHECK(SSL_CTX_set_min_proto_version(sc->ctx_.get(), version)); +} + + +void SecureContext::SetMaxProto(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsInt32()); + + int version = args[0].As()->Value(); + + CHECK(SSL_CTX_set_max_proto_version(sc->ctx_.get(), version)); +} + + +void SecureContext::GetMinProto(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 0); + + long version = // NOLINT(runtime/int) + SSL_CTX_get_min_proto_version(sc->ctx_.get()); + args.GetReturnValue().Set(static_cast(version)); +} + + +void SecureContext::GetMaxProto(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 0); + + long version = // NOLINT(runtime/int) + SSL_CTX_get_max_proto_version(sc->ctx_.get()); + args.GetReturnValue().Set(static_cast(version)); +} + + void SecureContext::SetOptions(const FunctionCallbackInfo& args) { SecureContext* sc; ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); @@ -1244,6 +1335,7 @@ void SecureContext::SetTicketKeys(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); Environment* env = wrap->env(); + // TODO(@sam-github) Move type and len check to js, and CHECK() in C++. if (args.Length() < 1) { return THROW_ERR_MISSING_ARGS(env, "Ticket keys argument is mandatory"); } @@ -2103,6 +2195,7 @@ void SSLWrap::LoadSession(const FunctionCallbackInfo& args) { Base* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + // TODO(@sam-github) check arg length and types in js, and CHECK in c++ if (args.Length() >= 1 && Buffer::HasInstance(args[0])) { ArrayBufferViewContents sbuf(args[0]); @@ -2139,7 +2232,7 @@ void SSLWrap::Renegotiate(const FunctionCallbackInfo& args) { ClearErrorOnReturn clear_error_on_return; - // XXX(sam) Return/throw an error, don't discard the SSL error reason. + // TODO(@sam-github) Return/throw an error, don't discard the SSL error info. bool yes = SSL_renegotiate(w->ssl_.get()) == 1; args.GetReturnValue().Set(yes); } @@ -2254,8 +2347,12 @@ void SSLWrap::GetEphemeralKeyInfo( EVP_PKEY_bits(key.get()))).FromJust(); } break; + default: + break; } } + // TODO(@sam-github) semver-major: else return ThrowCryptoError(env, + // ERR_get_error()) return args.GetReturnValue().Set(info); } @@ -2477,7 +2574,10 @@ int SSLWrap::TLSExtStatusCallback(SSL* s, void* arg) { w->MakeCallback(env->onocspresponse_string(), 1, &arg); - // Somehow, client is expecting different return value here + // No async acceptance is possible, so always return 1 to accept the + // response. The listener for 'OCSPResponse' event has no control over + // return value, but it can .destroy() the connection if the response is not + // acceptable. return 1; } else { // Outgoing response @@ -2519,6 +2619,8 @@ int SSLWrap::SSLCertCallback(SSL* s, void* arg) { return 1; if (w->cert_cb_running_) + // Not an error. Suspend handshake with SSL_ERROR_WANT_X509_LOOKUP, and + // handshake will continue after certcb is done. return -1; Environment* env = w->env(); @@ -2594,6 +2696,8 @@ void SSLWrap::CertCbDone(const FunctionCallbackInfo& args) { if (rv) rv = w->SetCACerts(sc); if (!rv) { + // Not clear why sometimes we throw error, and sometimes we call + // onerror(). Both cause .destroy(), but onerror does a bit more. unsigned long err = ERR_get_error(); // NOLINT(runtime/int) if (!err) return env->ThrowError("CertCbDone"); @@ -5923,6 +6027,24 @@ void GetSSLCiphers(const FunctionCallbackInfo& args) { SSL_CIPHER_get_name(cipher))).FromJust(); } + // TLSv1.3 ciphers aren't listed by EVP. There are only 5, we could just + // document them, but since there are only 5, easier to just add them manually + // and not have to explain their absence in the API docs. They are lower-cased + // because the docs say they will be. + static const char* TLS13_CIPHERS[] = { + "tls_aes_256_gcm_sha384", + "tls_chacha20_poly1305_sha256", + "tls_aes_128_gcm_sha256", + "tls_aes_128_ccm_8_sha256", + "tls_aes_128_ccm_sha256" + }; + + for (unsigned i = 0; i < arraysize(TLS13_CIPHERS); ++i) { + const char* name = TLS13_CIPHERS[i]; + arr->Set(env->context(), + arr->Length(), OneByteString(args.GetIsolate(), name)).FromJust(); + } + args.GetReturnValue().Set(arr); } diff --git a/src/node_crypto.h b/src/node_crypto.h index 45e6271816fc61..7bf8bdf6f4f0fd 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -130,6 +130,7 @@ class SecureContext : public BaseObject { static void AddCACert(const v8::FunctionCallbackInfo& args); static void AddCRL(const v8::FunctionCallbackInfo& args); static void AddRootCerts(const v8::FunctionCallbackInfo& args); + static void SetCipherSuites(const v8::FunctionCallbackInfo& args); static void SetCiphers(const v8::FunctionCallbackInfo& args); static void SetECDHCurve(const v8::FunctionCallbackInfo& args); static void SetDHParam(const v8::FunctionCallbackInfo& args); @@ -138,6 +139,10 @@ class SecureContext : public BaseObject { const v8::FunctionCallbackInfo& args); static void SetSessionTimeout( const v8::FunctionCallbackInfo& args); + static void SetMinProto(const v8::FunctionCallbackInfo& args); + static void SetMaxProto(const v8::FunctionCallbackInfo& args); + static void GetMinProto(const v8::FunctionCallbackInfo& args); + static void GetMaxProto(const v8::FunctionCallbackInfo& args); static void Close(const v8::FunctionCallbackInfo& args); static void LoadPKCS12(const v8::FunctionCallbackInfo& args); #ifndef OPENSSL_NO_ENGINE diff --git a/src/node_crypto_clienthello.cc b/src/node_crypto_clienthello.cc index 268d4773570f8b..8c1be7871acfa1 100644 --- a/src/node_crypto_clienthello.cc +++ b/src/node_crypto_clienthello.cc @@ -86,6 +86,8 @@ void ClientHelloParser::ParseHeader(const uint8_t* data, size_t avail) { // (3,2) TLS v1.1 // (3,3) TLS v1.2 // + // Note that TLS v1.3 uses a TLS v1.2 handshake so requires no specific + // support here. if (data[body_offset_ + 4] != 0x03 || data[body_offset_ + 5] < 0x01 || data[body_offset_ + 5] > 0x03) { diff --git a/src/node_crypto_clienthello.h b/src/node_crypto_clienthello.h index d1661735f5dc4b..725be889e26d64 100644 --- a/src/node_crypto_clienthello.h +++ b/src/node_crypto_clienthello.h @@ -33,6 +33,12 @@ namespace crypto { // Parse the client hello so we can do async session resumption. OpenSSL's // session resumption uses synchronous callbacks, see SSL_CTX_sess_set_get_cb // and get_session_cb. +// +// TLS1.3 handshakes masquerade as TLS1.2 session resumption, and to do this, +// they always include a session_id in the ClientHello, making up a bogus value +// if necessary. The parser can't know if its a bogus id, and will cause a +// 'newSession' event to be emitted. This should do no harm, the id won't be +// found, and the handshake will continue. class ClientHelloParser { public: inline ClientHelloParser(); diff --git a/src/node_errors.h b/src/node_errors.h index 2e9fd761a8319d..60abddf9f279a2 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -55,6 +55,7 @@ void FatalException(v8::Isolate* isolate, V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ V(ERR_STRING_TOO_LONG, Error) \ + V(ERR_TLS_INVALID_PROTOCOL_METHOD, Error) \ V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \ #define V(code, type) \ diff --git a/src/node_options.cc b/src/node_options.cc index 16880ecda95eeb..94e9f16ad218a9 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -327,6 +327,35 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddAlias("-i", "--interactive"); AddOption("--napi-modules", "", NoOp{}, kAllowedInEnvironment); + + AddOption("--tls-min-v1.0", + "set default TLS minimum to TLSv1.0 (default: TLSv1)", + &EnvironmentOptions::tls_min_v1_0, + kAllowedInEnvironment); + AddOption("--tls-min-v1.1", + "set default TLS minimum to TLSv1.1 (default: TLSv1)", + &EnvironmentOptions::tls_min_v1_1, + kAllowedInEnvironment); + AddOption("--tls-min-v1.2", + "set default TLS minimum to TLSv1.2 (default: TLSv1)", + &EnvironmentOptions::tls_min_v1_2, + kAllowedInEnvironment); + AddOption("--tls-min-v1.3", + "set default TLS minimum to TLSv1.3 (default: TLSv1)", + &EnvironmentOptions::tls_min_v1_3, + kAllowedInEnvironment); + AddOption("--tls-max-v1.2", + "set default TLS maximum to TLSv1.2 (default: TLSv1.2)", + &EnvironmentOptions::tls_max_v1_2, + kAllowedInEnvironment); + // Current plan is: + // - 11.x and below: TLS1.3 is opt-in with --tls-max-v1.3 + // - 12.x: TLS1.3 is opt-out with --tls-max-v1.2 + // In either case, support both options they are uniformly available. + AddOption("--tls-max-v1.3", + "set default TLS maximum to TLSv1.3 (default: TLSv1.2)", + &EnvironmentOptions::tls_max_v1_3, + kAllowedInEnvironment); } PerIsolateOptionsParser::PerIsolateOptionsParser( diff --git a/src/node_options.h b/src/node_options.h index 441b8a43c645df..c21e0caf407094 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -136,6 +136,13 @@ class EnvironmentOptions : public Options { bool print_eval = false; bool force_repl = false; + bool tls_min_v1_0 = false; + bool tls_min_v1_1 = false; + bool tls_min_v1_2 = false; + bool tls_min_v1_3 = false; + bool tls_max_v1_2 = false; + bool tls_max_v1_3 = false; + std::vector preload_modules; std::vector user_argv; diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc index 412d9e8e86eac6..59400d8f3a4a72 100644 --- a/src/tls_wrap.cc +++ b/src/tls_wrap.cc @@ -21,6 +21,7 @@ #include "tls_wrap.h" #include "async_wrap-inl.h" +#include "debug_utils.h" #include "node_buffer.h" // Buffer #include "node_crypto.h" // SecureContext #include "node_crypto_bio.h" // NodeBIO @@ -40,6 +41,7 @@ using v8::Exception; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; +using v8::Isolate; using v8::Local; using v8::Object; using v8::ReadOnly; @@ -71,15 +73,18 @@ TLSWrap::TLSWrap(Environment* env, stream->PushStreamListener(this); InitSSL(); + Debug(this, "Created new TLSWrap"); } TLSWrap::~TLSWrap() { + Debug(this, "~TLSWrap()"); sc_ = nullptr; } bool TLSWrap::InvokeQueued(int status, const char* error_str) { + Debug(this, "InvokeQueued(%d, %s)", status, error_str); if (!write_callback_scheduled_) return false; @@ -94,6 +99,7 @@ bool TLSWrap::InvokeQueued(int status, const char* error_str) { void TLSWrap::NewSessionDoneCb() { + Debug(this, "NewSessionDoneCb()"); Cycle(); } @@ -112,6 +118,12 @@ void TLSWrap::InitSSL() { SSL_set_mode(ssl_.get(), SSL_MODE_RELEASE_BUFFERS); #endif // SSL_MODE_RELEASE_BUFFERS + // This is default in 1.1.1, but set it anyway, Cycle() doesn't currently + // re-call ClearIn() if SSL_read() returns SSL_ERROR_WANT_READ, so data can be + // left sitting in the incoming enc_in_ and never get processed. + // - https://wiki.openssl.org/index.php/TLS1.3#Non-application_data_records + SSL_set_mode(ssl_.get(), SSL_MODE_AUTO_RETRY); + SSL_set_app_data(ssl_.get(), this); // Using InfoCallback isn't how we are supposed to check handshake progress: // https://github.com/openssl/openssl/issues/7199#issuecomment-420915993 @@ -177,6 +189,7 @@ void TLSWrap::Receive(const FunctionCallbackInfo& args) { CHECK(Buffer::HasInstance(args[0])); char* data = Buffer::Data(args[0]); size_t len = Buffer::Length(args[0]); + Debug(wrap, "Receiving %zu bytes injected from JS", len); // Copy given buffer entirely or partiall if handle becomes closed while (len > 0 && wrap->IsAlive() && !wrap->IsClosing()) { @@ -223,6 +236,9 @@ void TLSWrap::SSLInfoCallback(const SSL* ssl_, int where, int ret) { Local object = c->object(); if (where & SSL_CB_HANDSHAKE_START) { + Debug(c, "SSLInfoCallback(SSL_CB_HANDSHAKE_START);"); + // Start is tracked to limit number and frequency of renegotiation attempts, + // since excessive renegotiation may be an attack. Local callback; if (object->Get(env->context(), env->onhandshakestart_string()) @@ -236,6 +252,8 @@ void TLSWrap::SSLInfoCallback(const SSL* ssl_, int where, int ret) { // sending HelloRequest in OpenSSL-1.1.1. // We need to check whether this is in a renegotiation state or not. if (where & SSL_CB_HANDSHAKE_DONE && !SSL_renegotiate_pending(ssl)) { + Debug(c, "SSLInfoCallback(SSL_CB_HANDSHAKE_DONE);"); + CHECK(!SSL_renegotiate_pending(ssl)); Local callback; c->established_ = true; @@ -249,29 +267,59 @@ void TLSWrap::SSLInfoCallback(const SSL* ssl_, int where, int ret) { void TLSWrap::EncOut() { + Debug(this, "Trying to write encrypted output"); + // Ignore cycling data if ClientHello wasn't yet parsed - if (!hello_parser_.IsEnded()) + if (!hello_parser_.IsEnded()) { + Debug(this, "Returning from EncOut(), hello_parser_ active"); return; + } // Write in progress - if (write_size_ != 0) + if (write_size_ != 0) { + Debug(this, "Returning from EncOut(), write currently in progress"); return; + } // Wait for `newSession` callback to be invoked - if (is_awaiting_new_session()) + if (is_awaiting_new_session()) { + Debug(this, "Returning from EncOut(), awaiting new session"); return; + } // Split-off queue - if (established_ && current_write_ != nullptr) + if (established_ && current_write_ != nullptr) { + Debug(this, "EncOut() setting write_callback_scheduled_"); write_callback_scheduled_ = true; + } - if (ssl_ == nullptr) + if (ssl_ == nullptr) { + Debug(this, "Returning from EncOut(), ssl_ == nullptr"); return; + } // No encrypted output ready to write to the underlying stream. if (BIO_pending(enc_out_) == 0) { - if (pending_cleartext_input_.empty()) - InvokeQueued(0); + Debug(this, "No pending encrypted output"); + if (pending_cleartext_input_.empty()) { + if (!in_dowrite_) { + Debug(this, "No pending cleartext input, not inside DoWrite()"); + InvokeQueued(0); + } else { + Debug(this, "No pending cleartext input, inside DoWrite()"); + // TODO(@sam-github, @addaleax) If in_dowrite_ is true, appdata was + // passed to SSL_write(). If we are here, the data was not encrypted to + // enc_out_ yet. Calling Done() "works", but since the write is not + // flushed, its too soon. Just returning and letting the next EncOut() + // call Done() passes the test suite, but without more careful analysis, + // its not clear if it is always correct. Not calling Done() could block + // data flow, so for now continue to call Done(), just do it in the next + // tick. + env()->SetImmediate([](Environment* env, void* data) { + static_cast(data)->InvokeQueued(0); + }, this, object()); + } + } return; } @@ -288,6 +336,7 @@ void TLSWrap::EncOut() { for (size_t i = 0; i < count; i++) buf[i] = uv_buf_init(data[i], size[i]); + Debug(this, "Writing %zu buffers to the underlying stream", count); StreamWriteResult res = underlying_stream()->Write(bufs, count); if (res.err != 0) { InvokeQueued(res.err); @@ -295,6 +344,7 @@ void TLSWrap::EncOut() { } if (!res.async) { + Debug(this, "Write finished synchronously"); HandleScope handle_scope(env()->isolate()); // Simulate asynchronous finishing, TLS cannot handle this at the moment. @@ -306,21 +356,26 @@ void TLSWrap::EncOut() { void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) { + Debug(this, "OnStreamAfterWrite(status = %d)", status); if (current_empty_write_ != nullptr) { + Debug(this, "Had empty write"); WriteWrap* finishing = current_empty_write_; current_empty_write_ = nullptr; finishing->Done(status); return; } - if (ssl_ == nullptr) + if (ssl_ == nullptr) { + Debug(this, "ssl_ == nullptr, marking as cancelled"); status = UV_ECANCELED; + } // Handle error if (status) { - // Ignore errors after shutdown - if (shutdown_) + if (shutdown_) { + Debug(this, "Ignoring error after shutdown"); return; + } // Notify about error InvokeQueued(status); @@ -367,9 +422,43 @@ Local TLSWrap::GetSSLError(int status, int* err, std::string* msg) { BUF_MEM* mem; BIO_get_mem_ptr(bio, &mem); + Isolate* isolate = env()->isolate(); + Local context = isolate->GetCurrentContext(); + Local message = - OneByteString(env()->isolate(), mem->data, mem->length); + OneByteString(isolate, mem->data, mem->length); Local exception = Exception::Error(message); + Local obj = exception->ToObject(context).ToLocalChecked(); + + const char* ls = ERR_lib_error_string(ssl_err); + const char* fs = ERR_func_error_string(ssl_err); + const char* rs = ERR_reason_error_string(ssl_err); + + if (ls != nullptr) + obj->Set(context, env()->library_string(), + OneByteString(isolate, ls)).FromJust(); + if (fs != nullptr) + obj->Set(context, env()->function_string(), + OneByteString(isolate, fs)).FromJust(); + if (rs != nullptr) { + obj->Set(context, env()->reason_string(), + OneByteString(isolate, rs)).FromJust(); + + // SSL has no API to recover the error name from the number, so we + // transform reason strings like "this error" to "ERR_SSL_THIS_ERROR", + // which ends up being close to the original error macro name. + std::string code(rs); + + for (auto& c : code) { + if (c == ' ') + c = '_'; + else + c = ::toupper(c); + } + obj->Set(context, env()->code_string(), + OneByteString(isolate, ("ERR_SSL_" + code).c_str())) + .FromJust(); + } if (msg != nullptr) msg->assign(mem->data, mem->data + mem->length); @@ -387,16 +476,23 @@ Local TLSWrap::GetSSLError(int status, int* err, std::string* msg) { void TLSWrap::ClearOut() { + Debug(this, "Trying to read cleartext output"); // Ignore cycling data if ClientHello wasn't yet parsed - if (!hello_parser_.IsEnded()) + if (!hello_parser_.IsEnded()) { + Debug(this, "Returning from ClearOut(), hello_parser_ active"); return; + } // No reads after EOF - if (eof_) + if (eof_) { + Debug(this, "Returning from ClearOut(), EOF reached"); return; + } - if (ssl_ == nullptr) + if (ssl_ == nullptr) { + Debug(this, "Returning from ClearOut(), ssl_ == nullptr"); return; + } crypto::MarkPopErrorOnReturn mark_pop_error_on_return; @@ -404,6 +500,7 @@ void TLSWrap::ClearOut() { int read; for (;;) { read = SSL_read(ssl_.get(), out, sizeof(out)); + Debug(this, "Read %d bytes of cleartext output", read); if (read <= 0) break; @@ -421,8 +518,10 @@ void TLSWrap::ClearOut() { // Caveat emptor: OnRead() calls into JS land which can result in // the SSL context object being destroyed. We have to carefully // check that ssl_ != nullptr afterwards. - if (ssl_ == nullptr) + if (ssl_ == nullptr) { + Debug(this, "Returning from read loop, ssl_ == nullptr"); return; + } read -= avail; current += avail; @@ -448,6 +547,7 @@ void TLSWrap::ClearOut() { return; if (!arg.IsEmpty()) { + Debug(this, "Got SSL error (%d), calling onerror", err); // When TLS Alert are stored in wbio, // it should be flushed to socket before destroyed. if (BIO_pending(enc_out_) != 0) @@ -460,12 +560,17 @@ void TLSWrap::ClearOut() { void TLSWrap::ClearIn() { + Debug(this, "Trying to write cleartext input"); // Ignore cycling data if ClientHello wasn't yet parsed - if (!hello_parser_.IsEnded()) + if (!hello_parser_.IsEnded()) { + Debug(this, "Returning from ClearIn(), hello_parser_ active"); return; + } - if (ssl_ == nullptr) + if (ssl_ == nullptr) { + Debug(this, "Returning from ClearIn(), ssl_ == nullptr"); return; + } std::vector buffers; buffers.swap(pending_cleartext_input_); @@ -478,6 +583,7 @@ void TLSWrap::ClearIn() { size_t avail = buffers[i].len; char* data = buffers[i].base; written = SSL_write(ssl_.get(), data, avail); + Debug(this, "Writing %zu bytes, written = %d", avail, written); CHECK(written == -1 || written == static_cast(avail)); if (written == -1) break; @@ -485,6 +591,7 @@ void TLSWrap::ClearIn() { // All written if (i == buffers.size()) { + Debug(this, "Successfully wrote all data to SSL"); // We wrote all the buffers, so no writes failed (written < 0 on failure). CHECK_GE(written, 0); return; @@ -498,11 +605,13 @@ void TLSWrap::ClearIn() { std::string error_str; Local arg = GetSSLError(written, &err, &error_str); if (!arg.IsEmpty()) { + Debug(this, "Got SSL error (%d)", err); write_callback_scheduled_ = true; - // XXX(sam) Should forward an error object with .code/.function/.etc, if - // possible. + // TODO(@sam-github) Should forward an error object with + // .code/.function/.etc, if possible. InvokeQueued(UV_EPROTO, error_str.c_str()); } else { + Debug(this, "Pushing back %zu buffers", buffers.size() - i); // Push back the not-yet-written pending buffers into their queue. // This can be skipped in the error case because no further writes // would succeed anyway. @@ -515,6 +624,17 @@ void TLSWrap::ClearIn() { } +std::string TLSWrap::diagnostic_name() const { + std::string name = "TLSWrap "; + if (is_server()) + name += "server ("; + else + name += "client ("; + name += std::to_string(static_cast(get_async_id())) + ")"; + return name; +} + + AsyncWrap* TLSWrap::GetAsyncWrap() { return static_cast(this); } @@ -544,6 +664,7 @@ bool TLSWrap::IsClosing() { int TLSWrap::ReadStart() { + Debug(this, "ReadStart()"); if (stream_ != nullptr) return stream_->ReadStart(); return 0; @@ -551,6 +672,7 @@ int TLSWrap::ReadStart() { int TLSWrap::ReadStop() { + Debug(this, "ReadStop()"); if (stream_ != nullptr) return stream_->ReadStop(); return 0; @@ -568,11 +690,13 @@ void TLSWrap::ClearError() { // Called by StreamBase::Write() to request async write of clear text into SSL. +// TODO(@sam-github) Should there be a TLSWrap::DoTryWrite()? int TLSWrap::DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count, uv_stream_t* send_handle) { CHECK_NULL(send_handle); + Debug(this, "DoWrite()"); if (ssl_ == nullptr) { ClearError(); @@ -600,8 +724,10 @@ int TLSWrap::DoWrite(WriteWrap* w, // onto the socket, we just want the side-effects. After, make sure the // WriteWrap was accepted by the stream, or that we call Done() on it. if (empty) { + Debug(this, "Empty write"); ClearOut(); if (BIO_pending(enc_out_) == 0) { + Debug(this, "No pending encrypted output, writing to underlying stream"); CHECK_NULL(current_empty_write_); current_empty_write_ = w; StreamWriteResult res = @@ -632,6 +758,7 @@ int TLSWrap::DoWrite(WriteWrap* w, for (i = 0; i < count; i++) { written = SSL_write(ssl_.get(), bufs[i].base, bufs[i].len); CHECK(written == -1 || written == static_cast(bufs[i].len)); + Debug(this, "Writing %zu bytes, written = %d", bufs[i].len, written); if (written == -1) break; } @@ -642,10 +769,12 @@ int TLSWrap::DoWrite(WriteWrap* w, // If we stopped writing because of an error, it's fatal, discard the data. if (!arg.IsEmpty()) { + Debug(this, "Got SSL error (%d), returning UV_EPROTO", err); current_write_ = nullptr; return UV_EPROTO; } + Debug(this, "Saving %zu buffers for later write", count - i); // Otherwise, save unwritten data so it can be written later by ClearIn(). pending_cleartext_input_.insert(pending_cleartext_input_.end(), &bufs[i], @@ -653,7 +782,10 @@ int TLSWrap::DoWrite(WriteWrap* w, } // Write any encrypted/handshake output that may be ready. + // Guard against sync call of current_write_->Done(), its unsupported. + in_dowrite_ = true; EncOut(); + in_dowrite_ = false; return 0; } @@ -669,6 +801,7 @@ uv_buf_t TLSWrap::OnStreamAlloc(size_t suggested_size) { void TLSWrap::OnStreamRead(ssize_t nread, const uv_buf_t& buf) { + Debug(this, "Read %zd bytes from underlying stream", nread); if (nread < 0) { // Error should be emitted only after all data was read ClearOut(); @@ -684,10 +817,10 @@ void TLSWrap::OnStreamRead(ssize_t nread, const uv_buf_t& buf) { return; } - if (ssl_ == nullptr) { - EmitRead(UV_EPROTO); - return; - } + // DestroySSL() is the only thing that un-sets ssl_, but that also removes + // this TLSWrap as a stream listener, so we should not receive OnStreamRead() + // calls anymore. + CHECK(ssl_); // Commit the amount of data actually read into the peeked/allocated buffer // from the underlying stream. @@ -702,6 +835,7 @@ void TLSWrap::OnStreamRead(ssize_t nread, const uv_buf_t& buf) { size_t avail = 0; uint8_t* data = reinterpret_cast(enc_in->Peek(&avail)); CHECK_IMPLIES(data == nullptr, avail == 0); + Debug(this, "Passing %zu bytes to the hello parser", avail); return hello_parser_.Parse(data, avail); } @@ -716,6 +850,7 @@ ShutdownWrap* TLSWrap::CreateShutdownWrap(Local req_wrap_object) { int TLSWrap::DoShutdown(ShutdownWrap* req_wrap) { + Debug(this, "DoShutdown()"); crypto::MarkPopErrorOnReturn mark_pop_error_on_return; if (ssl_ && SSL_shutdown(ssl_.get()) == 0) @@ -781,6 +916,7 @@ void TLSWrap::EnableSessionCallbacks( void TLSWrap::DestroySSL(const FunctionCallbackInfo& args) { TLSWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + Debug(wrap, "DestroySSL()"); // If there is a write happening, mark it as finished. wrap->write_callback_scheduled_ = true; @@ -795,6 +931,7 @@ void TLSWrap::DestroySSL(const FunctionCallbackInfo& args) { if (wrap->stream_ != nullptr) wrap->stream_->RemoveStreamListener(wrap); + Debug(wrap, "DestroySSL() finished"); } @@ -807,6 +944,7 @@ void TLSWrap::EnableCertCb(const FunctionCallbackInfo& args) { void TLSWrap::OnClientHelloParseEnd(void* arg) { TLSWrap* c = static_cast(arg); + Debug(c, "OnClientHelloParseEnd()"); c->Cycle(); } diff --git a/src/tls_wrap.h b/src/tls_wrap.h index 1ab19d3ee02bb9..85a53f236df5c9 100644 --- a/src/tls_wrap.h +++ b/src/tls_wrap.h @@ -88,6 +88,8 @@ class TLSWrap : public AsyncWrap, SET_MEMORY_INFO_NAME(TLSWrap) SET_SELF_SIZE(TLSWrap) + std::string diagnostic_name() const override; + protected: // Alternative to StreamListener::stream(), that returns a StreamBase instead // of a StreamResource. @@ -172,6 +174,7 @@ class TLSWrap : public AsyncWrap, std::vector pending_cleartext_input_; size_t write_size_ = 0; WriteWrap* current_write_ = nullptr; + bool in_dowrite_ = false; WriteWrap* current_empty_write_ = nullptr; bool write_callback_scheduled_ = false; bool started_ = false; diff --git a/test/async-hooks/test-graph.tls-write-12.js b/test/async-hooks/test-graph.tls-write-12.js new file mode 100644 index 00000000000000..748234402c0e92 --- /dev/null +++ b/test/async-hooks/test-graph.tls-write-12.js @@ -0,0 +1,11 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); + +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +require('./test-graph.tls-write.js'); diff --git a/test/async-hooks/test-graph.tls-write.js b/test/async-hooks/test-graph.tls-write.js index 2ea03283e4c861..580264316dea90 100644 --- a/test/async-hooks/test-graph.tls-write.js +++ b/test/async-hooks/test-graph.tls-write.js @@ -39,8 +39,10 @@ function onlistening() { function onsecureConnection() {} function onsecureConnect() { - // Destroying client socket - this.destroy(); + // end() client socket, which causes slightly different hook events than + // destroy(), but with TLS1.3 destroy() rips the connection down before the + // server completes the handshake. + this.end(); // Closing server server.close(common.mustCall(onserverClosed)); @@ -68,7 +70,8 @@ function onexit() { { type: 'WRITEWRAP', id: 'write:2', triggerAsyncId: null }, { type: 'WRITEWRAP', id: 'write:3', triggerAsyncId: null }, { type: 'WRITEWRAP', id: 'write:4', triggerAsyncId: null }, - { type: 'Immediate', id: 'immediate:1', triggerAsyncId: 'tcp:1' }, - { type: 'Immediate', id: 'immediate:2', triggerAsyncId: 'tcp:2' } ] + { type: 'Immediate', id: 'immediate:1', triggerAsyncId: 'tcp:2' }, + { type: 'Immediate', id: 'immediate:2', triggerAsyncId: 'tcp:1' }, + ] ); } diff --git a/test/async-hooks/test-tlswrap.js b/test/async-hooks/test-tlswrap.js index 354cd7ad0cd91d..d6dcd204703d9d 100644 --- a/test/async-hooks/test-tlswrap.js +++ b/test/async-hooks/test-tlswrap.js @@ -15,6 +15,10 @@ const { checkInvocations } = require('./hook-checks'); const hooks = initHooks(); hooks.enable(); +// TODO(@sam-github) assumes server handshake completes before client, true for +// 1.2, not for 1.3. Might need a rewrite for TLS1.3. +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + // // Creating server and listening on port // @@ -52,6 +56,7 @@ function onsecureConnection() { // const as = hooks.activitiesOfTypes('TLSWRAP'); assert.strictEqual(as.length, 2); + // TODO(@sam-github) This happens after onsecureConnect, with TLS1.3. client = as[1]; assert.strictEqual(client.type, 'TLSWRAP'); assert.strictEqual(typeof client.uid, 'number'); @@ -78,7 +83,7 @@ function onsecureConnect() { // // Destroying client socket // - this.destroy(); + this.destroy(); // This destroys client before server handshakes, with TLS1.3 checkInvocations(svr, { init: 1, before: 2, after: 1 }, 'server: when destroying client'); checkInvocations(client, { init: 1, before: 2, after: 2 }, diff --git a/test/parallel/test-crypto.js b/test/parallel/test-crypto.js index 10a8f529ddfd58..680a6eae6975ea 100644 --- a/test/parallel/test-crypto.js +++ b/test/parallel/test-crypto.js @@ -130,6 +130,7 @@ validateList(cryptoCiphers); // Assume that we have at least AES256-SHA. const tlsCiphers = tls.getCiphers(); assert(tls.getCiphers().includes('aes256-sha')); +assert(tls.getCiphers().includes('tls_aes_128_ccm_8_sha256')); // There should be no capital letters in any element. const noCapitals = /^[^A-Z]+$/; assert(tlsCiphers.every((value) => noCapitals.test(value))); diff --git a/test/parallel/test-https-agent-additional-options.js b/test/parallel/test-https-agent-additional-options.js index eaa6ea710e4d9f..a04ef7461d033f 100644 --- a/test/parallel/test-https-agent-additional-options.js +++ b/test/parallel/test-https-agent-additional-options.js @@ -1,3 +1,4 @@ +// Flags: --tls-min-v1.1 'use strict'; const common = require('../common'); if (!common.hasCrypto) @@ -34,7 +35,7 @@ const updatedValues = new Map([ ['ecdhCurve', 'secp384r1'], ['honorCipherOrder', true], ['secureOptions', crypto.constants.SSL_OP_CIPHER_SERVER_PREFERENCE], - ['secureProtocol', 'TLSv1_method'], + ['secureProtocol', 'TLSv1_1_method'], ['sessionIdContext', 'sessionIdContext'], ]); diff --git a/test/parallel/test-https-agent-session-eviction.js b/test/parallel/test-https-agent-session-eviction.js index cf6a1341c1e03f..8e13b150bb1362 100644 --- a/test/parallel/test-https-agent-session-eviction.js +++ b/test/parallel/test-https-agent-session-eviction.js @@ -1,3 +1,4 @@ +// Flags: --tls-min-v1.0 'use strict'; const common = require('../common'); diff --git a/test/parallel/test-https-client-renegotiation-limit.js b/test/parallel/test-https-client-renegotiation-limit.js index 1fccc7bb5d1629..3527c48f4b9580 100644 --- a/test/parallel/test-https-client-renegotiation-limit.js +++ b/test/parallel/test-https-client-renegotiation-limit.js @@ -32,6 +32,9 @@ const tls = require('tls'); const https = require('https'); const fixtures = require('../common/fixtures'); +// Renegotiation as a protocol feature was dropped after TLS1.2. +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + // renegotiation limits to test const LIMITS = [0, 1, 2, 3, 5, 10, 16]; diff --git a/test/parallel/test-https-client-resume.js b/test/parallel/test-https-client-resume.js index cf1bbdf2626823..8904e24180b329 100644 --- a/test/parallel/test-https-client-resume.js +++ b/test/parallel/test-https-client-resume.js @@ -55,7 +55,8 @@ server.listen(0, common.mustCall(function() { '\r\n'); })); - client1.on('session', common.mustCall((session) => { + // TLS1.2 servers issue 1 ticket, TLS1.3 issues more, but only use the first. + client1.once('session', common.mustCall((session) => { console.log('session'); const opts = { diff --git a/test/parallel/test-process-env-allowed-flags.js b/test/parallel/test-process-env-allowed-flags.js index 8b6adfb2224ad5..17d58e139a04fd 100644 --- a/test/parallel/test-process-env-allowed-flags.js +++ b/test/parallel/test-process-env-allowed-flags.js @@ -51,7 +51,7 @@ require('../common'); // Assert all "canonical" flags begin with dash(es) { process.allowedNodeEnvironmentFlags.forEach((flag) => { - assert(/^--?[a-z8_-]+$/.test(flag), `Unexpected format for flag ${flag}`); + assert(/^--?[a-z0-9._-]+$/.test(flag), `Unexpected format for flag ${flag}`); }); } diff --git a/test/parallel/test-tls-alert-handling.js b/test/parallel/test-tls-alert-handling.js index e88453b115e0ea..f9f42e2d51c04d 100644 --- a/test/parallel/test-tls-alert-handling.js +++ b/test/parallel/test-tls-alert-handling.js @@ -7,6 +7,7 @@ if (!common.hasCrypto) if (!common.opensslCli) common.skip('node compiled without OpenSSL CLI'); +const assert = require('assert'); const net = require('net'); const tls = require('tls'); const fixtures = require('../common/fixtures'); @@ -29,7 +30,11 @@ const opts = { const max_iter = 20; let iter = 0; -const errorHandler = common.mustCall(() => { +const errorHandler = common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_WRONG_VERSION_NUMBER'); + assert.strictEqual(err.library, 'SSL routines'); + assert.strictEqual(err.function, 'ssl3_get_record'); + assert.strictEqual(err.reason, 'wrong version number'); errorReceived = true; if (canCloseServer()) server.close(); @@ -59,7 +64,7 @@ function sendClient() { } client.end(); }, max_iter)); - client.write('a'); + client.write('a', common.mustCall()); client.on('error', common.mustNotCall()); client.on('close', common.mustCall(function() { clientClosed = true; @@ -81,5 +86,10 @@ function sendBADTLSRecord() { socket.end(BAD_RECORD); }); })); - client.on('error', common.mustCall()); + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); + assert.strictEqual(err.library, 'SSL routines'); + assert.strictEqual(err.function, 'ssl3_read_bytes'); + assert.strictEqual(err.reason, 'tlsv1 alert protocol version'); + })); } diff --git a/test/parallel/test-tls-async-cb-after-socket-end.js b/test/parallel/test-tls-async-cb-after-socket-end.js index 5c812c8f0432ea..49ca0cebc9b524 100644 --- a/test/parallel/test-tls-async-cb-after-socket-end.js +++ b/test/parallel/test-tls-async-cb-after-socket-end.js @@ -14,7 +14,6 @@ const tls = require('tls'); // new and resume session events will never be emitted on the server. const options = { - maxVersion: 'TLSv1.2', secureOptions: SSL_OP_NO_TICKET, key: fixtures.readSync('test_key.pem'), cert: fixtures.readSync('test_cert.pem') @@ -38,6 +37,10 @@ server.on('resumeSession', common.mustCall((id, cb) => { server.listen(0, common.mustCall(() => { const clientOpts = { + // Don't send a TLS1.3/1.2 ClientHello, they contain a fake session_id, + // which triggers a 'resumeSession' event for client1. TLS1.2 ClientHello + // won't have a session_id until client2, which will have a valid session. + maxVersion: 'TLSv1.2', port: server.address().port, rejectUnauthorized: false, session: false diff --git a/test/parallel/test-tls-basic-validations.js b/test/parallel/test-tls-basic-validations.js index 8d39b8d45c9c65..9bcc63c45455c7 100644 --- a/test/parallel/test-tls-basic-validations.js +++ b/test/parallel/test-tls-basic-validations.js @@ -12,7 +12,8 @@ common.expectsError( { code: 'ERR_INVALID_ARG_TYPE', type: TypeError, - message: 'Ciphers must be a string' + message: 'The "options.ciphers" property must be of type string.' + + ' Received type number' }); common.expectsError( @@ -20,7 +21,8 @@ common.expectsError( { code: 'ERR_INVALID_ARG_TYPE', type: TypeError, - message: 'Ciphers must be a string' + message: 'The "options.ciphers" property must be of type string.' + + ' Received type number' }); common.expectsError( diff --git a/test/parallel/test-tls-cli-max-version-1.2.js b/test/parallel/test-tls-cli-max-version-1.2.js new file mode 100644 index 00000000000000..8ef4eaf79a4600 --- /dev/null +++ b/test/parallel/test-tls-cli-max-version-1.2.js @@ -0,0 +1,15 @@ +// Flags: --tls-max-v1.2 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-max-v1.2` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); diff --git a/test/parallel/test-tls-cli-max-version-1.3.js b/test/parallel/test-tls-cli-max-version-1.3.js new file mode 100644 index 00000000000000..aada5b553a7554 --- /dev/null +++ b/test/parallel/test-tls-cli-max-version-1.3.js @@ -0,0 +1,15 @@ +// Flags: --tls-max-v1.3 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-max-v1.3` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.3'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); diff --git a/test/parallel/test-tls-cli-min-version-1.0.js b/test/parallel/test-tls-cli-min-version-1.0.js new file mode 100644 index 00000000000000..0a227c0b949556 --- /dev/null +++ b/test/parallel/test-tls-cli-min-version-1.0.js @@ -0,0 +1,15 @@ +// Flags: --tls-min-v1.0 --tls-min-v1.1 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that `node --tls-v1.0` is supported, and overrides --tls-v1.1. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); diff --git a/test/parallel/test-tls-cli-min-version-1.1.js b/test/parallel/test-tls-cli-min-version-1.1.js new file mode 100644 index 00000000000000..1219c82030da9b --- /dev/null +++ b/test/parallel/test-tls-cli-min-version-1.1.js @@ -0,0 +1,15 @@ +// Flags: --tls-min-v1.1 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-v1.1` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.1'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); diff --git a/test/parallel/test-tls-cli-min-version-1.2.js b/test/parallel/test-tls-cli-min-version-1.2.js new file mode 100644 index 00000000000000..058dc180f6065e --- /dev/null +++ b/test/parallel/test-tls-cli-min-version-1.2.js @@ -0,0 +1,15 @@ +// Flags: --tls-min-v1.2 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-min-v1.2` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.2'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); diff --git a/test/parallel/test-tls-cli-min-version-1.3.js b/test/parallel/test-tls-cli-min-version-1.3.js new file mode 100644 index 00000000000000..3ac335ecdd5bfb --- /dev/null +++ b/test/parallel/test-tls-cli-min-version-1.3.js @@ -0,0 +1,15 @@ +// Flags: --tls-min-v1.3 +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +// Check that node `--tls-min-v1.3` is supported. + +const assert = require('assert'); +const tls = require('tls'); + +assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2'); +assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.3'); + +// Check the min-max version protocol versions against these CLI settings. +require('./test-tls-min-max-version.js'); diff --git a/test/parallel/test-tls-client-auth.js b/test/parallel/test-tls-client-auth.js index 1f8c7e6096ff11..476238961986cd 100644 --- a/test/parallel/test-tls-client-auth.js +++ b/test/parallel/test-tls-client-auth.js @@ -1,10 +1,10 @@ 'use strict'; -require('../common'); +const common = require('../common'); const fixtures = require('../common/fixtures'); const { - assert, connect, keys + assert, connect, keys, tls } = require(fixtures.path('tls-connect')); // Use ec10 and agent10, they are the only identities with intermediate CAs. @@ -63,9 +63,10 @@ connect({ return cleanup(); }); -// Request cert from client that doesn't have one. +// Request cert from TLS1.2 client that doesn't have one. connect({ client: { + maxVersion: 'TLSv1.2', ca: server.ca, checkServerIdentity, }, @@ -76,10 +77,38 @@ connect({ requestCert: true, }, }, function(err, pair, cleanup) { - assert.strictEqual(err.code, 'ECONNRESET'); + assert.strictEqual(pair.server.err.code, + 'ERR_SSL_PEER_DID_NOT_RETURN_A_CERTIFICATE'); + assert.strictEqual(pair.client.err.code, 'ECONNRESET'); return cleanup(); }); +// Request cert from TLS1.3 client that doesn't have one. +if (tls.DEFAULT_MAX_VERSION === 'TLSv1.3') connect({ + client: { + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(pair.server.err.code, + 'ERR_SSL_PEER_DID_NOT_RETURN_A_CERTIFICATE'); + + // TLS1.3 client completes handshake before server, and its only after the + // server handshakes, requests certs, gets back a zero-length list of certs, + // and sends a fatal Alert to the client that the client discovers there has + // been a fatal error. + pair.client.conn.once('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'); + cleanup(); + })); +}); + // Typical configuration error, incomplete cert chains sent, we have to know the // peer's subordinate CAs in order to verify the peer. connect({ diff --git a/test/parallel/test-tls-client-getephemeralkeyinfo.js b/test/parallel/test-tls-client-getephemeralkeyinfo.js index 8a9cc65a1cac25..113b452db60583 100644 --- a/test/parallel/test-tls-client-getephemeralkeyinfo.js +++ b/test/parallel/test-tls-client-getephemeralkeyinfo.js @@ -10,6 +10,9 @@ const tls = require('tls'); const key = fixtures.readKey('agent2-key.pem'); const cert = fixtures.readKey('agent2-cert.pem'); +// TODO(@sam-github) test works with TLS1.3, rework test to add +// 'ECDH' with 'TLS_AES_128_GCM_SHA256', + function loadDHParam(n) { return fixtures.readKey(`dh${n}.pem`); } diff --git a/test/parallel/test-tls-client-reject-12.js b/test/parallel/test-tls-client-reject-12.js new file mode 100644 index 00000000000000..f77d463f44dc71 --- /dev/null +++ b/test/parallel/test-tls-client-reject-12.js @@ -0,0 +1,13 @@ +'use strict'; + +// test-tls-client-reject specifically for TLS1.2. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); + +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +require('./test-tls-client-reject.js'); diff --git a/test/parallel/test-tls-client-reject.js b/test/parallel/test-tls-client-reject.js index 9eff6cb9cead01..329b78c271baaa 100644 --- a/test/parallel/test-tls-client-reject.js +++ b/test/parallel/test-tls-client-reject.js @@ -35,6 +35,7 @@ const options = { const server = tls.createServer(options, function(socket) { socket.pipe(socket); + // Pipe already ends... but leaving this here tests .end() after .end(). socket.on('end', () => socket.end()); }).listen(0, common.mustCall(function() { unauthorized(); @@ -47,13 +48,19 @@ function unauthorized() { servername: 'localhost', rejectUnauthorized: false }, common.mustCall(function() { - console.log('... unauthorized'); + let _data; assert(!socket.authorized); socket.on('data', common.mustCall((data) => { assert.strictEqual(data.toString(), 'ok'); + _data = data; + })); + socket.on('end', common.mustCall(() => { + assert(_data, 'data failed to echo!'); })); socket.on('end', () => rejectUnauthorized()); })); + socket.once('session', common.mustCall(() => { + })); socket.on('error', common.mustNotCall()); socket.end('ok'); } @@ -65,7 +72,6 @@ function rejectUnauthorized() { }, common.mustNotCall()); socket.on('data', common.mustNotCall()); socket.on('error', common.mustCall(function(err) { - console.log('... rejected:', err); authorized(); })); socket.end('ng'); diff --git a/test/parallel/test-tls-client-renegotiation-13.js b/test/parallel/test-tls-client-renegotiation-13.js new file mode 100644 index 00000000000000..cce41578ba31b2 --- /dev/null +++ b/test/parallel/test-tls-client-renegotiation-13.js @@ -0,0 +1,41 @@ +// Flags: --tls-max-v1.3 +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); + +if (!require('constants').TLS1_3_VERSION) + common.skip(`openssl ${process.versions.openssl} does not support TLSv1.3`); + +// Confirm that for TLSv1.3, renegotiate() is disallowed. + +const { + assert, connect, keys +} = require(fixtures.path('tls-connect')); + +const server = keys.agent10; + +connect({ + client: { + ca: server.ca, + checkServerIdentity: common.mustCall(), + }, + server: { + key: server.key, + cert: server.cert, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + + const client = pair.client.conn; + + assert.strictEqual(client.getProtocol(), 'TLSv1.3'); + + const ok = client.renegotiate({}, common.mustCall((err) => { + assert(err.code, 'ERR_TLS_RENEGOTIATE'); + assert(err.message, 'Attempt to renegotiate TLS session failed'); + cleanup(); + })); + + assert.strictEqual(ok, false); +}); diff --git a/test/parallel/test-tls-client-renegotiation-limit.js b/test/parallel/test-tls-client-renegotiation-limit.js index daae92eeb46d67..010fd8596aa0c8 100644 --- a/test/parallel/test-tls-client-renegotiation-limit.js +++ b/test/parallel/test-tls-client-renegotiation-limit.js @@ -31,6 +31,9 @@ const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); +// Renegotiation as a protocol feature was dropped after TLS1.2. +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + // renegotiation limits to test const LIMITS = [0, 1, 2, 3, 5, 10, 16]; diff --git a/test/parallel/test-tls-client-resume-12.js b/test/parallel/test-tls-client-resume-12.js new file mode 100644 index 00000000000000..7767d3dd2a5cc9 --- /dev/null +++ b/test/parallel/test-tls-client-resume-12.js @@ -0,0 +1,13 @@ +'use strict'; + +// test-tls-client-resume specifically for TLS1.2. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); + +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +require('./test-tls-client-resume.js'); diff --git a/test/parallel/test-tls-client-resume.js b/test/parallel/test-tls-client-resume.js index 9f868fdcdc0a49..0f5c9caa7c3f8f 100644 --- a/test/parallel/test-tls-client-resume.js +++ b/test/parallel/test-tls-client-resume.js @@ -44,32 +44,59 @@ const server = tls.Server(options, common.mustCall((socket) => { // start listening server.listen(0, common.mustCall(function() { - - let sessionx = null; - let session1 = null; + let sessionx = null; // From right after connect, invalid for TLS1.3 + let session1 = null; // Delivered by the session event, always valid. + let sessions = 0; + let tls13; const client1 = tls.connect({ port: this.address().port, rejectUnauthorized: false }, common.mustCall(() => { - console.log('connect1'); + tls13 = client1.getProtocol() === 'TLSv1.3'; assert.strictEqual(client1.isSessionReused(), false); sessionx = client1.getSession(); + assert(sessionx); + + if (session1) + reconnect(); + })); + + client1.on('data', common.mustCall((d) => { })); client1.once('session', common.mustCall((session) => { console.log('session1'); session1 = session; + assert(session1); + if (sessionx) + reconnect(); })); - client1.on('close', common.mustCall(() => { + client1.on('session', () => { + console.log('client1 session#', ++sessions); + }); + + client1.on('close', () => { + console.log('client1 close'); + assert.strictEqual(sessions, tls13 ? 2 : 1); + }); + + function reconnect() { assert(sessionx); assert(session1); - assert.strictEqual(sessionx.compare(session1), 0); + if (tls13) + // For TLS1.3, the session immediately after handshake is a dummy, + // unresumable session. The one delivered later in session event is + // resumable. + assert.notStrictEqual(sessionx.compare(session1), 0); + else + // For TLS1.2, they are identical. + assert.strictEqual(sessionx.compare(session1), 0); const opts = { port: server.address().port, rejectUnauthorized: false, - session: session1 + session: session1, }; const client2 = tls.connect(opts, common.mustCall(() => { @@ -83,7 +110,7 @@ server.listen(0, common.mustCall(function() { })); client2.resume(); - })); + } client1.resume(); })); diff --git a/test/parallel/test-tls-destroy-stream-12.js b/test/parallel/test-tls-destroy-stream-12.js new file mode 100644 index 00000000000000..69861868cf23b6 --- /dev/null +++ b/test/parallel/test-tls-destroy-stream-12.js @@ -0,0 +1,13 @@ +'use strict'; + +// test-tls-destroy-stream specifically for TLS1.2. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); + +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +require('./test-tls-destroy-stream.js'); diff --git a/test/parallel/test-tls-destroy-stream.js b/test/parallel/test-tls-destroy-stream.js index eb7a2ca338bd40..237f472f0281f9 100644 --- a/test/parallel/test-tls-destroy-stream.js +++ b/test/parallel/test-tls-destroy-stream.js @@ -21,10 +21,17 @@ const tlsServer = tls.createServer( ca: [fixtures.readSync('test_ca.pem')], }, (socket) => { - socket.on('error', common.mustNotCall()); socket.on('close', common.mustCall()); socket.write(CONTENT); socket.destroy(); + + socket.on('error', (err) => { + // destroy() is sync, write() is async, whether write completes depends + // on the protocol, it is not guaranteed by stream API. + if (err.code === 'ERR_STREAM_DESTROYED') + return; + assert.ifError(err); + }); }, ); @@ -57,13 +64,12 @@ const server = net.createServer((conn) => { server.listen(0, () => { const port = server.address().port; const conn = tls.connect({ port, rejectUnauthorized: false }, () => { - conn.on('data', common.mustCall((data) => { + // Whether the server's write() completed before its destroy() is + // indeterminate, but if data was written, we should receive it correctly. + conn.on('data', (data) => { assert.strictEqual(data.toString('utf8'), CONTENT); - })); + }); conn.on('error', common.mustNotCall()); - conn.on( - 'close', - common.mustCall(() => server.close()), - ); + conn.on('close', common.mustCall(() => server.close())); }); }); diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js index 0fc98641a69800..8db20331d1a616 100644 --- a/test/parallel/test-tls-disable-renegotiation.js +++ b/test/parallel/test-tls-disable-renegotiation.js @@ -10,6 +10,9 @@ if (!common.hasCrypto) const tls = require('tls'); +// Renegotiation as a protocol feature was dropped after TLS1.2. +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + const options = { key: fixtures.readKey('agent1-key.pem'), cert: fixtures.readKey('agent1-cert.pem'), @@ -64,5 +67,9 @@ server.listen(0, common.mustCall(() => { })); })); assert.strictEqual(ok, true); + client.on('secureConnect', common.mustCall(() => { + })); + client.on('secure', common.mustCall(() => { + })); })); })); diff --git a/test/parallel/test-tls-getcipher.js b/test/parallel/test-tls-getcipher.js index 37677ada7f411c..4379d74897a7dd 100644 --- a/test/parallel/test-tls-getcipher.js +++ b/test/parallel/test-tls-getcipher.js @@ -45,7 +45,7 @@ server.listen(0, '127.0.0.1', common.mustCall(function() { const client = tls.connect({ host: '127.0.0.1', port: this.address().port, - ciphers: cipher_list.join(':'), + ciphers: 'AES128-SHA256', rejectUnauthorized: false }, common.mustCall(function() { const cipher = client.getCipher(); @@ -55,3 +55,27 @@ server.listen(0, '127.0.0.1', common.mustCall(function() { server.close(); })); })); + +if (!require('constants').TLS1_3_VERSION) + return console.log('cannot test TLSv1.3 against 1.3-incapable shared lib'); + +tls.createServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + ciphers: 'TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_8_SHA256', + maxVersion: 'TLSv1.3', +}, common.mustCall(function() { + this.close(); +})).listen(0, common.mustCall(function() { + const client = tls.connect({ + port: this.address().port, + ciphers: 'TLS_AES_128_CCM_8_SHA256', + maxVersion: 'TLSv1.3', + rejectUnauthorized: false + }, common.mustCall(() => { + const cipher = client.getCipher(); + assert.strictEqual(cipher.name, 'TLS_AES_128_CCM_8_SHA256'); + assert.strictEqual(cipher.version, 'TLSv1/SSLv3'); + client.end(); + })); +})); diff --git a/test/parallel/test-tls-getprotocol.js b/test/parallel/test-tls-getprotocol.js index bf75eb8a398647..20018241e33572 100644 --- a/test/parallel/test-tls-getprotocol.js +++ b/test/parallel/test-tls-getprotocol.js @@ -17,6 +17,7 @@ const clientConfigs = [ ]; const serverConfig = { + secureProtocol: 'TLS_method', key: fixtures.readSync('/keys/agent2-key.pem'), cert: fixtures.readSync('/keys/agent2-cert.pem') }; diff --git a/test/parallel/test-tls-min-max-version.js b/test/parallel/test-tls-min-max-version.js index 521bc5ce9f193e..cd8bccca04d612 100644 --- a/test/parallel/test-tls-min-max-version.js +++ b/test/parallel/test-tls-min-max-version.js @@ -9,12 +9,25 @@ const { } = require(fixtures.path('tls-connect')); const DEFAULT_MIN_VERSION = tls.DEFAULT_MIN_VERSION; const DEFAULT_MAX_VERSION = tls.DEFAULT_MAX_VERSION; +const tls13 = !!require('constants').TLS1_3_VERSION; -// For v11.x, the default is fixed and cannot be changed via CLI. -assert.strictEqual(DEFAULT_MIN_VERSION, 'TLSv1'); +if (!tls13 && ( + DEFAULT_MAX_VERSION === 'TLSv1.3' || + DEFAULT_MIN_VERSION === 'TLSv1.3')) { + return common.skip('cannot test TLSv1.3 against 1.3-incapable shared lib'); +} function test(cmin, cmax, cprot, smin, smax, sprot, proto, cerr, serr) { assert(proto || cerr || serr, 'test missing any expectations'); + // Report where test was called from. Strip leading garbage from + // at Object. (file:line) + // from the stack location, we only want the file:line part. + const where = (new Error()).stack.split('\n')[2].replace(/[^(]*/, ''); + if (Array.prototype.includes.call(arguments, 'TLSv1.3')) { + console.log('test: skip because TLSv1.3 is not supported'); + console.log(' ', where); + return; + } connect({ client: { checkServerIdentity: (servername, cert) => { }, @@ -34,26 +47,10 @@ function test(cmin, cmax, cprot, smin, smax, sprot, proto, cerr, serr) { function u(_) { return _ === undefined ? 'U' : _; } console.log('test:', u(cmin), u(cmax), u(cprot), u(smin), u(smax), u(sprot), 'expect', u(proto), u(cerr), u(serr)); + console.log(' ', where); if (!proto) { console.log('client', pair.client.err ? pair.client.err.code : undefined); console.log('server', pair.server.err ? pair.server.err.code : undefined); - // 11.x doesn't have https://github.com/nodejs/node/pull/24729 - if (cerr === 'ERR_TLS_INVALID_PROTOCOL_METHOD' && - pair.client.err && - pair.client.err.message.includes('methods disabled')) - pair.client.err.code = 'ERR_TLS_INVALID_PROTOCOL_METHOD'; - if (serr === 'ERR_TLS_INVALID_PROTOCOL_METHOD' && - pair.server.err && - pair.server.err.message.includes('methods disabled')) - pair.server.err.code = 'ERR_TLS_INVALID_PROTOCOL_METHOD'; - if (cerr === 'ERR_TLS_INVALID_PROTOCOL_METHOD' && - pair.client.err && - pair.client.err.message.includes('Unknown method')) - pair.client.err.code = 'ERR_TLS_INVALID_PROTOCOL_METHOD'; - if (serr === 'ERR_TLS_INVALID_PROTOCOL_METHOD' && - pair.server.err && - pair.server.err.message.includes('Unknown method')) - pair.server.err.code = 'ERR_TLS_INVALID_PROTOCOL_METHOD'; if (cerr) { assert(pair.client.err); // Accept these codes as aliases, the one reported depends on the @@ -83,8 +80,13 @@ function test(cmin, cmax, cprot, smin, smax, sprot, proto, cerr, serr) { const U = undefined; -// Default protocol is TLSv1.2. -test(U, U, U, U, U, U, 'TLSv1.2'); +if (DEFAULT_MAX_VERSION === 'TLSv1.2' && DEFAULT_MIN_VERSION === 'TLSv1.3') { + // No connections are possible by default. + test(U, U, U, U, U, U, U, 'ERR_SSL_NO_PROTOCOLS_AVAILABLE', U); +} else { + // Default protocol is the max version. + test(U, U, U, U, U, U, DEFAULT_MAX_VERSION); +} // Insecure or invalid protocols cannot be enabled. test(U, U, U, U, U, 'SSLv2_method', @@ -120,7 +122,23 @@ test(U, U, 'TLS_method', U, U, 'TLSv1_method', 'TLSv1'); // SSLv23 also means "any supported protocol" greater than the default // minimum (which is configurable via command line). -test(U, U, 'TLSv1_2_method', U, U, 'SSLv23_method', 'TLSv1.2'); +if (DEFAULT_MIN_VERSION === 'TLSv1.3') { + test(U, U, 'TLSv1_2_method', U, U, 'SSLv23_method', + U, 'ECONNRESET', 'ERR_SSL_INTERNAL_ERROR'); +} else { + test(U, U, 'TLSv1_2_method', U, U, 'SSLv23_method', 'TLSv1.2'); +} + +if (DEFAULT_MIN_VERSION === 'TLSv1.3') { + test(U, U, 'TLSv1_1_method', U, U, 'SSLv23_method', + U, 'ECONNRESET', 'ERR_SSL_INTERNAL_ERROR'); + test(U, U, 'TLSv1_method', U, U, 'SSLv23_method', + U, 'ECONNRESET', 'ERR_SSL_INTERNAL_ERROR'); + test(U, U, 'SSLv23_method', U, U, 'TLSv1_1_method', + U, 'ERR_SSL_NO_PROTOCOLS_AVAILABLE', 'ERR_SSL_UNEXPECTED_MESSAGE'); + test(U, U, 'SSLv23_method', U, U, 'TLSv1_method', + U, 'ERR_SSL_NO_PROTOCOLS_AVAILABLE', 'ERR_SSL_UNEXPECTED_MESSAGE'); +} if (DEFAULT_MIN_VERSION === 'TLSv1.2') { test(U, U, 'TLSv1_1_method', U, U, 'SSLv23_method', @@ -168,7 +186,11 @@ if (DEFAULT_MIN_VERSION === 'TLSv1.2') { test(U, U, U, U, U, 'TLSv1_method', U, 'ERR_SSL_UNSUPPORTED_PROTOCOL', 'ERR_SSL_WRONG_VERSION_NUMBER'); } else { - assert(false, 'unreachable'); + // TLS1.3 client hellos are are not understood by TLS1.1 or below. + test(U, U, U, U, U, 'TLSv1_1_method', + U, 'ECONNRESET', 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + test(U, U, U, U, U, 'TLSv1_method', + U, 'ECONNRESET', 'ERR_SSL_UNSUPPORTED_PROTOCOL'); } } @@ -183,7 +205,9 @@ if (DEFAULT_MIN_VERSION === 'TLSv1.1') { test(U, U, U, U, U, 'TLSv1_method', U, 'ERR_SSL_UNSUPPORTED_PROTOCOL', 'ERR_SSL_WRONG_VERSION_NUMBER'); } else { - assert(false, 'unreachable'); + // TLS1.3 client hellos are are not understood by TLS1.1 or below. + test(U, U, U, U, U, 'TLSv1_method', + U, 'ECONNRESET', 'ERR_SSL_UNSUPPORTED_PROTOCOL'); } } @@ -199,14 +223,32 @@ if (DEFAULT_MIN_VERSION === 'TLSv1') { test('TLSv1', 'TLSv1.2', U, U, U, 'TLSv1_method', 'TLSv1'); test('TLSv1', 'TLSv1.2', U, U, U, 'TLSv1_1_method', 'TLSv1.1'); test('TLSv1', 'TLSv1.2', U, U, U, 'TLSv1_2_method', 'TLSv1.2'); +test('TLSv1', 'TLSv1.2', U, U, U, 'TLS_method', 'TLSv1.2'); test(U, U, 'TLSv1_method', 'TLSv1', 'TLSv1.2', U, 'TLSv1'); test(U, U, 'TLSv1_1_method', 'TLSv1', 'TLSv1.2', U, 'TLSv1.1'); test(U, U, 'TLSv1_2_method', 'TLSv1', 'TLSv1.2', U, 'TLSv1.2'); +test('TLSv1', 'TLSv1.1', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.1'); test('TLSv1', 'TLSv1.1', U, 'TLSv1', 'TLSv1.2', U, 'TLSv1.1'); test('TLSv1', 'TLSv1.2', U, 'TLSv1', 'TLSv1.1', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1', 'TLSv1.1', U, 'TLSv1.1'); test('TLSv1', 'TLSv1', U, 'TLSv1', 'TLSv1.1', U, 'TLSv1'); test('TLSv1', 'TLSv1.2', U, 'TLSv1', 'TLSv1', U, 'TLSv1'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1', 'TLSv1', U, 'TLSv1'); test('TLSv1.1', 'TLSv1.1', U, 'TLSv1', 'TLSv1.2', U, 'TLSv1.1'); test('TLSv1', 'TLSv1.2', U, 'TLSv1.1', 'TLSv1.1', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.2', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.2'); + +// v-any client can connect to v-specific server +test('TLSv1', 'TLSv1.3', U, 'TLSv1.3', 'TLSv1.3', U, 'TLSv1.3'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1.2', 'TLSv1.3', U, 'TLSv1.3'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1.2', 'TLSv1.2', U, 'TLSv1.2'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1.1', 'TLSv1.1', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1.3', U, 'TLSv1', 'TLSv1', U, 'TLSv1'); + +// v-specific client can connect to v-any server +test('TLSv1.3', 'TLSv1.3', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.3'); +test('TLSv1.2', 'TLSv1.2', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.2'); +test('TLSv1.1', 'TLSv1.1', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1.1'); +test('TLSv1', 'TLSv1', U, 'TLSv1', 'TLSv1.3', U, 'TLSv1'); diff --git a/test/parallel/test-tls-net-socket-keepalive-12.js b/test/parallel/test-tls-net-socket-keepalive-12.js new file mode 100644 index 00000000000000..d2fb230796e553 --- /dev/null +++ b/test/parallel/test-tls-net-socket-keepalive-12.js @@ -0,0 +1,13 @@ +'use strict'; + +// test-tls-net-socket-keepalive specifically for TLS1.2. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const tls = require('tls'); + +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +require('./test-tls-net-socket-keepalive.js'); diff --git a/test/parallel/test-tls-net-socket-keepalive.js b/test/parallel/test-tls-net-socket-keepalive.js index c184e902b42685..4acb4e80224ee9 100644 --- a/test/parallel/test-tls-net-socket-keepalive.js +++ b/test/parallel/test-tls-net-socket-keepalive.js @@ -20,8 +20,11 @@ const options = { }; const server = tls.createServer(options, common.mustCall((conn) => { - conn.write('hello'); + conn.write('hello', common.mustCall()); conn.on('data', common.mustCall()); + conn.on('end', common.mustCall()); + conn.on('data', common.mustCall()); + conn.on('close', common.mustCall()); conn.end(); })).listen(0, common.mustCall(() => { const netSocket = new net.Socket({ @@ -42,6 +45,7 @@ const server = tls.createServer(options, common.mustCall((conn) => { address, }); + socket.on('secureConnect', common.mustCall()); socket.on('end', common.mustCall()); socket.on('data', common.mustCall()); socket.on('close', common.mustCall(() => { diff --git a/test/parallel/test-tls-server-verify.js b/test/parallel/test-tls-server-verify.js index 2fd815d627fdf7..347dfd985ab97d 100644 --- a/test/parallel/test-tls-server-verify.js +++ b/test/parallel/test-tls-server-verify.js @@ -272,6 +272,8 @@ function runTest(port, testIndex) { if (tcase.renegotiate) { serverOptions.secureOptions = SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; + // Renegotiation as a protocol feature was dropped after TLS1.2. + serverOptions.maxVersion = 'TLSv1.2'; } let renegotiated = false; diff --git a/test/parallel/test-tls-session-cache.js b/test/parallel/test-tls-session-cache.js index 55dd92e81d259c..2a74be0521df21 100644 --- a/test/parallel/test-tls-session-cache.js +++ b/test/parallel/test-tls-session-cache.js @@ -48,7 +48,8 @@ function doTest(testOptions, callback) { cert, ca: [cert], requestCert: true, - rejectUnauthorized: false + rejectUnauthorized: false, + secureProtocol: 'TLS_method', }; let requestCount = 0; let resumeCount = 0; diff --git a/test/parallel/test-tls-set-ciphers-error.js b/test/parallel/test-tls-set-ciphers-error.js index 5ef08dda041f01..a09c12b321cba0 100644 --- a/test/parallel/test-tls-set-ciphers-error.js +++ b/test/parallel/test-tls-set-ciphers-error.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (!require('constants').TLS1_3_VERSION) + return common.skip('openssl before TLS1.3 does not check for failure'); + const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); @@ -19,4 +22,7 @@ const fixtures = require('../common/fixtures'); options.ciphers = 'FOOBARBAZ'; assert.throws(() => tls.createServer(options, common.mustNotCall()), /no cipher match/i); + options.ciphers = 'TLS_not_a_cipher'; + assert.throws(() => tls.createServer(options, common.mustNotCall()), + /no cipher match/i); } diff --git a/test/parallel/test-tls-set-ciphers.js b/test/parallel/test-tls-set-ciphers.js index ef2c2543517e5f..254cc52ad4ef37 100644 --- a/test/parallel/test-tls-set-ciphers.js +++ b/test/parallel/test-tls-set-ciphers.js @@ -1,62 +1,102 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - 'use strict'; const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); -if (!common.opensslCli) - common.skip('node compiled without OpenSSL CLI.'); +// Test cipher: option for TLS. -if (!common.hasCrypto) - common.skip('missing crypto'); +const { + assert, connect, keys, tls +} = require(fixtures.path('tls-connect')); -const assert = require('assert'); -const exec = require('child_process').exec; -const tls = require('tls'); -const fixtures = require('../common/fixtures'); +const tls13 = !!require('constants').TLS1_3_VERSION; -const options = { - key: fixtures.readKey('agent2-key.pem'), - cert: fixtures.readKey('agent2-cert.pem'), - ciphers: 'AES256-SHA' -}; +if (tls13) + tls.DEFAULT_MAX_VERSION = 'TLSv1.3'; -const reply = 'I AM THE WALRUS'; // something recognizable -let response = ''; +function test(cciphers, sciphers, cipher, cerr, serr) { + if (!tls13 && (/TLS_/.test(cciphers) || /TLS_/.test(sciphers))) { + // Test relies on TLS1.3, skip it. + return; + } + assert(cipher || cerr || serr, 'test missing any expectations'); + const where = (new Error()).stack.split('\n')[2].replace(/[^(]*/, ''); + connect({ + client: { + checkServerIdentity: (servername, cert) => { }, + ca: `${keys.agent1.cert}\n${keys.agent6.ca}`, + ciphers: cciphers, + }, + server: { + cert: keys.agent6.cert, + key: keys.agent6.key, + ciphers: sciphers, + }, + }, common.mustCall((err, pair, cleanup) => { + function u(_) { return _ === undefined ? 'U' : _; } + console.log('test:', u(cciphers), u(sciphers), + 'expect', u(cipher), u(cerr), u(serr)); + console.log(' ', where); + if (!cipher) { + console.log('client', pair.client.err ? pair.client.err.code : undefined); + console.log('server', pair.server.err ? pair.server.err.code : undefined); + if (cerr) { + assert(pair.client.err); + assert.strictEqual(pair.client.err.code, cerr); + } + if (serr) { + assert(pair.server.err); + assert.strictEqual(pair.server.err.code || pair.server.err.message, + serr); + } + return cleanup(); + } -process.on('exit', function() { - assert.ok(response.includes(reply)); -}); + const reply = 'So long and thanks for all the fish.'; -const server = tls.createServer(options, common.mustCall(function(conn) { - conn.end(reply); -})); + assert.ifError(err); + assert.ifError(pair.server.err); + assert.ifError(pair.client.err); + assert(pair.server.conn); + assert(pair.client.conn); + assert.strictEqual(pair.client.conn.getCipher().name, cipher); + assert.strictEqual(pair.server.conn.getCipher().name, cipher); -server.listen(0, '127.0.0.1', function() { - const cmd = `"${common.opensslCli}" s_client -cipher ${ - options.ciphers} -connect 127.0.0.1:${this.address().port}`; + pair.server.conn.write(reply); - exec(cmd, function(err, stdout, stderr) { - assert.ifError(err); - response = stdout; - server.close(); - }); -}); + pair.client.conn.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), reply); + return cleanup(); + })); + })); +} + +const U = undefined; + +// Have shared ciphers. +test(U, 'AES256-SHA', 'AES256-SHA'); +test('AES256-SHA', U, 'AES256-SHA'); + +test(U, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); +test('TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); + +// Do not have shared ciphers. +test('TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', + U, 'ECONNRESET', 'ERR_SSL_NO_SHARED_CIPHER'); + +test('AES128-SHA', 'AES256-SHA', U, 'ECONNRESET', 'ERR_SSL_NO_SHARED_CIPHER'); +test('AES128-SHA:TLS_AES_256_GCM_SHA384', + 'TLS_CHACHA20_POLY1305_SHA256:AES256-SHA', + U, 'ECONNRESET', 'ERR_SSL_NO_SHARED_CIPHER'); + +// Cipher order ignored, TLS1.3 chosen before TLS1.2. +test('AES256-SHA:TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); +test(U, 'AES256-SHA:TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + +// TLS_AES_128_CCM_8_SHA256 & TLS_AES_128_CCM_SHA256 are not enabled by +// default, but work. +test('TLS_AES_128_CCM_8_SHA256', U, + U, 'ECONNRESET', 'ERR_SSL_NO_SHARED_CIPHER'); + +test('TLS_AES_128_CCM_8_SHA256', 'TLS_AES_128_CCM_8_SHA256', + 'TLS_AES_128_CCM_8_SHA256'); diff --git a/test/parallel/test-tls-ticket-12.js b/test/parallel/test-tls-ticket-12.js new file mode 100644 index 00000000000000..600c571a03bec4 --- /dev/null +++ b/test/parallel/test-tls-ticket-12.js @@ -0,0 +1,12 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// Run test-tls-ticket.js with TLS1.2 + +const tls = require('tls'); + +tls.DEFAULT_MAX_VERSION = 'TLSv1.2'; + +require('./test-tls-ticket.js'); diff --git a/test/parallel/test-tls-ticket-cluster.js b/test/parallel/test-tls-ticket-cluster.js index 98fe533b6969d6..234c1bad09c9a7 100644 --- a/test/parallel/test-tls-ticket-cluster.js +++ b/test/parallel/test-tls-ticket-cluster.js @@ -40,13 +40,17 @@ if (cluster.isMaster) { let workerPort = null; function shoot() { - console.error('[master] connecting', workerPort); + console.error('[master] connecting', workerPort, 'session?', !!lastSession); const c = tls.connect(workerPort, { session: lastSession, rejectUnauthorized: false }, () => { c.end(); - + }).on('close', () => { + // Wait for close to shoot off another connection. We don't want to shoot + // until a new session is allocated, if one will be. The new session is + // not guaranteed on secureConnect (it depends on TLS1.2 vs TLS1.3), but + // it is guaranteed to happen before the connection is closed. if (++reqCount === expectedReqCount) { Object.keys(cluster.workers).forEach(function(id) { cluster.workers[id].send('die'); @@ -55,8 +59,11 @@ if (cluster.isMaster) { shoot(); } }).once('session', (session) => { + assert(!lastSession); lastSession = session; }); + + c.resume(); // See close_notify comment in server } function fork() { @@ -93,12 +100,15 @@ const cert = fixtures.readSync('agent.crt'); const options = { key, cert }; const server = tls.createServer(options, (c) => { + console.error('[worker] connection reused?', c.isSessionReused()); if (c.isSessionReused()) { process.send({ msg: 'reused' }); } else { process.send({ msg: 'not-reused' }); } - c.end(); + // Used to just .end(), but that means client gets close_notify before + // NewSessionTicket. Send data until that problem is solved. + c.end('x'); }); server.listen(0, () => { diff --git a/test/parallel/test-tls-ticket.js b/test/parallel/test-tls-ticket.js index d11535dd3a5255..8d9cd8cdd25155 100644 --- a/test/parallel/test-tls-ticket.js +++ b/test/parallel/test-tls-ticket.js @@ -34,6 +34,8 @@ const keys = crypto.randomBytes(48); const serverLog = []; const ticketLog = []; +let s; + let serverCount = 0; function createServer() { const id = serverCount++; @@ -47,16 +49,37 @@ function createServer() { ticketKeys: keys }, function(c) { serverLog.push(id); - c.end(); + // TODO(@sam-github) Triggers close_notify before NewSessionTicket bug. + // c.end(); + c.end('x'); counter++; // Rotate ticket keys + // + // Take especial care to account for TLS1.2 and TLS1.3 differences around + // when ticket keys are encrypted. In TLS1.2, they are encrypted before the + // handshake complete callback, but in TLS1.3, they are encrypted after. + // There is no callback or way for us to know when they were sent, so hook + // the client's reception of the keys, and use it as proof that the current + // keys were used, and its safe to rotate them. + // + // Rotation can occur right away if the session was reused, the keys were + // already decrypted or we wouldn't have a reused session. + function setTicketKeys(keys) { + if (c.isSessionReused()) + server.setTicketKeys(keys); + else + s.once('session', () => { + server.setTicketKeys(keys); + }); + } if (counter === 1) { previousKey = server.getTicketKeys(); - server.setTicketKeys(crypto.randomBytes(48)); + assert.strictEqual(previousKey.compare(keys), 0); + setTicketKeys(crypto.randomBytes(48)); } else if (counter === 2) { - server.setTicketKeys(previousKey); + setTicketKeys(previousKey); } else if (counter === 3) { // Use keys from counter=2 } else { @@ -95,12 +118,15 @@ function start(callback) { let left = servers.length; function connect() { - const s = tls.connect(shared.address().port, { + s = tls.connect(shared.address().port, { session: sess, rejectUnauthorized: false }, function() { - sess = sess || s.getSession(); - ticketLog.push(s.getTLSTicket().toString('hex')); + if (s.isSessionReused()) + ticketLog.push(s.getTLSTicket().toString('hex')); + }); + s.on('data', () => { + s.end(); }); s.on('close', function() { if (--left === 0) @@ -108,7 +134,11 @@ function start(callback) { else connect(); }); + s.on('session', (session) => { + sess = sess || session; + }); s.once('session', (session) => onNewSession(s, session)); + s.once('session', () => ticketLog.push(s.getTLSTicket().toString('hex'))); } connect();