Skip to content

Commit ef57c6f

Browse files
committed
Read SNI data from hellos, and restructure API to include that
1 parent ff681e9 commit ef57c6f

File tree

3 files changed

+84
-17
lines changed

3 files changed

+84
-17
lines changed

README.md

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

1313
## Docs
1414

15-
The easiest way to use this is with the exported `enableFingerprinting` helper, which can be applied to any `tls.TLSServer` instance, including `https.Server` instances, like so:
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+
* `fingerprintData` - The raw components used for JA3 TLS fingerprinting (see the next section)
27+
28+
### TLS fingerprinting
29+
30+
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:
1631

1732
```javascript
1833
const https = require('https');
19-
const { enableFingerprinting } = require('read-tls-fingerprint');
34+
const { enableFingerprinting } = require('read-tls-client-hello');
2035

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

@@ -40,6 +55,6 @@ The `tlsFingerprint` property contains two fields:
4055

4156
It is also possible to calculate TLS fingerprints manually. The module exports a few methods for this:
4257

43-
* `getTlsFingerprintData(stream)` - Reads from a stream of incoming TLS client data, returning a promise for the raw fingerprint data, 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).
44-
* `getTlsFingerprintAsJa3` - Reads from a stream like `getTlsFingerprintData` but returns a promise for the JA3 hash, instead of raw data.
58+
* `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.
59+
* `getTlsFingerprintAsJa3` - Reads from a stream, just like `readTlsClientHello`, but returns a promise for the JA3 hash, instead of raw hello data.
4560
* `calculateJa3FromFingerprintData(data)` - Takes raw TLS fingerprint data, and returns the corresponding JA3 hash.

src/index.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ const getUint16BE = (buffer: Buffer, offset: number) =>
5454
// TLS fields, reserving 0a0a, 1a1a, 2a2a, etc for ciphers, extension ids & supported groups.
5555
const isGREASE = (value: number) => (value & 0x0f0f) == 0x0a0a;
5656

57+
export type TlsHelloData = {
58+
serverName: string | undefined;
59+
fingerprintData: TlsFingerprintData;
60+
};
61+
5762
export type TlsFingerprintData = [
5863
tlsVersion: number,
5964
ciphers: number[],
@@ -107,7 +112,33 @@ async function extractTlsHello(inputStream: stream.Readable): Promise<Buffer> {
107112
}
108113
}
109114

110-
export async function getTlsFingerprintData(inputStream: stream.Readable): Promise<TlsFingerprintData> {
115+
function parseSniData(data: Buffer) {
116+
// SNI is almost always just one value - and is arguably required to be, since there's only one type
117+
// in the RFC and you're only allowed one name per type, but it's still structured as a list:
118+
let offset = 0;
119+
while (offset < data.byteLength) {
120+
const entryLength = data.readUInt16BE(offset);
121+
offset += 2;
122+
const entryType = data[offset];
123+
offset += 1;
124+
const nameLength = data.readUInt16BE(offset);
125+
offset += 2;
126+
127+
if (nameLength !== entryLength - 3) {
128+
throw new Error('Invalid length in SNI entry');
129+
}
130+
131+
const name = data.slice(offset, offset + nameLength).toString('ascii');
132+
offset += nameLength;
133+
134+
if (entryType === 0x0) return name;
135+
}
136+
137+
// No data, or no names with DNS hostname type.
138+
return undefined;
139+
}
140+
141+
export async function readTlsClientHello(inputStream: stream.Readable): Promise<TlsHelloData> {
111142
const wasFlowing = inputStream.readableFlowing;
112143
if (wasFlowing) inputStream.pause(); // Pause other readers, so we have time to precisely get the data we need.
113144

@@ -164,7 +195,7 @@ export async function getTlsFingerprintData(inputStream: stream.Readable): Promi
164195
readExtensionsDataLength += 4 + extensionLength;
165196
}
166197

167-
// All data parsed! Now turn it into the fingerprint format:
198+
// All data received & parsed! Now turn it into the fingerprint format:
168199
//SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat
169200

170201
const tlsVersionFingerprint = clientTlsVersion.readUint16BE()
@@ -196,13 +227,25 @@ export async function getTlsFingerprintData(inputStream: stream.Readable): Promi
196227
?? Buffer.from([]);
197228
const curveFormatsFingerprint: number[] = Array.from(curveFormatsData.slice(1)); // Drop length prefix
198229

199-
return [
230+
const fingerprintData = [
200231
tlsVersionFingerprint,
201232
cipherFingerprint,
202233
extensionsFingerprint,
203234
groupsFingerprint,
204235
curveFormatsFingerprint
205-
];
236+
] as TlsFingerprintData;
237+
238+
// And capture other client hello data that might be interesting:
239+
const sniExtensionData = extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x0])))?.data;
240+
241+
const serverName = sniExtensionData
242+
? parseSniData(sniExtensionData)
243+
: undefined;
244+
245+
return {
246+
serverName,
247+
fingerprintData
248+
};
206249
}
207250

208251
export function calculateJa3FromFingerprintData(fingerprintData: TlsFingerprintData) {
@@ -218,7 +261,9 @@ export function calculateJa3FromFingerprintData(fingerprintData: TlsFingerprintD
218261
}
219262

220263
export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
221-
return calculateJa3FromFingerprintData(await getTlsFingerprintData(rawStream));
264+
return calculateJa3FromFingerprintData(
265+
(await readTlsClientHello(rawStream)).fingerprintData
266+
);
222267
}
223268

224269
interface FingerprintedSocket extends net.Socket {
@@ -266,7 +311,7 @@ export function enableFingerprinting(tlsServer: tls.Server) {
266311
// Listen ourselves for connections, get the fingerprint first, then let TLS setup resume:
267312
tlsServer.on('connection', async (socket: FingerprintedSocket) => {
268313
try {
269-
const fingerprintData = await getTlsFingerprintData(socket);
314+
const { fingerprintData } = await readTlsClientHello(socket);
270315

271316
socket.tlsFingerprint = {
272317
data: fingerprintData,

test/test.spec.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from './test-util';
1616

1717
import {
18-
getTlsFingerprintData,
18+
readTlsClientHello,
1919
getTlsFingerprintAsJa3,
2020
calculateJa3FromFingerprintData,
2121
enableFingerprinting,
@@ -24,7 +24,7 @@ import {
2424

2525
const nodeMajorVersion = parseInt(process.version.slice(1).split('.')[0], 10);
2626

27-
describe("Read-TLS-Fingerprint", () => {
27+
describe("Read-TLS-Client-Hello", () => {
2828

2929
let server: DestroyableServer<net.Server>;
3030

@@ -46,15 +46,15 @@ describe("Read-TLS-Fingerprint", () => {
4646
}).on('error', () => {}); // Socket will fail, since server never responds, that's OK
4747

4848
const incomingSocket = await incomingSocketPromise;
49-
const fingerprint = await getTlsFingerprintData(incomingSocket);
49+
const { fingerprintData } = await readTlsClientHello(incomingSocket);
5050

5151
const [
5252
tlsVersion,
5353
ciphers,
5454
extension,
5555
groups,
5656
curveFormats
57-
] = fingerprint;
57+
] = fingerprintData;
5858

5959
expect(tlsVersion).to.equal(771); // TLS 1.2 - now set even for TLS 1.3 for backward compat
6060
expect(ciphers.slice(0, 3)).to.deep.equal([4866, 4867, 4865]);
@@ -137,10 +137,17 @@ describe("Read-TLS-Fingerprint", () => {
137137
expect(ourFingerprint).to.equal(remoteFingerprint);
138138
});
139139

140+
it("can capture the server name from a Chrome request", async () => {
141+
const incomingData = fs.createReadStream(path.join(__dirname, 'fixtures', 'chrome-tls-connect.bin'));
142+
143+
const { serverName } = await readTlsClientHello(incomingData);
144+
expect(serverName).to.equal('localhost');
145+
});
146+
140147
it("can calculate the correct TLS fingerprint from a Chrome request", async () => {
141148
const incomingData = fs.createReadStream(path.join(__dirname, 'fixtures', 'chrome-tls-connect.bin'));
142149

143-
const fingerprintData = await getTlsFingerprintData(incomingData);
150+
const { fingerprintData } = await readTlsClientHello(incomingData);
144151

145152
const [
146153
tlsVersion,
@@ -303,15 +310,15 @@ describe("Read-TLS-Fingerprint", () => {
303310
}).on('error', () => {}); // Socket will fail, since server never responds, that's OK
304311

305312
const incomingSocket = await incomingSocketPromise;
306-
const fingerprint = await getTlsFingerprintData(incomingSocket);
313+
const { fingerprintData } = await readTlsClientHello(incomingSocket);
307314

308315
const [
309316
tlsVersion,
310317
ciphers,
311318
extension,
312319
groups,
313320
curveFormats
314-
] = fingerprint;
321+
] = fingerprintData;
315322

316323
expect(tlsVersion).to.equal(769); // TLS 1!
317324
expect(ciphers.slice(0, 3)).to.deep.equal([49162, 49172, 57]);

0 commit comments

Comments
 (0)