Skip to content

Commit

Permalink
tls: expose keylog event on TLSSocket
Browse files Browse the repository at this point in the history
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
Backport-PR-URL: #31582
Refs: #2363
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
  • Loading branch information
mildsunrise authored and BethGriggs committed Mar 12, 2020
1 parent 05f5b3e commit a2b0e9e
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 0 deletions.
49 changes: 49 additions & 0 deletions doc/api/tls.md
Expand Up @@ -299,6 +299,34 @@ added: v0.3.2
The `tls.Server` class is a subclass of `net.Server` that accepts encrypted
connections using TLS or SSL.

### 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 to this server (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
const logFile = fs.createWriteStream('/tmp/ssl-keys.log', { flags: 'a' });
// ...
server.on('keylog', (line, tlsSocket) => {
if (tlsSocket.remoteAddress !== '...')
return; // Only log keys for a particular IP
logFile.write(line);
});
```

### Event: 'newSession'
<!-- YAML
added: v0.9.2
Expand Down Expand Up @@ -573,6 +601,27 @@ changes:

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

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

* `line` {Buffer} Line of ASCII text, in NSS `SSLKEYLOGFILE` format.

The `keylog` event is emitted on a client `tls.TLSSocket` when key material
is generated or received by the socket. This keying material can be stored
for debugging, as it allows captured TLS traffic to be decrypted. It may
be emitted multiple times, before or after the handshake completes.

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
const logFile = fs.createWriteStream('/tmp/ssl-keys.log', { flags: 'a' });
// ...
tlsSocket.on('keylog', (line) => logFile.write(line));
```

### Event: 'OCSPResponse'
<!-- YAML
added: v0.11.13
Expand Down
30 changes: 30 additions & 0 deletions lib/_tls_wrap.js
Expand Up @@ -243,6 +243,18 @@ function onnewsession(key, session) {
}


function onkeylogclient(line) {
debug('client onkeylog');
this[owner_symbol].emit('keylog', line);
}

function onkeylog(line) {
debug('server onkeylog');
const owner = this[owner_symbol];
if (owner.server)
owner.server.emit('keylog', line, owner);
}

function onocspresponse(resp) {
this[owner_symbol].emit('OCSPResponse', resp);
}
Expand Down Expand Up @@ -492,6 +504,7 @@ TLSSocket.prototype._init = function(socket, wrap) {
ssl.onclienthello = loadSession;
ssl.oncertcb = loadSNI;
ssl.onnewsession = onnewsession;
ssl.onkeylog = onkeylog;
ssl.lastHandshakeTime = 0;
ssl.handshakes = 0;

Expand All @@ -500,6 +513,8 @@ TLSSocket.prototype._init = function(socket, wrap) {
this.server.listenerCount('newSession') > 0) {
ssl.enableSessionCallbacks();
}
if (this.server.listenerCount('keylog') > 0)
ssl.enableKeylogCallback();
if (this.server.listenerCount('OCSPRequest') > 0)
ssl.enableCertCb();
}
Expand All @@ -510,6 +525,21 @@ TLSSocket.prototype._init = function(socket, wrap) {

if (options.session)
ssl.setSession(options.session);

ssl.onkeylog = onkeylogclient;

// Only call .onkeylog if there is a keylog listener.
this.on('newListener', keylogNewListener);

function keylogNewListener(event) {
if (event !== 'keylog')
return;

ssl.enableKeylogCallback();

// Remove this listener since it's no longer needed.
this.removeListener('newListener', keylogNewListener);
}
}

ssl.onerror = onerror;
Expand Down
1 change: 1 addition & 0 deletions src/env.h
Expand Up @@ -225,6 +225,7 @@ struct PackageConfig {
V(onhandshakedone_string, "onhandshakedone") \
V(onhandshakestart_string, "onhandshakestart") \
V(onheaders_string, "onheaders") \
V(onkeylog_string, "onkeylog") \
V(onmessage_string, "onmessage") \
V(onnewsession_string, "onnewsession") \
V(onocspresponse_string, "onocspresponse") \
Expand Down
17 changes: 17 additions & 0 deletions src/node_crypto.cc
Expand Up @@ -132,6 +132,8 @@ template SSL_SESSION* SSLWrap<TLSWrap>::GetSessionCallback(
int* copy);
template int SSLWrap<TLSWrap>::NewSessionCallback(SSL* s,
SSL_SESSION* sess);
template void SSLWrap<TLSWrap>::KeylogCallback(const SSL* s,
const char* line);
template void SSLWrap<TLSWrap>::OnClientHello(
void* arg,
const ClientHelloParser::ClientHello& hello);
Expand Down Expand Up @@ -1482,6 +1484,21 @@ int SSLWrap<Base>::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
}


template <class Base>
void SSLWrap<Base>::KeylogCallback(const SSL* s, const char* line) {
Base* w = static_cast<Base*>(SSL_get_app_data(s));
Environment* env = w->ssl_env();
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());

const size_t size = strlen(line);
Local<Value> line_bf = Buffer::Copy(env, line, 1 + size).ToLocalChecked();
char* data = Buffer::Data(line_bf);
data[size] = '\n';
w->MakeCallback(env->onkeylog_string(), 1, &line_bf);
}


template <class Base>
void SSLWrap<Base>::OnClientHello(void* arg,
const ClientHelloParser::ClientHello& hello) {
Expand Down
1 change: 1 addition & 0 deletions src/node_crypto.h
Expand Up @@ -267,6 +267,7 @@ class SSLWrap {
int* copy);
#endif
static int NewSessionCallback(SSL* s, SSL_SESSION* sess);
static void KeylogCallback(const SSL* s, const char* line);
static void OnClientHello(void* arg,
const ClientHelloParser::ClientHello& hello);

Expand Down
11 changes: 11 additions & 0 deletions src/tls_wrap.cc
Expand Up @@ -836,6 +836,16 @@ void TLSWrap::EnableSessionCallbacks(
wrap);
}

void TLSWrap::EnableKeylogCallback(
const FunctionCallbackInfo<Value>& args) {
TLSWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
CHECK_NOT_NULL(wrap->sc_);
#if OPENSSL_VERSION_NUMBER >= 0x1010100fL
SSL_CTX_set_keylog_callback(wrap->sc_->ctx_.get(),
SSLWrap<TLSWrap>::KeylogCallback);
#endif
}

void TLSWrap::DestroySSL(const FunctionCallbackInfo<Value>& args) {
TLSWrap* wrap;
Expand Down Expand Up @@ -1001,6 +1011,7 @@ void TLSWrap::Initialize(Local<Object> target,
env->SetProtoMethod(t, "start", Start);
env->SetProtoMethod(t, "setVerifyMode", SetVerifyMode);
env->SetProtoMethod(t, "enableSessionCallbacks", EnableSessionCallbacks);
env->SetProtoMethod(t, "enableKeylogCallback", EnableKeylogCallback);
env->SetProtoMethod(t, "destroySSL", DestroySSL);
env->SetProtoMethod(t, "enableCertCb", EnableCertCb);

Expand Down
2 changes: 2 additions & 0 deletions src/tls_wrap.h
Expand Up @@ -154,6 +154,8 @@ class TLSWrap : public AsyncWrap,
static void SetVerifyMode(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableSessionCallbacks(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableKeylogCallback(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableTrace(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
Expand Down
34 changes: 34 additions & 0 deletions test/parallel/test-tls-keylog-tlsv12.js
@@ -0,0 +1,34 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
if (/^(0\.|1\.0\.|1\.1\.0)/.test(process.versions.openssl))
common.skip('keylog support not available');

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

const server = tls.createServer({
key: fixtures.readSync('/keys/agent2-key.pem'),
cert: fixtures.readSync('/keys/agent2-cert.pem'),
// Amount of keylog events depends on negotiated protocol
// version, so force a specific one:
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.2',
}).listen(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
});

const verifyBuffer = (line) => assert(Buffer.isBuffer(line));
server.on('keylog', common.mustCall(verifyBuffer, 1));
client.on('keylog', common.mustCall(verifyBuffer, 1));

client.once('secureConnect', () => {
server.close();
client.end();
});
});

0 comments on commit a2b0e9e

Please sign in to comment.