Skip to content

Commit 80efb80

Browse files
committed
tls: cli option to enable TLS key logging to file
Debugging HTTPS or TLS connections from a Node.js app with (for example) Wireshark is unreasonably difficult without the ability to get the TLS key log. In theory, the application can be modified to use the `'keylog'` event directly, but for complex apps, or apps that define there own HTTPS Agent (like npm), this is unreasonably difficult. Use of the option triggers a warning to be emitted so the user is clearly notified of what is happening and its effect. PR-URL: nodejs#30055 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Daniel Bevenius <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent f4ea918 commit 80efb80

File tree

6 files changed

+99
-0
lines changed

6 files changed

+99
-0
lines changed

doc/api/cli.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,15 @@ added: v4.0.0
679679
Specify an alternative default TLS cipher list. Requires Node.js to be built
680680
with crypto support (default).
681681

682+
### `--tls-keylog=file`
683+
<!-- YAML
684+
added: REPLACEME
685+
-->
686+
687+
Log TLS key material to a file. The key material is in NSS `SSLKEYLOGFILE`
688+
format and can be used by software (such as Wireshark) to decrypt the TLS
689+
traffic.
690+
682691
### `--tls-max-v1.2`
683692
<!-- YAML
684693
added: v12.0.0
@@ -1073,6 +1082,7 @@ Node.js options that are allowed are:
10731082
* `--throw-deprecation`
10741083
* `--title`
10751084
* `--tls-cipher-list`
1085+
* `--tls-keylog`
10761086
* `--tls-max-v1.2`
10771087
* `--tls-max-v1.3`
10781088
* `--tls-min-v1.0`

doc/node.1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,11 @@ Specify process.title on startup.
302302
Specify an alternative default TLS cipher list.
303303
Requires Node.js to be built with crypto support. (Default)
304304
.
305+
.It Fl -tls-keylog Ns = Ns Ar file
306+
Log TLS key material to a file. The key material is in NSS SSLKEYLOGFILE
307+
format and can be used by software (such as Wireshark) to decrypt the TLS
308+
traffic.
309+
.
305310
.It Fl -tls-max-v1.2
306311
Set default maxVersion to 'TLSv1.2'. Use to disable support for TLSv1.3.
307312
.

lib/_tls_wrap.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const {
6060
const { getOptionValue } = require('internal/options');
6161
const { validateString } = require('internal/validators');
6262
const traceTls = getOptionValue('--trace-tls');
63+
const tlsKeylog = getOptionValue('--tls-keylog');
64+
const { appendFile } = require('fs');
6365
const kConnectOptions = Symbol('connect-options');
6466
const kDisableRenegotiation = Symbol('disable-renegotiation');
6567
const kErrorEmitted = Symbol('error-emitted');
@@ -560,6 +562,8 @@ TLSSocket.prototype._destroySSL = function _destroySSL() {
560562
};
561563

562564
// Constructor guts, arbitrarily factored out.
565+
let warnOnTlsKeylog = true;
566+
let warnOnTlsKeylogError = true;
563567
TLSSocket.prototype._init = function(socket, wrap) {
564568
const options = this._tlsOptions;
565569
const ssl = this._handle;
@@ -643,6 +647,24 @@ TLSSocket.prototype._init = function(socket, wrap) {
643647
}
644648
}
645649

650+
if (tlsKeylog) {
651+
if (warnOnTlsKeylog) {
652+
warnOnTlsKeylog = false;
653+
process.emitWarning('Using --tls-keylog makes TLS connections insecure ' +
654+
'by writing secret key material to file ' + tlsKeylog);
655+
ssl.enableKeylogCallback();
656+
this.on('keylog', (line) => {
657+
appendFile(tlsKeylog, line, { mode: 0o600 }, (err) => {
658+
if (err && warnOnTlsKeylogError) {
659+
warnOnTlsKeylogError = false;
660+
process.emitWarning('Failed to write TLS keylog (this warning ' +
661+
'will not be repeated): ' + err);
662+
}
663+
});
664+
});
665+
}
666+
}
667+
646668
ssl.onerror = onerror;
647669

648670
// If custom SNICallback was given, or if

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
506506

507507
AddOption("--napi-modules", "", NoOp{}, kAllowedInEnvironment);
508508

509+
AddOption("--tls-keylog",
510+
"log TLS decryption keys to named file for traffic analysis",
511+
&EnvironmentOptions::tls_keylog, kAllowedInEnvironment);
512+
509513
AddOption("--tls-min-v1.0",
510514
"set default TLS minimum to TLSv1.0 (default: TLSv1.2)",
511515
&EnvironmentOptions::tls_min_v1_0,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ class EnvironmentOptions : public Options {
161161
bool tls_min_v1_3 = false;
162162
bool tls_max_v1_2 = false;
163163
bool tls_max_v1_3 = false;
164+
std::string tls_keylog;
164165

165166
std::vector<std::string> preload_modules;
166167

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto) common.skip('missing crypto');
4+
const fixtures = require('../common/fixtures');
5+
6+
// Test --tls-keylog CLI flag.
7+
8+
const assert = require('assert');
9+
const path = require('path');
10+
const fs = require('fs');
11+
const { fork } = require('child_process');
12+
13+
if (process.argv[2] === 'test')
14+
return test();
15+
16+
const tmpdir = require('../common/tmpdir');
17+
tmpdir.refresh();
18+
const file = path.resolve(tmpdir.path, 'keylog.log');
19+
20+
const child = fork(__filename, ['test'], {
21+
execArgv: ['--tls-keylog=' + file]
22+
});
23+
24+
child.on('close', common.mustCall((code, signal) => {
25+
assert.strictEqual(code, 0);
26+
assert.strictEqual(signal, null);
27+
const log = fs.readFileSync(file, 'utf8');
28+
assert(/SECRET/.test(log));
29+
}));
30+
31+
function test() {
32+
const {
33+
connect, keys
34+
} = require(fixtures.path('tls-connect'));
35+
36+
connect({
37+
client: {
38+
checkServerIdentity: (servername, cert) => { },
39+
ca: `${keys.agent1.cert}\n${keys.agent6.ca}`,
40+
},
41+
server: {
42+
cert: keys.agent6.cert,
43+
key: keys.agent6.key
44+
},
45+
}, common.mustCall((err, pair, cleanup) => {
46+
if (pair.server.err) {
47+
console.trace('server', pair.server.err);
48+
}
49+
if (pair.client.err) {
50+
console.trace('client', pair.client.err);
51+
}
52+
assert.ifError(pair.server.err);
53+
assert.ifError(pair.client.err);
54+
55+
return cleanup();
56+
}));
57+
}

0 commit comments

Comments
 (0)