Skip to content

Commit f38555e

Browse files
committed
Filter GREASE, and add a test against a real Chrome hello
1 parent b826b1a commit f38555e

File tree

3 files changed

+75
-10
lines changed

3 files changed

+75
-10
lines changed

src/index.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,19 @@ const collectBytes = (stream: stream.Readable, byteLength: number) => {
4343
const getUint16BE = (buffer: Buffer, offset: number) =>
4444
(buffer[offset] << 8) + buffer[offset+1];
4545

46-
export async function getTlsFingerprintData(rawStream: stream.Readable) {
46+
// https://datatracker.ietf.org/doc/html/draft-davidben-tls-grease-01 defines GREASE values for various
47+
// TLS fields, reserving 0a0a, 1a1a, 2a2a, etc for ciphers, extension ids & supported groups.
48+
const isGREASE = (value: number) => (value & 0x0f0f) == 0x0a0a;
49+
50+
export type TlsFingerprintData = [
51+
tlsVersion: number,
52+
ciphers: number[],
53+
extensions: number[],
54+
groups: number[],
55+
curveFormats: number[]
56+
];
57+
58+
export async function getTlsFingerprintData(rawStream: stream.Readable): Promise<TlsFingerprintData> {
4759
// Create a separate stream, which isn't flowing, so we can read byte-by-byte regardless of how else
4860
// the stream is being used.
4961
const inputStream = new stream.PassThrough();
@@ -100,10 +112,14 @@ export async function getTlsFingerprintData(rawStream: stream.Readable) {
100112

101113
const cipherFingerprint: number[] = [];
102114
for (let i = 0; i < cipherSuites.length; i += 2) {
103-
cipherFingerprint.push(getUint16BE(cipherSuites, i));
115+
const cipherId = getUint16BE(cipherSuites, i);
116+
if (isGREASE(cipherId)) continue;
117+
cipherFingerprint.push(cipherId);
104118
}
105119

106-
const extensionsFingerprint: number[] = extensions.map(({ id }) => getUint16BE(id, 0));
120+
const extensionsFingerprint: number[] = extensions
121+
.map(({ id }) => getUint16BE(id, 0))
122+
.filter(id => !isGREASE(id));
107123

108124
const supportedGroupsData = (
109125
extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x0a])))?.data
@@ -112,7 +128,9 @@ export async function getTlsFingerprintData(rawStream: stream.Readable) {
112128

113129
const groupsFingerprint: number[] = [];
114130
for (let i = 0; i < supportedGroupsData.length; i += 2) {
115-
groupsFingerprint.push(getUint16BE(supportedGroupsData, i));
131+
const groupId = getUint16BE(supportedGroupsData, i)
132+
if (isGREASE(groupId)) continue;
133+
groupsFingerprint.push(groupId);
116134
}
117135

118136
const curveFormatsData = extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x0b])))?.data
@@ -125,12 +143,10 @@ export async function getTlsFingerprintData(rawStream: stream.Readable) {
125143
extensionsFingerprint,
126144
groupsFingerprint,
127145
curveFormatsFingerprint
128-
] as const;
146+
];
129147
}
130148

131-
export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
132-
const fingerprintData = await getTlsFingerprintData(rawStream);
133-
149+
export function calculateJa3FromFingerprintData(fingerprintData: TlsFingerprintData) {
134150
const fingerprintString = [
135151
fingerprintData[0],
136152
fingerprintData[1].join('-'),
@@ -140,4 +156,8 @@ export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
140156
].join(',');
141157

142158
return crypto.createHash('md5').update(fingerprintString).digest('hex');
159+
}
160+
161+
export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
162+
return calculateJa3FromFingerprintData(await getTlsFingerprintData(rawStream));
143163
}

test/fixtures/chrome-tls-connect.bin

517 Bytes
Binary file not shown.

test/test.spec.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as path from 'path';
2+
import * as fs from 'fs';
13
import * as net from 'net';
24
import * as tls from 'tls';
35
import * as https from 'https';
@@ -8,7 +10,8 @@ import { getDeferred, streamToBuffer } from './test-util';
810

911
import {
1012
getTlsFingerprintData,
11-
getTlsFingerprintAsJa3
13+
getTlsFingerprintAsJa3,
14+
calculateJa3FromFingerprintData
1215
} from '../src/index';
1316

1417
const nodeMajorVersion = parseInt(process.version.slice(1).split('.')[0], 10);
@@ -17,7 +20,7 @@ describe("Read-TLS-Fingerprint", () => {
1720

1821
let server: DestroyableServer<net.Server>;
1922

20-
afterEach(() => server?.destroy());
23+
afterEach(() => server?.destroy().catch(() => {}));
2124

2225
it("can read Node's fingerprint data", async () => {
2326
server = makeDestroyable(new net.Server());
@@ -126,4 +129,46 @@ describe("Read-TLS-Fingerprint", () => {
126129
expect(ourFingerprint).to.equal(remoteFingerprint);
127130
});
128131

132+
it("can calculate the correct TLS fingerprint from a Chrome request", async () => {
133+
const incomingData = fs.createReadStream(path.join(__dirname, 'fixtures', 'chrome-tls-connect.bin'));
134+
135+
const fingerprintData = await getTlsFingerprintData(incomingData);
136+
137+
const [
138+
tlsVersion,
139+
ciphers,
140+
extension,
141+
groups,
142+
curveFormats
143+
] = fingerprintData;
144+
145+
expect(tlsVersion).to.equal(771); // TLS 1.2 - now set even for TLS 1.3 for backward compat
146+
expect(ciphers.slice(0, 3)).to.deep.equal([4865, 4866, 4867]);
147+
expect(ciphers.length).to.equal(15);
148+
console.log(extension);
149+
expect(extension).to.deep.equal([
150+
0,
151+
23,
152+
65281,
153+
10,
154+
11,
155+
35,
156+
16,
157+
5,
158+
13,
159+
18,
160+
51,
161+
45,
162+
43,
163+
27,
164+
17513,
165+
21
166+
]);
167+
expect(groups).to.deep.equal([29, 23, 24]);
168+
expect(curveFormats).to.deep.equal([0]);
169+
170+
const fingerprint = calculateJa3FromFingerprintData(fingerprintData);
171+
expect(fingerprint).to.equal('cd08e31494f9531f560d64c695473da9');
172+
});
173+
129174
});

0 commit comments

Comments
 (0)