@anonympins/fingerprint 0.6.7 → 0.7.0

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.
@@ -1025,13 +1025,20 @@ class RequestUtils
1025
1025
 
1026
1026
  public static function getThreatIntelScore(RequestContext $context, array $threatIntelConfig): array
1027
1027
  {
1028
+ $score = 0.0;
1028
1029
  $zkpY = $context->zkpY;
1029
- if (empty($zkpY)) {
1030
- return ['threatIntelScore' => 0.0];
1030
+ if (!empty($zkpY)) {
1031
+ $store = StoreManager::getStore();
1032
+ $isBanned = $store->has("banned-zkp-y:{$zkpY}");
1033
+ if ($isBanned) {
1034
+ $score = 100.0;
1035
+ }
1031
1036
  }
1032
- $store = StoreManager::getStore();
1033
- $isBanned = $store->has("banned-zkp-y:{$zkpY}");
1034
- return ['threatIntelScore' => $isBanned ? 100.0 : 0.0];
1037
+
1038
+ $rttProxyScore = \Anonympins\Fingerprint\Ja3AnomalyDetector::getRttProxyScore($context);
1039
+ $score = max($score, (float)$rttProxyScore);
1040
+
1041
+ return ['threatIntelScore' => $score];
1035
1042
  }
1036
1043
 
1037
1044
  /**
@@ -1355,23 +1362,24 @@ class RequestUtils
1355
1362
  return ['subnetScore' => 0.0];
1356
1363
  }
1357
1364
 
1358
- $suspicionDensity = $highScoreCount / $deviceCount;
1359
- $ipDeviceRatio = $ipCount / $deviceCount;
1365
+ // 1. Régularisation bayésienne continue de la densité
1366
+ $bayesianDensity = ((float)$highScoreCount + 0.5) / ((float)$deviceCount + 2.5);
1360
1367
 
1361
- // Ratio User-Agent / Device : détecte la rotation/spoofing de navigateurs sur une même empreinte matérielle
1362
- $uaDeviceRatio = (double)max(1, $uaCount) / $deviceCount;
1363
- $uaMultiplier = 0.6 + (0.4 * min(2.5, $uaDeviceRatio));
1368
+ // 2. Dispersion IP / Terminal (CGNAT vs Proxy Pool)
1369
+ $ipDispersion = min(2.0, (float)$ipCount / (float)$deviceCount);
1370
+ $ipMultiplier = 0.6 + 0.4 * tanh($ipDispersion);
1364
1371
 
1365
- $baseScore = 100.0 * (1.0 - exp(-0.15 * $highScoreCount));
1372
+ // 3. Dispersion User-Agent / Terminal
1373
+ $uaDispersion = min(3.0, (float)max(1, $uaCount) / (float)$deviceCount);
1374
+ $uaMultiplier = 0.7 + 0.3 * tanh($uaDispersion - 1.0);
1366
1375
 
1367
- $densityMultiplier = 0.4 + (1.6 * $suspicionDensity);
1368
- $distributionMultiplier = 0.5 + (1.0 * $ipDeviceRatio);
1376
+ // 4. Intensité continue de la menace
1377
+ $rawIntensity = (float)$highScoreCount * $bayesianDensity * $ipMultiplier * $uaMultiplier;
1369
1378
 
1370
- $dampening = 1.0;
1371
- if( $highScoreCount < 3 ){
1372
- $dampening = $highScoreCount / 3.0;
1373
- }
1374
- $finalScore = min(100.0, round($baseScore * $densityMultiplier * $distributionMultiplier * $uaMultiplier * $dampening * 10.0) / 10.0);
1379
+ // 5. Asymptote continue via tanh (99.9 maximum strict, dérivable et sans saut)
1380
+ $asymptote = 99.9;
1381
+ $scale = 4.0;
1382
+ $finalScore = round($asymptote * tanh($rawIntensity / $scale) * 10.0) / 10.0;
1375
1383
 
1376
1384
  return ['subnetScore' => $finalScore];
1377
1385
  }
@@ -2031,4 +2039,91 @@ class RequestUtils
2031
2039
 
2032
2040
  return !empty($deviceId) && !empty($deviceHash); // Match d'identité matérielle stricte (deviceId + deviceHash validés par HMAC)
2033
2041
  }
2042
+
2043
+ private static function imul(int $a, int $b): int
2044
+ {
2045
+ return ($a * $b) & 0xffffffff;
2046
+ }
2047
+
2048
+ public static function cyrb53(string $str, int $seed = 0): int
2049
+ {
2050
+ $h1 = (0xdeadbeef ^ $seed) & 0xffffffff;
2051
+ $h2 = (0x41c6ce57 ^ $seed) & 0xffffffff;
2052
+ for ($i = 0; $i < strlen($str); $i++) {
2053
+ $ch = ord($str[$i]);
2054
+ $h1 = self::imul($h1 ^ $ch, 2654435761);
2055
+ $h2 = self::imul($h2 ^ $ch, 1597334677);
2056
+ }
2057
+ $h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
2058
+ $h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
2059
+ $unsigned_h1 = $h1 & 0xffffffff;
2060
+ return 4294967296 * (2097151 & $h2) + $unsigned_h1;
2061
+ }
2062
+
2063
+ public static function getVirtualizationAnomalyScore(RequestContext $context): float
2064
+ {
2065
+ $clientFp = $context->headers['x-device-fingerprint'] ?? null;
2066
+ if (!$clientFp) {
2067
+ return 0.0;
2068
+ }
2069
+
2070
+ $fpMap = [];
2071
+ foreach (explode('|', $clientFp) as $part) {
2072
+ $pair = explode(':', $part, 2);
2073
+ if (count($pair) === 2) {
2074
+ $fpMap[$pair[0]] = $pair[1];
2075
+ }
2076
+ }
2077
+
2078
+ $score = 0.0;
2079
+ $clientGpuHash = $fpMap['gpu'] ?? null;
2080
+ if ($clientGpuHash) {
2081
+ $virtualGpus = [
2082
+ "Google SwiftShader", "SwiftShader",
2083
+ "Mesa llvmpipe", "llvmpipe", "Mesa Gallium",
2084
+ "Microsoft Basic Render Driver", "HeadlessChrome",
2085
+ "Intel(R) HD Graphics"
2086
+ ];
2087
+ $virtualGpuHashes = [];
2088
+ foreach ($virtualGpus as $gpu) {
2089
+ $virtualGpuHashes[] = (string)self::cyrb53($gpu);
2090
+ }
2091
+ if (in_array($clientGpuHash, $virtualGpuHashes, true)) {
2092
+ $score += 75.0;
2093
+ }
2094
+ }
2095
+
2096
+ $clientScreenHash = $fpMap['scr'] ?? null;
2097
+ if ($clientScreenHash) {
2098
+ $headlessResolutions = ["800x600_24", "1024x768_24"];
2099
+ $headlessHashes = [];
2100
+ foreach ($headlessResolutions as $res) {
2101
+ $headlessHashes[] = (string)self::cyrb53($res);
2102
+ }
2103
+ if (in_array($clientScreenHash, $headlessHashes, true)) {
2104
+ $score += 25.0;
2105
+ }
2106
+ }
2107
+
2108
+ return min(100.0, $score);
2109
+ }
2110
+
2111
+ /**
2112
+ * Calcule un score d'incohérence analogique et lisse plafonnant à 99.9 max.
2113
+ */
2114
+ public static function calculateAnalogInconsistencyScore(float $consistencyScore, float $inflectionPoint = 0.72, float $steepness = 12.0): float
2115
+ {
2116
+ $s = max(0.0, min(1.0, $consistencyScore));
2117
+ if ($s >= 0.98) {
2118
+ return 0.0;
2119
+ }
2120
+
2121
+ $asymptote = 99.9;
2122
+ $raw = 1.0 / (1.0 + exp($steepness * ($s - $inflectionPoint)));
2123
+ $minVal = 1.0 / (1.0 + exp($steepness * (1.0 - $inflectionPoint)));
2124
+ $maxVal = 1.0 / (1.0 + exp($steepness * (0.0 - $inflectionPoint)));
2125
+ $normalized = (($raw - $minVal) / ($maxVal - $minVal)) * $asymptote;
2126
+
2127
+ return min($asymptote, round($normalized, 1));
2128
+ }
2034
2129
  }
@@ -14,6 +14,150 @@ class TLSClientHelloParser
14
14
  35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
15
15
  ];
16
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
+
17
161
  /**
18
162
  * Parse le Client Hello brut et retourne l'empreinte JA3 et une approximation JA4.
19
163
  *
@@ -72,6 +216,7 @@ class TLSClientHelloParser
72
216
  $supportedVersions = [];
73
217
  $hasSni = false;
74
218
  $alpnProtocol = '';
219
+ $quicParams = null;
75
220
 
76
221
  $extLimit = $offset + $extensionsLen;
77
222
  while ($offset < $extLimit && $offset + 4 <= $len) {
@@ -127,6 +272,11 @@ class TLSClientHelloParser
127
272
  }
128
273
  }
129
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
+ }
130
280
  }
131
281
  $offset += $extLen;
132
282
  }
@@ -204,10 +354,17 @@ class TLSClientHelloParser
204
354
 
205
355
  $ja4_hash = $ja4_a . "_" . $ja4_b . "_" . $ja4_c;
206
356
 
207
- return [
357
+ $result = [
208
358
  'ja3_string' => $ja3String,
209
359
  'ja3_hash' => md5($ja3String),
210
360
  'ja4_raw' => $ja4_hash
211
361
  ];
362
+
363
+ if ($quicParams !== null) {
364
+ $result['quic_params'] = $quicParams;
365
+ $result['quic_fp'] = self::formatQuicFingerprint($quicParams);
366
+ }
367
+
368
+ return $result;
212
369
  }
213
370
  }