Skip to content

Commit f25b00a

Browse files
sam-githubMylesBorins
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 966404f commit f25b00a

File tree

4 files changed

+100
-3
lines changed

4 files changed

+100
-3
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
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const {
3232
ERR_INVALID_ARG_TYPE,
3333
},
3434
} = require('internal/errors');
35+
const kOnKeylog = Symbol('onkeylog');
3536
// New Agent code.
3637

3738
// The largest departure from the previous implementation is that
@@ -124,10 +125,29 @@ function Agent(options) {
124125
}
125126
}
126127
});
128+
129+
// Don't emit keylog events unless there is a listener for them.
130+
this.on('newListener', maybeEnableKeylog);
127131
}
128132
Object.setPrototypeOf(Agent.prototype, EventEmitter.prototype);
129133
Object.setPrototypeOf(Agent, EventEmitter);
130134

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

133153
Agent.prototype.createConnection = net.createConnection;
@@ -306,6 +326,10 @@ function installListeners(agent, s, options) {
306326
s.removeListener('agentRemove', onRemove);
307327
}
308328
s.on('agentRemove', onRemove);
329+
330+
if (agent[kOnKeylog]) {
331+
s.on('keylog', agent[kOnKeylog]);
332+
}
309333
}
310334

311335
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)