@@ -25,6 +25,9 @@ export function toJA4(bytes) {
25
25
let tlsr = { } ;
26
26
try {
27
27
tlsr = parseTLSRecord ( bytes ) ;
28
+ if ( tlsr . handshake . value . handshakeType . value !== 0x01 ) {
29
+ throw new Error ( ) ;
30
+ }
28
31
} catch ( err ) {
29
32
throw new OperationError ( "Data is not a valid TLS Client Hello. QUIC is not yet supported.\n" + err ) ;
30
33
}
@@ -48,16 +51,7 @@ export function toJA4(bytes) {
48
51
break ;
49
52
}
50
53
}
51
- switch ( version ) {
52
- case 0x0304 : version = "13" ; break ; // TLS 1.3
53
- case 0x0303 : version = "12" ; break ; // TLS 1.2
54
- case 0x0302 : version = "11" ; break ; // TLS 1.1
55
- case 0x0301 : version = "10" ; break ; // TLS 1.0
56
- case 0x0300 : version = "s3" ; break ; // SSL 3.0
57
- case 0x0200 : version = "s2" ; break ; // SSL 2.0
58
- case 0x0100 : version = "s1" ; break ; // SSL 1.0
59
- default : version = "00" ; // Unknown
60
- }
54
+ version = tlsVersionMapper ( version ) ;
61
55
62
56
/* SNI
63
57
If the SNI extension (0x0000) exists, then the destination of the connection is a domain, or “d” in the fingerprint.
@@ -99,6 +93,7 @@ export function toJA4(bytes) {
99
93
if ( ext . type . value === "application_layer_protocol_negotiation" ) {
100
94
alpn = parseFirstALPNValue ( ext . value . data ) ;
101
95
alpn = alpn . charAt ( 0 ) + alpn . charAt ( alpn . length - 1 ) ;
96
+ if ( alpn . charCodeAt ( 0 ) > 127 ) alpn = "99" ;
102
97
break ;
103
98
}
104
99
}
@@ -164,3 +159,106 @@ export function toJA4(bytes) {
164
159
"JA4_ro" : `${ ptype } ${ version } ${ sni } ${ cipherLen } ${ extLen } ${ alpn } _${ originalCiphersRaw } _${ originalExtensionsRaw } ` ,
165
160
} ;
166
161
}
162
+
163
+
164
+ /**
165
+ * Calculate the JA4Server from a given TLS Server Hello Stream
166
+ * @param {Uint8Array } bytes
167
+ * @returns {string }
168
+ */
169
+ export function toJA4S ( bytes ) {
170
+ let tlsr = { } ;
171
+ try {
172
+ tlsr = parseTLSRecord ( bytes ) ;
173
+ if ( tlsr . handshake . value . handshakeType . value !== 0x02 ) {
174
+ throw new Error ( ) ;
175
+ }
176
+ } catch ( err ) {
177
+ throw new OperationError ( "Data is not a valid TLS Server Hello. QUIC is not yet supported.\n" + err ) ;
178
+ }
179
+
180
+ /* QUIC
181
+ “q” or “t”, which denotes whether the hello packet is for QUIC or TCP.
182
+ TODO: Implement QUIC
183
+ */
184
+ const ptype = "t" ;
185
+
186
+ /* TLS Version
187
+ TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version
188
+ is the highest value in the extension. Remember to ignore GREASE values. If the extension doesn’t exist, then
189
+ the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet)
190
+ should be ignored.
191
+ */
192
+ let version = tlsr . version . value ;
193
+ for ( const ext of tlsr . handshake . value . extensions . value ) {
194
+ if ( ext . type . value === "supported_versions" ) {
195
+ version = parseHighestSupportedVersion ( ext . value . data ) ;
196
+ break ;
197
+ }
198
+ }
199
+ version = tlsVersionMapper ( version ) ;
200
+
201
+ /* Number of Extensions
202
+ 2 character number of cipher suites, so if there’s 6 cipher suites in the hello packet, then the value should be “06”.
203
+ If there’s > 99, which there should never be, then output “99”.
204
+ */
205
+ let extLen = tlsr . handshake . value . extensions . value . length ;
206
+ extLen = extLen > 99 ? "99" : extLen . toString ( ) . padStart ( 2 , "0" ) ;
207
+
208
+ /* ALPN Extension Chosen Value
209
+ The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value.
210
+ If there are no ALPN values or no ALPN extension then we print “00” as the value in the fingerprint.
211
+ */
212
+ let alpn = "00" ;
213
+ for ( const ext of tlsr . handshake . value . extensions . value ) {
214
+ if ( ext . type . value === "application_layer_protocol_negotiation" ) {
215
+ alpn = parseFirstALPNValue ( ext . value . data ) ;
216
+ alpn = alpn . charAt ( 0 ) + alpn . charAt ( alpn . length - 1 ) ;
217
+ if ( alpn . charCodeAt ( 0 ) > 127 ) alpn = "99" ;
218
+ break ;
219
+ }
220
+ }
221
+
222
+ /* Chosen Cipher
223
+ The hex value of the chosen cipher suite
224
+ */
225
+ const cipher = toHexFast ( tlsr . handshake . value . cipherSuite . data ) ;
226
+
227
+ /* Extension hash
228
+ A 12 character truncated sha256 hash of the list of extensions.
229
+ The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited.
230
+ */
231
+ const extensionsList = [ ] ;
232
+ for ( const ext of tlsr . handshake . value . extensions . value ) {
233
+ extensionsList . push ( toHexFast ( ext . type . data ) ) ;
234
+ }
235
+ const extensionsRaw = extensionsList . join ( "," ) ;
236
+ const extensionsHash = runHash (
237
+ "sha256" ,
238
+ Utils . strToArrayBuffer ( extensionsRaw )
239
+ ) . substring ( 0 , 12 ) ;
240
+
241
+ return {
242
+ "JA4S" : `${ ptype } ${ version } ${ extLen } ${ alpn } _${ cipher } _${ extensionsHash } ` ,
243
+ "JA4S_r" : `${ ptype } ${ version } ${ extLen } ${ alpn } _${ cipher } _${ extensionsRaw } ` ,
244
+ } ;
245
+ }
246
+
247
+
248
+ /**
249
+ * Takes a TLS version value and returns a JA4 TLS version string
250
+ * @param {Uint8Array } version - Two byte array of version number
251
+ * @returns {string }
252
+ */
253
+ function tlsVersionMapper ( version ) {
254
+ switch ( version ) {
255
+ case 0x0304 : return "13" ; // TLS 1.3
256
+ case 0x0303 : return "12" ; // TLS 1.2
257
+ case 0x0302 : return "11" ; // TLS 1.1
258
+ case 0x0301 : return "10" ; // TLS 1.0
259
+ case 0x0300 : return "s3" ; // SSL 3.0
260
+ case 0x0200 : return "s2" ; // SSL 2.0
261
+ case 0x0100 : return "s1" ; // SSL 1.0
262
+ default : return "00" ; // Unknown
263
+ }
264
+ }
0 commit comments