ja4plus 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,387 @@
1
+ """
2
+ Enhanced TLS utility functions for JA4+ fingerprinting.
3
+ """
4
+
5
+ import struct
6
+ from scapy.all import Raw
7
+
8
+
9
+ def extract_tls_info(packet):
10
+ """
11
+ Extract TLS information from a packet.
12
+
13
+ Args:
14
+ packet: A network packet
15
+
16
+ Returns:
17
+ Dictionary with TLS handshake information or None if not applicable
18
+ """
19
+ # Special handling for test packets with embedded TLS info
20
+ if hasattr(packet, 'tls_info'):
21
+ return packet.tls_info
22
+
23
+ if Raw not in packet:
24
+ return None
25
+
26
+ try:
27
+ raw_data = bytes(packet[Raw])
28
+ return parse_tls_handshake(raw_data)
29
+ except Exception:
30
+ return None
31
+
32
+
33
+ def parse_tls_handshake(raw_data):
34
+ """
35
+ Parse a TLS handshake message from raw bytes.
36
+
37
+ Args:
38
+ raw_data: Raw bytes of a TLS record
39
+
40
+ Returns:
41
+ Dictionary with TLS handshake information or None
42
+ """
43
+ # Check minimum length for TLS record header
44
+ if len(raw_data) < 5:
45
+ return None
46
+
47
+ record_type = raw_data[0]
48
+ if record_type != 0x16: # 0x16 = Handshake
49
+ return None
50
+
51
+ # Get TLS record version and length
52
+ record_length = (raw_data[3] << 8) | raw_data[4]
53
+
54
+ # Ensure we have enough data
55
+ if len(raw_data) < 5 + record_length:
56
+ return None
57
+
58
+ # Extract handshake type
59
+ if len(raw_data) < 6:
60
+ return None
61
+
62
+ handshake_type = raw_data[5]
63
+
64
+ if handshake_type == 1:
65
+ return _parse_client_hello(raw_data)
66
+ elif handshake_type == 2:
67
+ return _parse_server_hello(raw_data)
68
+ else:
69
+ return None
70
+
71
+
72
+ def _parse_client_hello(raw_data):
73
+ """Parse a TLS ClientHello message."""
74
+ if len(raw_data) < 11:
75
+ return None
76
+
77
+ # ClientHello version is at offset 9-10 (after record header + handshake header)
78
+ version = (raw_data[9] << 8) | raw_data[10]
79
+
80
+ tls_info = {
81
+ 'handshake_type': 'client_hello',
82
+ 'type': 'client_hello',
83
+ 'version': version,
84
+ 'is_quic': False,
85
+ 'is_dtls': False,
86
+ }
87
+
88
+ # Skip past record header (5) + handshake header (4) + version (2) + random (32)
89
+ pos = 11 + 32
90
+
91
+ # Session ID
92
+ if pos + 1 > len(raw_data):
93
+ return tls_info
94
+ session_id_len = raw_data[pos]
95
+ pos += 1 + session_id_len
96
+
97
+ # Cipher suites
98
+ if pos + 2 > len(raw_data):
99
+ return tls_info
100
+ cipher_suites_len = (raw_data[pos] << 8) | raw_data[pos + 1]
101
+ pos += 2
102
+
103
+ ciphers = []
104
+ for i in range(0, cipher_suites_len, 2):
105
+ if pos + i + 2 > len(raw_data):
106
+ break
107
+ cipher = (raw_data[pos + i] << 8) | raw_data[pos + i + 1]
108
+ ciphers.append(cipher)
109
+
110
+ tls_info['ciphers'] = ciphers
111
+ pos += cipher_suites_len
112
+
113
+ # Compression methods
114
+ if pos + 1 > len(raw_data):
115
+ return tls_info
116
+ compression_len = raw_data[pos]
117
+ pos += 1 + compression_len
118
+
119
+ # Parse extensions
120
+ extensions = []
121
+ extension_data = {}
122
+ supported_versions = []
123
+ alpn_protocols = []
124
+ signature_algorithms = []
125
+ sni = None
126
+
127
+ if pos + 2 <= len(raw_data):
128
+ extensions_len = (raw_data[pos] << 8) | raw_data[pos + 1]
129
+ pos += 2
130
+ extensions_end = min(pos + extensions_len, len(raw_data))
131
+
132
+ while pos + 4 <= extensions_end:
133
+ ext_type = (raw_data[pos] << 8) | raw_data[pos + 1]
134
+ ext_len = (raw_data[pos + 2] << 8) | raw_data[pos + 3]
135
+ ext_data_start = pos + 4
136
+ ext_data_end = min(ext_data_start + ext_len, len(raw_data))
137
+
138
+ extensions.append(ext_type)
139
+
140
+ # Parse SNI (0x0000)
141
+ if ext_type == 0x0000:
142
+ sni = _parse_sni(raw_data[ext_data_start:ext_data_end])
143
+
144
+ # Parse supported_versions (0x002b)
145
+ elif ext_type == 0x002b:
146
+ supported_versions = _parse_supported_versions_client(
147
+ raw_data[ext_data_start:ext_data_end]
148
+ )
149
+
150
+ # Parse ALPN (0x0010)
151
+ elif ext_type == 0x0010:
152
+ alpn_protocols = _parse_alpn(raw_data[ext_data_start:ext_data_end])
153
+
154
+ # Parse signature_algorithms (0x000d)
155
+ elif ext_type == 0x000d:
156
+ signature_algorithms = _parse_signature_algorithms(
157
+ raw_data[ext_data_start:ext_data_end]
158
+ )
159
+
160
+ pos = ext_data_start + ext_len
161
+
162
+ tls_info['extensions'] = extensions
163
+ tls_info['extension_data'] = extension_data
164
+ tls_info['supported_versions'] = supported_versions
165
+ tls_info['alpn_protocols'] = alpn_protocols
166
+ tls_info['signature_algorithms'] = signature_algorithms
167
+ if sni is not None:
168
+ tls_info['sni'] = sni
169
+
170
+ return tls_info
171
+
172
+
173
+ def _parse_server_hello(raw_data):
174
+ """Parse a TLS ServerHello message."""
175
+ if len(raw_data) < 11:
176
+ return None
177
+
178
+ # ServerHello version at offset 9-10
179
+ version = (raw_data[9] << 8) | raw_data[10]
180
+
181
+ tls_info = {
182
+ 'handshake_type': 'server_hello',
183
+ 'type': 'server_hello',
184
+ 'version': version,
185
+ 'is_quic': False,
186
+ }
187
+
188
+ # Skip past record header (5) + handshake header (4) + version (2) + random (32)
189
+ pos = 11 + 32
190
+
191
+ # Session ID
192
+ if pos + 1 > len(raw_data):
193
+ return tls_info
194
+ session_id_len = raw_data[pos]
195
+ pos += 1 + session_id_len
196
+
197
+ # Cipher suite (single cipher for ServerHello)
198
+ if pos + 2 > len(raw_data):
199
+ return tls_info
200
+ cipher = (raw_data[pos] << 8) | raw_data[pos + 1]
201
+ tls_info['cipher'] = cipher
202
+ pos += 2
203
+
204
+ # Compression method
205
+ if pos + 1 > len(raw_data):
206
+ return tls_info
207
+ pos += 1
208
+
209
+ # Parse extensions
210
+ extensions = []
211
+ extension_data = {}
212
+ alpn_protocols = []
213
+ supported_versions = []
214
+
215
+ if pos + 2 <= len(raw_data):
216
+ extensions_len = (raw_data[pos] << 8) | raw_data[pos + 1]
217
+ pos += 2
218
+ extensions_end = min(pos + extensions_len, len(raw_data))
219
+
220
+ while pos + 4 <= extensions_end:
221
+ ext_type = (raw_data[pos] << 8) | raw_data[pos + 1]
222
+ ext_len = (raw_data[pos + 2] << 8) | raw_data[pos + 3]
223
+ ext_data_start = pos + 4
224
+ ext_data_end = min(ext_data_start + ext_len, len(raw_data))
225
+
226
+ extensions.append(ext_type)
227
+
228
+ # Parse ALPN (0x0010)
229
+ if ext_type == 0x0010:
230
+ alpn_protocols = _parse_alpn(raw_data[ext_data_start:ext_data_end])
231
+ extension_data[0x0010] = {'protocols': alpn_protocols}
232
+
233
+ # Parse supported_versions (0x002b) - server selects one version
234
+ elif ext_type == 0x002b:
235
+ if ext_len >= 2:
236
+ sv = (raw_data[ext_data_start] << 8) | raw_data[ext_data_start + 1]
237
+ supported_versions = [sv]
238
+
239
+ pos = ext_data_start + ext_len
240
+
241
+ tls_info['extensions'] = extensions
242
+ tls_info['extension_data'] = extension_data
243
+ tls_info['alpn_protocols'] = alpn_protocols
244
+ tls_info['supported_versions'] = supported_versions
245
+
246
+ # If supported_versions indicates TLS 1.3, update the version
247
+ if supported_versions:
248
+ non_grease = [v for v in supported_versions if not is_grease_value(v)]
249
+ if non_grease:
250
+ tls_info['version'] = non_grease[0]
251
+
252
+ return tls_info
253
+
254
+
255
+ def _parse_sni(data):
256
+ """Parse Server Name Indication extension data."""
257
+ if len(data) < 5:
258
+ return True # Extension exists but can't parse hostname
259
+
260
+ try:
261
+ # SNI list length (2 bytes)
262
+ sni_list_len = (data[0] << 8) | data[1]
263
+ pos = 2
264
+
265
+ if pos + 3 > len(data):
266
+ return True
267
+
268
+ # SNI type (1 byte) - 0 = hostname
269
+ sni_type = data[pos]
270
+ pos += 1
271
+
272
+ # Hostname length (2 bytes)
273
+ hostname_len = (data[pos] << 8) | data[pos + 1]
274
+ pos += 2
275
+
276
+ if sni_type == 0 and pos + hostname_len <= len(data):
277
+ hostname = data[pos:pos + hostname_len].decode('ascii', errors='ignore')
278
+ return hostname if hostname else True
279
+
280
+ return True
281
+ except Exception:
282
+ return True
283
+
284
+
285
+ def _parse_supported_versions_client(data):
286
+ """Parse supported_versions extension from ClientHello."""
287
+ versions = []
288
+ if len(data) < 1:
289
+ return versions
290
+
291
+ try:
292
+ # First byte is the length of the version list
293
+ list_len = data[0]
294
+ pos = 1
295
+
296
+ while pos + 2 <= min(1 + list_len, len(data)):
297
+ ver = (data[pos] << 8) | data[pos + 1]
298
+ versions.append(ver)
299
+ pos += 2
300
+ except Exception:
301
+ pass
302
+
303
+ return versions
304
+
305
+
306
+ def _parse_alpn(data):
307
+ """Parse Application-Layer Protocol Negotiation extension data."""
308
+ protocols = []
309
+ if len(data) < 2:
310
+ return protocols
311
+
312
+ try:
313
+ # ALPN list length (2 bytes)
314
+ alpn_list_len = (data[0] << 8) | data[1]
315
+ pos = 2
316
+
317
+ while pos < min(2 + alpn_list_len, len(data)):
318
+ if pos + 1 > len(data):
319
+ break
320
+ proto_len = data[pos]
321
+ pos += 1
322
+
323
+ if pos + proto_len > len(data):
324
+ break
325
+ protocol = data[pos:pos + proto_len].decode('ascii', errors='ignore')
326
+ protocols.append(protocol)
327
+ pos += proto_len
328
+ except Exception:
329
+ pass
330
+
331
+ return protocols
332
+
333
+
334
+ def _parse_signature_algorithms(data):
335
+ """Parse signature_algorithms extension data."""
336
+ algorithms = []
337
+ if len(data) < 2:
338
+ return algorithms
339
+
340
+ try:
341
+ # Signature algorithms list length (2 bytes)
342
+ list_len = (data[0] << 8) | data[1]
343
+ pos = 2
344
+
345
+ while pos + 2 <= min(2 + list_len, len(data)):
346
+ alg = (data[pos] << 8) | data[pos + 1]
347
+ algorithms.append(alg)
348
+ pos += 2
349
+ except Exception:
350
+ pass
351
+
352
+ return algorithms
353
+
354
+
355
+ def is_grease_value(value):
356
+ """
357
+ Check if a value is a TLS GREASE value.
358
+
359
+ GREASE values match the pattern 0x?A?A where ? is the same nibble.
360
+ The canonical check is: (value & 0x0F0F) == 0x0A0A and high byte == low byte.
361
+
362
+ Known GREASE values: 0x0A0A, 0x1A1A, 0x2A2A, 0x3A3A, 0x4A4A, 0x5A5A,
363
+ 0x6A6A, 0x7A7A, 0x8A8A, 0x9A9A, 0xAAAA, 0xBABA,
364
+ 0xCACA, 0xDADA, 0xEAEA, 0xFAFA
365
+ """
366
+ if not value and value != 0:
367
+ return False
368
+
369
+ try:
370
+ if isinstance(value, str):
371
+ int_val = int(value, 16)
372
+ elif isinstance(value, int):
373
+ int_val = value
374
+ else:
375
+ return False
376
+
377
+ return (int_val & 0x0F0F) == 0x0A0A and ((int_val >> 8) & 0xFF) == (int_val & 0xFF)
378
+ except (ValueError, TypeError):
379
+ return False
380
+
381
+
382
+ def find_tls_extension(extensions, extension_type):
383
+ """Find a specific TLS extension by type."""
384
+ for ext in extensions:
385
+ if hasattr(ext, 'type') and ext.type == extension_type:
386
+ return ext
387
+ return None