Skip to content

Commit 4ac1dfd

Browse files
committed
Ensure fingerprinting doesn't interfere with normal TLS
1 parent f38555e commit 4ac1dfd

File tree

5 files changed

+104
-11
lines changed

5 files changed

+104
-11
lines changed

src/index.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,26 @@ export type TlsFingerprintData = [
5555
curveFormats: number[]
5656
];
5757

58-
export async function getTlsFingerprintData(rawStream: stream.Readable): Promise<TlsFingerprintData> {
59-
// Create a separate stream, which isn't flowing, so we can read byte-by-byte regardless of how else
60-
// the stream is being used.
61-
const inputStream = new stream.PassThrough();
62-
rawStream.pipe(inputStream);
58+
export async function getTlsFingerprintData(inputStream: stream.Readable): Promise<TlsFingerprintData> {
59+
const wasFlowing = inputStream.readableFlowing;
60+
if (wasFlowing) inputStream.pause(); // Pause other readers, so we have time to precisely get the data we need.
6361

64-
const [recordType] = await collectBytes(inputStream, 1);
62+
const recordTypeBytes = await collectBytes(inputStream, 1);
63+
const [recordType] = recordTypeBytes;
6564
if (recordType !== 0x16) throw new Error("Can't calculate TLS fingerprint - not a TLS stream");
6665

6766
const tlsRecordVersion = await collectBytes(inputStream, 2);
68-
const recordLength = (await collectBytes(inputStream, 2)).readUint16BE();
67+
const recordLengthBytes = await collectBytes(inputStream, 2);
68+
const recordLength = recordLengthBytes.readUint16BE();
6969

7070
// Collect all the hello bytes, and then give us a stream of exactly only those bytes, so we can
7171
// still process them step by step in order:
72-
const helloDataStream = stream.Readable.from(await collectBytes(inputStream, recordLength), { objectMode: false });
73-
rawStream.unpipe(inputStream); // Don't need any more data now, thanks.
72+
const recordBytes = await collectBytes(inputStream, recordLength);
73+
const helloDataStream = stream.Readable.from(recordBytes, { objectMode: false });
74+
75+
// Put all the bytes back, so that this stream can still be used to create a real TLS session
76+
inputStream.unshift(Buffer.concat([recordTypeBytes, tlsRecordVersion, recordLengthBytes, recordBytes]));
77+
if (wasFlowing) inputStream.resume(); // If there were other readers, resume and let them continue
7478

7579
const [helloType] = (await collectBytes(helloDataStream, 1));
7680
if (helloType !== 0x1) throw new Error("Can't calculate TLS fingerprint - not a TLS client hello");

test/fixtures/server.crt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDJzCCAg+gAwIBAgIUfxc7qPwEIlAJQtS+67g2MhKq9zkwDQYJKoZIhvcNAQEL
3+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIxMDgzMDE1MjUxNFoYDzIxMjEw
4+
ODA2MTUyNTE0WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
5+
AQUAA4IBDwAwggEKAoIBAQC4Cf55DdM7CE40miw2k2B1m+gihcHl+GkEu5ulrX9i
6+
NbgqsWC3FixNMejDk0DdyEi2QGdQNFha7w/jlQ/u4HGJTQBmKsqVFaV03N/dnnCV
7+
3yll7tjDJoA7yxohGSs2ouOhvytM/w2JflJ/bV5OWhsTrbV8V7omPGm83F/3RXer
8+
r76VUvhNbtpwc/QEV+zZ9HWDA9p+bjtsusFb+HD4XIn7lYfBBXQgUfGFJR1VCftQ
9+
7b8VCoMZTGj6+NL1+k+EQvgdlwi2u7BfXYI1q765gUk5XS3/eennvOKi8Jn5kzuh
10+
gQRyhErzYHwKv+KUYvJvOctIjHL6egNUqeVXYB3Q9AphAgMBAAGjbzBtMB0GA1Ud
11+
DgQWBBRWMwFWKMABMs5WSuxEnFoYPwlAUjAfBgNVHSMEGDAWgBRWMwFWKMABMs5W
12+
SuxEnFoYPwlAUjAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z
13+
dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAdjlHqfdKUdEdGofuL5CLMOZtMgmU
14+
7BYRuBVkBBRb+aysRf+vb8OvmCHK+j4HzWe5hlsrJkyDhvTWQ6IMxuufEdK9NwOX
15+
8zLut+k7N7LRkJvqC+RX8bbD+2a56n+TJjdvCqsyBFyuYdlxw07s7So2gW818ooK
16+
qTJSAjBcSkrdgZhYOEkG/AWKkelZRw2R9WLQv5wB2b+R7q0wrGtB2b9IXOs6JTaS
17+
HA5dFMDW8YaGiUQLm53eEZeTeZg+l5+izl4mTi4ytV4xa5KDyQTzzTttkLHK4cb3
18+
kVgGmAHsFu5/W1u/1u/WG/JnUW1XeEml7HVRuyA2E+GWCJc2fvTd4A0DaQ==
19+
-----END CERTIFICATE-----

test/fixtures/server.key

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4Cf55DdM7CE40
3+
miw2k2B1m+gihcHl+GkEu5ulrX9iNbgqsWC3FixNMejDk0DdyEi2QGdQNFha7w/j
4+
lQ/u4HGJTQBmKsqVFaV03N/dnnCV3yll7tjDJoA7yxohGSs2ouOhvytM/w2JflJ/
5+
bV5OWhsTrbV8V7omPGm83F/3RXerr76VUvhNbtpwc/QEV+zZ9HWDA9p+bjtsusFb
6+
+HD4XIn7lYfBBXQgUfGFJR1VCftQ7b8VCoMZTGj6+NL1+k+EQvgdlwi2u7BfXYI1
7+
q765gUk5XS3/eennvOKi8Jn5kzuhgQRyhErzYHwKv+KUYvJvOctIjHL6egNUqeVX
8+
YB3Q9AphAgMBAAECggEAYCeQlizb/Q7U1XTrvsP3dNs1SLw712yXagqfQsvIL0bD
9+
50JvtpjWIqr94xkPnhCjtN0nXWdL9o7K7WwXPAZ2K3dYywh2ebgqj0lLiZ3bUuKa
10+
3ZASHrwB6buu9jYRNuWaKwsXk436w6iFb+BzklpPpVNv6/xl3M5ZrHwzg5z+7mrs
11+
LQ83IgjFe6kVnkLYFPWLrFEsoiS3jOoILiDCUO5k4j0ZNYjRlmPpG7Iq+oPv+0rf
12+
JHiOV1Q9L1SMeubYSZ7NSiKPAqbi93BjJOpbNS2BrSL5YPt5pp8tKhwLj0gB8hux
13+
+XBwpn8GwwUypNy0YdbtYbLs/CbPcg1t5NEvhkrysQKBgQDfnBiQck9Xd8nqvwkb
14+
N0nw5UBXiPoKqHOxfQC5fsYjNdwKEvUj4OiGwAFtUwTSMqyqdmrj1Sy9SUuYfm3w
15+
ehOdQnabVyxYZj5Z1nLrP+wtXdCWv66ezq2S4dRsygs2fuy3uiL+ryVMojlBYj6M
16+
s53ipMN3qjDKt2NmwGWt33DJrQKBgQDSsokjbg7OMDebiWoimViE+eWRJ3YEXAtk
17+
WnDttCcG0qEGVi2gqZh6NjnCpiK3+dC07Sh24wfW6TS9Z9SZeNPrQ3BTnY4e0EDC
18+
hLo4MXeSEeXa+ndlTzpNcw/q63RNlKg7L+FJOV7Q3HaHKZvmC+kw2Y2dI9zYlK+X
19+
RH9ymffCBQKBgHdJHS2JXVwK0hNBX8k+AFra4S0RLFodLMKlLYrG30oPRFe3b0B5
20+
jXG84cYBQJQlZkj1LOZnZRuBCyvJXjqn1OjSeNU7drOdr2tbZCitC//TiR+yF6Qu
21+
Gxg9EoYKblre8Ma+LEbzBhHQhHylvTpv4yzxujiO+MJbfFJnFpbfmJptAoGARxx4
22+
yptvpcmCSx1y0+Cbjq3k/DusSkZileksah2+ekAGluPpHGuBCeZZUkfOOfe3qAjO
23+
+mkfkTo+UZrEl1O/eozVUXNAr0esQ7qWOzb+2y7tPB4CxA+cZt1pxujW5QRCT0+W
24+
oqcZSDbQTkgN1PO6LYGPmTSsafCs3soAlcY/Z50CgYBRhbOo9mq+hNXrKYifJwWZ
25+
dVzvlV1hJZ8ViXCzJIFwkcq9yokUwsppF/K+5c/fKN+6IWAwep7I7035GEAwtLTc
26+
J4oxpxDBYgHAON7OqHuns/xQ1Lvvg6fzAIQzR3NTXoPBSOyT2TbZJLDUf9jQ/TN/
27+
ts7xHGcDnvSUE/8RItnM5w==
28+
-----END PRIVATE KEY-----

test/test-util.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import * as stream from 'stream';
2+
import * as fs from 'fs';
3+
4+
export const testKey = fs.readFileSync(__dirname + '/fixtures/server.key');
5+
export const testCert = fs.readFileSync(__dirname + '/fixtures/server.crt');
26

37
export type Deferred<T> = Promise<T> & {
48
resolve(value: T): void,

test/test.spec.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import * as https from 'https';
66
import { makeDestroyable, DestroyableServer } from 'destroyable-server';
77

88
import { expect } from 'chai';
9-
import { getDeferred, streamToBuffer } from './test-util';
9+
import {
10+
getDeferred,
11+
streamToBuffer,
12+
testKey,
13+
testCert
14+
} from './test-util';
1015

1116
import {
1217
getTlsFingerprintData,
@@ -145,7 +150,6 @@ describe("Read-TLS-Fingerprint", () => {
145150
expect(tlsVersion).to.equal(771); // TLS 1.2 - now set even for TLS 1.3 for backward compat
146151
expect(ciphers.slice(0, 3)).to.deep.equal([4865, 4866, 4867]);
147152
expect(ciphers.length).to.equal(15);
148-
console.log(extension);
149153
expect(extension).to.deep.equal([
150154
0,
151155
23,
@@ -171,4 +175,38 @@ describe("Read-TLS-Fingerprint", () => {
171175
expect(fingerprint).to.equal('cd08e31494f9531f560d64c695473da9');
172176
});
173177

178+
it("can be calculated manually alongside a real TLS session", async () => {
179+
const tlsServer = tls.createServer({ key: testKey, cert: testCert })
180+
server = makeDestroyable(new net.Server());
181+
182+
server.on('connection', async (socket: any) => {
183+
socket.tlsFingerprint = await getTlsFingerprintAsJa3(socket);
184+
tlsServer.emit('connection', socket);
185+
});
186+
187+
const tlsSocketPromise = new Promise<tls.TLSSocket>((resolve) =>
188+
tlsServer.on('secureConnection', (tlsSocket: any) => {
189+
tlsSocket.tlsFingerprint = tlsSocket._parent.tlsFingerprint;
190+
resolve(tlsSocket);
191+
})
192+
);
193+
194+
server.listen();
195+
await new Promise((resolve) => server.on('listening', resolve));
196+
197+
const port = (server.address() as net.AddressInfo).port;
198+
tls.connect({
199+
host: 'localhost',
200+
ca: [testCert],
201+
port
202+
});
203+
204+
const tlsSocket = await tlsSocketPromise;
205+
const fingerprint = (tlsSocket as any).tlsFingerprint;
206+
expect(fingerprint).to.be.oneOf([
207+
'76cd17e0dc73c98badbb6ee3752dcf4c', // Node 12 - 16
208+
'6521bd74aad3476cdb3daa827288ec35' // Node 17+
209+
]);
210+
});
211+
174212
});

0 commit comments

Comments
 (0)