diff --git a/CHANGELOG.md b/CHANGELOG.md index 265978a..0017347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,10 @@ All notable changes will be documented in this file. ## [0.1.1] - 2018-11-23 - testing npm release with Travis-CI + +## next + +- use SSLKEYLOGFILE env var as default +- add `hook_*` API, deprecate `update_log` and `get_session_key` +- compatibility with Node 12 and Node 10.0 - 10.7 +- some refactoring and changes to the build process diff --git a/README.md b/README.md index 23649ac..dc5b517 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,110 @@ Further reading about SSLKEYLOG: * [SSL/TLS Decryption: uncovering secrets](https://sharkfesteurope.wireshark.org/assets/presentations17eu/15.pdf) (PDF, SharkFest'17) * [Decrypting TLS browser traffic with Wireshark: the easy way](https://jimshaver.net/2015/02/11/decrypting-tls-browser-traffic-with-wireshark-the-easy-way/) -### Installation +[Node.js](https://nodejs.org/) v10+ is **required**. Tested on v10 (LTS) and v11 (CURRENT), OS X and Linux. -[Node.js](https://nodejs.org/) v10+ is required. Tested on v10 (LTS) and v11 (CURRENT), OS X and Linux. + +## Usage + +### Getting started + +Install the module: + +~~~ bash +npm install sslkeylog +~~~ + +Set the `SSLKEYLOGFILE` environment variable as usual: + +~~~ bash +export SSLKEYLOGFILE=/tmp/keys.log +~~~ + +Then in your code, call the `hook_all` function at startup: + +~~~ js +require('sslkeylog').hook_all(); +~~~ + +That's it! Run your code and decryption keys will be logged to the specified file. + +### Setting log file + +If you don't want to use `SSLKEYLOGFILE` or want to override it, you can use `set_log`: + +~~~ js +sslkeylog.set_log('/tmp/otherkeys.log'); +~~~ + +### Logging specific connections + +`hook_all` will log decryption keys for *every TLS connection initiated or received by the Node.JS process*. This is okay for quick debugging, but is bad practice and may fail (because it patches Node.JS internals) or may be inconvenient if you have lots of connections. + +Instead of calling `hook_all`, you may use (a combination of) other functions to log only certain connections: + +#### Incoming connections to a server + +To log all incoming connections to a `tls.Server` (or derivates such as `https.Server`), use `hook_server`: + +~~~ js +const myServer = https.createServer(...); + +// ... + +myServer.listen(...); +sslkeylog.hook_server(myServer); +~~~ + +#### HTTPS requests + +To log outgoing connections for HTTPS requests made by your code, use `hook_agent`: + +~~~ js +sslkeylog.hook_agent(); +~~~ + +This will only work for requests that use the default agent. If the requests you're interested in specify a custom `agent`, you must hook this agent instead: + +~~~ js +const myAgent = new Agent(...); +sslkeylog.hook_agent(myAgent); + +// ... + +https.request({ ..., agent: myAgent, ... }) + +// ... +~~~ + +#### Specific connections + +For more advanced use cases where you want to log a particular connection, +you can pass the created `TLSSocket` to `hook_socket`. For example, with `tls.connect`: + +~~~ js +const mySocket = tls.connect(...); +sslkeylog.hook_socket(mySocket); + +// ... +~~~ + +With `http2.connect`: + +~~~ js +const http2Session = http2.connect(...); +sslkeylog.hook_socket(http2Session.socket); + +// ... +~~~ + +Note that you **must** call `hook_socket` as soon as the `TLSSocket` is created (in +the same loop tick), otherwise keys may not be logged properly. + + +## Installation + +This is a native addon, so first make sure usual compiling tools (`make`, `cc`, etc.) are installed. +On Ubuntu / Debian, `sudo apt-get install build-essentials` should suffice. To use in your project, install as usual: @@ -31,28 +132,6 @@ $ npm install $ cd examples ``` -### Usage - -When you have connected `TLSSocket`, you may call `get_sesion_key()` to get session key for this connection: - -```javascript -let server = https.createServer({key, cert}); -server.on('secureConnection', tls_socket=>{ - const {client_random, master_key} = sslkeylog.get_session_key(tls_socket); - const hex1 = client_random.toString('hex'); - const hex2 = master_key.toString('hex'); - fs.appendFileSync('/tmp/sslkeylog.txt', `CLIENT_RANDOM ${hex1} ${hex2}\n`); -}; -``` - -Or just use `set_log()` and `update_log()` to do exactly the same: - -```javascript -sslkeylog.set_log('sslkeylog.txt'); -server = https.createServer({key, cert}); -server.on('secureConnection', sslkeylog.update_log); -``` - ### Demo Clone the repository, build with `npm install` and go to `examples/` subdir. Open few terminal tabs or tmux/screen windows. @@ -70,6 +149,57 @@ Now you can see decrypted packets: ![wireshark screenshot](https://cdn.jsdelivr.net/gh/kolontsov/node-sslkeylog/wireshark.png) + +## API reference + +### set_log(filename) + + - `filename` (String): Set filename at which (future) decryption keys will be logged. + +Sets the log filename. + +### hook_socket(socket) + + - `socket` (`tls.TLSSocket`): Socket to log decryption keys for. + +Log keys for a particular socket. This method must be called after creating +the socket (i.e. at the same event loop tick) to guarantee that all keys are +logged. Logging the same socket multiple times has no effect. + +Returns the passed socket. + +### hook_server(server) + + - `server` (`tls.Server`): Server to log decryption keys for. + +Log keys for all (future) incoming connections to the passed server. +Returns the passed server. + +### hook_agent(agent) + + - `agent` (`https.Agent | undefined`): Agent to log decryption keys for. + +Log keys for all (future) outgoing connections created by the passed agent. +If no agent is passed, `https.globalAgent` will be used. + +### hook_all() + +Log every TLS socket that is created. This relies on patching `TLSSocket#_init`, +so it may break and is not guaranteed to remain compatible with future Node.JS releases. +Calling this method multiple times has no effect. + +### update_log(socket) + +**Deprecated.** Log keys for a particular, connected socket. Do not use. + +### get_session_key(socket) + +**Deprecated.** For a particular socket, returns master key of the current session +and client random. Throws if there's no session at the time. Do not use. + + +## Project status + ### TODO - windows support? diff --git a/examples/server.js b/examples/server.js index bcbd67b..c696455 100644 --- a/examples/server.js +++ b/examples/server.js @@ -16,6 +16,6 @@ const req_handler = (req, res)=>{ sslkeylog.set_log('sslkeylog.txt'); -https.createServer(ssl_opt, req_handler) - .on('secureConnection', sock=>sslkeylog.update_log(sock)) - .listen(port, ()=>console.log(`Started on port ${port}`)); +const server = https.createServer(ssl_opt, req_handler); +server.listen(port, ()=>console.log(`Started on port ${port}`)); +sslkeylog.hook_server(server); diff --git a/test/test.js b/test/test.js index 4567591..56764ef 100644 --- a/test/test.js +++ b/test/test.js @@ -1,48 +1,130 @@ 'use strict'; const assert = require('assert'); +const util = require('util'); const https = require('https'); const path = require('path'); const fs = require('fs'); +const tls = require('tls'); const sslkeylog = require('../index.js'); +const unlink = util.promisify(fs.unlink); -describe('sslkeylog', function(){ +const once = (obj, event)=>new Promise((resolve, reject)=>{ + function listener() { + resolve(Array.from(arguments)); + obj.removeListener('error', errorListener); + } + function errorListener(err) { + reject(err); + obj.removeListener(event, listener); + } + obj.once(event, listener).once('error', errorListener); +}) + +describe('sslkeylog API', function(){ const hello = "Hello, world"; - let server, result; - before(()=>new Promise(resolve=>{ + const client_random_line_ws = /^CLIENT_RANDOM [0-9a-f]{64} [0-9a-f]{96}\n$/; + const client_random_line = /^CLIENT_RANDOM [0-9a-f]{64} [0-9a-f]{96}$/; + let server, key; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; + const logFile = `${__dirname}/keys.log`; + // care has to be taken with order of tests, because they have side-effects + + before(async ()=>{ const ssl_opt = { key: fs.readFileSync(`${__dirname}/test.key`), cert: fs.readFileSync(`${__dirname}/test.crt`), host: '127.0.0.1', + maxVersion: 'TLSv1.2', }; server = https.createServer(ssl_opt, (req, res)=>{ res.writeHead(200); res.end(hello); }); server.on('secureConnection', socket=>{ - result = sslkeylog.get_session_key(socket); + key = sslkeylog.get_session_key(socket); }); - server.listen(resolve); - })); + server.listen(); + await once(server, 'listening'); + }); after(()=>{ server.close(); }); - it('basic', ()=>new Promise(resolve=>{ - process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; - let req = https.request({port: server.address().port}, res=>{ - let response = []; - res.on('data', chunk=>response.push(chunk)); - res.on('end', ()=>{ - let str = Buffer.concat(response).toString(); - assert(str==hello); - assert(result instanceof Object); - let {client_random, master_key} = result; - assert(client_random instanceof Buffer); - assert(client_random.length==32); - assert(master_key instanceof Buffer); - assert(master_key.length==48); - resolve(); - }); - }); - req.end(); - })); + + it('should retrieve session key', async ()=>{ + let req = https.get({port: server.address().port}); + let [res] = await once(req, 'response'); + + let response = []; + res.on('data', chunk=>response.push(chunk)); + await once(res, 'end'); + let str = Buffer.concat(response).toString(); + + assert.equal(str, hello); + assert(key instanceof Object); + let {client_random, master_key} = key; + assert(client_random instanceof Buffer); + assert.equal(client_random.length, 32); + assert(master_key instanceof Buffer); + assert.equal(master_key.length, 48); + }); + + it('should intercept default agent', async ()=>{ + await unlink(logFile).catch(() => {}); + sslkeylog.set_log(logFile); + + sslkeylog.hook_agent(); + let req = https.get({port: server.address().port}); + let [res] = await once(req, 'response'); + assert(client_random_line_ws.test(fs.readFileSync(logFile, 'utf8'))); + res.resume(); + await once(res, 'end'); + }); + + it("shouldn't intercept other agent connections", async ()=>{ + await unlink(logFile).catch(() => {}); + + let req = https.get({port: server.address().port, agent: false}); + let [res] = await once(req, 'response'); + assert(!fs.existsSync(logFile)); + res.resume(); + await once(res, 'end'); + }); + + it('should intercept a particular connection', async ()=>{ + await unlink(logFile).catch(() => {}); + const socket1 = tls.connect({port: server.address().port}); + const socket2 = tls.connect({port: server.address().port}); + sslkeylog.hook_socket(socket1); + await once(socket1, 'secureConnect'); + await once(socket2, 'secureConnect'); + assert(client_random_line_ws.test(fs.readFileSync(logFile, 'utf8'))); + socket1.destroy(); + socket2.destroy(); + }); + + it('should intercept server connections', async ()=>{ + await unlink(logFile).catch(() => {}); + + sslkeylog.hook_server(server); + let req = https.get({port: server.address().port}); + let [res] = await once(req, 'response'); + const [line1, line2] = fs.readFileSync(logFile, 'utf8').trimRight().split('\n'); + assert.equal(line1, line2); + assert(client_random_line.test(line1)); + res.resume(); + await once(res, 'end'); + }); + + it('should intercept all connections', async ()=>{ + await unlink(logFile).catch(() => {}); + + sslkeylog.hook_all(); + const socket = tls.connect({port: server.address().port}); + await once(socket, 'secureConnect'); + const [line1, line2] = fs.readFileSync(logFile, 'utf8').trimRight().split('\n'); + assert.equal(line1, line2); + assert(client_random_line.test(line1)); + socket.destroy(); + }); + });