Skip to content

Commit eb1f4e5

Browse files
mildsunrisetargos
authored andcommitted
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: #27654 Refs: #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 700459e commit eb1f4e5

File tree

8 files changed

+143
-1
lines changed

8 files changed

+143
-1
lines changed

doc/api/tls.md

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

337+
### Event: 'keylog'
338+
<!-- YAML
339+
added: REPLACEME
340+
-->
341+
342+
* `line` {Buffer} Line of ASCII text, in NSS `SSLKEYLOGFILE` format.
343+
* `tlsSocket` {tls.TLSSocket} The `tls.TLSSocket` instance on which it was
344+
generated.
345+
346+
The `keylog` event is emitted when key material is generated or received by
347+
a connection to this server (typically before handshake has completed, but not
348+
necessarily). This keying material can be stored for debugging, as it allows
349+
captured TLS traffic to be decrypted. It may be emitted multiple times for
350+
each socket.
351+
352+
A typical use case is to append received lines to a common text file, which
353+
is later used by software (such as Wireshark) to decrypt the traffic:
354+
355+
```js
356+
const logFile = fs.createWriteStream('/tmp/ssl-keys.log', { flags: 'a' });
357+
// ...
358+
server.on('keylog', (line, tlsSocket) => {
359+
if (tlsSocket.remoteAddress !== '...')
360+
return; // Only log keys for a particular IP
361+
logFile.write(line);
362+
});
363+
```
364+
337365
### Event: 'newSession'
338366
<!-- YAML
339367
added: v0.9.2
@@ -624,6 +652,27 @@ changes:
624652

625653
Construct a new `tls.TLSSocket` object from an existing TCP socket.
626654

655+
### Event: 'keylog'
656+
<!-- YAML
657+
added: REPLACEME
658+
-->
659+
660+
* `line` {Buffer} Line of ASCII text, in NSS `SSLKEYLOGFILE` format.
661+
662+
The `keylog` event is emitted on a client `tls.TLSSocket` when key material
663+
is generated or received by the socket. This keying material can be stored
664+
for debugging, as it allows captured TLS traffic to be decrypted. It may
665+
be emitted multiple times, before or after the handshake completes.
666+
667+
A typical use case is to append received lines to a common text file, which
668+
is later used by software (such as Wireshark) to decrypt the traffic:
669+
670+
```js
671+
const logFile = fs.createWriteStream('/tmp/ssl-keys.log', { flags: 'a' });
672+
// ...
673+
tlsSocket.on('keylog', (line) => logFile.write(line));
674+
```
675+
627676
### Event: 'OCSPResponse'
628677
<!-- YAML
629678
added: v0.11.13

lib/_tls_wrap.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,18 @@ function onnewsession(sessionId, session) {
286286
}
287287

288288

289+
function onkeylogclient(line) {
290+
debug('client onkeylog');
291+
this[owner_symbol].emit('keylog', line);
292+
}
293+
294+
function onkeylog(line) {
295+
debug('server onkeylog');
296+
const owner = this[owner_symbol];
297+
if (owner.server)
298+
owner.server.emit('keylog', line, owner);
299+
}
300+
289301
function onocspresponse(resp) {
290302
debug('client onocspresponse');
291303
this[owner_symbol].emit('OCSPResponse', resp);
@@ -571,6 +583,7 @@ TLSSocket.prototype._init = function(socket, wrap) {
571583
ssl.onclienthello = loadSession;
572584
ssl.oncertcb = loadSNI;
573585
ssl.onnewsession = onnewsession;
586+
ssl.onkeylog = onkeylog;
574587
ssl.lastHandshakeTime = 0;
575588
ssl.handshakes = 0;
576589

@@ -580,6 +593,8 @@ TLSSocket.prototype._init = function(socket, wrap) {
580593
// Also starts the client hello parser as a side effect.
581594
ssl.enableSessionCallbacks();
582595
}
596+
if (this.server.listenerCount('keylog') > 0)
597+
ssl.enableKeylogCallback();
583598
if (this.server.listenerCount('OCSPRequest') > 0)
584599
ssl.enableCertCb();
585600
}
@@ -605,9 +620,24 @@ TLSSocket.prototype._init = function(socket, wrap) {
605620

606621
ssl.enableSessionCallbacks();
607622

608-
// Remover this listener since its no longer needed.
623+
// Remove this listener since it's no longer needed.
609624
this.removeListener('newListener', newListener);
610625
}
626+
627+
ssl.onkeylog = onkeylogclient;
628+
629+
// Only call .onkeylog if there is a keylog listener.
630+
this.on('newListener', keylogNewListener);
631+
632+
function keylogNewListener(event) {
633+
if (event !== 'keylog')
634+
return;
635+
636+
ssl.enableKeylogCallback();
637+
638+
// Remove this listener since it's no longer needed.
639+
this.removeListener('newListener', keylogNewListener);
640+
}
611641
}
612642

613643
ssl.onerror = onerror;

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
252252
V(onexit_string, "onexit") \
253253
V(onhandshakedone_string, "onhandshakedone") \
254254
V(onhandshakestart_string, "onhandshakestart") \
255+
V(onkeylog_string, "onkeylog") \
255256
V(onmessage_string, "onmessage") \
256257
V(onnewsession_string, "onnewsession") \
257258
V(onocspresponse_string, "onocspresponse") \

src/node_crypto.cc

+17
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ template SSL_SESSION* SSLWrap<TLSWrap>::GetSessionCallback(
149149
int* copy);
150150
template int SSLWrap<TLSWrap>::NewSessionCallback(SSL* s,
151151
SSL_SESSION* sess);
152+
template void SSLWrap<TLSWrap>::KeylogCallback(const SSL* s,
153+
const char* line);
152154
template void SSLWrap<TLSWrap>::OnClientHello(
153155
void* arg,
154156
const ClientHelloParser::ClientHello& hello);
@@ -1749,6 +1751,21 @@ int SSLWrap<Base>::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
17491751
}
17501752

17511753

1754+
template <class Base>
1755+
void SSLWrap<Base>::KeylogCallback(const SSL* s, const char* line) {
1756+
Base* w = static_cast<Base*>(SSL_get_app_data(s));
1757+
Environment* env = w->ssl_env();
1758+
HandleScope handle_scope(env->isolate());
1759+
Context::Scope context_scope(env->context());
1760+
1761+
const size_t size = strlen(line);
1762+
Local<Value> line_bf = Buffer::Copy(env, line, 1 + size).ToLocalChecked();
1763+
char* data = Buffer::Data(line_bf);
1764+
data[size] = '\n';
1765+
w->MakeCallback(env->onkeylog_string(), 1, &line_bf);
1766+
}
1767+
1768+
17521769
template <class Base>
17531770
void SSLWrap<Base>::OnClientHello(void* arg,
17541771
const ClientHelloParser::ClientHello& hello) {

src/node_crypto.h

+1
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class SSLWrap {
256256
int* copy);
257257
#endif
258258
static int NewSessionCallback(SSL* s, SSL_SESSION* sess);
259+
static void KeylogCallback(const SSL* s, const char* line);
259260
static void OnClientHello(void* arg,
260261
const ClientHelloParser::ClientHello& hello);
261262

src/tls_wrap.cc

+10
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,15 @@ void TLSWrap::EnableSessionCallbacks(
912912
wrap);
913913
}
914914

915+
void TLSWrap::EnableKeylogCallback(
916+
const FunctionCallbackInfo<Value>& args) {
917+
TLSWrap* wrap;
918+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
919+
CHECK_NOT_NULL(wrap->sc_);
920+
SSL_CTX_set_keylog_callback(wrap->sc_->ctx_.get(),
921+
SSLWrap<TLSWrap>::KeylogCallback);
922+
}
923+
915924
// Check required capabilities were not excluded from the OpenSSL build:
916925
// - OPENSSL_NO_SSL_TRACE excludes SSL_trace()
917926
// - OPENSSL_NO_STDIO excludes BIO_new_fp()
@@ -1105,6 +1114,7 @@ void TLSWrap::Initialize(Local<Object> target,
11051114
env->SetProtoMethod(t, "start", Start);
11061115
env->SetProtoMethod(t, "setVerifyMode", SetVerifyMode);
11071116
env->SetProtoMethod(t, "enableSessionCallbacks", EnableSessionCallbacks);
1117+
env->SetProtoMethod(t, "enableKeylogCallback", EnableKeylogCallback);
11081118
env->SetProtoMethod(t, "enableTrace", EnableTrace);
11091119
env->SetProtoMethod(t, "destroySSL", DestroySSL);
11101120
env->SetProtoMethod(t, "enableCertCb", EnableCertCb);

src/tls_wrap.h

+2
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ class TLSWrap : public AsyncWrap,
160160
static void SetVerifyMode(const v8::FunctionCallbackInfo<v8::Value>& args);
161161
static void EnableSessionCallbacks(
162162
const v8::FunctionCallbackInfo<v8::Value>& args);
163+
static void EnableKeylogCallback(
164+
const v8::FunctionCallbackInfo<v8::Value>& args);
163165
static void EnableTrace(const v8::FunctionCallbackInfo<v8::Value>& args);
164166
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
165167
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 tls = require('tls');
9+
const fixtures = require('../common/fixtures');
10+
11+
const server = tls.createServer({
12+
key: fixtures.readSync('/keys/agent2-key.pem'),
13+
cert: fixtures.readSync('/keys/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+
}).listen(() => {
19+
const client = tls.connect({
20+
port: server.address().port,
21+
rejectUnauthorized: false,
22+
});
23+
24+
const verifyBuffer = (line) => assert(Buffer.isBuffer(line));
25+
server.on('keylog', common.mustCall(verifyBuffer, 5));
26+
client.on('keylog', common.mustCall(verifyBuffer, 5));
27+
28+
client.once('secureConnect', () => {
29+
server.close();
30+
client.end();
31+
});
32+
});

0 commit comments

Comments
 (0)