@anonympins/fingerprint 0.6.7 → 0.7.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +3 -7
  3. package/composer.json +8 -1
  4. package/package.json +1 -1
  5. package/public/fingerprint-wordpress.zip +0 -0
  6. package/public/fp.wasm +0 -0
  7. package/src/js/build-client.js +9 -11
  8. package/src/js/dynamic-wasm.js +7 -7
  9. package/src/js/fingerprint.builder.js +37 -38
  10. package/src/js/fingerprint.client.js +236 -152
  11. package/src/js/fingerprint.client.obfuscated.js +1 -1
  12. package/src/js/fingerprint.js +517 -101
  13. package/src/js/fingerprint.utils.js +213 -213
  14. package/src/js/library.js +355 -384
  15. package/src/js/mongodb-store.js +1 -1
  16. package/src/js/optimization.worker.js +27 -27
  17. package/src/js/pow.solver.inline.js +206 -62
  18. package/src/js/pow.solver.js +235 -101
  19. package/src/js/pow.worker.js +48 -26
  20. package/src/js/redis-store.js +1 -1
  21. package/src/js/tests/fingerprint.client.init.test.js +146 -143
  22. package/src/js/tests/fingerprint.test.js +101 -40
  23. package/src/js/tests/ip-reputation.test.js +1 -0
  24. package/src/js/upow-model-task.js +14 -15
  25. package/src/php/AutoTuner.php +34 -34
  26. package/src/php/Challenge/ChallengeUtils.php +204 -32
  27. package/src/php/Config/SecurityProfiles.php +21 -0
  28. package/src/php/DirectFingerprint.php +190 -145
  29. package/src/php/FingerprintBuilder.php +29 -30
  30. package/src/php/FingerprintClient.php +24 -25
  31. package/src/php/FingerprintEngine.php +1836 -1880
  32. package/src/php/Ja3AnomalyDetector.php +258 -227
  33. package/src/php/Optimization/FunctionRegistry.php +7 -7
  34. package/src/php/Optimization/Optimization.php +24 -24
  35. package/src/php/Optimization/OptimizationOperators.php +28 -27
  36. package/src/php/Optimization/ProblemInitializers.php +52 -52
  37. package/src/php/ProblemManager.php +31 -34
  38. package/src/php/RequestContext.php +8 -1
  39. package/src/php/Store/IStore.php +7 -8
  40. package/src/php/Store/InMemoryStore.php +66 -66
  41. package/src/php/Store/MongoDbStore.php +5 -5
  42. package/src/php/Store/RedisStore.php +5 -5
  43. package/src/php/Store/StoreManager.php +35 -35
  44. package/src/php/Tests/ChallengeUtilsTest.php +453 -453
  45. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  46. package/src/php/Tests/FingerprintEngineTest.php +662 -693
  47. package/src/php/Tests/IpReputationTest.php +10 -0
  48. package/src/php/Tests/MaliciousPatternsTest.php +103 -103
  49. package/src/php/Tests/MetricsTest.php +97 -97
  50. package/src/php/Tests/ProblemManagerTest.php +376 -376
  51. package/src/php/Tests/QuicFingerprintTest.php +53 -53
  52. package/src/php/Tests/RequestUtilsTest.php +471 -471
  53. package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
  54. package/src/php/Tests/config/ed25519_key.json +3 -3
  55. package/src/php/Utils/Env.php +49 -49
  56. package/src/php/Utils/MaliciousPatterns.php +74 -74
  57. package/src/php/Utils/MetricsManager.php +209 -209
  58. package/src/php/Utils/RequestUtils.php +236 -19
  59. package/src/php/Utils/TLSClientHelloParser.php +369 -212
  60. package/src/php/WordPress/WpDbStore.php +155 -0
  61. package/src/php/WordPress/fingerprint-wordpress.php +217 -0
  62. package/src/php/WordPress/package.php +123 -0
  63. package/src/php/bin/auto-tune.php +118 -118
@@ -1,213 +1,370 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Utils;
6
-
7
- /**
8
- * Décode nativement un paquet TLS Client Hello binaire pour calculer l'empreinte JA3/JA4.
9
- */
10
- class TLSClientHelloParser
11
- {
12
- private const GREASE_VALUES = [
13
- 2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
14
- 35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
15
- ];
16
-
17
- /**
18
- * Parse le Client Hello brut et retourne l'empreinte JA3 et une approximation JA4.
19
- *
20
- * @param string $binary Le premier paquet TCP reçu sur la socket.
21
- * @return array{ja3_string: string, ja3_hash: string, ja4_raw: string}|null
22
- */
23
- public static function parse(string $binary): ?array
24
- {
25
- $len = strlen($binary);
26
- if ($len < 43) {
27
- return null; // Paquet trop court
28
- }
29
-
30
- // 1. Vérification du Record Layer Type (0x16 = Handshake)
31
- if (ord($binary[0]) !== 0x16) {
32
- return null;
33
- }
34
-
35
- // 2. Vérification du Handshake Type (0x01 = Client Hello)
36
- if (ord($binary[5]) !== 0x01) {
37
- return null;
38
- }
39
-
40
- $offset = 43; // Sauter l'en-tête, la version et le Random Client (32 octets)
41
- if ($len < $offset + 1) return null;
42
-
43
- // 3. Lecture du Session ID
44
- $sessionLen = ord($binary[$offset]);
45
- $offset += 1 + $sessionLen;
46
- if ($len < $offset + 2) return null;
47
-
48
- // 4. Lecture des Cipher Suites
49
- $ciphersLen = unpack('n', substr($binary, $offset, 2))[1];
50
- $offset += 2;
51
- if ($len < $offset + $ciphersLen + 1) return null;
52
-
53
- $ciphers = [];
54
- for ($i = 0; $i < $ciphersLen; $i += 2) {
55
- $ciphers[] = unpack('n', substr($binary, $offset + $i, 2))[1];
56
- }
57
- $offset += $ciphersLen;
58
-
59
- // 5. Lecture des Compression Methods
60
- $compressionLen = ord($binary[$offset]);
61
- $offset += 1 + $compressionLen;
62
- if ($len < $offset + 2) return null;
63
-
64
- // 6. Lecture des Extensions
65
- $extensionsLen = unpack('n', substr($binary, $offset, 2))[1];
66
- $offset += 2;
67
-
68
- $extensions = [];
69
- $curves = [];
70
- $points = [];
71
- $sigAlgs = [];
72
- $supportedVersions = [];
73
- $hasSni = false;
74
- $alpnProtocol = '';
75
-
76
- $extLimit = $offset + $extensionsLen;
77
- while ($offset < $extLimit && $offset + 4 <= $len) {
78
- $extType = unpack('n', substr($binary, $offset, 2))[1];
79
- $extLen = unpack('n', substr($binary, $offset + 2, 2))[1];
80
- $offset += 4;
81
-
82
- if ($offset + $extLen > $len) break;
83
-
84
- $extensions[] = $extType;
85
-
86
- if ($extType === 0) {
87
- $hasSni = true;
88
- } elseif ($extType === 10) { // Extension Supported Groups (Elliptic Curves)
89
- if ($extLen >= 2) {
90
- $curvesLen = unpack('n', substr($binary, $offset, 2))[1];
91
- for ($j = 2; $j < $curvesLen + 2; $j += 2) {
92
- $curves[] = unpack('n', substr($binary, $offset + $j, 2))[1];
93
- }
94
- }
95
- } elseif ($extType === 11) { // Extension EC Point Formats
96
- if ($extLen >= 1) {
97
- $pointsLen = ord($binary[$offset]);
98
- for ($j = 1; $j < $pointsLen + 1; $j++) {
99
- $points[] = ord($binary[$offset + $j]);
100
- }
101
- }
102
- } elseif ($extType === 13) { // Signature Algorithms
103
- if ($extLen >= 2 && $offset + 2 <= $len) {
104
- $sigAlgsLen = unpack('n', substr($binary, $offset, 2))[1];
105
- for ($j = 2; $j < $sigAlgsLen + 2; $j += 2) {
106
- if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
107
- $sigAlgs[] = unpack('n', substr($binary, $offset + $j, 2))[1];
108
- }
109
- }
110
- }
111
- } elseif ($extType === 16) { // ALPN
112
- if ($extLen >= 3 && $offset + 2 <= $len) {
113
- $alpnListLen = unpack('n', substr($binary, $offset, 2))[1];
114
- if ($extLen >= 2 + $alpnListLen && $offset + 2 + $alpnListLen <= $len) {
115
- $alpnStrLen = ord($binary[$offset + 2]);
116
- if ($alpnListLen >= 1 + $alpnStrLen) {
117
- $alpnProtocol = substr($binary, $offset + 3, $alpnStrLen);
118
- }
119
- }
120
- }
121
- } elseif ($extType === 43) { // Supported Versions
122
- if ($extLen >= 1 && $offset + 1 <= $len) {
123
- $versionsLen = ord($binary[$offset]);
124
- for ($j = 1; $j < $versionsLen + 1; $j += 2) {
125
- if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
126
- $supportedVersions[] = unpack('n', substr($binary, $offset + $j, 2))[1];
127
- }
128
- }
129
- }
130
- }
131
- $offset += $extLen;
132
- }
133
-
134
- // Nettoyage des valeurs GREASE (RFC 8701) pour la conformité JA3
135
- $filterGrease = fn(array $arr) => array_values(array_filter($arr, fn($v) => !in_array($v, self::GREASE_VALUES, true)));
136
-
137
- $cleanCiphers = $filterGrease($ciphers);
138
- $cleanExtensions = $filterGrease($extensions);
139
- $cleanCurves = $filterGrease($curves);
140
- $cleanPoints = $filterGrease($points);
141
- $cleanSigAlgs = $filterGrease($sigAlgs);
142
- $cleanSupportedVersions = $filterGrease($supportedVersions);
143
-
144
- $sslVersion = unpack('n', substr($binary, 9, 2))[1];
145
- $ja3String = implode(',', [
146
- $sslVersion,
147
- implode('-', $cleanCiphers),
148
- implode('-', $cleanExtensions),
149
- implode('-', $cleanCurves),
150
- implode('-', $cleanPoints)
151
- ]);
152
-
153
- // Calcul natif de JA4
154
- $highestVersion = $sslVersion;
155
- if (!empty($cleanSupportedVersions)) {
156
- $highestVersion = max($cleanSupportedVersions);
157
- }
158
-
159
- $ja4Version = "12";
160
- if ($highestVersion === 0x0304) {
161
- $ja4Version = "13";
162
- } elseif ($highestVersion === 0x0303) {
163
- $ja4Version = "12";
164
- } elseif ($highestVersion === 0x0302) {
165
- $ja4Version = "11";
166
- } elseif ($highestVersion === 0x0301) {
167
- $ja4Version = "10";
168
- }
169
-
170
- $sniStatus = $hasSni ? "d" : "i";
171
- $numCiphers = min(99, count($cleanCiphers));
172
- $numExtensions = min(99, count($cleanExtensions));
173
-
174
- $ja4Alpn = "00";
175
- if ($alpnProtocol !== '') {
176
- $lenAlpn = strlen($alpnProtocol);
177
- if ($lenAlpn === 1) {
178
- $ja4Alpn = $alpnProtocol . $alpnProtocol;
179
- } else {
180
- $ja4Alpn = $alpnProtocol[0] . $alpnProtocol[$lenAlpn - 1];
181
- }
182
- }
183
-
184
- $ja4_a = "t" . $ja4Version . $sniStatus . sprintf("%02d", $numCiphers) . sprintf("%02d", $numExtensions) . $ja4Alpn;
185
-
186
- $sortedCiphers = $cleanCiphers;
187
- sort($sortedCiphers);
188
- $ciphersHex = array_map(fn($c) => sprintf('%04x', $c), $sortedCiphers);
189
- $ciphersString = implode(',', $ciphersHex);
190
- $ja4_b = substr(hash('sha256', $ciphersString), 0, 12);
191
-
192
- $sortedExtensions = $cleanExtensions;
193
- sort($sortedExtensions);
194
- $extensionsHex = array_map(fn($e) => sprintf('%04x', $e), $sortedExtensions);
195
- $extensionsString = implode(',', $extensionsHex);
196
-
197
- $sortedSigAlgs = $cleanSigAlgs;
198
- sort($sortedSigAlgs);
199
- $sigAlgsHex = array_map(fn($s) => sprintf('%04x', $s), $sortedSigAlgs);
200
- $sigAlgsString = implode(',', $sigAlgsHex);
201
-
202
- $ja4_c_input = $extensionsString . '_' . $sigAlgsString;
203
- $ja4_c = substr(hash('sha256', $ja4_c_input), 0, 12);
204
-
205
- $ja4_hash = $ja4_a . "_" . $ja4_b . "_" . $ja4_c;
206
-
207
- return [
208
- 'ja3_string' => $ja3String,
209
- 'ja3_hash' => md5($ja3String),
210
- 'ja4_raw' => $ja4_hash
211
- ];
212
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Utils;
6
+
7
+ /**
8
+ * Décode nativement un paquet TLS Client Hello binaire pour calculer l'empreinte JA3/JA4.
9
+ */
10
+ class TLSClientHelloParser
11
+ {
12
+ private const GREASE_VALUES = [
13
+ 2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
14
+ 35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
15
+ ];
16
+
17
+ /**
18
+ * Lit un entier encodé en longueur variable QUIC (RFC 9000 section 16).
19
+ */
20
+ public static function readVarInt(string $data, int &$offset): ?int
21
+ {
22
+ $len = strlen($data);
23
+ if ($offset >= $len) {
24
+ return null;
25
+ }
26
+
27
+ $first = ord($data[$offset]);
28
+ $prefix = $first >> 6;
29
+ $firstVal = $first & 0x3f;
30
+
31
+ if ($prefix === 0) {
32
+ $offset += 1;
33
+ return $firstVal;
34
+ } elseif ($prefix === 1) {
35
+ if ($offset + 2 > $len) return null;
36
+ $val = ($firstVal << 8) | ord($data[$offset + 1]);
37
+ $offset += 2;
38
+ return $val;
39
+ } elseif ($prefix === 2) {
40
+ if ($offset + 4 > $len) return null;
41
+ $val = ($firstVal << 24) | (ord($data[$offset + 1]) << 16) | (ord($data[$offset + 2]) << 8) | ord($data[$offset + 3]);
42
+ $offset += 4;
43
+ return $val;
44
+ } else {
45
+ if ($offset + 8 > $len) return null;
46
+ $val = $firstVal;
47
+ for ($i = 1; $i < 8; $i++) {
48
+ $val = ($val << 8) | ord($data[$offset + $i]);
49
+ }
50
+ $offset += 8;
51
+ return $val;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Parse les paramètres de transport QUIC (RFC 9000 section 18.2).
57
+ */
58
+ public static function parseQuicTransportParameters(string $data): array
59
+ {
60
+ $params = [];
61
+ $offset = 0;
62
+ $len = strlen($data);
63
+
64
+ while ($offset < $len) {
65
+ $paramId = self::readVarInt($data, $offset);
66
+ if ($paramId === null) break;
67
+ $paramLen = self::readVarInt($data, $offset);
68
+ if ($paramLen === null || $offset + $paramLen > $len) break;
69
+
70
+ $paramValBytes = substr($data, $offset, $paramLen);
71
+ $offset += $paramLen;
72
+
73
+ if ($paramLen > 0 && in_array($paramId, [1, 3, 4, 5, 6, 7, 8, 9, 11, 14], true)) {
74
+ $valOffset = 0;
75
+ $numVal = self::readVarInt($paramValBytes, $valOffset);
76
+ $params[$paramId] = $numVal ?? $paramValBytes;
77
+ } else {
78
+ $params[$paramId] = bin2hex($paramValBytes);
79
+ }
80
+ }
81
+
82
+ return $params;
83
+ }
84
+
85
+ /**
86
+ * Analyse l'ordre des trames de contrôle QUIC / HTTP/3 (SETTINGS, MAX_STREAMS, PRIORITY).
87
+ */
88
+ public static function parseQuicControlFrames(string $streamData): array
89
+ {
90
+ $frames = [];
91
+ $frameOrder = [];
92
+ $settings = [];
93
+ $offset = 0;
94
+ $len = strlen($streamData);
95
+
96
+ if ($len === 0) {
97
+ return ['frames' => [], 'frame_order' => '', 'settings' => []];
98
+ }
99
+
100
+ if (ord($streamData[0]) === 0x00) {
101
+ $offset = 1; // Stream type Control Stream
102
+ }
103
+
104
+ while ($offset < $len) {
105
+ $frameType = self::readVarInt($streamData, $offset);
106
+ if ($frameType === null) break;
107
+ $frameLen = self::readVarInt($streamData, $offset);
108
+ if ($frameLen === null || $offset + $frameLen > $len) break;
109
+
110
+ $payload = substr($streamData, $offset, $frameLen);
111
+ $offset += $frameLen;
112
+
113
+ $abbr = match ($frameType) {
114
+ 0x04 => 's', // SETTINGS
115
+ 0x12, 0x02 => 'm', // MAX_STREAMS
116
+ 0x0f, 0xaf, 0xf0700 => 'p', // PRIORITY_UPDATE
117
+ 0x10, 0x0d => 'd', // MAX_DATA
118
+ 0x07 => 'g', // GOAWAY
119
+ default => 'u'
120
+ };
121
+
122
+ $frames[] = ['type' => $frameType, 'length' => $frameLen];
123
+ $frameOrder[] = $abbr;
124
+
125
+ if ($frameType === 0x04) {
126
+ $sOffset = 0;
127
+ $sLen = strlen($payload);
128
+ while ($sOffset < $sLen) {
129
+ $sId = self::readVarInt($payload, $sOffset);
130
+ if ($sId === null) break;
131
+ $sVal = self::readVarInt($payload, $sOffset);
132
+ if ($sVal === null) break;
133
+ $settings[$sId] = $sVal;
134
+ }
135
+ }
136
+ }
137
+
138
+ return [
139
+ 'frames' => $frames,
140
+ 'frame_order' => implode(',', $frameOrder),
141
+ 'settings' => $settings
142
+ ];
143
+ }
144
+
145
+ public static function formatQuicFingerprint(array $params, string $priority = '', string $frameOrder = ''): string
146
+ {
147
+ $paramParts = [];
148
+ foreach ($params as $k => $v) {
149
+ $paramParts[] = "{$k}={$v}";
150
+ }
151
+ $fp = "1;" . implode(',', $paramParts);
152
+ if ($priority !== '' || $frameOrder !== '') {
153
+ $fp .= ";{$priority}";
154
+ }
155
+ if ($frameOrder !== '') {
156
+ $fp .= ";{$frameOrder}";
157
+ }
158
+ return $fp;
159
+ }
160
+
161
+ /**
162
+ * Parse le Client Hello brut et retourne l'empreinte JA3 et une approximation JA4.
163
+ *
164
+ * @param string $binary Le premier paquet TCP reçu sur la socket.
165
+ * @return array{ja3_string: string, ja3_hash: string, ja4_raw: string}|null
166
+ */
167
+ public static function parse(string $binary): ?array
168
+ {
169
+ $len = strlen($binary);
170
+ if ($len < 43) {
171
+ return null; // Paquet trop court
172
+ }
173
+
174
+ // 1. Vérification du Record Layer Type (0x16 = Handshake)
175
+ if (ord($binary[0]) !== 0x16) {
176
+ return null;
177
+ }
178
+
179
+ // 2. Vérification du Handshake Type (0x01 = Client Hello)
180
+ if (ord($binary[5]) !== 0x01) {
181
+ return null;
182
+ }
183
+
184
+ $offset = 43; // Sauter l'en-tête, la version et le Random Client (32 octets)
185
+ if ($len < $offset + 1) return null;
186
+
187
+ // 3. Lecture du Session ID
188
+ $sessionLen = ord($binary[$offset]);
189
+ $offset += 1 + $sessionLen;
190
+ if ($len < $offset + 2) return null;
191
+
192
+ // 4. Lecture des Cipher Suites
193
+ $ciphersLen = unpack('n', substr($binary, $offset, 2))[1];
194
+ $offset += 2;
195
+ if ($len < $offset + $ciphersLen + 1) return null;
196
+
197
+ $ciphers = [];
198
+ for ($i = 0; $i < $ciphersLen; $i += 2) {
199
+ $ciphers[] = unpack('n', substr($binary, $offset + $i, 2))[1];
200
+ }
201
+ $offset += $ciphersLen;
202
+
203
+ // 5. Lecture des Compression Methods
204
+ $compressionLen = ord($binary[$offset]);
205
+ $offset += 1 + $compressionLen;
206
+ if ($len < $offset + 2) return null;
207
+
208
+ // 6. Lecture des Extensions
209
+ $extensionsLen = unpack('n', substr($binary, $offset, 2))[1];
210
+ $offset += 2;
211
+
212
+ $extensions = [];
213
+ $curves = [];
214
+ $points = [];
215
+ $sigAlgs = [];
216
+ $supportedVersions = [];
217
+ $hasSni = false;
218
+ $alpnProtocol = '';
219
+ $quicParams = null;
220
+
221
+ $extLimit = $offset + $extensionsLen;
222
+ while ($offset < $extLimit && $offset + 4 <= $len) {
223
+ $extType = unpack('n', substr($binary, $offset, 2))[1];
224
+ $extLen = unpack('n', substr($binary, $offset + 2, 2))[1];
225
+ $offset += 4;
226
+
227
+ if ($offset + $extLen > $len) break;
228
+
229
+ $extensions[] = $extType;
230
+
231
+ if ($extType === 0) {
232
+ $hasSni = true;
233
+ } elseif ($extType === 10) { // Extension Supported Groups (Elliptic Curves)
234
+ if ($extLen >= 2) {
235
+ $curvesLen = unpack('n', substr($binary, $offset, 2))[1];
236
+ for ($j = 2; $j < $curvesLen + 2; $j += 2) {
237
+ $curves[] = unpack('n', substr($binary, $offset + $j, 2))[1];
238
+ }
239
+ }
240
+ } elseif ($extType === 11) { // Extension EC Point Formats
241
+ if ($extLen >= 1) {
242
+ $pointsLen = ord($binary[$offset]);
243
+ for ($j = 1; $j < $pointsLen + 1; $j++) {
244
+ $points[] = ord($binary[$offset + $j]);
245
+ }
246
+ }
247
+ } elseif ($extType === 13) { // Signature Algorithms
248
+ if ($extLen >= 2 && $offset + 2 <= $len) {
249
+ $sigAlgsLen = unpack('n', substr($binary, $offset, 2))[1];
250
+ for ($j = 2; $j < $sigAlgsLen + 2; $j += 2) {
251
+ if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
252
+ $sigAlgs[] = unpack('n', substr($binary, $offset + $j, 2))[1];
253
+ }
254
+ }
255
+ }
256
+ } elseif ($extType === 16) { // ALPN
257
+ if ($extLen >= 3 && $offset + 2 <= $len) {
258
+ $alpnListLen = unpack('n', substr($binary, $offset, 2))[1];
259
+ if ($extLen >= 2 + $alpnListLen && $offset + 2 + $alpnListLen <= $len) {
260
+ $alpnStrLen = ord($binary[$offset + 2]);
261
+ if ($alpnListLen >= 1 + $alpnStrLen) {
262
+ $alpnProtocol = substr($binary, $offset + 3, $alpnStrLen);
263
+ }
264
+ }
265
+ }
266
+ } elseif ($extType === 43) { // Supported Versions
267
+ if ($extLen >= 1 && $offset + 1 <= $len) {
268
+ $versionsLen = ord($binary[$offset]);
269
+ for ($j = 1; $j < $versionsLen + 1; $j += 2) {
270
+ if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
271
+ $supportedVersions[] = unpack('n', substr($binary, $offset + $j, 2))[1];
272
+ }
273
+ }
274
+ }
275
+ } elseif ($extType === 57 || $extType === 0xffa5) { // quic_transport_parameters (RFC 9001 / draft)
276
+ if ($extLen > 0 && $offset + $extLen <= $len) {
277
+ $quicData = substr($binary, $offset, $extLen);
278
+ $quicParams = self::parseQuicTransportParameters($quicData);
279
+ }
280
+ }
281
+ $offset += $extLen;
282
+ }
283
+
284
+ // Nettoyage des valeurs GREASE (RFC 8701) pour la conformité JA3
285
+ $filterGrease = fn(array $arr) => array_values(array_filter($arr, fn($v) => !in_array($v, self::GREASE_VALUES, true)));
286
+
287
+ $cleanCiphers = $filterGrease($ciphers);
288
+ $cleanExtensions = $filterGrease($extensions);
289
+ $cleanCurves = $filterGrease($curves);
290
+ $cleanPoints = $filterGrease($points);
291
+ $cleanSigAlgs = $filterGrease($sigAlgs);
292
+ $cleanSupportedVersions = $filterGrease($supportedVersions);
293
+
294
+ $sslVersion = unpack('n', substr($binary, 9, 2))[1];
295
+ $ja3String = implode(',', [
296
+ $sslVersion,
297
+ implode('-', $cleanCiphers),
298
+ implode('-', $cleanExtensions),
299
+ implode('-', $cleanCurves),
300
+ implode('-', $cleanPoints)
301
+ ]);
302
+
303
+ // Calcul natif de JA4
304
+ $highestVersion = $sslVersion;
305
+ if (!empty($cleanSupportedVersions)) {
306
+ $highestVersion = max($cleanSupportedVersions);
307
+ }
308
+
309
+ $ja4Version = "12";
310
+ if ($highestVersion === 0x0304) {
311
+ $ja4Version = "13";
312
+ } elseif ($highestVersion === 0x0303) {
313
+ $ja4Version = "12";
314
+ } elseif ($highestVersion === 0x0302) {
315
+ $ja4Version = "11";
316
+ } elseif ($highestVersion === 0x0301) {
317
+ $ja4Version = "10";
318
+ }
319
+
320
+ $sniStatus = $hasSni ? "d" : "i";
321
+ $numCiphers = min(99, count($cleanCiphers));
322
+ $numExtensions = min(99, count($cleanExtensions));
323
+
324
+ $ja4Alpn = "00";
325
+ if ($alpnProtocol !== '') {
326
+ $lenAlpn = strlen($alpnProtocol);
327
+ if ($lenAlpn === 1) {
328
+ $ja4Alpn = $alpnProtocol . $alpnProtocol;
329
+ } else {
330
+ $ja4Alpn = $alpnProtocol[0] . $alpnProtocol[$lenAlpn - 1];
331
+ }
332
+ }
333
+
334
+ $ja4_a = "t" . $ja4Version . $sniStatus . sprintf("%02d", $numCiphers) . sprintf("%02d", $numExtensions) . $ja4Alpn;
335
+
336
+ $sortedCiphers = $cleanCiphers;
337
+ sort($sortedCiphers);
338
+ $ciphersHex = array_map(fn($c) => sprintf('%04x', $c), $sortedCiphers);
339
+ $ciphersString = implode(',', $ciphersHex);
340
+ $ja4_b = substr(hash('sha256', $ciphersString), 0, 12);
341
+
342
+ $sortedExtensions = $cleanExtensions;
343
+ sort($sortedExtensions);
344
+ $extensionsHex = array_map(fn($e) => sprintf('%04x', $e), $sortedExtensions);
345
+ $extensionsString = implode(',', $extensionsHex);
346
+
347
+ $sortedSigAlgs = $cleanSigAlgs;
348
+ sort($sortedSigAlgs);
349
+ $sigAlgsHex = array_map(fn($s) => sprintf('%04x', $s), $sortedSigAlgs);
350
+ $sigAlgsString = implode(',', $sigAlgsHex);
351
+
352
+ $ja4_c_input = $extensionsString . '_' . $sigAlgsString;
353
+ $ja4_c = substr(hash('sha256', $ja4_c_input), 0, 12);
354
+
355
+ $ja4_hash = $ja4_a . "_" . $ja4_b . "_" . $ja4_c;
356
+
357
+ $result = [
358
+ 'ja3_string' => $ja3String,
359
+ 'ja3_hash' => md5($ja3String),
360
+ 'ja4_raw' => $ja4_hash
361
+ ];
362
+
363
+ if ($quicParams !== null) {
364
+ $result['quic_params'] = $quicParams;
365
+ $result['quic_fp'] = self::formatQuicFingerprint($quicParams);
366
+ }
367
+
368
+ return $result;
369
+ }
213
370
  }