diff --git a/doc/api/cli.md b/doc/api/cli.md index e88b16b1ead80c..265139465363ac 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -679,6 +679,15 @@ added: v4.0.0 Specify an alternative default TLS cipher list. Requires Node.js to be built with crypto support (default). +### `--tls-keylog=file` +<!-- YAML +added: REPLACEME +--> + +Log TLS key material to a file. The key material is in NSS `SSLKEYLOGFILE` +format and can be used by software (such as Wireshark) to decrypt the TLS +traffic. + ### `--tls-max-v1.2` <!-- YAML added: v12.0.0 @@ -1073,6 +1082,7 @@ Node.js options that are allowed are: * `--throw-deprecation` * `--title` * `--tls-cipher-list` +* `--tls-keylog` * `--tls-max-v1.2` * `--tls-max-v1.3` * `--tls-min-v1.0` diff --git a/doc/node.1 b/doc/node.1 index 4b653e702d6500..e3628034e832e5 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -302,6 +302,11 @@ 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-keylog Ns = Ns Ar file +Log TLS key material to a file. The key material is in NSS SSLKEYLOGFILE +format and can be used by software (such as Wireshark) to decrypt the TLS +traffic. +. .It Fl -tls-max-v1.2 Set default maxVersion to 'TLSv1.2'. Use to disable support for TLSv1.3. . diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index 778afa732869f3..69fc05475896a1 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -60,6 +60,8 @@ const { const { getOptionValue } = require('internal/options'); const { validateString } = require('internal/validators'); const traceTls = getOptionValue('--trace-tls'); +const tlsKeylog = getOptionValue('--tls-keylog'); +const { appendFile } = require('fs'); const kConnectOptions = Symbol('connect-options'); const kDisableRenegotiation = Symbol('disable-renegotiation'); const kErrorEmitted = Symbol('error-emitted'); @@ -560,6 +562,8 @@ TLSSocket.prototype._destroySSL = function _destroySSL() { }; // Constructor guts, arbitrarily factored out. +let warnOnTlsKeylog = true; +let warnOnTlsKeylogError = true; TLSSocket.prototype._init = function(socket, wrap) { const options = this._tlsOptions; const ssl = this._handle; @@ -643,6 +647,24 @@ TLSSocket.prototype._init = function(socket, wrap) { } } + if (tlsKeylog) { + if (warnOnTlsKeylog) { + warnOnTlsKeylog = false; + process.emitWarning('Using --tls-keylog makes TLS connections insecure ' + + 'by writing secret key material to file ' + tlsKeylog); + ssl.enableKeylogCallback(); + this.on('keylog', (line) => { + appendFile(tlsKeylog, line, { mode: 0o600 }, (err) => { + if (err && warnOnTlsKeylogError) { + warnOnTlsKeylogError = false; + process.emitWarning('Failed to write TLS keylog (this warning ' + + 'will not be repeated): ' + err); + } + }); + }); + } + } + ssl.onerror = onerror; // If custom SNICallback was given, or if diff --git a/src/node_options.cc b/src/node_options.cc index 85256a7e0a8e80..0bc6730156ce12 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -506,6 +506,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--napi-modules", "", NoOp{}, kAllowedInEnvironment); + AddOption("--tls-keylog", + "log TLS decryption keys to named file for traffic analysis", + &EnvironmentOptions::tls_keylog, kAllowedInEnvironment); + AddOption("--tls-min-v1.0", "set default TLS minimum to TLSv1.0 (default: TLSv1.2)", &EnvironmentOptions::tls_min_v1_0, diff --git a/src/node_options.h b/src/node_options.h index 30a976f48d5b4e..ce0cee5fe56784 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -161,6 +161,7 @@ class EnvironmentOptions : public Options { bool tls_min_v1_3 = false; bool tls_max_v1_2 = false; bool tls_max_v1_3 = false; + std::string tls_keylog; std::vector<std::string> preload_modules; diff --git a/test/parallel/test-tls-enable-keylog-cli.js b/test/parallel/test-tls-enable-keylog-cli.js new file mode 100644 index 00000000000000..5d05069b15f87c --- /dev/null +++ b/test/parallel/test-tls-enable-keylog-cli.js @@ -0,0 +1,57 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); + +// Test --tls-keylog CLI flag. + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const { fork } = require('child_process'); + +if (process.argv[2] === 'test') + return test(); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const file = path.resolve(tmpdir.path, 'keylog.log'); + +const child = fork(__filename, ['test'], { + execArgv: ['--tls-keylog=' + file] +}); + +child.on('close', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + const log = fs.readFileSync(file, 'utf8'); + assert(/SECRET/.test(log)); +})); + +function test() { + const { + connect, keys + } = require(fixtures.path('tls-connect')); + + connect({ + client: { + checkServerIdentity: (servername, cert) => { }, + ca: `${keys.agent1.cert}\n${keys.agent6.ca}`, + }, + server: { + cert: keys.agent6.cert, + key: keys.agent6.key + }, + }, common.mustCall((err, pair, cleanup) => { + if (pair.server.err) { + console.trace('server', pair.server.err); + } + if (pair.client.err) { + console.trace('client', pair.client.err); + } + assert.ifError(pair.server.err); + assert.ifError(pair.client.err); + + return cleanup(); + })); +}