Skip to content

Commit

Permalink
https: add client support for TLS keylog events
Browse files Browse the repository at this point in the history
The keylog event is implemented on TLS sockets, but client HTTPS uses
TLS sockets managed by an agent, so accessing the underlying socket
before the TLS handshake completed was not possible.  Note that server
HTTPS already supports the keylog event because it inherits from the TLS
server.

PR-URL: #30053
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
sam-github authored and targos committed Jan 13, 2020
1 parent 3b4b0de commit dc521b0
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 4 deletions.
25 changes: 25 additions & 0 deletions doc/api/https.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ changes:

See [`Session Resumption`][] for information about TLS session reuse.

#### Event: `'keylog'`
<!-- YAML
added: REPLACEME
-->

* `line` {Buffer} Line of ASCII text, in NSS `SSLKEYLOGFILE` format.
* `tlsSocket` {tls.TLSSocket} The `tls.TLSSocket` instance on which it was
generated.

The `keylog` event is emitted when key material is generated or received by a
connection managed by this agent (typically before handshake has completed, but
not necessarily). This keying material can be stored for debugging, as it
allows captured TLS traffic to be decrypted. It may be emitted multiple times
for each socket.

A typical use case is to append received lines to a common text file, which is
later used by software (such as Wireshark) to decrypt the traffic:

```js
// ...
https.globalAgent.on('keylog', (line, tlsSocket) => {
fs.appendFileSync('/tmp/ssl-keys.log', line, { mode: 0o600 });
});
```

## Class: `https.Server`
<!-- YAML
added: v0.3.4
Expand Down
25 changes: 24 additions & 1 deletion lib/_http_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const net = require('net');
const EventEmitter = require('events');
const debug = require('internal/util/debuglog').debuglog('http');
const { async_id_symbol } = require('internal/async_hooks').symbols;

const kOnKeylog = Symbol('onkeylog');
// New Agent code.

// The largest departure from the previous implementation is that
Expand Down Expand Up @@ -120,10 +120,29 @@ function Agent(options) {
}
}
});

// Don't emit keylog events unless there is a listener for them.
this.on('newListener', maybeEnableKeylog);
}
Object.setPrototypeOf(Agent.prototype, EventEmitter.prototype);
Object.setPrototypeOf(Agent, EventEmitter);

function maybeEnableKeylog(eventName) {
if (eventName === 'keylog') {
this.removeListener('newListener', maybeEnableKeylog);
// Future sockets will listen on keylog at creation.
const agent = this;
this[kOnKeylog] = function onkeylog(keylog) {
agent.emit('keylog', keylog, this);
};
// Existing sockets will start listening on keylog now.
const sockets = Object.values(this.sockets);
for (let i = 0; i < sockets.length; i++) {
sockets[i].on('keylog', this[kOnKeylog]);
}
}
}

Agent.defaultMaxSockets = Infinity;

Agent.prototype.createConnection = net.createConnection;
Expand Down Expand Up @@ -297,6 +316,10 @@ function installListeners(agent, s, options) {
s.removeListener('agentRemove', onRemove);
}
s.on('agentRemove', onRemove);

if (agent[kOnKeylog]) {
s.on('keylog', agent[kOnKeylog]);
}
}

Agent.prototype.removeSocket = function removeSocket(s, options) {
Expand Down
44 changes: 44 additions & 0 deletions test/parallel/test-https-agent-keylog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const https = require('https');
const fixtures = require('../common/fixtures');

const server = https.createServer({
key: fixtures.readKey('agent2-key.pem'),
cert: fixtures.readKey('agent2-cert.pem'),
// Amount of keylog events depends on negotiated protocol
// version, so force a specific one:
minVersion: 'TLSv1.3',
maxVersion: 'TLSv1.3',
}, (req, res) => {
res.end('bye');
}).listen(() => {
https.get({
port: server.address().port,
rejectUnauthorized: false,
}, (res) => {
res.resume();
res.on('end', () => {
// Trigger TLS connection reuse
https.get({
port: server.address().port,
rejectUnauthorized: false,
}, (res) => {
server.close();
res.resume();
});
});
});
});

const verifyKeylog = (line, tlsSocket) => {
assert(Buffer.isBuffer(line));
assert.strictEqual(tlsSocket.encrypted, true);
};
server.on('keylog', common.mustCall(verifyKeylog, 10));
https.globalAgent.on('keylog', common.mustCall(verifyKeylog, 10));
10 changes: 7 additions & 3 deletions test/parallel/test-tls-keylog-tlsv13.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ const server = tls.createServer({
rejectUnauthorized: false,
});

const verifyBuffer = (line) => assert(Buffer.isBuffer(line));
server.on('keylog', common.mustCall(verifyBuffer, 5));
client.on('keylog', common.mustCall(verifyBuffer, 5));
server.on('keylog', common.mustCall((line, tlsSocket) => {
assert(Buffer.isBuffer(line));
assert.strictEqual(tlsSocket.encrypted, true);
}, 5));
client.on('keylog', common.mustCall((line) => {
assert(Buffer.isBuffer(line));
}, 5));

client.once('secureConnect', () => {
server.close();
Expand Down

0 comments on commit dc521b0

Please sign in to comment.