Skip to content
This repository has been archived by the owner on Jun 4, 2021. It is now read-only.

Commit

Permalink
Document API, tests, example, CHANGELOG
Browse files Browse the repository at this point in the history
  • Loading branch information
mildsunrise committed May 10, 2019
1 parent 682fa30 commit 8af95ce
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 52 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
178 changes: 154 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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.
Expand All @@ -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?
Expand Down
6 changes: 3 additions & 3 deletions examples/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
132 changes: 107 additions & 25 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -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();
});

});

0 comments on commit 8af95ce

Please sign in to comment.