Skip to content

Commit 9a33498

Browse files
committed
Added 'TLS JA3 Fingerprint' operation
1 parent a3b873f commit 9a33498

File tree

4 files changed

+255
-0
lines changed

4 files changed

+255
-0
lines changed

src/core/config/Categories.json

+1
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@
193193
"Protobuf Decode",
194194
"VarInt Encode",
195195
"VarInt Decode",
196+
"TLS JA3 Fingerprint",
196197
"Format MAC addresses",
197198
"Change IP format",
198199
"Group IP addresses",
+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* @author n1474335 [[email protected]]
3+
* @copyright Crown Copyright 2021
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import OperationError from "../errors/OperationError.mjs";
9+
import Utils from "../Utils.mjs";
10+
import Stream from "../lib/Stream.mjs";
11+
import {runHash} from "../lib/Hash.mjs";
12+
13+
/**
14+
* TLS JA3 Fingerprint operation
15+
*/
16+
class TLSJA3Fingerprint extends Operation {
17+
18+
/**
19+
* TLSJA3Fingerprint constructor
20+
*/
21+
constructor() {
22+
super();
23+
24+
this.name = "TLS JA3 Fingerprint";
25+
this.module = "Crypto";
26+
this.description = "Generates a JA3 fingerprint to help identify TLS clients based on hashing together values from the Client Hello.<br><br>Input: A hex stream of the TLS Client Hello application layer.";
27+
this.infoURL = "https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967";
28+
this.inputType = "string";
29+
this.outputType = "string";
30+
this.args = [
31+
{
32+
name: "Input format",
33+
type: "option",
34+
value: ["Hex", "Base64", "Raw"]
35+
},
36+
{
37+
name: "Output format",
38+
type: "option",
39+
value: ["Hash digest", "JA3 string", "Full details"]
40+
}
41+
];
42+
}
43+
44+
/**
45+
* @param {string} input
46+
* @param {Object[]} args
47+
* @returns {string}
48+
*/
49+
run(input, args) {
50+
const [inputFormat, outputFormat] = args;
51+
52+
input = Utils.convertToByteArray(input, inputFormat);
53+
const s = new Stream(new Uint8Array(input));
54+
55+
const handshake = s.readInt(1);
56+
if (handshake !== 0x16)
57+
throw new OperationError("Not handshake data.");
58+
59+
// Version
60+
s.moveForwardsBy(2);
61+
62+
// Length
63+
const length = s.readInt(2);
64+
if (s.length !== length + 5)
65+
throw new OperationError("Incorrect handshake length.");
66+
67+
// Handshake type
68+
const handshakeType = s.readInt(1);
69+
if (handshakeType !== 1)
70+
throw new OperationError("Not a Client Hello.");
71+
72+
// Handshake length
73+
const handshakeLength = s.readInt(3);
74+
if (s.length !== handshakeLength + 9)
75+
throw new OperationError("Not enough data in Client Hello.");
76+
77+
// Hello version
78+
const helloVersion = s.readInt(2);
79+
80+
// Random
81+
s.moveForwardsBy(32);
82+
83+
// Session ID
84+
const sessionIDLength = s.readInt(1);
85+
s.moveForwardsBy(sessionIDLength);
86+
87+
// Cipher suites
88+
const cipherSuitesLength = s.readInt(2);
89+
const cipherSuites = s.getBytes(cipherSuitesLength);
90+
const cs = new Stream(cipherSuites);
91+
const cipherSegment = parseJA3Segment(cs, 2);
92+
93+
// Compression Methods
94+
const compressionMethodsLength = s.readInt(1);
95+
s.moveForwardsBy(compressionMethodsLength);
96+
97+
// Extensions
98+
const extensionsLength = s.readInt(2);
99+
const extensions = s.getBytes(extensionsLength);
100+
const es = new Stream(extensions);
101+
let ecsLen, ecs, ellipticCurves = "", ellipticCurvePointFormats = "";
102+
const exts = [];
103+
while (es.hasMore()) {
104+
const type = es.readInt(2);
105+
const length = es.readInt(2);
106+
switch (type) {
107+
case 0x0a: // Elliptic curves
108+
ecsLen = es.readInt(2);
109+
ecs = new Stream(es.getBytes(ecsLen));
110+
ellipticCurves = parseJA3Segment(ecs, 2);
111+
break;
112+
case 0x0b: // Elliptic curve point formats
113+
ecsLen = es.readInt(1);
114+
ecs = new Stream(es.getBytes(ecsLen));
115+
ellipticCurvePointFormats = parseJA3Segment(ecs, 1);
116+
break;
117+
default:
118+
es.moveForwardsBy(length);
119+
}
120+
if (!GREASE_CIPHERSUITES.includes(type))
121+
exts.push(type);
122+
}
123+
124+
// Output
125+
const ja3 = [
126+
helloVersion.toString(),
127+
cipherSegment,
128+
exts.join("-"),
129+
ellipticCurves,
130+
ellipticCurvePointFormats
131+
];
132+
const ja3Str = ja3.join(",");
133+
const ja3Hash = runHash("md5", Utils.strToArrayBuffer(ja3Str));
134+
135+
switch (outputFormat) {
136+
case "JA3 string":
137+
return ja3Str;
138+
case "Full details":
139+
return `Hash digest:
140+
${ja3Hash}
141+
142+
Full JA3 string:
143+
${ja3Str}
144+
145+
TLS Version:
146+
${helloVersion.toString()}
147+
Cipher Suites:
148+
${cipherSegment}
149+
Extensions:
150+
${exts.join("-")}
151+
Elliptic Curves:
152+
${ellipticCurves}
153+
Elliptic Curve Point Formats:
154+
${ellipticCurvePointFormats}`;
155+
case "Hash digest":
156+
default:
157+
return ja3Hash;
158+
}
159+
}
160+
161+
}
162+
163+
/**
164+
* Parses a JA3 segment, returning a "-" separated list
165+
*
166+
* @param {Stream} stream
167+
* @returns {string}
168+
*/
169+
function parseJA3Segment(stream, size=2) {
170+
const segment = [];
171+
while (stream.hasMore()) {
172+
const element = stream.readInt(size);
173+
if (!GREASE_CIPHERSUITES.includes(element))
174+
segment.push(element);
175+
}
176+
return segment.join("-");
177+
}
178+
179+
const GREASE_CIPHERSUITES = [
180+
0x0a0a,
181+
0x1a1a,
182+
0x2a2a,
183+
0x3a3a,
184+
0x4a4a,
185+
0x5a5a,
186+
0x6a6a,
187+
0x7a7a,
188+
0x8a8a,
189+
0x9a9a,
190+
0xaaaa,
191+
0xbaba,
192+
0xcaca,
193+
0xdada,
194+
0xeaea,
195+
0xfafa
196+
];
197+
198+
export default TLSJA3Fingerprint;

tests/operations/index.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import "./tests/Unicode.mjs";
104104
import "./tests/RSA.mjs";
105105
import "./tests/CBOREncode.mjs";
106106
import "./tests/CBORDecode.mjs";
107+
import "./tests/TLSJA3Fingerprint.mjs";
107108

108109

109110
// Cannot test operations that use the File type yet
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* TLSJA3Fingerprint tests.
3+
*
4+
* @author n1474335 [[email protected]]
5+
* @copyright Crown Copyright 2021
6+
* @license Apache-2.0
7+
*/
8+
import TestRegister from "../../lib/TestRegister.mjs";
9+
10+
TestRegister.addTests([
11+
{
12+
name: "TLS JA3 Fingerprint: TLS 1.0",
13+
input: "16030100a4010000a00301543dd2dd48f517ca9a93b1e599f019fdece704a23e86c1dcac588427abbaddf200005cc014c00a0039003800880087c00fc00500350084c012c00800160013c00dc003000ac013c00900330032009a009900450044c00ec004002f009600410007c011c007c00cc002000500040015001200090014001100080006000300ff0100001b000b000403000102000a000600040018001700230000000f000101",
14+
expectedOutput: "503053a0c5b2bd9b9334bf7f3d3b8852",
15+
recipeConfig: [
16+
{
17+
"op": "TLS JA3 Fingerprint",
18+
"args": ["Hex", "Hash digest"]
19+
}
20+
],
21+
},
22+
{
23+
name: "TLS JA3 Fingerprint: TLS 1.1",
24+
input: "16030100a4010000a00302543dd2ed907e47d0086f34bee2c52dd6ccd8de63ba9387f5e810b09d9d49b38000005cc014c00a0039003800880087c00fc00500350084c012c00800160013c00dc003000ac013c00900330032009a009900450044c00ec004002f009600410007c011c007c00cc002000500040015001200090014001100080006000300ff0100001b000b000403000102000a000600040018001700230000000f000101",
25+
expectedOutput: "a314eb64cee6cb832aaaa372c8295bab",
26+
recipeConfig: [
27+
{
28+
"op": "TLS JA3 Fingerprint",
29+
"args": ["Hex", "Hash digest"]
30+
}
31+
],
32+
},
33+
{
34+
name: "TLS JA3 Fingerprint: TLS 1.2",
35+
input: "1603010102010000fe0303543dd3283283692d85f9416b5ccc65d2aafca45c6530b3c6eafbf6d371b6a015000094c030c02cc028c024c014c00a00a3009f006b006a0039003800880087c032c02ec02ac026c00fc005009d003d00350084c012c00800160013c00dc003000ac02fc02bc027c023c013c00900a2009e0067004000330032009a009900450044c031c02dc029c025c00ec004009c003c002f009600410007c011c007c00cc002000500040015001200090014001100080006000300ff01000041000b000403000102000a000600040018001700230000000d002200200601060206030501050205030401040204030301030203030201020202030101000f000101",
36+
expectedOutput: "c1a36e1a870786cc75edddc0009eaf3a",
37+
recipeConfig: [
38+
{
39+
"op": "TLS JA3 Fingerprint",
40+
"args": ["Hex", "Hash digest"]
41+
}
42+
],
43+
},
44+
{
45+
name: "TLS JA3 Fingerprint: TLS 1.3",
46+
input: "1603010200010001fc03034355d402c132771a9386b6e9994ae37069e0621af504c26673b1343843c21d8d0000264a4a130113021303c02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010001addada0000ff01000100000000180016000013626c6f672e636c6f7564666c6172652e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b000201000028002b00295a5a000100001d0020cf78b9167af054b922a96752b43973107b2a57766357dd288b2b42ab5df30e08002d00020101002b000b0acaca7f12030303020301000a000a00085a5a001d001700180a0a000100001500e4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
47+
expectedOutput: "4826a90ec2daf4f7b4b64cc1c8bd343b",
48+
recipeConfig: [
49+
{
50+
"op": "TLS JA3 Fingerprint",
51+
"args": ["Hex", "Hash digest"]
52+
}
53+
],
54+
},
55+
]);

0 commit comments

Comments
 (0)