Skip to content

Commit f82b4ae

Browse files
committed
tls: expose keylog event on TLSSocket
Exposes SSL_CTX_set_keylog_callback in the form of a `keylog` event that is emitted on clients and servers. This enables easy debugging of TLS connections with i.e. Wireshark, which is a long-requested feature. PR-URL: nodejs#27654 Refs: nodejs#2363 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Sam Roberts <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent 84eec80 commit f82b4ae

File tree

8 files changed

+145
-0
lines changed

8 files changed

+145
-0
lines changed

doc/api/tls.md

+49
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,34 @@ added: v0.3.2
299299
The `tls.Server` class is a subclass of `net.Server` that accepts encrypted
300300
connections using TLS or SSL.
301301

302+
### Event: 'keylog'
303+
<!-- YAML
304+
added: REPLACEME
305+
-->
306+
307+
* `line` {Buffer} Line of ASCII text, in NSS `SSLKEYLOGFILE` format.
308+
* `tlsSocket` {tls.TLSSocket} The `tls.TLSSocket` instance on which it was
309+
generated.
310+
311+
The `keylog` event is emitted when key material is generated or received by
312+
a connection to this server (typically before handshake has completed, but not
313+
necessarily). This keying material can be stored for debugging, as it allows
314+
captured TLS traffic to be decrypted. It may be emitted multiple times for
315+
each socket.
316+
317+
A typical use case is to append received lines to a common text file, which
318+
is later used by software (such as Wireshark) to decrypt the traffic:
319+
320+
```js
321+
const logFile = fs.createWriteStream('/tmp/ssl-keys.log', { flags: 'a' });
322+
// ...
323+
server.on('keylog', (line, tlsSocket) => {
324+
if (tlsSocket.remoteAddress !== '...')
325+
return; // Only log keys for a particular IP
326+
logFile.write(line);
327+
});
328+
```
329+
302330
### Event: 'newSession'
303331
<!-- YAML
304332
added: v0.9.2
@@ -573,6 +601,27 @@ changes:
573601

574602
Construct a new `tls.TLSSocket` object from an existing TCP socket.
575603

604+
### Event: 'keylog'
605+
<!-- YAML
606+
added: REPLACEME
607+
-->
608+
609+
* `line` {Buffer} Line of ASCII text, in NSS `SSLKEYLOGFILE` format.
610+
611+
The `keylog` event is emitted on a client `tls.TLSSocket` when key material
612+
is generated or received by the socket. This keying material can be stored
613+
for debugging, as it allows captured TLS traffic to be decrypted. It may
614+
be emitted multiple times, before or after the handshake completes.
615+
616+
A typical use case is to append received lines to a common text file, which
617+
is later used by software (such as Wireshark) to decrypt the traffic:
618+
619+
```js
620+
const logFile = fs.createWriteStream('/tmp/ssl-keys.log', { flags: 'a' });
621+
// ...
622+
tlsSocket.on('keylog', (line) => logFile.write(line));
623+
```
624+
576625
### Event: 'OCSPResponse'
577626
<!-- YAML
578627
added: v0.11.13

lib/_tls_wrap.js

+30
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,18 @@ function onnewsession(key, session) {
243243
}
244244

245245

246+
function onkeylogclient(line) {
247+
debug('client onkeylog');
248+
this[owner_symbol].emit('keylog', line);
249+
}
250+
251+
function onkeylog(line) {
252+
debug('server onkeylog');
253+
const owner = this[owner_symbol];
254+
if (owner.server)
255+
owner.server.emit('keylog', line, owner);
256+
}
257+
246258
function onocspresponse(resp) {
247259
this[owner_symbol].emit('OCSPResponse', resp);
248260
}
@@ -492,6 +504,7 @@ TLSSocket.prototype._init = function(socket, wrap) {
492504
ssl.onclienthello = loadSession;
493505
ssl.oncertcb = loadSNI;
494506
ssl.onnewsession = onnewsession;
507+
ssl.onkeylog = onkeylog;
495508
ssl.lastHandshakeTime = 0;
496509
ssl.handshakes = 0;
497510

@@ -500,6 +513,8 @@ TLSSocket.prototype._init = function(socket, wrap) {
500513
this.server.listenerCount('newSession') > 0) {
501514
ssl.enableSessionCallbacks();
502515
}
516+
if (this.server.listenerCount('keylog') > 0)
517+
ssl.enableKeylogCallback();
503518
if (this.server.listenerCount('OCSPRequest') > 0)
504519
ssl.enableCertCb();
505520
}
@@ -510,6 +525,21 @@ TLSSocket.prototype._init = function(socket, wrap) {
510525

511526
if (options.session)
512527
ssl.setSession(options.session);
528+
529+
ssl.onkeylog = onkeylogclient;
530+
531+
// Only call .onkeylog if there is a keylog listener.
532+
this.on('newListener', keylogNewListener);
533+
534+
function keylogNewListener(event) {
535+
if (event !== 'keylog')
536+
return;
537+
538+
ssl.enableKeylogCallback();
539+
540+
// Remove this listener since it's no longer needed.
541+
this.removeListener('newListener', keylogNewListener);
542+
}
513543
}
514544

515545
ssl.onerror = onerror;

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ struct PackageConfig {
226226
V(onhandshakedone_string, "onhandshakedone") \
227227
V(onhandshakestart_string, "onhandshakestart") \
228228
V(onheaders_string, "onheaders") \
229+
V(onkeylog_string, "onkeylog") \
229230
V(onmessage_string, "onmessage") \
230231
V(onnewsession_string, "onnewsession") \
231232
V(onocspresponse_string, "onocspresponse") \

src/node_crypto.cc

+17
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ template SSL_SESSION* SSLWrap<TLSWrap>::GetSessionCallback(
132132
int* copy);
133133
template int SSLWrap<TLSWrap>::NewSessionCallback(SSL* s,
134134
SSL_SESSION* sess);
135+
template void SSLWrap<TLSWrap>::KeylogCallback(const SSL* s,
136+
const char* line);
135137
template void SSLWrap<TLSWrap>::OnClientHello(
136138
void* arg,
137139
const ClientHelloParser::ClientHello& hello);
@@ -1482,6 +1484,21 @@ int SSLWrap<Base>::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
14821484
}
14831485

14841486

1487+
template <class Base>
1488+
void SSLWrap<Base>::KeylogCallback(const SSL* s, const char* line) {
1489+
Base* w = static_cast<Base*>(SSL_get_app_data(s));
1490+
Environment* env = w->ssl_env();
1491+
HandleScope handle_scope(env->isolate());
1492+
Context::Scope context_scope(env->context());
1493+
1494+
const size_t size = strlen(line);
1495+
Local<Value> line_bf = Buffer::Copy(env, line, 1 + size).ToLocalChecked();
1496+
char* data = Buffer::Data(line_bf);
1497+
data[size] = '\n';
1498+
w->MakeCallback(env->onkeylog_string(), 1, &line_bf);
1499+
}
1500+
1501+
14851502
template <class Base>
14861503
void SSLWrap<Base>::OnClientHello(void* arg,
14871504
const ClientHelloParser::ClientHello& hello) {

src/node_crypto.h

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ class SSLWrap {
267267
int* copy);
268268
#endif
269269
static int NewSessionCallback(SSL* s, SSL_SESSION* sess);
270+
static void KeylogCallback(const SSL* s, const char* line);
270271
static void OnClientHello(void* arg,
271272
const ClientHelloParser::ClientHello& hello);
272273

src/tls_wrap.cc

+11
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,16 @@ void TLSWrap::EnableSessionCallbacks(
836836
wrap);
837837
}
838838

839+
void TLSWrap::EnableKeylogCallback(
840+
const FunctionCallbackInfo<Value>& args) {
841+
TLSWrap* wrap;
842+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
843+
CHECK_NOT_NULL(wrap->sc_);
844+
#if OPENSSL_VERSION_NUMBER >= 0x1010100fL
845+
SSL_CTX_set_keylog_callback(wrap->sc_->ctx_.get(),
846+
SSLWrap<TLSWrap>::KeylogCallback);
847+
#endif
848+
}
839849

840850
void TLSWrap::DestroySSL(const FunctionCallbackInfo<Value>& args) {
841851
TLSWrap* wrap;
@@ -1001,6 +1011,7 @@ void TLSWrap::Initialize(Local<Object> target,
10011011
env->SetProtoMethod(t, "start", Start);
10021012
env->SetProtoMethod(t, "setVerifyMode", SetVerifyMode);
10031013
env->SetProtoMethod(t, "enableSessionCallbacks", EnableSessionCallbacks);
1014+
env->SetProtoMethod(t, "enableKeylogCallback", EnableKeylogCallback);
10041015
env->SetProtoMethod(t, "destroySSL", DestroySSL);
10051016
env->SetProtoMethod(t, "enableCertCb", EnableCertCb);
10061017

src/tls_wrap.h

+2
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ class TLSWrap : public AsyncWrap,
154154
static void SetVerifyMode(const v8::FunctionCallbackInfo<v8::Value>& args);
155155
static void EnableSessionCallbacks(
156156
const v8::FunctionCallbackInfo<v8::Value>& args);
157+
static void EnableKeylogCallback(
158+
const v8::FunctionCallbackInfo<v8::Value>& args);
157159
static void EnableTrace(const v8::FunctionCallbackInfo<v8::Value>& args);
158160
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
159161
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
if (/^(0\.|1\.0\.|1\.1\.0)/.test(process.versions.openssl))
7+
common.skip('keylog support not available');
8+
9+
const assert = require('assert');
10+
const tls = require('tls');
11+
const fixtures = require('../common/fixtures');
12+
13+
const server = tls.createServer({
14+
key: fixtures.readSync('/keys/agent2-key.pem'),
15+
cert: fixtures.readSync('/keys/agent2-cert.pem'),
16+
// Amount of keylog events depends on negotiated protocol
17+
// version, so force a specific one:
18+
minVersion: 'TLSv1.2',
19+
maxVersion: 'TLSv1.2',
20+
}).listen(() => {
21+
const client = tls.connect({
22+
port: server.address().port,
23+
rejectUnauthorized: false,
24+
});
25+
26+
const verifyBuffer = (line) => assert(Buffer.isBuffer(line));
27+
server.on('keylog', common.mustCall(verifyBuffer, 1));
28+
client.on('keylog', common.mustCall(verifyBuffer, 1));
29+
30+
client.once('secureConnect', () => {
31+
server.close();
32+
client.end();
33+
});
34+
});

0 commit comments

Comments
 (0)