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: https://github.com/nodejs/node/pull/27654
Backport-PR-URL: https://github.com/nodejs/node/pull/31582
Refs: https://github.com/nodejs/node/issues/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>
This commit is contained in:
Alba Mendez 2019-05-11 23:07:06 +02:00 committed by Beth Griggs
parent 05f5b3ecc4
commit a2b0e9ef6a
No known key found for this signature in database
GPG Key ID: D7062848A1AB005C
8 changed files with 145 additions and 0 deletions

View File

@ -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
@ -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

View File

@ -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);
}
@ -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;
@ -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();
}
@ -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;

View File

@ -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") \

View File

@ -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);
@ -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) {

View File

@ -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);

View File

@ -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;
@ -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);

View File

@ -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);

View File

@ -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();
});
});