Skip to content

Commit dc521b0

Browse files
sam-githubtargos
authored andcommitted
https: add client support for TLS keylog events
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 <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 3b4b0de commit dc521b0

File tree

4 files changed

+100
-4
lines changed

4 files changed

+100
-4
lines changed

doc/api/https.md

+25
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,31 @@ changes:
4545

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

48+
#### Event: `'keylog'`
49+
<!-- YAML
50+
added: REPLACEME
51+
-->
52+
53+
* `line` {Buffer} Line of ASCII text, in NSS `SSLKEYLOGFILE` format.
54+
* `tlsSocket` {tls.TLSSocket} The `tls.TLSSocket` instance on which it was
55+
generated.
56+
57+
The `keylog` event is emitted when key material is generated or received by a
58+
connection managed by this agent (typically before handshake has completed, but
59+
not necessarily). This keying material can be stored for debugging, as it
60+
allows captured TLS traffic to be decrypted. It may be emitted multiple times
61+
for each socket.
62+
63+
A typical use case is to append received lines to a common text file, which is
64+
later used by software (such as Wireshark) to decrypt the traffic:
65+
66+
```js
67+
// ...
68+
https.globalAgent.on('keylog', (line, tlsSocket) => {
69+
fs.appendFileSync('/tmp/ssl-keys.log', line, { mode: 0o600 });
70+
});
71+
```
72+
4873
## Class: `https.Server`
4974
<!-- YAML
5075
added: v0.3.4

lib/_http_agent.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const net = require('net');
2727
const EventEmitter = require('events');
2828
const debug = require('internal/util/debuglog').debuglog('http');
2929
const { async_id_symbol } = require('internal/async_hooks').symbols;
30-
30+
const kOnKeylog = Symbol('onkeylog');
3131
// New Agent code.
3232

3333
// The largest departure from the previous implementation is that
@@ -120,10 +120,29 @@ function Agent(options) {
120120
}
121121
}
122122
});
123+
124+
// Don't emit keylog events unless there is a listener for them.
125+
this.on('newListener', maybeEnableKeylog);
123126
}
124127
Object.setPrototypeOf(Agent.prototype, EventEmitter.prototype);
125128
Object.setPrototypeOf(Agent, EventEmitter);
126129

130+
function maybeEnableKeylog(eventName) {
131+
if (eventName === 'keylog') {
132+
this.removeListener('newListener', maybeEnableKeylog);
133+
// Future sockets will listen on keylog at creation.
134+
const agent = this;
135+
this[kOnKeylog] = function onkeylog(keylog) {
136+
agent.emit('keylog', keylog, this);
137+
};
138+
// Existing sockets will start listening on keylog now.
139+
const sockets = Object.values(this.sockets);
140+
for (let i = 0; i < sockets.length; i++) {
141+
sockets[i].on('keylog', this[kOnKeylog]);
142+
}
143+
}
144+
}
145+
127146
Agent.defaultMaxSockets = Infinity;
128147

129148
Agent.prototype.createConnection = net.createConnection;
@@ -297,6 +316,10 @@ function installListeners(agent, s, options) {
297316
s.removeListener('agentRemove', onRemove);
298317
}
299318
s.on('agentRemove', onRemove);
319+
320+
if (agent[kOnKeylog]) {
321+
s.on('keylog', agent[kOnKeylog]);
322+
}
300323
}
301324

302325
Agent.prototype.removeSocket = function removeSocket(s, options) {
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const https = require('https');
9+
const fixtures = require('../common/fixtures');
10+
11+
const server = https.createServer({
12+
key: fixtures.readKey('agent2-key.pem'),
13+
cert: fixtures.readKey('agent2-cert.pem'),
14+
// Amount of keylog events depends on negotiated protocol
15+
// version, so force a specific one:
16+
minVersion: 'TLSv1.3',
17+
maxVersion: 'TLSv1.3',
18+
}, (req, res) => {
19+
res.end('bye');
20+
}).listen(() => {
21+
https.get({
22+
port: server.address().port,
23+
rejectUnauthorized: false,
24+
}, (res) => {
25+
res.resume();
26+
res.on('end', () => {
27+
// Trigger TLS connection reuse
28+
https.get({
29+
port: server.address().port,
30+
rejectUnauthorized: false,
31+
}, (res) => {
32+
server.close();
33+
res.resume();
34+
});
35+
});
36+
});
37+
});
38+
39+
const verifyKeylog = (line, tlsSocket) => {
40+
assert(Buffer.isBuffer(line));
41+
assert.strictEqual(tlsSocket.encrypted, true);
42+
};
43+
server.on('keylog', common.mustCall(verifyKeylog, 10));
44+
https.globalAgent.on('keylog', common.mustCall(verifyKeylog, 10));

test/parallel/test-tls-keylog-tlsv13.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ const server = tls.createServer({
2121
rejectUnauthorized: false,
2222
});
2323

24-
const verifyBuffer = (line) => assert(Buffer.isBuffer(line));
25-
server.on('keylog', common.mustCall(verifyBuffer, 5));
26-
client.on('keylog', common.mustCall(verifyBuffer, 5));
24+
server.on('keylog', common.mustCall((line, tlsSocket) => {
25+
assert(Buffer.isBuffer(line));
26+
assert.strictEqual(tlsSocket.encrypted, true);
27+
}, 5));
28+
client.on('keylog', common.mustCall((line) => {
29+
assert(Buffer.isBuffer(line));
30+
}, 5));
2731

2832
client.once('secureConnect', () => {
2933
server.close();

0 commit comments

Comments
 (0)