tls: add min/max protocol version options
authorSam Roberts <vieuxtech@gmail.com>
Sun, 6 May 2018 04:52:34 +0000 (13:52 +0900)
committerJérémy Lal <kapouer@melix.org>
Mon, 8 Apr 2019 13:06:40 +0000 (14:06 +0100)
The existing secureProtocol option only allows setting the allowed
protocol to a specific version, or setting it to "all supported
versions". It also used obscure strings based on OpenSSL C API
functions. Directly setting the min or max is easier to use and explain.

Backport-PR-URL: https://github.com/nodejs/node/pull/24676
PR-URL: https://github.com/nodejs/node/pull/24405
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Rod Vagg <rod@vagg.org>
Gbp-Pq: Topic ssl
Gbp-Pq: Name acb73518b7274bacdfc133fd121e91dfd6ba460b.patch

doc/api/errors.md
doc/api/tls.md
lib/_tls_common.js
lib/_tls_wrap.js
lib/https.js
lib/internal/errors.js
lib/tls.js
src/node_constants.cc
src/node_crypto.cc
test/fixtures/tls-connect.js
test/parallel/test-https-agent-getname.js

index 72df6b27cb7b08389d7b2745fe645720faded35b..ea0ff3cb54cb9cc1dd5ffcdbd1abb10a9581312b 100644 (file)
@@ -1652,6 +1652,17 @@ recommended to use 2048 bits or larger for stronger security.
 A TLS/SSL handshake timed out. In this case, the server must also abort the
 connection.
 
+<a id="ERR_TLS_INVALID_PROTOCOL_VERSION"></a>
+### ERR_TLS_INVALID_PROTOCOL_VERSION
+
+Valid TLS protocol versions are `'TLSv1'`, `'TLSv1.1'`, or `'TLSv1.2'`.
+
+<a id="ERR_TLS_PROTOCOL_VERSION_CONFLICT"></a>
+### ERR_TLS_PROTOCOL_VERSION_CONFLICT
+
+Attempting to set a TLS protocol `minVersion` or `maxVersion` conflicts with an
+attempt to set the `secureProtocol` explicitly. Use one mechanism or the other.
+
 <a id="ERR_TLS_RENEGOTIATE"></a>
 ### ERR_TLS_RENEGOTIATE
 
index e66e7ffe719506b477a821a3c71c9e3fec80ff40..6aeb5817531545b5dc156a00b7e20d065c1e709e 100644 (file)
@@ -1069,6 +1069,14 @@ changes:
     passphrase: <string>]}`. The object form can only occur in an array.
     `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'`.
+  * `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.
+    **Default:** `'TLSv1'`.
   * `passphrase` {string} Shared passphrase used for a single private key and/or
     a PFX.
   * `pfx` {string|string[]|Buffer|Buffer[]|Object[]} PFX or PKCS12 encoded
@@ -1084,9 +1092,12 @@ 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} SSL method to use. The possible values are listed
-    as [SSL_METHODS][], use the function names as strings. For example,
-    `'TLSv1_2_method'` to force TLS version 1.2. **Default:** `'TLS_method'`.
+  * `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`.
   * `sessionIdContext` {string} Opaque identifier used by servers to ensure
     session state is not shared between applications. Unused by clients.
 
index 1073f5d520e0b92cd1eed425a303f3f934419571..658bd1eaaae9eaf88b54bd792aa957431f8614ce 100644 (file)
@@ -26,19 +26,36 @@ const { isArrayBufferView } = require('internal/util/types');
 const tls = require('tls');
 const {
   ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED,
-  ERR_INVALID_ARG_TYPE
+  ERR_INVALID_ARG_TYPE,
+  ERR_TLS_INVALID_PROTOCOL_VERSION,
+  ERR_TLS_PROTOCOL_VERSION_CONFLICT,
 } = require('internal/errors').codes;
 
-const { SSL_OP_CIPHER_SERVER_PREFERENCE } = process.binding('constants').crypto;
+const {
+  SSL_OP_CIPHER_SERVER_PREFERENCE,
+  TLS1_VERSION,
+  TLS1_1_VERSION,
+  TLS1_2_VERSION,
+} = internalBinding('constants').crypto;
 
 // Lazily loaded
 var crypto = null;
 
+function toV(which, v, def) {
+  if (v == null) v = def;
+  if (v === 'TLSv1') return TLS1_VERSION;
+  if (v === 'TLSv1.1') return TLS1_1_VERSION;
+  if (v === 'TLSv1.2') return TLS1_2_VERSION;
+  throw new ERR_TLS_INVALID_PROTOCOL_VERSION(v, which);
+}
+
 const { SecureContext: NativeSecureContext } = internalBinding('crypto');
 
-function SecureContext(secureProtocol, secureOptions, context) {
+function SecureContext(secureProtocol, secureOptions, context,
+                       minVersion, maxVersion) {
   if (!(this instanceof SecureContext)) {
-    return new SecureContext(secureProtocol, secureOptions, context);
+    return new SecureContext(secureProtocol, secureOptions, context,
+                             minVersion, maxVersion);
   }
 
   if (context) {
@@ -47,10 +64,15 @@ function SecureContext(secureProtocol, secureOptions, context) {
     this.context = new NativeSecureContext();
 
     if (secureProtocol) {
-      this.context.init(secureProtocol);
-    } else {
-      this.context.init();
+      if (minVersion != null)
+        throw new ERR_TLS_PROTOCOL_VERSION_CONFLICT(minVersion, secureProtocol);
+      if (maxVersion != null)
+        throw new ERR_TLS_PROTOCOL_VERSION_CONFLICT(maxVersion, secureProtocol);
     }
+
+    this.context.init(secureProtocol,
+                      toV('minimum', minVersion, tls.DEFAULT_MIN_VERSION),
+                      toV('maximum', maxVersion, tls.DEFAULT_MAX_VERSION));
   }
 
   if (secureOptions) this.context.setOptions(secureOptions);
@@ -76,7 +98,8 @@ exports.createSecureContext = function createSecureContext(options, context) {
   if (options.honorCipherOrder)
     secureOptions |= SSL_OP_CIPHER_SERVER_PREFERENCE;
 
-  const c = new SecureContext(options.secureProtocol, secureOptions, context);
+  const c = new SecureContext(options.secureProtocol, secureOptions, context,
+                              options.minVersion, options.maxVersion);
   var i;
   var val;
 
index 9bfdd4062fc76279af801c2fc14b8cff93853f22..88b7bad40255f1112fc6f089aeceabe5d04e8ea5 100644 (file)
@@ -877,6 +877,8 @@ function Server(options, listener) {
     ciphers: this.ciphers,
     ecdhCurve: this.ecdhCurve,
     dhparam: this.dhparam,
+    minVersion: this.minVersion,
+    maxVersion: this.maxVersion,
     secureProtocol: this.secureProtocol,
     secureOptions: this.secureOptions,
     honorCipherOrder: this.honorCipherOrder,
@@ -948,6 +950,8 @@ Server.prototype.setOptions = function(options) {
   if (options.clientCertEngine)
     this.clientCertEngine = options.clientCertEngine;
   if (options.ca) this.ca = options.ca;
+  if (options.minVersion) this.minVersion = options.minVersion;
+  if (options.maxVersion) this.maxVersion = options.maxVersion;
   if (options.secureProtocol) this.secureProtocol = options.secureProtocol;
   if (options.crl) this.crl = options.crl;
   if (options.ciphers) this.ciphers = options.ciphers;
index c1053da1a13eb6ea78a839185c57ba7ae0803323..6f31315a559ef900288ad5944e59dd1af0b28726 100644 (file)
@@ -188,6 +188,14 @@ Agent.prototype.getName = function getName(options) {
   if (options.servername && options.servername !== options.host)
     name += options.servername;
 
+  name += ':';
+  if (options.minVersion)
+    name += options.minVersion;
+
+  name += ':';
+  if (options.maxVersion)
+    name += options.maxVersion;
+
   name += ':';
   if (options.secureProtocol)
     name += options.secureProtocol;
index b418fa295d2dcc6590453343fa94e742ef061806..79f7085f254d830caa53f04bebea464de1c6b0cd 100644 (file)
@@ -870,6 +870,10 @@ E('ERR_TLS_CERT_ALTNAME_INVALID',
   'Hostname/IP does not match certificate\'s altnames: %s', Error);
 E('ERR_TLS_DH_PARAM_SIZE', 'DH parameter size %s is less than 2048', Error);
 E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout', Error);
+E('ERR_TLS_INVALID_PROTOCOL_VERSION',
+  '%j is not a valid %s TLS protocol version', TypeError);
+E('ERR_TLS_PROTOCOL_VERSION_CONFLICT',
+  'TLS protocol version %j conflicts with secureProtocol %j', TypeError);
 E('ERR_TLS_RENEGOTIATE', 'Attempt to renegotiate TLS session failed', Error);
 E('ERR_TLS_RENEGOTIATION_DISABLED',
   'TLS session renegotiation disabled for this socket', Error);
index b8de6efc8e402d638b1bbdd35abae2e56750c74b..1e8686c8efa31531761f1a7ca2828bddcd3cd353 100644 (file)
@@ -52,6 +52,10 @@ exports.DEFAULT_CIPHERS =
 
 exports.DEFAULT_ECDH_CURVE = 'auto';
 
+exports.DEFAULT_MAX_VERSION = 'TLSv1.2';
+
+exports.DEFAULT_MIN_VERSION = 'TLSv1';
+
 exports.getCiphers = internalUtil.cachedResult(
   () => internalUtil.filterDuplicateStrings(binding.getSSLCiphers(), true)
 );
index 753b8f8fe166022426bb288166f0cc82e5b16a9b..9cd50fe4e9e87b4a30a461ebf59b7c0e7b473175 100644 (file)
@@ -1237,6 +1237,10 @@ void DefineCryptoConstants(Local<Object> target) {
   NODE_DEFINE_STRING_CONSTANT(target,
                               "defaultCipherList",
                               per_process_opts->tls_cipher_list.c_str());
+
+  NODE_DEFINE_CONSTANT(target, TLS1_VERSION);
+  NODE_DEFINE_CONSTANT(target, TLS1_1_VERSION);
+  NODE_DEFINE_CONSTANT(target, TLS1_2_VERSION);
 #endif
   NODE_DEFINE_CONSTANT(target, INT_MAX);
 }
index 1027f5f760a56ae93f5a33d0a6a9801517f5dee7..a43789a9b4b4622c5e4070af100331cbad2b48e7 100644 (file)
@@ -395,11 +395,15 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
   ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
   Environment* env = sc->env();
 
-  int min_version = 0;
-  int max_version = 0;
+  CHECK_EQ(args.Length(), 3);
+  CHECK(args[1]->IsInt32());
+  CHECK(args[2]->IsInt32());
+
+  int min_version = args[1].As<Int32>()->Value();
+  int max_version = args[2].As<Int32>()->Value();
   const SSL_METHOD* method = TLS_method();
 
-  if (args.Length() == 1 && args[0]->IsString()) {
+  if (args[0]->IsString()) {
     const node::Utf8Value sslmethod(env->isolate(), args[0]);
 
     // Note that SSLv2 and SSLv3 are disallowed but SSLv23_method and friends
@@ -424,6 +428,9 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
       method = TLS_server_method();
     } else if (strcmp(*sslmethod, "SSLv23_client_method") == 0) {
       method = TLS_client_method();
+    } else if (strcmp(*sslmethod, "TLS_method") == 0) {
+      min_version = 0;
+      max_version = 0;
     } else if (strcmp(*sslmethod, "TLSv1_method") == 0) {
       min_version = TLS1_VERSION;
       max_version = TLS1_VERSION;
index 43c3e6f0fd9fa02d1a1c8f4fe4f23389e10add65..303061adc3223e8793c93a266110e49e7a740700 100644 (file)
@@ -46,28 +46,45 @@ exports.connect = function connect(options, callback) {
   const client = {};
   const pair = { server, client };
 
-  tls.createServer(options.server, function(conn) {
-    server.conn = conn;
-    conn.pipe(conn);
-    maybeCallback()
-  }).listen(0, function() {
-    server.server = this;
+  try {
+    tls.createServer(options.server, function(conn) {
+      server.conn = conn;
+      conn.pipe(conn);
+      maybeCallback()
+    }).listen(0, function() {
+      server.server = this;
 
-    const optClient = util._extend({
-      port: this.address().port,
-    }, options.client);
+      const optClient = util._extend({
+        port: this.address().port,
+      }, options.client);
 
-    tls.connect(optClient)
-      .on('secureConnect', function() {
-        client.conn = this;
-        maybeCallback();
-      })
-      .on('error', function(err) {
+      try {
+        tls.connect(optClient)
+          .on('secureConnect', function() {
+            client.conn = this;
+            maybeCallback();
+          })
+          .on('error', function(err) {
+            client.err = err;
+            client.conn = this;
+            maybeCallback();
+          });
+      } catch (err) {
         client.err = err;
-        client.conn = this;
-        maybeCallback();
-      });
-  });
+        // The server won't get a connection, we are done.
+        callback(err, pair, cleanup);
+        callback = null;
+      }
+    }).on('tlsClientError', function(err, sock) {
+      server.conn = sock;
+      server.err = err;
+      maybeCallback();
+    });
+  } catch (err) {
+    // Invalid options can throw, report the error.
+    pair.server.err = err;
+    callback(err, pair, () => {});
+  }
 
   function maybeCallback() {
     if (!callback)
@@ -76,13 +93,13 @@ exports.connect = function connect(options, callback) {
       const err = pair.client.err || pair.server.err;
       callback(err, pair, cleanup);
       callback = null;
-
-      function cleanup() {
-        if (server.server)
-          server.server.close();
-        if (client.conn)
-          client.conn.end();
-      }
     }
   }
+
+  function cleanup() {
+    if (server.server)
+      server.server.close();
+    if (client.conn)
+      client.conn.end();
+  }
 }
index b68850f21d57ca1bdb0f13b5ddc40436d62ed539..5c95da549b20b55ac0b30540228a67cbbf6a2deb 100644 (file)
@@ -12,7 +12,7 @@ const agent = new https.Agent();
 // empty options
 assert.strictEqual(
   agent.getName({}),
-  'localhost:::::::::::::::::'
+  'localhost:::::::::::::::::::'
 );
 
 // pass all options arguments
@@ -40,5 +40,5 @@ const options = {
 assert.strictEqual(
   agent.getName(options),
   '0.0.0.0:443:192.168.1.1:ca:cert:dynamic:ciphers:key:pfx:false:localhost:' +
-    'secureProtocol:c,r,l:false:ecdhCurve:dhparam:0:sessionIdContext'
+    '::secureProtocol:c,r,l:false:ecdhCurve:dhparam:0:sessionIdContext'
 );