Skip to content

Commit f512f5e

Browse files
committed
tls: add min/max protocol version options
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. PR-URL: nodejs#24405 Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Rod Vagg <[email protected]>
1 parent 160ac0f commit f512f5e

14 files changed

+327
-45
lines changed

doc/api/errors.md

+11
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,17 @@ recommended to use 2048 bits or larger for stronger security.
16551655
A TLS/SSL handshake timed out. In this case, the server must also abort the
16561656
connection.
16571657

1658+
<a id="ERR_TLS_INVALID_PROTOCOL_VERSION"></a>
1659+
### ERR_TLS_INVALID_PROTOCOL_VERSION
1660+
1661+
Valid TLS protocol versions are `'TLSv1'`, `'TLSv1.1'`, or `'TLSv1.2'`.
1662+
1663+
<a id="ERR_TLS_PROTOCOL_VERSION_CONFLICT"></a>
1664+
### ERR_TLS_PROTOCOL_VERSION_CONFLICT
1665+
1666+
Attempting to set a TLS protocol `minVersion` or `maxVersion` conflicts with an
1667+
attempt to set the `secureProtocol` explicitly. Use one mechanism or the other.
1668+
16581669
<a id="ERR_TLS_RENEGOTIATE"></a>
16591670
### ERR_TLS_RENEGOTIATE
16601671

doc/api/tls.md

+15-4
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,10 @@ changes:
10701070
pr-url: https://github.com/nodejs/node/pull/4099
10711071
description: The `ca` option can now be a single string containing multiple
10721072
CA certificates.
1073+
- version: REPLACEME
1074+
pr-url: REPLACEME
1075+
description: The `minVersion` and `maxVersion` can be used to restrict
1076+
the allowed TLS protocol versions.
10731077
-->
10741078

10751079
* `options` {Object}
@@ -1130,6 +1134,16 @@ changes:
11301134
passphrase: <string>]}`. The object form can only occur in an array.
11311135
`object.passphrase` is optional. Encrypted keys will be decrypted with
11321136
`object.passphrase` if provided, or `options.passphrase` if it is not.
1137+
* `maxVersion` {string} Optionally set the maximum TLS version to allow. One
1138+
of `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified along with the
1139+
`secureProtocol` option, use one or the other. **Default:** `'TLSv1.2'`.
1140+
* `minVersion` {string} Optionally set the minimum TLS version to allow. One
1141+
of `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified along with the
1142+
`secureProtocol` option, use one or the other. It is not recommended to use
1143+
less than TLSv1.2, but it may be required for interoperability.
1144+
**Default:** `'TLSv1.2'`, unless changed using CLI options. Using
1145+
`--tls-v1.0` changes the default to `'TLSv1'`. Using `--tls-v1.1` changes
1146+
the default to `'TLSv1.1'`.
11331147
* `passphrase` {string} Shared passphrase used for a single private key and/or
11341148
a PFX.
11351149
* `pfx` {string|string[]|Buffer|Buffer[]|Object[]} PFX or PKCS12 encoded
@@ -1150,10 +1164,7 @@ changes:
11501164
example, use `'TLSv1_1_method'` to force TLS version 1.1, or `'TLS_method'`
11511165
to allow any TLS protocol version. It is not recommended to use TLS versions
11521166
less than 1.2, but it may be required for interoperability. **Default:**
1153-
`'TLSv1_2_method'`, unless changed using CLI options. Using the `--tlsv1.0`
1154-
CLI option is like `'TLS_method'` except protocols earlier than TLSv1.0 are
1155-
not allowed, and using the `--tlsv1.1` CLI option is like `'TLS_method'`
1156-
except that protocols earlier than TLSv1.1 are not allowed.
1167+
none, see `minVersion`.
11571168
* `sessionIdContext` {string} Opaque identifier used by servers to ensure
11581169
session state is not shared between applications. Unused by clients.
11591170

lib/_tls_common.js

+32-7
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,46 @@ const { isArrayBufferView } = require('internal/util/types');
2626
const tls = require('tls');
2727
const {
2828
ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED,
29-
ERR_INVALID_ARG_TYPE
29+
ERR_INVALID_ARG_TYPE,
30+
ERR_TLS_INVALID_PROTOCOL_VERSION,
31+
ERR_TLS_PROTOCOL_VERSION_CONFLICT,
3032
} = require('internal/errors').codes;
31-
32-
const { SSL_OP_CIPHER_SERVER_PREFERENCE } = internalBinding('constants').crypto;
33+
const {
34+
SSL_OP_CIPHER_SERVER_PREFERENCE,
35+
TLS1_VERSION,
36+
TLS1_1_VERSION,
37+
TLS1_2_VERSION,
38+
} = internalBinding('constants').crypto;
3339

3440
// Lazily loaded from internal/crypto/util.
3541
let toBuf = null;
3642

43+
function toV(which, v, def) {
44+
if (v == null) v = def;
45+
if (v === 'TLSv1') return TLS1_VERSION;
46+
if (v === 'TLSv1.1') return TLS1_1_VERSION;
47+
if (v === 'TLSv1.2') return TLS1_2_VERSION;
48+
throw new ERR_TLS_INVALID_PROTOCOL_VERSION(v, which);
49+
}
50+
3751
const { SecureContext: NativeSecureContext } = internalBinding('crypto');
38-
function SecureContext(secureProtocol, secureOptions) {
52+
function SecureContext(secureProtocol, secureOptions, minVersion, maxVersion) {
3953
if (!(this instanceof SecureContext)) {
40-
return new SecureContext(secureProtocol, secureOptions);
54+
return new SecureContext(secureProtocol, secureOptions, minVersion,
55+
maxVersion);
56+
}
57+
58+
if (secureProtocol) {
59+
if (minVersion != null)
60+
throw new ERR_TLS_PROTOCOL_VERSION_CONFLICT(minVersion, secureProtocol);
61+
if (maxVersion != null)
62+
throw new ERR_TLS_PROTOCOL_VERSION_CONFLICT(maxVersion, secureProtocol);
4163
}
4264

4365
this.context = new NativeSecureContext();
44-
this.context.init(secureProtocol);
66+
this.context.init(secureProtocol,
67+
toV('minimum', minVersion, tls.DEFAULT_MIN_VERSION),
68+
toV('maximum', maxVersion, tls.DEFAULT_MAX_VERSION));
4569

4670
if (secureOptions) this.context.setOptions(secureOptions);
4771
}
@@ -66,7 +90,8 @@ exports.createSecureContext = function createSecureContext(options) {
6690
if (options.honorCipherOrder)
6791
secureOptions |= SSL_OP_CIPHER_SERVER_PREFERENCE;
6892

69-
const c = new SecureContext(options.secureProtocol, secureOptions);
93+
const c = new SecureContext(options.secureProtocol, secureOptions,
94+
options.minVersion, options.maxVersion);
7095
var i;
7196
var val;
7297

lib/_tls_wrap.js

+14
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,16 @@ Server.prototype.setSecureContext = function(options) {
918918
else
919919
this.ca = undefined;
920920

921+
if (options.minVersion)
922+
this.minVersion = options.minVersion;
923+
else
924+
this.minVersion = undefined;
925+
926+
if (options.maxVersion)
927+
this.maxVersion = options.maxVersion;
928+
else
929+
this.maxVersion = undefined;
930+
921931
if (options.secureProtocol)
922932
this.secureProtocol = options.secureProtocol;
923933
else
@@ -974,6 +984,8 @@ Server.prototype.setSecureContext = function(options) {
974984
ciphers: this.ciphers,
975985
ecdhCurve: this.ecdhCurve,
976986
dhparam: this.dhparam,
987+
minVersion: this.minVersion,
988+
maxVersion: this.maxVersion,
977989
secureProtocol: this.secureProtocol,
978990
secureOptions: this.secureOptions,
979991
honorCipherOrder: this.honorCipherOrder,
@@ -1026,6 +1038,8 @@ Server.prototype.setOptions = util.deprecate(function(options) {
10261038
if (options.clientCertEngine)
10271039
this.clientCertEngine = options.clientCertEngine;
10281040
if (options.ca) this.ca = options.ca;
1041+
if (options.minVersion) this.minVersion = options.minVersion;
1042+
if (options.maxVersion) this.maxVersion = options.maxVersion;
10291043
if (options.secureProtocol) this.secureProtocol = options.secureProtocol;
10301044
if (options.crl) this.crl = options.crl;
10311045
if (options.ciphers) this.ciphers = options.ciphers;

lib/https.js

+8
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ Agent.prototype.getName = function getName(options) {
186186
if (options.servername && options.servername !== options.host)
187187
name += options.servername;
188188

189+
name += ':';
190+
if (options.minVersion)
191+
name += options.minVersion;
192+
193+
name += ':';
194+
if (options.maxVersion)
195+
name += options.maxVersion;
196+
189197
name += ':';
190198
if (options.secureProtocol)
191199
name += options.secureProtocol;

lib/internal/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,10 @@ E('ERR_TLS_CERT_ALTNAME_INVALID',
861861
'Hostname/IP does not match certificate\'s altnames: %s', Error);
862862
E('ERR_TLS_DH_PARAM_SIZE', 'DH parameter size %s is less than 2048', Error);
863863
E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout', Error);
864+
E('ERR_TLS_INVALID_PROTOCOL_VERSION',
865+
'%j is not a valid %s TLS protocol version', TypeError);
866+
E('ERR_TLS_PROTOCOL_VERSION_CONFLICT',
867+
'TLS protocol version %j conflicts with secureProtocol %j', TypeError);
864868
E('ERR_TLS_RENEGOTIATE', 'Attempt to renegotiate TLS session failed', Error);
865869
E('ERR_TLS_RENEGOTIATION_DISABLED',
866870
'TLS session renegotiation disabled for this socket', Error);

lib/tls.js

+10
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internalUtil.assertCrypto();
3131
const { isArrayBufferView } = require('internal/util/types');
3232

3333
const net = require('net');
34+
const { getOptionValue } = require('internal/options');
3435
const url = require('url');
3536
const binding = internalBinding('crypto');
3637
const { Buffer } = require('buffer');
@@ -53,6 +54,15 @@ exports.DEFAULT_CIPHERS =
5354

5455
exports.DEFAULT_ECDH_CURVE = 'auto';
5556

57+
exports.DEFAULT_MAX_VERSION = 'TLSv1.2';
58+
59+
if (getOptionValue('--tls-v1.0'))
60+
exports.DEFAULT_MIN_VERSION = 'TLSv1';
61+
else if (getOptionValue('--tls-v1.1'))
62+
exports.DEFAULT_MIN_VERSION = 'TLSv1.1';
63+
else
64+
exports.DEFAULT_MIN_VERSION = 'TLSv1.2';
65+
5666
exports.getCiphers = internalUtil.cachedResult(
5767
() => internalUtil.filterDuplicateStrings(binding.getSSLCiphers(), true)
5868
);

src/node_constants.cc

+4
Original file line numberDiff line numberDiff line change
@@ -1237,6 +1237,10 @@ void DefineCryptoConstants(Local<Object> target) {
12371237
NODE_DEFINE_STRING_CONSTANT(target,
12381238
"defaultCipherList",
12391239
per_process_opts->tls_cipher_list.c_str());
1240+
1241+
NODE_DEFINE_CONSTANT(target, TLS1_VERSION);
1242+
NODE_DEFINE_CONSTANT(target, TLS1_1_VERSION);
1243+
NODE_DEFINE_CONSTANT(target, TLS1_2_VERSION);
12401244
#endif
12411245
NODE_DEFINE_CONSTANT(target, INT_MAX);
12421246
}

src/node_crypto.cc

+8-6
Original file line numberDiff line numberDiff line change
@@ -405,14 +405,15 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
405405
ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
406406
Environment* env = sc->env();
407407

408-
int min_version = TLS1_2_VERSION;
409-
int max_version = 0;
410-
const SSL_METHOD* method = TLS_method();
408+
CHECK_EQ(args.Length(), 3);
409+
CHECK(args[1]->IsInt32());
410+
CHECK(args[2]->IsInt32());
411411

412-
if (env->options()->tls_v1_1) min_version = TLS1_1_VERSION;
413-
if (env->options()->tls_v1_0) min_version = TLS1_VERSION;
412+
int min_version = args[1].As<Int32>()->Value();
413+
int max_version = args[2].As<Int32>()->Value();
414+
const SSL_METHOD* method = TLS_method();
414415

415-
if (args.Length() == 1 && args[0]->IsString()) {
416+
if (args[0]->IsString()) {
416417
const node::Utf8Value sslmethod(env->isolate(), args[0]);
417418

418419
// Note that SSLv2 and SSLv3 are disallowed but SSLv23_method and friends
@@ -509,6 +510,7 @@ void SecureContext::Init(const FunctionCallbackInfo<Value>& args) {
509510

510511
SSL_CTX_set_min_proto_version(sc->ctx_.get(), min_version);
511512
SSL_CTX_set_max_proto_version(sc->ctx_.get(), max_version);
513+
512514
// OpenSSL 1.1.0 changed the ticket key size, but the OpenSSL 1.0.x size was
513515
// exposed in the public API. To retain compatibility, install a callback
514516
// which restores the old algorithm.

test/fixtures/tls-connect.js

+43-26
Original file line numberDiff line numberDiff line change
@@ -46,28 +46,45 @@ exports.connect = function connect(options, callback) {
4646
const client = {};
4747
const pair = { server, client };
4848

49-
tls.createServer(options.server, function(conn) {
50-
server.conn = conn;
51-
conn.pipe(conn);
52-
maybeCallback()
53-
}).listen(0, function() {
54-
server.server = this;
49+
try {
50+
tls.createServer(options.server, function(conn) {
51+
server.conn = conn;
52+
conn.pipe(conn);
53+
maybeCallback()
54+
}).listen(0, function() {
55+
server.server = this;
5556

56-
const optClient = util._extend({
57-
port: this.address().port,
58-
}, options.client);
57+
const optClient = util._extend({
58+
port: this.address().port,
59+
}, options.client);
5960

60-
tls.connect(optClient)
61-
.on('secureConnect', function() {
62-
client.conn = this;
63-
maybeCallback();
64-
})
65-
.on('error', function(err) {
61+
try {
62+
tls.connect(optClient)
63+
.on('secureConnect', function() {
64+
client.conn = this;
65+
maybeCallback();
66+
})
67+
.on('error', function(err) {
68+
client.err = err;
69+
client.conn = this;
70+
maybeCallback();
71+
});
72+
} catch (err) {
6673
client.err = err;
67-
client.conn = this;
68-
maybeCallback();
69-
});
70-
});
74+
// The server won't get a connection, we are done.
75+
callback(err, pair, cleanup);
76+
callback = null;
77+
}
78+
}).on('tlsClientError', function(err, sock) {
79+
server.conn = sock;
80+
server.err = err;
81+
maybeCallback();
82+
});
83+
} catch (err) {
84+
// Invalid options can throw, report the error.
85+
pair.server.err = err;
86+
callback(err, pair, () => {});
87+
}
7188

7289
function maybeCallback() {
7390
if (!callback)
@@ -76,13 +93,13 @@ exports.connect = function connect(options, callback) {
7693
const err = pair.client.err || pair.server.err;
7794
callback(err, pair, cleanup);
7895
callback = null;
79-
80-
function cleanup() {
81-
if (server.server)
82-
server.server.close();
83-
if (client.conn)
84-
client.conn.end();
85-
}
8696
}
8797
}
98+
99+
function cleanup() {
100+
if (server.server)
101+
server.server.close();
102+
if (client.conn)
103+
client.conn.end();
104+
}
88105
}

test/parallel/test-https-agent-getname.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const agent = new https.Agent();
1212
// empty options
1313
assert.strictEqual(
1414
agent.getName({}),
15-
'localhost:::::::::::::::::'
15+
'localhost:::::::::::::::::::'
1616
);
1717

1818
// pass all options arguments
@@ -40,5 +40,5 @@ const options = {
4040
assert.strictEqual(
4141
agent.getName(options),
4242
'0.0.0.0:443:192.168.1.1:ca:cert:dynamic:ciphers:key:pfx:false:localhost:' +
43-
'secureProtocol:c,r,l:false:ecdhCurve:dhparam:0:sessionIdContext'
43+
'::secureProtocol:c,r,l:false:ecdhCurve:dhparam:0:sessionIdContext'
4444
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Flags: --tls-v1.0 --tls-v1.1
2+
'use strict';
3+
const common = require('../common');
4+
if (!common.hasCrypto) common.skip('missing crypto');
5+
6+
// Check that `node --tls-v1.0` is supported, and overrides --tls-v1.1.
7+
8+
const assert = require('assert');
9+
const tls = require('tls');
10+
11+
assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2');
12+
assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1');
13+
14+
// Check the min-max version protocol versions against these CLI settings.
15+
require('./test-tls-min-max-version.js');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Flags: --tls-v1.1
2+
'use strict';
3+
const common = require('../common');
4+
if (!common.hasCrypto) common.skip('missing crypto');
5+
6+
// Check that node `--tls-v1.1` is supported.
7+
8+
const assert = require('assert');
9+
const tls = require('tls');
10+
11+
assert.strictEqual(tls.DEFAULT_MAX_VERSION, 'TLSv1.2');
12+
assert.strictEqual(tls.DEFAULT_MIN_VERSION, 'TLSv1.1');
13+
14+
// Check the min-max version protocol versions against these CLI settings.
15+
require('./test-tls-min-max-version.js');

0 commit comments

Comments
 (0)