@anonympins/fingerprint 0.7.0 → 0.7.2
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.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/build-client.js +8 -10
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +1531 -1463
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/src/js/fingerprint.js +190 -16
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +340 -376
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +154 -56
- package/src/js/pow.solver.js +180 -92
- package/src/js/pow.worker.js +48 -48
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +124 -10
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +57 -11
- package/src/php/Config/SecurityProfiles.php +20 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +147 -148
- package/src/php/FingerprintEngine.php +44 -17
- package/src/php/Ja3AnomalyDetector.php +33 -32
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +26 -25
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +38 -0
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/BlockList.php +99 -99
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +129 -7
- package/src/php/Utils/TLSClientHelloParser.php +369 -369
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +554 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
- package/src/php/WordPress/package.php +218 -0
- package/src/php/bin/auto-tune.php +118 -118
|
@@ -1,370 +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
|
-
* 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
|
-
}
|
|
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
|
+
}
|
|
370
370
|
}
|