Skip to content

Commit 32c527d

Browse files
tniessendanielleadams
authored andcommitted
tls: support automatic DHE
Node.js has so far only supported user-defined DHE parameters and even recommended generating custom parameters. This change lets users set the dhparam option to 'auto' instead, in which case DHE parameters of sufficient strength are selected automatically (from a small set of well-known parameters). This has been recommended by OpenSSL for quite a while, and it makes it much easier for Node.js TLS servers to properly support DHE-based perfect forward secrecy. This also updates the documentation to prioritize ECDHE over DHE, mostly because the former tends to be more efficient and is enabled by default. PR-URL: #46978 Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]>
1 parent 51253ba commit 32c527d

File tree

5 files changed

+87
-33
lines changed

5 files changed

+87
-33
lines changed

doc/api/tls.md

+22-20
Original file line numberDiff line numberDiff line change
@@ -123,23 +123,17 @@ all sessions). Methods implementing this technique are called "ephemeral".
123123
Currently two methods are commonly used to achieve perfect forward secrecy (note
124124
the character "E" appended to the traditional abbreviations):
125125

126-
* [DHE][]: An ephemeral version of the Diffie-Hellman key-agreement protocol.
127126
* [ECDHE][]: An ephemeral version of the Elliptic Curve Diffie-Hellman
128127
key-agreement protocol.
128+
* [DHE][]: An ephemeral version of the Diffie-Hellman key-agreement protocol.
129129

130-
To use perfect forward secrecy using `DHE` with the `node:tls` module, it is
131-
required to generate Diffie-Hellman parameters and specify them with the
132-
`dhparam` option to [`tls.createSecureContext()`][]. The following illustrates
133-
the use of the OpenSSL command-line interface to generate such parameters:
134-
135-
```bash
136-
openssl dhparam -outform PEM -out dhparam.pem 2048
137-
```
130+
Perfect forward secrecy using ECDHE is enabled by default. The `ecdhCurve`
131+
option can be used when creating a TLS server to customize the list of supported
132+
ECDH curves to use. See [`tls.createServer()`][] for more info.
138133

139-
If using perfect forward secrecy using `ECDHE`, Diffie-Hellman parameters are
140-
not required and a default ECDHE curve will be used. The `ecdhCurve` property
141-
can be used when creating a TLS Server to specify the list of names of supported
142-
curves to use, see [`tls.createServer()`][] for more info.
134+
DHE is disabled by default but can be enabled alongside ECDHE by setting the
135+
`dhparam` option to `'auto'`. Custom DHE parameters are also supported but
136+
discouraged in favor of automatically selected, well-known parameters.
143137

144138
Perfect forward secrecy was optional up to TLSv1.2. As of TLSv1.3, (EC)DHE is
145139
always used (with the exception of PSK-only connections).
@@ -1796,6 +1790,10 @@ argument.
17961790
<!-- YAML
17971791
added: v0.11.13
17981792
changes:
1793+
- version: REPLACEME
1794+
pr-url: https://github.com/nodejs/node/pull/46978
1795+
description: The `dhparam` option can now be set to `'auto'` to
1796+
enable DHE with appropriate well-known parameters.
17991797
- version: v12.12.0
18001798
pr-url: https://github.com/nodejs/node/pull/28973
18011799
description: Added `privateKeyIdentifier` and `privateKeyEngine` options
@@ -1880,13 +1878,10 @@ changes:
18801878
client certificate.
18811879
* `crl` {string|string\[]|Buffer|Buffer\[]} PEM formatted CRLs (Certificate
18821880
Revocation Lists).
1883-
* `dhparam` {string|Buffer} Diffie-Hellman parameters, required for non-ECDHE
1884-
[perfect forward secrecy][]. Use `openssl dhparam` to create the parameters.
1885-
The key length must be greater than or equal to 1024 bits or else an error
1886-
will be thrown. Although 1024 bits is permissible, use 2048 bits or larger
1887-
for stronger security. If omitted or invalid, the parameters are silently
1888-
discarded and DHE ciphers will not be available. [ECDHE][]-based [perfect
1889-
forward secrecy][] will still be available.
1881+
* `dhparam` {string|Buffer} `'auto'` or custom Diffie-Hellman parameters,
1882+
required for non-ECDHE [perfect forward secrecy][]. If omitted or invalid,
1883+
the parameters are silently discarded and DHE ciphers will not be available.
1884+
[ECDHE][]-based [perfect forward secrecy][] will still be available.
18901885
* `ecdhCurve` {string} A string describing a named curve or a colon separated
18911886
list of curve NIDs or names, for example `P-521:P-384:P-256`, to use for
18921887
ECDH key agreement. Set to `auto` to select the
@@ -1973,6 +1968,13 @@ A key is _required_ for ciphers that use certificates. Either `key` or
19731968
If the `ca` option is not given, then Node.js will default to using
19741969
[Mozilla's publicly trusted list of CAs][].
19751970

1971+
Custom DHE parameters are discouraged in favor of the new `dhparam: 'auto'`
1972+
option. When set to `'auto'`, well-known DHE parameters of sufficient strength
1973+
will be selected automatically. Otherwise, if necessary, `openssl dhparam` can
1974+
be used to create custom parameters. The key length must be greater than or
1975+
equal to 1024 bits or else an error will be thrown. Although 1024 bits is
1976+
permissible, use 2048 bits or larger for stronger security.
1977+
19761978
## `tls.createSecurePair([context][, isServer][, requestCert][, rejectUnauthorized][, options])`
19771979

19781980
<!-- YAML

lib/internal/tls/secure-context.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ function configSecureContext(context, options = kEmptyObject, name = 'options')
240240

241241
if (dhparam !== undefined && dhparam !== null) {
242242
validateKeyOrCertOption(`${name}.dhparam`, dhparam);
243-
const warning = context.setDHParam(dhparam);
243+
const warning = context.setDHParam(dhparam === 'auto' || dhparam);
244244
if (warning)
245245
process.emitWarning(warning, 'SecurityWarning');
246246
}

src/crypto/crypto_context.cc

+9
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,14 @@ void SecureContext::SetDHParam(const FunctionCallbackInfo<Value>& args) {
854854

855855
CHECK_GE(args.Length(), 1); // DH argument is mandatory
856856

857+
// If the user specified "auto" for dhparams, the JavaScript layer will pass
858+
// true to this function instead of the original string. Any other string
859+
// value will be interpreted as custom DH parameters below.
860+
if (args[0]->IsTrue()) {
861+
CHECK(SSL_CTX_set_dh_auto(sc->ctx_.get(), true));
862+
return;
863+
}
864+
857865
DHPointer dh;
858866
{
859867
BIOPointer bio(LoadBIO(env, args[0]));
@@ -864,6 +872,7 @@ void SecureContext::SetDHParam(const FunctionCallbackInfo<Value>& args) {
864872
}
865873

866874
// Invalid dhparam is silently discarded and DHE is no longer used.
875+
// TODO(tniessen): don't silently discard invalid dhparam.
867876
if (!dh)
868877
return;
869878

test/parallel/test-tls-client-getephemeralkeyinfo.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ if (!common.hasCrypto)
55
const fixtures = require('../common/fixtures');
66

77
const assert = require('assert');
8+
const { X509Certificate } = require('crypto');
89
const tls = require('tls');
910

1011
const key = fixtures.readKey('agent2-key.pem');
@@ -29,7 +30,20 @@ function test(size, type, name, cipher) {
2930

3031
if (name) options.ecdhCurve = name;
3132

32-
if (type === 'DH') options.dhparam = loadDHParam(size);
33+
if (type === 'DH') {
34+
if (size === 'auto') {
35+
options.dhparam = 'auto';
36+
// The DHE parameters selected by OpenSSL depend on the strength of the
37+
// certificate's key. For this test, we can assume that the modulus length
38+
// of the certificate's key is equal to the size of the DHE parameter, but
39+
// that is really only true for a few modulus lengths.
40+
({
41+
publicKey: { asymmetricKeyDetails: { modulusLength: size } }
42+
} = new X509Certificate(cert));
43+
} else {
44+
options.dhparam = loadDHParam(size);
45+
}
46+
}
3347

3448
const server = tls.createServer(options, common.mustCall((conn) => {
3549
assert.strictEqual(conn.getEphemeralKeyInfo(), null);
@@ -54,6 +68,7 @@ function test(size, type, name, cipher) {
5468
}
5569

5670
test(undefined, undefined, undefined, 'AES128-SHA256');
71+
test('auto', 'DH', undefined, 'DHE-RSA-AES128-GCM-SHA256');
5772
test(1024, 'DH', undefined, 'DHE-RSA-AES128-GCM-SHA256');
5873
test(2048, 'DH', undefined, 'DHE-RSA-AES128-GCM-SHA256');
5974
test(256, 'ECDH', 'prime256v1', 'ECDHE-RSA-AES128-GCM-SHA256');

test/parallel/test-tls-dhe.js

+39-11
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@ if (!common.opensslCli)
2929
common.skip('missing openssl-cli');
3030

3131
const assert = require('assert');
32+
const { X509Certificate } = require('crypto');
3233
const { once } = require('events');
3334
const tls = require('tls');
3435
const { execFile } = require('child_process');
3536
const fixtures = require('../common/fixtures');
3637

3738
const key = fixtures.readKey('agent2-key.pem');
3839
const cert = fixtures.readKey('agent2-cert.pem');
39-
const ciphers = 'DHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
40+
41+
// Prefer DHE over ECDHE when possible.
42+
const dheCipher = 'DHE-RSA-AES128-SHA256';
43+
const ecdheCipher = 'ECDHE-RSA-AES128-SHA256';
44+
const ciphers = `${dheCipher}:${ecdheCipher}`;
4045

4146
// Test will emit a warning because the DH parameter size is < 2048 bits
4247
common.expectWarning('SecurityWarning',
@@ -47,12 +52,12 @@ function loadDHParam(n) {
4752
return fixtures.readKey(keyname);
4853
}
4954

50-
function test(keylen, expectedCipher) {
55+
function test(dhparam, keylen, expectedCipher) {
5156
const options = {
52-
key: key,
53-
cert: cert,
54-
ciphers: ciphers,
55-
dhparam: loadDHParam(keylen),
57+
key,
58+
cert,
59+
ciphers,
60+
dhparam,
5661
maxVersion: 'TLSv1.2',
5762
};
5863

@@ -63,7 +68,7 @@ function test(keylen, expectedCipher) {
6368
'-cipher', ciphers];
6469

6570
execFile(common.opensslCli, args, common.mustSucceed((stdout) => {
66-
assert(keylen === 'error' ||
71+
assert(keylen === null ||
6772
stdout.includes(`Server Temp Key: DH, ${keylen} bits`));
6873
assert(stdout.includes(`Cipher : ${expectedCipher}`));
6974
server.close();
@@ -73,12 +78,35 @@ function test(keylen, expectedCipher) {
7378
return once(server, 'close');
7479
}
7580

81+
function testCustomParam(keylen, expectedCipher) {
82+
const dhparam = loadDHParam(keylen);
83+
if (keylen === 'error') keylen = null;
84+
return test(dhparam, keylen, expectedCipher);
85+
}
86+
7687
(async () => {
88+
// By default, DHE is disabled while ECDHE is enabled.
89+
for (const dhparam of [undefined, null]) {
90+
await test(dhparam, null, ecdheCipher);
91+
}
92+
93+
// The DHE parameters selected by OpenSSL depend on the strength of the
94+
// certificate's key. For this test, we can assume that the modulus length
95+
// of the certificate's key is equal to the size of the DHE parameter, but
96+
// that is really only true for a few modulus lengths.
97+
const {
98+
publicKey: { asymmetricKeyDetails: { modulusLength } }
99+
} = new X509Certificate(cert);
100+
await test('auto', modulusLength, dheCipher);
101+
77102
assert.throws(() => {
78-
test(512, 'DHE-RSA-AES128-SHA256');
103+
testCustomParam(512);
79104
}, /DH parameter is less than 1024 bits/);
80105

81-
await test(1024, 'DHE-RSA-AES128-SHA256');
82-
await test(2048, 'DHE-RSA-AES128-SHA256');
83-
await test('error', 'ECDHE-RSA-AES128-SHA256');
106+
// Custom DHE parameters are supported (but discouraged).
107+
await testCustomParam(1024, dheCipher);
108+
await testCustomParam(2048, dheCipher);
109+
110+
// Invalid DHE parameters are discarded. ECDHE remains enabled.
111+
await testCustomParam('error', ecdheCipher);
84112
})().then(common.mustCall());

0 commit comments

Comments
 (0)