Skip to content

Commit 0e7ce02

Browse files
committed
Replace 'enableFingerprinting' with 'trackClientHellos'
This also changes the resulting field name that's attached from tlsFingerprint to tlsClientHello, and restructures it to match the rest of the new API.
1 parent bf9c114 commit 0e7ce02

File tree

3 files changed

+55
-55
lines changed

3 files changed

+55
-55
lines changed

README.md

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,49 @@ Be aware that fingerprinting is _not_ a 100% reliable test. Most clients can mod
1212

1313
## Docs
1414

15-
### Reading a TLS client hello
16-
17-
To read all available data from a TLS client hello, pass a stream (e.g. a `net.Socket`) to the exported `readTlsClientHello(stream)`, before the TLS handshake (or any other processing) starts. This returns a promise containing all data parsed from the client hello.
18-
19-
This method reads the initial data from the socket, parses it, and then unshifts it back into the socket, so that once the returned promise resolves the stream can be used like new, to start a normal TLS session using the same client hello.
20-
21-
If parsing fails, this method will throw an error, but will still ensure all data is returned to the socket first, so that non-TLS streams can also be processed as normal.
22-
23-
The returned promise resolves to an object, containing:
24-
25-
* `serverName` - The server name requested in the client hello (or undefined if SNI was not used)
26-
* `alpnProtocols` - A list of ALPN protcol names requested in the client hello (or undefined if ALPN was not used)
27-
* `fingerprintData` - The raw components used for JA3 TLS fingerprinting (see the next section)
28-
29-
### TLS fingerprinting
15+
### TLS server helper
3016

31-
The easiest way to use this for fingerprinting is with the exported `enableFingerprinting` helper, which can be applied to any `tls.TLSServer` instance, including `https.Server` instances, like so:
17+
The easiest way to use this is to use the built-in `trackClientHellos` helper, which can be applied to any `tls.TLSServer` instance, including `https.Server` instances, like so:
3218

3319
```javascript
3420
const https = require('https');
35-
const { enableFingerprinting } = require('read-tls-client-hello');
21+
const { trackClientHellos } = require('read-tls-client-hello');
3622

3723
const server = new https.Server({ /* your TLS options etc */ });
3824

39-
enableFingerprinting(server);
25+
trackClientHellos(server); // <-- Automatically track everything on this server
4026

4127
server.on('request', (request, response) => {
42-
// In your normal request handler, check `tlsFingerprint` on the request's socket:
43-
console.log('Received request with fingerprint:', request.socket.tlsFingerprint);
28+
// In your normal request handler, check `tlsClientHello` on the request's socket:
29+
console.log('Received request with TLS client hello:', request.socket.tlsClientHello);
4430
});
4531
```
4632

47-
The `tlsFingerprint` property contains two fields:
33+
A `tlsClientHello` property will be attached to all sockets, containing the parsed data returned by `readTlsClientHello` (see below) and a `ja3` property with the JA3 TLS fingerprint for the client hello, e.g. `cd08e31494f9531f560d64c695473da9`.
4834

49-
* `ja3` - The JA3 hash for the incoming request, e.g. `cd08e31494f9531f560d64c695473da9`
50-
* `data` - The raw data components used to calculate the hash, as an array:
35+
### Reading a TLS client hello
36+
37+
To read all available data from a TLS client hello manually, pass a stream (e.g. a `net.Socket`) to the exported `readTlsClientHello(stream)`, before the TLS handshake (or any other processing) starts. This returns a promise containing all data parsed from the client hello.
38+
39+
This method reads the initial data from the socket, parses it, and then unshifts it back into the socket, so that once the returned promise resolves the stream can be used like new, to start a normal TLS session using the same client hello.
40+
41+
If parsing fails, this method will throw an error, but will still ensure all data is returned to the socket first, so that non-TLS streams can also be processed as normal.
42+
43+
The returned promise resolves to an object, containing:
44+
45+
* `serverName` - The server name requested in the client hello (or undefined if SNI was not used)
46+
* `alpnProtocols` - A array of ALPN protcol names requested in the client hello (or undefined if ALPN was not used)
47+
* `fingerprintData` - An array containing the raw components used for JA3 TLS fingerprinting:
5148
1. The TLS version number as a Uint16 (771 for TLS 1.2+)
5249
2. An array of cipher ids (excluding GREASE)
5350
3. An array of extension ids (excluding GREASE)
5451
4. An array of supported group ids (excluding GREASE)
5552
5. An array of supported elliptic curve ids
5653

57-
It is also possible to calculate TLS fingerprints manually. The module exports a few methods for this:
54+
### TLS fingerprinting
55+
56+
To calculate TLS fingerprints manually, there are a few options exported from this module:
5857

59-
* `readTlsClientHello(stream)` - Reads from a stream of incoming TLS client data, returning a promise for parsed TLS hello, and unshifting the data back into the stream when it's done. Nothing else should attempt to read from the stream until the returned promise resolves (i.e. don't start TLS negotiation until this completes). The `fingerprintData` of the resulting value contains the raw fingerprint components.
60-
* `getTlsFingerprintAsJa3` - Reads from a stream, just like `readTlsClientHello`, but returns a promise for the JA3 hash, instead of raw hello data.
58+
* `getTlsFingerprintAsJa3` - Reads from a stream, just like `readTlsClientHello` above, but returns a promise for the JA3 hash string, e.g. `cd08e31494f9531f560d64c695473da9`, instead of the raw hello components.
59+
* `readTlsClientHello(stream)` - Reads the entire hello (see above). In the returned object, you can read the raw data components used for fingerprinting from the `fingerprintData` property.
6160
* `calculateJa3FromFingerprintData(data)` - Takes raw TLS fingerprint data, and returns the corresponding JA3 hash.

src/index.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,8 @@ export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
292292
);
293293
}
294294

295-
interface FingerprintedSocket extends net.Socket {
296-
tlsFingerprint?: {
297-
data: TlsFingerprintData;
295+
interface SocketWithHello extends net.Socket {
296+
tlsClientHello?: TlsHelloData & {
298297
ja3: string;
299298
}
300299
}
@@ -306,46 +305,45 @@ declare module 'tls' {
306305
* TLS fingerprint data.
307306
*
308307
* This is only set if the socket came from a TLS server where fingerprinting
309-
* has been enabled with `enableFingerprinting`.
308+
* has been enabled with `trackClientHellos`.
310309
*/
311-
tlsFingerprint?: {
312-
data: TlsFingerprintData;
310+
tlsClientHello?: TlsHelloData & {
313311
ja3: string;
314312
}
315313
}
316314
}
317315

318316
/**
319-
* Modify a TLS server, so that the TLS fingerprint is always parsed and attached to all
320-
* sockets at the point when the 'secureConnection' event fires.
317+
* Modify a TLS server, so that the TLS client hello is always parsed and the result is
318+
* attached to all sockets at the point when the 'secureConnection' event fires.
321319
*
322-
* This method mutates and returns the TLS server provided. TLS fingerprint data is
323-
* available from all TLS sockets afterwards in the `socket.tlsFingerprint` property.
320+
* This method mutates and returns the TLS server provided. TLS client hello data is
321+
* available from all TLS sockets afterwards in the `socket.tlsClientHello` property.
324322
*
325323
* This will work for all standard uses of a TLS server or similar (e.g. an HTTPS server)
326324
* but may behave unpredictably for advanced use cases, e.g. if you are already
327325
* manually injecting connections, hooking methods or events or otherwise doing something
328326
* funky & complicated. In those cases you probably want to use the fingerprint
329327
* calculation methods directly inside your funky logic instead.
330328
*/
331-
export function enableFingerprinting(tlsServer: tls.Server) {
329+
export function trackClientHellos(tlsServer: tls.Server) {
332330
// Disable the normal TLS 'connection' event listener that triggers TLS setup:
333331
const tlsConnectionListener = tlsServer.listeners('connection')[0] as (socket: net.Socket) => {};
334332
if (!tlsConnectionListener) throw new Error('TLS server is not listening for connection events');
335333
tlsServer.removeListener('connection', tlsConnectionListener);
336334

337335
// Listen ourselves for connections, get the fingerprint first, then let TLS setup resume:
338-
tlsServer.on('connection', async (socket: FingerprintedSocket) => {
336+
tlsServer.on('connection', async (socket: SocketWithHello) => {
339337
try {
340-
const { fingerprintData } = await readTlsClientHello(socket);
338+
const helloData = await readTlsClientHello(socket);
341339

342-
socket.tlsFingerprint = {
343-
data: fingerprintData,
344-
ja3: calculateJa3FromFingerprintData(fingerprintData)
340+
socket.tlsClientHello = {
341+
...helloData,
342+
ja3: calculateJa3FromFingerprintData(helloData.fingerprintData)
345343
};
346344
} catch (e) {
347345
if (!(e instanceof NonTlsError)) { // Ignore totally non-TLS traffic
348-
console.warn(`TLS fingerprint not available for TLS connection from ${
346+
console.warn(`TLS client hello data not available for TLS connection from ${
349347
socket.remoteAddress ?? 'unknown address'
350348
}: ${(e as Error).message ?? e}`);
351349
}
@@ -357,10 +355,10 @@ export function enableFingerprinting(tlsServer: tls.Server) {
357355

358356
tlsServer.prependListener('secureConnection', (tlsSocket: tls.TLSSocket) => {
359357
const fingerprint = (tlsSocket as unknown as {
360-
_parent?: FingerprintedSocket, // Private TLS socket field which points to the source
361-
})._parent?.tlsFingerprint;
358+
_parent?: SocketWithHello, // Private TLS socket field which points to the source
359+
})._parent?.tlsClientHello;
362360

363-
tlsSocket.tlsFingerprint = fingerprint;
361+
tlsSocket.tlsClientHello = fingerprint;
364362
});
365363

366364
return tlsServer;

test/test.spec.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
readTlsClientHello,
1919
getTlsFingerprintAsJa3,
2020
calculateJa3FromFingerprintData,
21-
enableFingerprinting,
21+
trackClientHellos,
2222

2323
} from '../src/index';
2424

@@ -217,7 +217,7 @@ describe("Read-TLS-Client-Hello", () => {
217217
expect(fingerprint).to.equal('cd08e31494f9531f560d64c695473da9');
218218
});
219219

220-
it("can be calculated manually alongside a real TLS session", async () => {
220+
it("can be manually calculate the fingerprint alongside a real TLS session", async () => {
221221
const tlsServer = tls.createServer({ key: testKey, cert: testCert })
222222
server = makeDestroyable(new net.Server());
223223

@@ -243,17 +243,17 @@ describe("Read-TLS-Client-Hello", () => {
243243
port
244244
});
245245

246-
const tlsSocket = await tlsSocketPromise;
246+
const tlsSocket: any = await tlsSocketPromise;
247247
const fingerprint = tlsSocket.tlsFingerprint;
248248
expect(fingerprint).to.be.oneOf([
249249
'76cd17e0dc73c98badbb6ee3752dcf4c', // Node 12 - 16
250250
'6521bd74aad3476cdb3daa827288ec35' // Node 17+
251251
]);
252252
});
253253

254-
it("can be calculated automatically with the provided helper", async () => {
254+
it("can be parsed automatically with the provided helper", async () => {
255255
const httpsServer = makeDestroyable(
256-
enableFingerprinting(
256+
trackClientHellos(
257257
https.createServer({ key: testKey, cert: testCert })
258258
)
259259
);
@@ -276,12 +276,15 @@ describe("Read-TLS-Client-Hello", () => {
276276
}).on('error', () => {}); // No response, we don't care
277277

278278
const tlsSocket = await tlsSocketPromise;
279-
const fingerprint = tlsSocket.tlsFingerprint;
279+
const helloData = tlsSocket.tlsClientHello!;
280+
281+
expect(helloData.serverName).to.equal('localhost');
282+
expect(helloData.alpnProtocols).to.deep.equal(undefined);
280283

281-
expect(fingerprint!.data[0]).to.equal(771); // Is definitely a TLS 1.2+ fingerprint
282-
expect(fingerprint!.data.length).to.equal(5); // Full data is checked in other tests
284+
expect(helloData.fingerprintData[0]).to.equal(771); // Is definitely a TLS 1.2+ fingerprint
285+
expect(helloData.fingerprintData.length).to.equal(5); // Full data is checked in other tests
283286

284-
expect(fingerprint!.ja3).to.be.oneOf([
287+
expect(helloData.ja3).to.be.oneOf([
285288
'398430069e0a8ecfbc8db0778d658d77', // Node 12 - 16
286289
'0cce74b0d9b7f8528fb2181588d23793' // Node 17+
287290
]);

0 commit comments

Comments
 (0)