summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Roberts <vieuxtech@gmail.com>2018-11-28 17:58:08 -0800
committerSam Roberts <vieuxtech@gmail.com>2019-03-20 07:48:25 -0700
commit42dbaed4605f44c393a057aad75a31cac1d0e5f5 (patch)
tree096554b95dfb14cef568bfe898018d9bb874305c
parent4306300b5ea8d8c4ff3daf64c7ed5fd64055ec2f (diff)
downloadnode-new-42dbaed4605f44c393a057aad75a31cac1d0e5f5.tar.gz
tls: support TLSv1.3
This introduces TLS1.3 support and makes it the default max protocol, but also supports CLI/NODE_OPTIONS switches to disable it if necessary. TLS1.3 is a major update to the TLS protocol, with many security enhancements. It should be preferred over TLS1.2 whenever possible. TLS1.3 is different enough that even though the OpenSSL APIs are technically API/ABI compatible, that when TLS1.3 is negotiated, the timing of protocol records and of callbacks broke assumptions hard-coded into the 'tls' module. This change introduces no API incompatibilities when TLS1.2 is negotiated. It is the intention that it be backported to current and LTS release lines with the default maximum TLS protocol reset to 'TLSv1.2'. This will allow users of those lines to explicitly enable TLS1.3 if they want. API incompatibilities between TLS1.2 and TLS1.3 are: - Renegotiation is not supported by TLS1.3 protocol, attempts to call `.renegotiate()` will always fail. - Compiling against a system OpenSSL lower than 1.1.1 is no longer supported (OpenSSL-1.1.0 used to be supported with configure flags). - Variations of `conn.write('data'); conn.destroy()` have undefined behaviour according to the streams API. They may or may not send the 'data', and may or may not cause a ERR_STREAM_DESTROYED error to be emitted. This has always been true, but conditions under which the write suceeds is slightly but observably different when TLS1.3 is negotiated vs when TLS1.2 or below is negotiated. - If TLS1.3 is negotiated, and a server calls `conn.end()` in its 'secureConnection' listener without any data being written, the client will not receive session tickets (no 'session' events will be emitted, and `conn.getSession()` will never return a resumable session). - The return value of `conn.getSession()` API may not return a resumable session if called right after the handshake. The effect will be that clients using the legacy `getSession()` API will resume sessions if TLS1.2 is negotiated, but will do full handshakes if TLS1.3 is negotiated. See https://github.com/nodejs/node/pull/25831 for more information. PR-URL: https://github.com/nodejs/node/pull/26209 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rod Vagg <rod@vagg.org>
-rw-r--r--doc/api/cli.md37
-rw-r--r--doc/api/tls.md119
-rw-r--r--doc/node.122
-rw-r--r--lib/_tls_common.js36
-rw-r--r--lib/_tls_wrap.js88
-rw-r--r--lib/internal/stream_base_commons.js4
-rw-r--r--lib/tls.js16
-rw-r--r--src/node_constants.cc1
-rw-r--r--src/node_constants.h8
-rw-r--r--src/node_crypto.cc174
-rw-r--r--src/node_crypto.h5
-rw-r--r--src/node_crypto_clienthello.cc2
-rw-r--r--src/node_crypto_clienthello.h6
-rw-r--r--src/node_options.cc32
-rw-r--r--src/node_options.h9
-rw-r--r--src/tls_wrap.cc36
-rw-r--r--src/tls_wrap.h1
-rw-r--r--test/async-hooks/test-graph.tls-write-12.js11
-rw-r--r--test/async-hooks/test-graph.tls-write.js11
-rw-r--r--test/async-hooks/test-tlswrap.js7
-rw-r--r--test/parallel/test-crypto.js1
-rw-r--r--test/parallel/test-https-agent-additional-options.js2
-rw-r--r--test/parallel/test-https-agent-session-eviction.js2
-rw-r--r--test/parallel/test-https-client-renegotiation-limit.js3
-rw-r--r--test/parallel/test-https-client-resume.js3
-rw-r--r--test/parallel/test-tls-alert-handling.js2
-rw-r--r--test/parallel/test-tls-async-cb-after-socket-end.js5
-rw-r--r--test/parallel/test-tls-basic-validations.js6
-rw-r--r--test/parallel/test-tls-cli-max-version-1.2.js15
-rw-r--r--test/parallel/test-tls-cli-max-version-1.3.js15
-rw-r--r--test/parallel/test-tls-cli-min-version-1.0.js4
-rw-r--r--test/parallel/test-tls-cli-min-version-1.1.js4
-rw-r--r--test/parallel/test-tls-cli-min-version-1.3.js15
-rw-r--r--test/parallel/test-tls-client-auth.js37
-rw-r--r--test/parallel/test-tls-client-getephemeralkeyinfo.js3
-rw-r--r--test/parallel/test-tls-client-reject-12.js13
-rw-r--r--test/parallel/test-tls-client-reject.js10
-rw-r--r--test/parallel/test-tls-client-renegotiation-13.js37
-rw-r--r--test/parallel/test-tls-client-renegotiation-limit.js3
-rw-r--r--test/parallel/test-tls-client-resume-12.js13
-rw-r--r--test/parallel/test-tls-client-resume.js43
-rw-r--r--test/parallel/test-tls-destroy-stream-12.js13
-rw-r--r--test/parallel/test-tls-destroy-stream.js22
-rw-r--r--test/parallel/test-tls-disable-renegotiation.js7
-rw-r--r--test/parallel/test-tls-getcipher.js23
-rw-r--r--test/parallel/test-tls-min-max-version.js55
-rw-r--r--test/parallel/test-tls-net-socket-keepalive-12.js13
-rw-r--r--test/parallel/test-tls-net-socket-keepalive.js6
-rw-r--r--test/parallel/test-tls-server-verify.js2
-rw-r--r--test/parallel/test-tls-set-ciphers-error.js3
-rw-r--r--test/parallel/test-tls-set-ciphers.js133
-rw-r--r--test/parallel/test-tls-ticket-12.js12
-rw-r--r--test/parallel/test-tls-ticket-cluster.js16
-rw-r--r--test/parallel/test-tls-ticket.js42
54 files changed, 988 insertions, 220 deletions
diff --git a/doc/api/cli.md b/doc/api/cli.md
index b2589cd52f..cf28d9b2bf 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -443,21 +443,43 @@ added: v4.0.0
Specify an alternative default TLS cipher list. Requires Node.js to be built
with crypto support (default).
-### `--tls-v1.0`
+### `--tls-max-v1.2`
<!-- YAML
added: REPLACEME
-->
-Enable TLSv1.0 and greater in default [secureProtocol][]. Use for compatibility
-with old TLS clients or servers.
+Set default [`maxVersion`][] to `'TLSv1.2'`. Use to disable support for TLSv1.3.
-### `--tls-v1.1`
+### `--tls-max-v1.3`
<!-- YAML
added: REPLACEME
-->
-Enable TLSv1.1 and greater in default [secureProtocol][]. Use for compatibility
-with old TLS clients or servers.
+Set default [`maxVersion`][] to `'TLSv1.3'`. Use to enable support for TLSv1.3.
+
+### `--tls-min-v1.0`
+<!-- YAML
+added: REPLACEME
+-->
+
+Set default [`minVersion`][] to `'TLSv1'`. Use for compatibility with old TLS
+clients or servers.
+
+### `--tls-min-v1.1`
+<!-- YAML
+added: REPLACEME
+-->
+
+Set default [`minVersion`][] to `'TLSv1.1'`. Use for compatibility with old TLS
+clients or servers.
+
+### `--tls-min-v1.3`
+<!-- YAML
+added: REPLACEME
+-->
+
+Set default [`minVersion`][] to `'TLSv1.3'`. Use to disable support for TLSv1.2
+in favour of TLSv1.3, which is more secure.
### `--trace-deprecation`
<!-- YAML
@@ -896,6 +918,8 @@ greater than `4` (its current default value). For more information, see the
[`--openssl-config`]: #cli_openssl_config_file
[`Buffer`]: buffer.html#buffer_class_buffer
[`SlowBuffer`]: buffer.html#buffer_class_slowbuffer
+[`maxVersion`]: tls.html#tls_tls_createsecurecontext_options
+[`minVersion`]: tls.html#tls_tls_createsecurecontext_options
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
[REPL]: repl.html
@@ -907,4 +931,3 @@ greater than `4` (its current default value). For more information, see the
[experimental ECMAScript Module]: esm.html#esm_loader_hooks
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
-[secureProtocol]: tls.html#tls_tls_createsecurecontext_options
diff --git a/doc/api/tls.md b/doc/api/tls.md
index fa34711fa8..0ad7bd1d4d 100644
--- a/doc/api/tls.md
+++ b/doc/api/tls.md
@@ -104,6 +104,9 @@ not required and a default ECDHE curve will be used. The `ecdhCurve` property
can be used when creating a TLS Server to specify the list of names of supported
curves to use, see [`tls.createServer()`] for more info.
+Perfect Forward Secrecy was optional up to TLSv1.2, but it is not optional for
+TLSv1.3, because all TLSv1.3 cipher suites use ECDHE.
+
### ALPN and SNI
<!-- type=misc -->
@@ -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
<!-- YAML
@@ -634,11 +669,11 @@ On the client, the `session` can be provided to the `session` option of
See [Session Resumption][] for more information.
-Note: For TLS1.2 and below, [`tls.TLSSocket.getSession()`][] can be called once
-the handshake is complete. For TLS1.3, only ticket based resumption is allowed
+Note: For TLSv1.2 and below, [`tls.TLSSocket.getSession()`][] can be called once
+the handshake is complete. For TLSv1.3, only ticket based resumption is allowed
by the protocol, multiple tickets are sent, and the tickets aren't sent until
later, after the handshake completes, so it is necessary to wait for the
-`'session'` event to get a resumable session. Future-proof applications are
+`'session'` event to get a resumable session. Applications are
recommended to use the `'session'` event instead of `getSession()` to ensure
they will work for all TLS protocol versions. Applications that only expect to
get or use 1 session should listen for this event only once:
@@ -731,7 +766,7 @@ changes:
Returns an object containing information on the negotiated cipher suite.
-For example: `{ name: 'AES256-SHA', version: 'TLSv1/SSLv3' }`.
+For example: `{ name: 'AES256-SHA', version: 'TLSv1.2' }`.
See
[OpenSSL](https://www.openssl.org/docs/man1.1.1/man3/SSL_CIPHER_get_name.html)
@@ -904,12 +939,13 @@ be returned for server sockets or disconnected client sockets.
Protocol versions are:
+* `'SSLv3'`
* `'TLSv1'`
* `'TLSv1.1'`
* `'TLSv1.2'`
-* `'SSLv3'`
+* `'TLSv1.3'`
-See <https://www.openssl.org/docs/man1.1.0/ssl/SSL_get_version.html> for more
+See <https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html> for more
information.
### tlsSocket.getSession()
@@ -926,8 +962,8 @@ for debugging.
See [Session Resumption][] for more information.
-Note: `getSession()` works only for TLS1.2 and below. Future-proof applications
-should use the [`'session'`][] event.
+Note: `getSession()` works only for TLSv1.2 and below. For TLSv1.3, applications
+must use the [`'session'`][] event (it also works for TLSv1.2 and below).
### tlsSocket.getTLSTicket()
<!-- YAML
@@ -1009,8 +1045,12 @@ added: v0.11.8
verification fails; `err.code` contains the OpenSSL error code. **Default:**
`true`.
* `requestCert`
-* `callback` {Function} A function that will be called when the renegotiation
- request has been completed.
+* `callback` {Function} If `renegotiate()` returned `true`, callback is
+ attached once to the `'secure'` event. If it returned `false`, it will be
+ called in the next tick with `ERR_TLS_RENEGOTIATE`, unless the `tlsSocket`
+ has been destroyed, in which case it will not be called at all.
+
+* Returns: {boolean} `true` if renegotiation was initiated, `false` otherwise.
The `tlsSocket.renegotiate()` method initiates a TLS renegotiation process.
Upon completion, the `callback` function will be passed a single argument
@@ -1022,6 +1062,9 @@ connection has been established.
When running as the server, the socket will be destroyed with an error after
`handshakeTimeout` timeout.
+For TLSv1.3, renegotiation cannot be initiated, it is not supported by the
+protocol.
+
### tlsSocket.setMaxSendFragment(size)
<!-- YAML
added: v0.11.11
@@ -1220,6 +1263,9 @@ argument.
<!-- YAML
added: v0.11.13
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/26209
+ description: TLSv1.3 support added.
- version: v11.5.0
pr-url: https://github.com/nodejs/node/pull/24733
description: The `ca:` option now supports `BEGIN TRUSTED CERTIFICATE`.
@@ -1310,15 +1356,22 @@ changes:
`object.passphrase` is optional. Encrypted keys will be decrypted with
`object.passphrase` if provided, or `options.passphrase` if it is not.
* `maxVersion` {string} Optionally set the maximum TLS version to allow. One
- of `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified along with the
- `secureProtocol` option, use one or the other. **Default:** `'TLSv1.2'`.
+ of `TLSv1.3`, `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified
+ along with the `secureProtocol` option, use one or the other.
+ **Default:** `'TLSv1.3'`, 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.
* `minVersion` {string} Optionally set the minimum TLS version to allow. One
- of `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified along with the
- `secureProtocol` option, use one or the other. It is not recommended to use
- less than TLSv1.2, but it may be required for interoperability.
+ of `TLSv1.3`, `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified
+ along with the `secureProtocol` option, use one or the other. It is not
+ recommended to use less than TLSv1.2, but it may be required for
+ interoperability.
**Default:** `'TLSv1.2'`, unless changed using CLI options. Using
- `--tls-v1.0` changes the default to `'TLSv1'`. Using `--tls-v1.1` changes
- the default to `'TLSv1.1'`.
+ `--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.
* `passphrase` {string} Shared passphrase used for a single private key and/or
a PFX.
* `pfx` {string|string[]|Buffer|Buffer[]|Object[]} PFX or PKCS12 encoded
@@ -1334,12 +1387,15 @@ changes:
which is not usually necessary. This should be used carefully if at all!
Value is a numeric bitmask of the `SSL_OP_*` options from
[OpenSSL Options][].
- * `secureProtocol` {string} The TLS protocol version to use. The possible
- values are listed as [SSL_METHODS][], use the function names as strings. For
- example, use `'TLSv1_1_method'` to force TLS version 1.1, or `'TLS_method'`
- to allow any TLS protocol version. It is not recommended to use TLS versions
- less than 1.2, but it may be required for interoperability. **Default:**
- none, see `minVersion`.
+ * `secureProtocol` {string} Legacy mechanism to select the TLS protocol
+ version to use, it does not support independent control of the minimum and
+ maximum version, and does not support limiting the protocol to TLSv1.3. Use
+ `minVersion` and `maxVersion` instead. The possible values are listed as
+ [SSL_METHODS][], use the function names as strings. For example, use
+ `'TLSv1_1_method'` to force TLS version 1.1, or `'TLS_method'` to allow any
+ TLS protocol version up to TLSv1.3. It is not recommended to use TLS
+ versions less than 1.2, but it may be required for interoperability.
+ **Default:** none, see `minVersion`.
* `sessionIdContext` {string} Opaque identifier used by servers to ensure
session state is not shared between applications. Unused by clients.
@@ -1457,10 +1513,15 @@ added: v0.10.2
* Returns: {string[]}
-Returns an array with the names of the supported SSL ciphers.
+Returns an array with the names of the supported TLS ciphers. The names are
+lower-case for historical reasons, but must be uppercased to be used in
+the `ciphers` option of [`tls.createSecureContext()`][].
+
+Cipher names that start with `'tls_'` are for TLSv1.3, all the others are for
+TLSv1.2 and below.
```js
-console.log(tls.getCiphers()); // ['AES128-SHA', 'AES256-SHA', ...]
+console.log(tls.getCiphers()); // ['aes128-gcm-sha256', 'aes128-sha', ...]
```
## tls.DEFAULT_ECDH_CURVE
@@ -1619,16 +1680,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 6945654689..7bcb1edc59 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -236,13 +236,23 @@ 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-v1.0
-Enable TLSv1.0 and greater in default secureProtocol. Use for compatibility
-with old TLS clients or servers.
+.It Fl -tls-max-v1.2
+Set default maxVersion to 'TLSv1.2'. Use to disable support for TLSv1.3.
.
-.It Fl -tls-v1.1
-Enable TLSv1.1 and greater in default secureProtocol. Use for compatibility
-with old TLS clients or servers.
+.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.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 7ddb0d4757..16e78a62cf 100644
--- a/lib/_tls_common.js
+++ b/lib/_tls_common.js
@@ -27,6 +27,7 @@ const tls = require('tls');
const {
ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED,
ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_OPT_VALUE,
ERR_TLS_INVALID_PROTOCOL_VERSION,
ERR_TLS_PROTOCOL_VERSION_CONFLICT,
} = require('internal/errors').codes;
@@ -35,6 +36,7 @@ const {
TLS1_VERSION,
TLS1_1_VERSION,
TLS1_2_VERSION,
+ TLS1_3_VERSION,
} = internalBinding('constants').crypto;
// Lazily loaded from internal/crypto/util.
@@ -45,6 +47,7 @@ function toV(which, v, def) {
if (v === 'TLSv1') return TLS1_VERSION;
if (v === 'TLSv1.1') return TLS1_1_VERSION;
if (v === 'TLSv1.2') return TLS1_2_VERSION;
+ if (v === 'TLSv1.3') return TLS1_3_VERSION;
throw new ERR_TLS_INVALID_PROTOCOL_VERSION(v, which);
}
@@ -148,10 +151,35 @@ exports.createSecureContext = function createSecureContext(options) {
}
}
- if (options.ciphers)
- c.context.setCiphers(options.ciphers);
- else
- c.context.setCiphers(tls.DEFAULT_CIPHERS);
+ if (options.ciphers && typeof options.ciphers !== 'string') {
+ throw new ERR_INVALID_ARG_TYPE(
+ 'options.ciphers', 'string', options.ciphers);
+ }
+
+ // Work around an OpenSSL API quirk. cipherList is for TLSv1.2 and below,
+ // cipherSuites is for TLSv1.3 (and presumably any later versions). TLSv1.3
+ // cipher suites all have a standard name format beginning with TLS_, so split
+ // the ciphers and pass them to the appropriate API.
+ const ciphers = (options.ciphers || tls.DEFAULT_CIPHERS).split(':');
+ const cipherList = ciphers.filter((_) => !_.match(/^TLS_/)).join(':');
+ const cipherSuites = ciphers.filter((_) => _.match(/^TLS_/)).join(':');
+
+ if (cipherSuites === '' && cipherList === '') {
+ // Specifying empty cipher suites for both TLS1.2 and TLS1.3 is invalid, its
+ // not possible to handshake with no suites.
+ throw ERR_INVALID_OPT_VALUE('ciphers', ciphers);
+ }
+
+ c.context.setCipherSuites(cipherSuites);
+ c.context.setCiphers(cipherList);
+
+ if (cipherSuites === '' && c.context.getMaxProto() > TLS1_2_VERSION &&
+ c.context.getMinProto() < TLS1_3_VERSION)
+ c.context.setMaxProto(TLS1_2_VERSION);
+
+ if (cipherList === '' && c.context.getMinProto() < TLS1_3_VERSION &&
+ c.context.getMaxProto() > TLS1_2_VERSION)
+ c.context.setMinProto(TLS1_3_VERSION);
if (options.ecdhCurve === undefined)
c.context.setECDHCurve(tls.DEFAULT_ECDH_CURVE);
diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js
index 18f0ca5900..e91e9f8aae 100644
--- a/lib/_tls_wrap.js
+++ b/lib/_tls_wrap.js
@@ -65,7 +65,7 @@ let ipServernameWarned = false;
// Server side times how long a handshake is taking to protect against slow
// handshakes being used for DoS.
function onhandshakestart(now) {
- debug('onhandshakestart');
+ debug('server onhandshakestart');
const { lastHandshakeTime } = this;
assert(now >= lastHandshakeTime,
@@ -83,6 +83,9 @@ function onhandshakestart(now) {
this.handshakes++;
const owner = this[owner_symbol];
+
+ assert(owner._tlsOptions.isServer);
+
if (this.handshakes > tls.CLIENT_RENEG_LIMIT) {
owner._emitTLSError(new ERR_TLS_SESSION_ATTACK());
return;
@@ -93,9 +96,10 @@ function onhandshakestart(now) {
}
function onhandshakedone() {
- debug('onhandshakedone');
+ debug('server onhandshakedone');
const owner = this[owner_symbol];
+ assert(owner._tlsOptions.isServer);
// `newSession` callback wasn't called yet
if (owner._newSessionPending) {
@@ -108,10 +112,15 @@ function onhandshakedone() {
function loadSession(hello) {
+ debug('server onclienthello',
+ 'sessionid.len', hello.sessionId.length,
+ 'ticket?', hello.tlsTicket
+ );
const owner = this[owner_symbol];
var once = false;
function onSession(err, session) {
+ debug('server resumeSession callback(err %j, sess? %s)', err, !!session);
if (once)
return owner.destroy(new ERR_MULTIPLE_CALLBACK());
once = true;
@@ -193,6 +202,8 @@ function requestOCSP(socket, info) {
let once = false;
const onOCSP = (err, response) => {
+ debug('server OCSPRequest done', 'handle?', !!socket._handle, 'once?', once,
+ 'response?', !!response, 'err?', err);
if (once)
return socket.destroy(new ERR_MULTIPLE_CALLBACK());
once = true;
@@ -208,6 +219,7 @@ function requestOCSP(socket, info) {
requestOCSPDone(socket);
};
+ debug('server oncertcb emit OCSPRequest');
socket.server.emit('OCSPRequest',
ctx.getCertificate(),
ctx.getIssuer(),
@@ -215,16 +227,17 @@ function requestOCSP(socket, info) {
}
function requestOCSPDone(socket) {
+ debug('server certcb done');
try {
socket._handle.certCbDone();
} catch (e) {
+ debug('server certcb done errored', e);
socket.destroy(e);
}
}
-
function onnewsessionclient(sessionId, session) {
- debug('client onnewsessionclient', sessionId, session);
+ debug('client emit session');
const owner = this[owner_symbol];
owner.emit('session', session);
}
@@ -233,8 +246,9 @@ function onnewsession(sessionId, session) {
debug('onnewsession');
const owner = this[owner_symbol];
- // XXX(sam) no server to emit the event on, but handshake won't continue
- // unless newSessionDone() is called, should it be?
+ // TODO(@sam-github) no server to emit the event on, but handshake won't
+ // continue unless newSessionDone() is called, should it be, or is that
+ // situation unreachable, or only occurring during shutdown?
if (!owner.server)
return;
@@ -263,11 +277,15 @@ function onnewsession(sessionId, session) {
function onocspresponse(resp) {
+ debug('client onocspresponse');
this[owner_symbol].emit('OCSPResponse', resp);
}
function onerror(err) {
const owner = this[owner_symbol];
+ debug('%s onerror %s had? %j',
+ owner._tlsOptions.isServer ? 'server' : 'client', err,
+ owner._hadError);
if (owner._hadError)
return;
@@ -285,7 +303,7 @@ function onerror(err) {
// Ignore server's authorization errors
owner.destroy();
} else {
- // Throw error
+ // Emit error
owner._emitTLSError(err);
}
}
@@ -293,6 +311,11 @@ function onerror(err) {
// Used by both client and server TLSSockets to start data flowing from _handle,
// read(0) causes a StreamBase::ReadStart, via Socket._read.
function initRead(tlsSocket, socket) {
+ debug('%s initRead',
+ tlsSocket._tlsOptions.isServer ? 'server' : 'client',
+ 'handle?', !!tlsSocket._handle,
+ 'buffered?', !!socket && socket.readableLength
+ );
// If we were destroyed already don't bother reading
if (!tlsSocket._handle)
return;
@@ -493,12 +516,17 @@ TLSSocket.prototype._destroySSL = function _destroySSL() {
this.ssl = null;
};
+// Constructor guts, arbitrarily factored out.
TLSSocket.prototype._init = function(socket, wrap) {
var options = this._tlsOptions;
var ssl = this._handle;
-
this.server = options.server;
+ debug('%s _init',
+ options.isServer ? 'server' : 'client',
+ 'handle?', !!ssl
+ );
+
// Clients (!isServer) always request a cert, servers request a client cert
// only on explicit configuration.
const requestCert = !!options.requestCert || !options.isServer;
@@ -529,7 +557,10 @@ TLSSocket.prototype._init = function(socket, wrap) {
}
} else {
ssl.onhandshakestart = noop;
- ssl.onhandshakedone = this._finishInit.bind(this);
+ ssl.onhandshakedone = () => {
+ debug('client onhandshakedone');
+ this._finishInit();
+ };
ssl.onocspresponse = onocspresponse;
if (options.session)
@@ -600,6 +631,11 @@ TLSSocket.prototype.renegotiate = function(options, callback) {
if (callback !== undefined && typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK();
+ debug('%s renegotiate()',
+ this._tlsOptions.isServer ? 'server' : 'client',
+ 'destroyed?', this.destroyed
+ );
+
if (this.destroyed)
return;
@@ -667,9 +703,25 @@ TLSSocket.prototype._releaseControl = function() {
};
TLSSocket.prototype._finishInit = function() {
- debug('secure established');
+ // Guard against getting onhandshakedone() after .destroy().
+ // * 1.2: If destroy() during onocspresponse(), then write of next handshake
+ // record fails, the handshake done info callbacks does not occur, and the
+ // socket closes.
+ // * 1.3: The OCSP response comes in the same record that finishes handshake,
+ // so even after .destroy(), the handshake done info callback occurs
+ // immediately after onocspresponse(). Ignore it.
+ if (!this._handle)
+ return;
+
this.alpnProtocol = this._handle.getALPNNegotiatedProtocol();
this.servername = this._handle.getServername();
+
+ debug('%s _finishInit',
+ this._tlsOptions.isServer ? 'server' : 'client',
+ 'handle?', !!this._handle,
+ 'alpn', this.alpnProtocol,
+ 'servername', this.servername);
+
this._secureEstablished = true;
if (this._tlsOptions.handshakeTimeout > 0)
this.setTimeout(0, this._handleTimeout);
@@ -677,6 +729,12 @@ TLSSocket.prototype._finishInit = function() {
};
TLSSocket.prototype._start = function() {
+ debug('%s _start',
+ this._tlsOptions.isServer ? 'server' : 'client',
+ 'handle?', !!this._handle,
+ 'connecting?', this.connecting,
+ 'requestOCSP?', !!this._tlsOptions.requestOCSP,
+ );
if (this.connecting) {
this.once('connect', this._start);
return;
@@ -686,7 +744,6 @@ TLSSocket.prototype._start = function() {
if (!this._handle)
return;
- debug('start');
if (this._tlsOptions.requestOCSP)
this._handle.requestOCSP();
this._handle.start();
@@ -765,13 +822,16 @@ function onServerSocketSecure() {
}
}
- if (!this.destroyed && this._releaseControl())
+ if (!this.destroyed && this._releaseControl()) {
+ debug('server emit secureConnection');
this._tlsOptions.server.emit('secureConnection', this);
+ }
}
function onSocketTLSError(err) {
if (!this._controlReleased && !this[kErrorEmitted]) {
this[kErrorEmitted] = true;
+ debug('server emit tlsClientError:', err);
this._tlsOptions.server.emit('tlsClientError', err, this);
}
}
@@ -792,6 +852,7 @@ function onSocketClose(err) {
}
function tlsConnectionListener(rawSocket) {
+ debug('net.Server.on(connection): new TLSSocket');
const socket = new TLSSocket(rawSocket, {
secureContext: this._sharedCreds,
isServer: true,
@@ -1180,6 +1241,7 @@ function onConnectSecure() {
const ekeyinfo = this.getEphemeralKeyInfo();
if (ekeyinfo.type === 'DH' && ekeyinfo.size < options.minDHSize) {
const err = new ERR_TLS_DH_PARAM_SIZE(ekeyinfo.size);
+ debug('client emit:', err);
this.emit('error', err);
this.destroy();
return;
@@ -1206,10 +1268,12 @@ function onConnectSecure() {
this.destroy(verifyError);
return;
} else {
+ debug('client emit secureConnect');
this.emit('secureConnect');
}
} else {
this.authorized = true;
+ debug('client emit secureConnect');
this.emit('secureConnect');
}
diff --git a/lib/internal/stream_base_commons.js b/lib/internal/stream_base_commons.js
index 7b5798e82a..67abba0992 100644
--- a/lib/internal/stream_base_commons.js
+++ b/lib/internal/stream_base_commons.js
@@ -30,6 +30,8 @@ const kAfterAsyncWrite = Symbol('kAfterAsyncWrite');
const kHandle = Symbol('kHandle');
const kSession = Symbol('kSession');
+const debug = require('util').debuglog('stream');
+
function handleWriteReq(req, data, encoding) {
const { handle } = req;
@@ -66,6 +68,8 @@ function handleWriteReq(req, data, encoding) {
}
function onWriteComplete(status) {
+ debug('onWriteComplete', status, this.error);
+
const stream = this.handle[owner_symbol];
if (stream.destroyed) {
diff --git a/lib/tls.js b/lib/tls.js
index 645c3e9269..9d2b9add66 100644
--- a/lib/tls.js
+++ b/lib/tls.js
@@ -54,15 +54,25 @@ exports.DEFAULT_CIPHERS =
exports.DEFAULT_ECDH_CURVE = 'auto';
-exports.DEFAULT_MAX_VERSION = 'TLSv1.2';
+exports.DEFAULT_MAX_VERSION = 'TLSv1.3';
-if (getOptionValue('--tls-v1.0'))
+if (getOptionValue('--tls-min-v1.0'))
exports.DEFAULT_MIN_VERSION = 'TLSv1';
-else if (getOptionValue('--tls-v1.1'))
+else if (getOptionValue('--tls-min-v1.1'))
exports.DEFAULT_MIN_VERSION = 'TLSv1.1';
+else if (getOptionValue('--tls-min-v1.3'))
+ exports.DEFAULT_MIN_VERSION = 'TLSv1.3';
else
exports.DEFAULT_MIN_VERSION = 'TLSv1.2';
+if (getOptionValue('--tls-max-v1.3'))
+ exports.DEFAULT_MAX_VERSION = 'TLSv1.3';
+else if (getOptionValue('--tls-max-v1.2'))
+ exports.DEFAULT_MAX_VERSION = 'TLSv1.2';
+else
+ exports.DEFAULT_MAX_VERSION = 'TLSv1.3'; // Will depend on node version.
+
+
exports.getCiphers = internalUtil.cachedResult(
() => internalUtil.filterDuplicateStrings(binding.getSSLCiphers(), true)
);
diff --git a/src/node_constants.cc b/src/node_constants.cc
index be27de4ed6..f08bcbcb25 100644
--- a/src/node_constants.cc
+++ b/src/node_constants.cc
@@ -1245,6 +1245,7 @@ void DefineCryptoConstants(Local<Object> target) {
NODE_DEFINE_CONSTANT(target, TLS1_VERSION);
NODE_DEFINE_CONSTANT(target, TLS1_1_VERSION);
NODE_DEFINE_CONSTANT(target, TLS1_2_VERSION);
+ NODE_DEFINE_CONSTANT(target, TLS1_3_VERSION);
#endif
NODE_DEFINE_CONSTANT(target, INT_MAX);
}
diff --git a/src/node_constants.h b/src/node_constants.h
index 6f73fb4d7d..af5aa002eb 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 d4d0af4795..de94ca9d26 100644
--- a/src/node_crypto.cc
+++ b/src/node_crypto.cc
@@ -339,9 +339,14 @@ void SecureContext::Initialize(Environment* env, Local<Object> 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);
@@ -392,6 +397,9 @@ void SecureContext::New(const FunctionCallbackInfo<Value>& 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 = TLS1_3_VERSION;
void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
SecureContext* sc;
@@ -406,13 +414,16 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
int max_version = args[2].As<Int32>()->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) {
THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv2 methods disabled");
return;
@@ -432,21 +443,23 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
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 = 0;
+ max_version = MAX_SUPPORTED_VERSION;
method = TLS_server_method();
} else if (strcmp(*sslmethod, "TLS_client_method") == 0) {
min_version = 0;
- max_version = 0;
+ max_version = MAX_SUPPORTED_VERSION;
method = TLS_client_method();
} else if (strcmp(*sslmethod, "TLSv1_method") == 0) {
min_version = TLS1_VERSION;
@@ -510,12 +523,6 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& 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
@@ -936,42 +943,54 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
}
-void SecureContext::SetCiphers(const FunctionCallbackInfo<Value>& args) {
+void SecureContext::SetCipherSuites(const FunctionCallbackInfo<Value>& args) {
+ // BoringSSL doesn't allow API config of TLS1.3 cipher suites.
+#ifndef 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<Value>& 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);
}
}
@@ -1040,6 +1059,56 @@ void SecureContext::SetDHParam(const FunctionCallbackInfo<Value>& args) {
}
+void SecureContext::SetMinProto(const FunctionCallbackInfo<Value>& args) {
+ SecureContext* sc;
+ ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
+
+ CHECK_EQ(args.Length(), 1);
+ CHECK(args[0]->IsInt32());
+
+ int version = args[0].As<Int32>()->Value();
+
+ CHECK(SSL_CTX_set_min_proto_version(sc->ctx_.get(), version));
+}
+
+
+void SecureContext::SetMaxProto(const FunctionCallbackInfo<Value>& args) {
+ SecureContext* sc;
+ ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
+
+ CHECK_EQ(args.Length(), 1);
+ CHECK(args[0]->IsInt32());
+
+ int version = args[0].As<Int32>()->Value();
+
+ CHECK(SSL_CTX_set_max_proto_version(sc->ctx_.get(), version));
+}
+
+
+void SecureContext::GetMinProto(const FunctionCallbackInfo<Value>& 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<uint32_t>(version));
+}
+
+
+void SecureContext::GetMaxProto(const FunctionCallbackInfo<Value>& 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<uint32_t>(version));
+}
+
+
void SecureContext::SetOptions(const FunctionCallbackInfo<Value>& args) {
SecureContext* sc;
ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
@@ -1259,6 +1328,7 @@ void SecureContext::SetTicketKeys(const FunctionCallbackInfo<Value>& 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");
}
@@ -2118,6 +2188,7 @@ void SSLWrap<Base>::LoadSession(const FunctionCallbackInfo<Value>& 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<unsigned char> sbuf(args[0]);
@@ -2154,7 +2225,7 @@ void SSLWrap<Base>::Renegotiate(const FunctionCallbackInfo<Value>& 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);
}
@@ -2269,8 +2340,12 @@ void SSLWrap<Base>::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);
}
@@ -2484,7 +2559,10 @@ int SSLWrap<Base>::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
@@ -2526,6 +2604,8 @@ int SSLWrap<Base>::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();
@@ -2601,6 +2681,8 @@ void SSLWrap<Base>::CertCbDone(const FunctionCallbackInfo<Value>& 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");
@@ -5954,6 +6036,24 @@ void GetSSLCiphers(const FunctionCallbackInfo<Value>& 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 c9ca39d3e6..7de42a07f1 100644
--- a/src/node_crypto.h
+++ b/src/node_crypto.h
@@ -147,6 +147,7 @@ class SecureContext : public BaseObject {
static void AddCACert(const v8::FunctionCallbackInfo<v8::Value>& args);
static void AddCRL(const v8::FunctionCallbackInfo<v8::Value>& args);
static void AddRootCerts(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void SetCipherSuites(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetCiphers(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetECDHCurve(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetDHParam(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -155,6 +156,10 @@ class SecureContext : public BaseObject {
const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetSessionTimeout(
const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void SetMinProto(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void SetMaxProto(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void GetMinProto(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void GetMaxProto(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Close(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadPKCS12(const v8::FunctionCallbackInfo<v8::Value>& args);
#ifndef OPENSSL_NO_ENGINE
diff --git a/src/node_crypto_clienthello.cc b/src/node_crypto_clienthello.cc
index 268d477357..8c1be7871a 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 d1661735f5..725be889e2 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_options.cc b/src/node_options.cc
index 0bf8cb540c..5687fb327b 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -335,16 +335,30 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
AddOption("--napi-modules", "", NoOp{}, kAllowedInEnvironment);
-#if HAVE_OPENSSL
- AddOption("--tls-v1.0",
- "enable TLSv1.0 and greater by default",
- &EnvironmentOptions::tls_v1_0,
- kAllowedInEnvironment);
- AddOption("--tls-v1.1",
- "enable TLSv1.1 and greater by default",
- &EnvironmentOptions::tls_v1_1,
+ AddOption("--tls-min-v1.0",
+ "set default TLS minimum to TLSv1.0 (default: TLSv1.2)",
+ &EnvironmentOptions::tls_min_v1_0,
+ kAllowedInEnvironment);
+ AddOption("--tls-min-v1.1",
+ "set default TLS minimum to TLSv1.1 (default: TLSv1.2)",
+ &EnvironmentOptions::tls_min_v1_1,
+ kAllowedInEnvironment);
+ AddOption("--tls-min-v1.3",
+ "set default TLS minimum to TLSv1.3 (default: TLSv1.2)",
+ &EnvironmentOptions::tls_min_v1_3,
+ kAllowedInEnvironment);
+ AddOption("--tls-max-v1.2",
+ "set default TLS maximum to TLSv1.2 (default: TLSv1.3)",
+ &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.3)",
+ &EnvironmentOptions::tls_max_v1_3,
kAllowedInEnvironment);
-#endif
}
PerIsolateOptionsParser::PerIsolateOptionsParser(
diff --git a/src/node_options.h b/src/node_options.h
index e8fb002094..bcd6d2457d 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -121,10 +121,11 @@ class EnvironmentOptions : public Options {
bool print_eval = false;
bool force_repl = false;
-#if HAVE_OPENSSL
- bool tls_v1_0 = false;
- bool tls_v1_1 = false;
-#endif
+ bool tls_min_v1_0 = false;
+ bool tls_min_v1_1 = false;
+ bool tls_min_v1_3 = false;
+ bool tls_max_v1_2 = false;
+ bool tls_max_v1_3 = false;
std::vector<std::string> preload_modules;
diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc
index a0eb51e6ee..0ca7ad7845 100644
--- a/src/tls_wrap.cc
+++ b/src/tls_wrap.cc
@@ -113,6 +113,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
@@ -224,6 +230,8 @@ void TLSWrap::SSLInfoCallback(const SSL* ssl_, int where, int ret) {
Local<Object> object = c->object();
if (where & SSL_CB_HANDSHAKE_START) {
+ // Start is tracked to limit number and frequency of renegotiation attempts,
+ // since excessive renegotiation may be an attack.
Local<Value> callback;
if (object->Get(env->context(), env->onhandshakestart_string())
@@ -237,6 +245,7 @@ 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)) {
+ CHECK(!SSL_renegotiate_pending(ssl));
Local<Value> callback;
c->established_ = true;
@@ -271,8 +280,23 @@ void TLSWrap::EncOut() {
// No encrypted output ready to write to the underlying stream.
if (BIO_pending(enc_out_) == 0) {
- if (pending_cleartext_input_.empty())
- InvokeQueued(0);
+ if (pending_cleartext_input_.empty()) {
+ if (!in_dowrite_) {
+ InvokeQueued(0);
+ } else {
+ // 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<TLSWrap*>(data)->InvokeQueued(0);
+ }, this, object());
+ }
+ }
return;
}
@@ -534,8 +558,8 @@ void TLSWrap::ClearIn() {
Local<Value> arg = GetSSLError(written, &err, &error_str);
if (!arg.IsEmpty()) {
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 {
// Push back the not-yet-written pending buffers into their queue.
@@ -603,6 +627,7 @@ 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,
@@ -688,7 +713,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;
}
diff --git a/src/tls_wrap.h b/src/tls_wrap.h
index be694526ab..d15d509db5 100644
--- a/src/tls_wrap.h
+++ b/src/tls_wrap.h
@@ -173,6 +173,7 @@ class TLSWrap : public AsyncWrap,
std::vector<uv_buf_t> 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 0000000000..748234402c
--- /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 2ea03283e4..580264316d 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 354cd7ad0c..d6dcd20470 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 73b81ec1b1..005a733c07 100644
--- a/test/parallel/test-crypto.js
+++ b/test/parallel/test-crypto.js
@@ -129,6 +129,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 1bbff96001..a04ef7461d 100644
--- a/test/parallel/test-https-agent-additional-options.js
+++ b/test/parallel/test-https-agent-additional-options.js
@@ -1,4 +1,4 @@
-// Flags: --tls-v1.1
+// Flags: --tls-min-v1.1
'use strict';
const common = require('../common');
if (!common.hasCrypto)
diff --git a/test/parallel/test-https-agent-session-eviction.js b/test/parallel/test-https-agent-session-eviction.js
index 785f4737bd..8e13b150bb 100644
--- a/test/parallel/test-https-agent-session-eviction.js
+++ b/test/parallel/test-https-agent-session-eviction.js
@@ -1,4 +1,4 @@
-// Flags: --tls-v1.0
+// 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 1fccc7bb5d..3527c48f4b 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 cf1bbdf262..8904e24180 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-tls-alert-handling.js b/test/parallel/test-tls-alert-handling.js
index 63b845122f..f9f42e2d51 100644
--- a/test/parallel/test-tls-alert-handling.js
+++ b/test/parallel/test-tls-alert-handling.js
@@ -64,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;
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 5c812c8f04..49ca0cebc9 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 8d39b8d45c..9bcc63c454 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 0000000000..9bbc9ff0ec
--- /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.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-max-version-1.3.js b/test/parallel/test-tls-cli-max-version-1.3.js
new file mode 100644
index 0000000000..c04354fe4a
--- /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.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.0.js b/test/parallel/test-tls-cli-min-version-1.0.js
index f2f39ce60f..577562782e 100644
--- a/test/parallel/test-tls-cli-min-version-1.0.js
+++ b/test/parallel/test-tls-cli-min-version-1.0.js
@@ -1,4 +1,4 @@
-// Flags: --tls-v1.0 --tls-v1.1
+// Flags: --tls-min-v1.0 --tls-min-v1.1
'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
@@ -8,7 +8,7 @@ if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const tls = require('tls');
-assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2');
+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.
diff --git a/test/parallel/test-tls-cli-min-version-1.1.js b/test/parallel/test-tls-cli-min-version-1.1.js
index 404ee98ff3..3af2b39546 100644
--- a/test/parallel/test-tls-cli-min-version-1.1.js
+++ b/test/parallel/test-tls-cli-min-version-1.1.js
@@ -1,4 +1,4 @@
-// Flags: --tls-v1.1
+// Flags: --tls-min-v1.1
'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
@@ -8,7 +8,7 @@ if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const tls = require('tls');
-assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2');
+assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.3');
assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.1');
// Check the min-max version protocol versions against these CLI settings.
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 0000000000..1bccc2f6cd
--- /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.3');
+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 1f8c7e6096..4762389619 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 8a9cc65a1c..113b452db6 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 0000000000..f77d463f44
--- /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 9eff6cb9ce..329b78c271 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 0000000000..8af63c4f79
--- /dev/null
+++ b/test/parallel/test-tls-client-renegotiation-13.js
@@ -0,0 +1,37 @@
+'use strict';
+
+const common = require('../common');
+const fixtures = require('../common/fixtures');
+
+// 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 daae92eeb4..010fd8596a 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 0000000000..7767d3dd2a
--- /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 9f868fdcdc..0f5c9caa7c 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 0000000000..69861868cf
--- /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 eb7a2ca338..b06d7728dc 100644
--- a/test/parallel/test-tls-destroy-stream.js
+++ b/test/parallel/test-tls-destroy-stream.js
@@ -9,6 +9,8 @@ const net = require('net');
const assert = require('assert');
const tls = require('tls');
+tls.DEFAULT_MAX_VERSION = 'TLSv1.3';
+
// This test ensures that an instance of StreamWrap should emit "end" and
// "close" when the socket on the other side call `destroy()` instead of
// `end()`.
@@ -21,10 +23,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 +66,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 13e08112e5..3fd6710622 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'),
@@ -86,5 +89,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 f819161f7f..624f8efd24 100644
--- a/test/parallel/test-tls-getcipher.js
+++ b/test/parallel/test-tls-getcipher.js
@@ -60,7 +60,7 @@ server.listen(0, '127.0.0.1', common.mustCall(function() {
tls.connect({
host: '127.0.0.1',
port: this.address().port,
- cipher: 'ECDHE-RSA-AES128-GCM-SHA256',
+ ciphers: 'ECDHE-RSA-AES128-GCM-SHA256',
rejectUnauthorized: false
}, common.mustCall(function() {
const cipher = this.getCipher();
@@ -69,3 +69,24 @@ server.listen(0, '127.0.0.1', common.mustCall(function() {
this.end();
}));
}));
+
+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.3');
+ client.end();
+ }));
+}));
diff --git a/test/parallel/test-tls-min-max-version.js b/test/parallel/test-tls-min-max-version.js
index 9a8b73c40e..179ae1fa63 100644
--- a/test/parallel/test-tls-min-max-version.js
+++ b/test/parallel/test-tls-min-max-version.js
@@ -13,6 +13,10 @@ const DEFAULT_MAX_VERSION = tls.DEFAULT_MAX_VERSION;
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.<anonymous> (file:line)
+ // from the stack location, we only want the file:line part.
+ const where = (new Error()).stack.split('\n')[2].replace(/[^(]*/, '');
connect({
client: {
checkServerIdentity: (servername, cert) => { },
@@ -32,6 +36,7 @@ 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);
@@ -64,8 +69,8 @@ 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');
+// 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',
@@ -101,7 +106,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',
@@ -149,7 +170,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');
}
}
@@ -164,7 +189,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');
}
}
@@ -180,14 +207,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 0000000000..d2fb230796
--- /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 c184e902b4..4acb4e8022 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 2fd815d627..347dfd985a 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-set-ciphers-error.js b/test/parallel/test-tls-set-ciphers-error.js
index 5ef08dda04..f963b414f4 100644
--- a/test/parallel/test-tls-set-ciphers-error.js
+++ b/test/parallel/test-tls-set-ciphers-error.js
@@ -19,4 +19,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 ef2c254351..96eb0944f4 100644
--- a/test/parallel/test-tls-set-ciphers.js
+++ b/test/parallel/test-tls-set-ciphers.js
@@ -1,62 +1,93 @@
-// 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
+} = 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 options = {
- key: fixtures.readKey('agent2-key.pem'),
- cert: fixtures.readKey('agent2-cert.pem'),
- ciphers: 'AES256-SHA'
-};
+function test(cciphers, sciphers, cipher, cerr, serr) {
+ 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, serr);
+ }
+ return cleanup();
+ }
-const reply = 'I AM THE WALRUS'; // something recognizable
-let response = '';
+ const reply = 'So long and thanks for all the fish.';
-process.on('exit', function() {
- assert.ok(response.includes(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);
-const server = tls.createServer(options, common.mustCall(function(conn) {
- conn.end(reply);
-}));
+ pair.server.conn.write(reply);
-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.client.conn.on('data', common.mustCall((data) => {
+ assert.strictEqual(data.toString(), reply);
+ return cleanup();
+ }));
+ }));
+}
- exec(cmd, function(err, stdout, stderr) {
- assert.ifError(err);
- response = stdout;
- server.close();
- });
-});
+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 0000000000..600c571a03
--- /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 98fe533b69..234c1bad09 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 d11535dd3a..8d9cd8cdd2 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();