@anonympins/fingerprint 0.4.6 → 0.5.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.
@@ -14,6 +14,48 @@ use Anonympins\Fingerprint\Utils\RequestUtils;
14
14
  */
15
15
  class ChallengeUtils
16
16
  {
17
+ private static function fround(float $value): float
18
+ {
19
+ return unpack('f', pack('f', $value))[1];
20
+ }
21
+
22
+ public static function hashSeedToFloat(string $seed): float
23
+ {
24
+ $hash = 0;
25
+ for ($i = 0; $i < strlen($seed); $i++) {
26
+ $hash = (($hash << 5) - $hash + ord($seed[$i])) & 0xffffffff;
27
+ if ($hash & 0x80000000) {
28
+ $hash = $hash - 0x100000000;
29
+ }
30
+ }
31
+ return abs($hash % 1000000) / 1000000;
32
+ }
33
+
34
+ public static function verifyGpuPow(string $seed, int $iterations, string $solution, array $sampleIndices = [0, 12, 35, 57]): bool
35
+ {
36
+ $values = explode(',', $solution);
37
+ if (count($values) !== 64) {
38
+ return false;
39
+ }
40
+ $numericSeed = self::hashSeedToFloat($seed);
41
+ $r = 3.9999;
42
+ foreach ($sampleIndices as $idx) {
43
+ if ($idx < 0 || $idx >= 64) {
44
+ return false;
45
+ }
46
+ $x = self::fround($numericSeed + $idx * 0.015);
47
+ $rFloat = self::fround($r);
48
+ for ($i = 0; $i < $iterations; $i++) {
49
+ $x = self::fround($rFloat * $x * self::fround(1.0 - $x));
50
+ }
51
+ $clientVal = (float)$values[$idx];
52
+ if (abs($clientVal - $x) > 1e-4) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+
17
59
  private const TRAP_URL_TEMPLATES = [
18
60
  '/includes/config-{RANDOM}.php',
19
61
  '/.env.{RANDOM}',
@@ -48,6 +90,123 @@ class ChallengeUtils
48
90
  return $block;
49
91
  }
50
92
 
93
+ public static function registerCooperativeNode(string $clientIp, string $nodeId, string $seed): void
94
+ {
95
+ $subnet = RequestUtils::getIpSubnet($clientIp);
96
+ if ($subnet === null) {
97
+ return;
98
+ }
99
+ $store = StoreManager::getStore();
100
+ $key = "coop-pospace:subnet:{$subnet}";
101
+ $nodes = $store->get($key) ?? [];
102
+
103
+ $now = time();
104
+ // Nettoyage des nœuds expirés (vieux de plus de 2 minutes)
105
+ $nodes = array_filter($nodes, fn($n) => ($now - $n['timestamp']) < 120);
106
+
107
+ $nodes[$nodeId] = [
108
+ 'nodeId' => $nodeId,
109
+ 'seed' => $seed,
110
+ 'timestamp' => $now
111
+ ];
112
+
113
+ $store->set($key, $nodes, 120);
114
+ }
115
+
116
+ public static function findPeerInSubnet(string $clientIp, string $excludeNodeId): ?array
117
+ {
118
+ $subnet = RequestUtils::getIpSubnet($clientIp);
119
+ if ($subnet === null) {
120
+ return null;
121
+ }
122
+ $store = StoreManager::getStore();
123
+ $key = "coop-pospace:subnet:{$subnet}";
124
+ $nodes = $store->get($key) ?? [];
125
+
126
+ $now = time();
127
+ $activePeers = [];
128
+ foreach ($nodes as $id => $node) {
129
+ if ($id !== $excludeNodeId && ($now - $node['timestamp']) < 120) {
130
+ $activePeers[] = $node;
131
+ }
132
+ }
133
+
134
+ if (empty($activePeers)) {
135
+ return null;
136
+ }
137
+
138
+ return $activePeers[array_rand($activePeers)];
139
+ }
140
+
141
+ public static function handleCooperativeRequest(array $params): ?array
142
+ {
143
+ $op = $params['coop_op'] ?? null;
144
+ if (!$op) {
145
+ return null;
146
+ }
147
+
148
+ $store = StoreManager::getStore();
149
+ $nodeId = $params['node_id'] ?? '';
150
+ if (empty($nodeId)) {
151
+ return ['error' => 'Missing node_id'];
152
+ }
153
+
154
+ switch ($op) {
155
+ case 'register':
156
+ $clientIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
157
+ $seed = $params['seed'] ?? '';
158
+ self::registerCooperativeNode($clientIp, $nodeId, $seed);
159
+ return ['status' => 'registered'];
160
+
161
+ case 'request_peer_block':
162
+ $peerId = $params['peer_id'] ?? '';
163
+ $blockIdx = (int)($params['block_idx'] ?? 0);
164
+ $requestId = $params['req_id'] ?? '';
165
+ if (empty($peerId) || empty($requestId)) {
166
+ return ['error' => 'Invalid parameters'];
167
+ }
168
+
169
+ $queueKey = "coop-mailbox:queue:{$peerId}";
170
+ $requests = $store->get($queueKey) ?? [];
171
+ $requests[] = [
172
+ 'req_id' => $requestId,
173
+ 'requester_id' => $nodeId,
174
+ 'block_idx' => $blockIdx
175
+ ];
176
+ $store->set($queueKey, $requests, 30);
177
+ return ['status' => 'queued'];
178
+
179
+ case 'poll_requests':
180
+ $queueKey = "coop-mailbox:queue:{$nodeId}";
181
+ $requests = $store->get($queueKey) ?? [];
182
+ $store->delete($queueKey);
183
+ return ['requests' => $requests];
184
+
185
+ case 'respond_block':
186
+ $requesterId = $params['requester_id'] ?? '';
187
+ $requestId = $params['req_id'] ?? '';
188
+ $blockData = $params['block_data'] ?? '';
189
+ if (empty($requesterId) || empty($requestId)) {
190
+ return ['error' => 'Invalid parameters'];
191
+ }
192
+
193
+ $responseKey = "coop-mailbox:res:{$requesterId}:{$requestId}";
194
+ $store->set($responseKey, ['block_data' => $blockData], 30);
195
+ return ['status' => 'delivered'];
196
+
197
+ case 'poll_response':
198
+ $requestId = $params['req_id'] ?? '';
199
+ $responseKey = "coop-mailbox:res:{$nodeId}:{$requestId}";
200
+ $data = $store->get($responseKey);
201
+ if ($data) {
202
+ $store->delete($responseKey);
203
+ return ['status' => 'ready', 'block_data' => $data['block_data']];
204
+ }
205
+ return ['status' => 'pending'];
206
+ }
207
+ return null;
208
+ }
209
+
51
210
  public static function generateSpaceChallenge(string $clientIp, string $nonce, float $suspicionFactor, string $originalUrl, array $securityConfig): array
52
211
  {
53
212
  $pospaceConfig = $securityConfig['pospace'] ?? [];
@@ -63,13 +222,29 @@ class ChallengeUtils
63
222
  }
64
223
  }
65
224
 
66
- return [
225
+ $challenge = [
67
226
  'type' => 'pospace',
68
227
  'nonce' => $nonce,
69
228
  'sizeMb' => $sizeMb,
70
229
  'queries' => $queries,
71
230
  'path' => $originalUrl
72
231
  ];
232
+
233
+ // Tentative de couplage coopératif avec un nœud du même sous-réseau
234
+ $peer = self::findPeerInSubnet($clientIp, $nonce);
235
+ if ($peer !== null) {
236
+ $challenge['peerId'] = $peer['nodeId'];
237
+ $challenge['peerBlockIdx'] = random_int(0, $maxBlocks - 1);
238
+
239
+ $store = StoreManager::getStore();
240
+ $store->set("coop-assoc:{$nonce}", [
241
+ 'peerNodeId' => $peer['nodeId'],
242
+ 'peerSeed' => $peer['seed'],
243
+ 'peerBlockIdx' => $challenge['peerBlockIdx']
244
+ ], 120);
245
+ }
246
+
247
+ return $challenge;
73
248
  }
74
249
 
75
250
  public static function verifySpacePoW(string $nonce, string $solution, array $queries, string $seed, string $clientSecret): bool
@@ -78,11 +253,48 @@ class ChallengeUtils
78
253
  foreach ($queries as $idx) {
79
254
  $combined .= self::generateBlock($seed, (int)$idx);
80
255
  }
256
+
257
+ // Vérification de la preuve coopérative
258
+ $store = StoreManager::getStore();
259
+ $assoc = $store->get("coop-assoc:{$nonce}");
260
+ if ($assoc !== null) {
261
+ $peerSeed = $assoc['peerSeed'] ?? null;
262
+ $peerBlockIdx = $assoc['peerBlockIdx'] ?? null;
263
+ if ($peerSeed !== null && $peerBlockIdx !== null) {
264
+ $combined .= self::generateBlock($peerSeed, (int)$peerBlockIdx);
265
+ }
266
+ $store->delete("coop-assoc:{$nonce}");
267
+ }
268
+
81
269
  $finalBlock = $combined . $nonce . ":" . $clientSecret;
82
270
  $hash = hash('sha256', $finalBlock);
83
271
  return hash_equals($hash, $solution);
84
272
  }
85
273
 
274
+ public static function verifyZkpProof(string $yStr, string $tStr, string $sStr): bool
275
+ {
276
+ try {
277
+ $p = BigInt::fromHex('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); // secp256k1 prime
278
+ $g = new BigInt(2);
279
+
280
+ $y = BigInt::fromHex($yStr);
281
+ $t = BigInt::fromHex($tStr);
282
+ $s = BigInt::fromHex($sStr);
283
+
284
+ $cStr = (string)$g . (string)$y . (string)$t;
285
+ $cHex = hash('sha256', $cStr);
286
+ $c = BigInt::fromHex($cHex)->mod($p);
287
+
288
+ $left = $g->modPow($s, $p);
289
+ $y_c = $y->modPow($c, $p);
290
+ $right = $t->mul($y_c)->mod($p);
291
+
292
+ return $left->compareTo($right) === 0;
293
+ } catch (\Throwable $e) {
294
+ return false;
295
+ }
296
+ }
297
+
86
298
  /**
87
299
  * Récupère la clé secrète pour les PoW depuis les variables d'environnement.
88
300
  */
@@ -102,6 +314,22 @@ class ChallengeUtils
102
314
  */
103
315
  public static function generateStatelessTicket(array $payload): string
104
316
  {
317
+ $ed25519Key = $_ENV['ED25519_PRIVATE_KEY'] ?? getenv('ED25519_PRIVATE_KEY');
318
+ if ($ed25519Key) {
319
+ try {
320
+ $serialized = json_encode($payload);
321
+ $privateKey = openssl_pkey_get_private($ed25519Key);
322
+ if ($privateKey && openssl_sign($serialized, $signature, $privateKey, null)) {
323
+ $base64UrlEncode = function ($input) {
324
+ return rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
325
+ };
326
+ return 'ed25519.' . $base64UrlEncode($serialized) . '.' . $base64UrlEncode($signature);
327
+ }
328
+ } catch (\Throwable $e) {
329
+ error_log("[ChallengeUtils] Ed25519 signing failed: " . $e->getMessage());
330
+ }
331
+ }
332
+
105
333
  $key = hash('sha256', self::getPowSecret(), true);
106
334
  $iv = random_bytes(16);
107
335
  $encrypted = openssl_encrypt(json_encode($payload), 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
@@ -115,10 +343,40 @@ class ChallengeUtils
115
343
  /**
116
344
  * Décode et valide un ticket stateless chiffré et signé.
117
345
  * @param string $ticket
346
+ * @param string $secret
118
347
  * @return array|null
119
348
  */
120
- public static function parseStatelessTicket(string $ticket): ?array
349
+ public static function parseStatelessTicket(string $ticket, string $secret = ''): ?array
121
350
  {
351
+ try {
352
+ if (str_starts_with($ticket, 'ed25519.')) {
353
+ $parts = explode('.', $ticket);
354
+ if (count($parts) !== 3) {
355
+ return null;
356
+ }
357
+ $base64UrlDecode = function ($input) {
358
+ return base64_decode(strtr($input, '-_', '+/'));
359
+ };
360
+ $payloadJson = $base64UrlDecode($parts[1]);
361
+ $signature = $base64UrlDecode($parts[2]);
362
+
363
+ $ed25519PubKey = $_ENV['ED25519_PUBLIC_KEY'] ?? getenv('ED25519_PUBLIC_KEY');
364
+ if (!$ed25519PubKey) {
365
+ error_log("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
366
+ return null;
367
+ }
368
+
369
+ $publicKey = openssl_pkey_get_public($ed25519PubKey);
370
+ if ($publicKey && openssl_verify($payloadJson, $signature, $publicKey, null) === 1) {
371
+ return json_decode($payloadJson, true);
372
+ }
373
+ return null;
374
+ }
375
+ } catch (\Throwable $e) {
376
+ error_log("[ChallengeUtils] Ed25519 verification failed: " . $e->getMessage());
377
+ return null;
378
+ }
379
+
122
380
  $parts = explode('.', $ticket);
123
381
  if (count($parts) !== 3) {
124
382
  return null;
@@ -132,7 +390,7 @@ class ChallengeUtils
132
390
  if (!$iv || !$encrypted || !$signature || strlen($iv) !== 16) {
133
391
  return null;
134
392
  }
135
- $key = hash('sha256', self::getPowSecret(), true);
393
+ $key = hash('sha256', !empty($secret) ? $secret : self::getPowSecret(), true);
136
394
  $expectedSignature = hash_hmac('sha256', $iv . $encrypted, $key, true);
137
395
  if (!hash_equals($expectedSignature, $signature)) {
138
396
  return null;
@@ -143,20 +401,23 @@ class ChallengeUtils
143
401
 
144
402
  /**
145
403
  * Vérifie si un ticket de passage est valide (supporte les tickets opaques via store et le fallback legacy).
404
+ * Supporte une clé secrète optionnelle passée en paramètre pour la compatibilité avec les tests.
146
405
  */
147
406
  public static function isTicketValid(
148
407
  ?string $ip,
149
408
  ?string $ticket,
150
409
  string $deviceId = '',
151
410
  string $deviceHash = '',
152
- bool $allowCrossNetworkRoaming = false
411
+ bool $allowCrossNetworkRoaming = false,
412
+ string $secret = '',
413
+ string $zkpProof = ''
153
414
  ): bool {
154
415
  if (empty($ip) || empty($ticket)) {
155
416
  return false;
156
417
  }
157
418
 
158
419
  // Tentative de validation stateless d'abord
159
- $ticketData = self::parseStatelessTicket($ticket);
420
+ $ticketData = self::parseStatelessTicket($ticket, $secret);
160
421
  if ($ticketData !== null) {
161
422
  $expiry = $ticketData['expiry'] ?? null;
162
423
  $originalIp = $ticketData['originalIp'] ?? null;
@@ -166,6 +427,18 @@ class ChallengeUtils
166
427
  if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
167
428
  return false;
168
429
  }
430
+ if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
431
+ $expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
432
+ if (!empty($zkpProof)) {
433
+ $zkpParts = explode(':', $zkpProof);
434
+ if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
435
+ if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
436
+ return true;
437
+ }
438
+ }
439
+ }
440
+ return false;
441
+ }
169
442
  if ($ip === $originalIp) {
170
443
  return true;
171
444
  }
@@ -193,6 +466,18 @@ class ChallengeUtils
193
466
  $store->delete("ticket:{$ticket}");
194
467
  return false;
195
468
  }
469
+ if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
470
+ $expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
471
+ if (!empty($zkpProof)) {
472
+ $zkpParts = explode(':', $zkpProof);
473
+ if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
474
+ if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
475
+ return true;
476
+ }
477
+ }
478
+ }
479
+ return false;
480
+ }
196
481
 
197
482
  if ($ip === $originalIp) {
198
483
  return true;
@@ -221,7 +506,7 @@ class ChallengeUtils
221
506
  return false;
222
507
  }
223
508
 
224
- $expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", self::getPowSecret());
509
+ $expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", !empty($secret) ? $secret : self::getPowSecret());
225
510
 
226
511
  return hash_equals($expectedSig, $sig);
227
512
  }
@@ -467,6 +752,9 @@ class ChallengeUtils
467
752
  $sizeMb = $challengeDetails['sizeMb'];
468
753
  $queries = $challengeDetails['queries'];
469
754
  $path = $challengeDetails['path'];
755
+ $peerId = $challengeDetails['peerId'] ?? '';
756
+ $peerBlockIdx = $challengeDetails['peerBlockIdx'] ?? -1;
757
+ $nodeId = $nonce;
470
758
 
471
759
  $solverCode = self::getPowSolverCode();
472
760
  $queriesJson = json_encode($queries);
@@ -478,16 +766,63 @@ class ChallengeUtils
478
766
  const clientSecret = "{$clientSecret}";
479
767
  const queries = {$queriesJson};
480
768
  const sizeMb = {$sizeMb};
769
+ const nodeId = "{$nodeId}";
770
+ const peerId = "{$peerId}";
771
+ const peerBlockIdx = {$peerBlockIdx};
481
772
 
482
773
  document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
483
774
  await new Promise(r => setTimeout(r, 10));
484
775
 
485
776
  try {
486
777
  await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
487
- document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
488
- const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
489
778
 
490
- window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
779
+ // Enregistrement coopératif
780
+ await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
781
+
782
+ // Écoute des requêtes entrantes de nos pairs suspects
783
+ setInterval(async () => {
784
+ try {
785
+ const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId);
786
+ const data = await res.json();
787
+ if (data.requests && data.requests.length > 0) {
788
+ for (const req of data.requests) {
789
+ document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
790
+ const blockData = await window.readSpaceBlock(req.block_idx);
791
+ await fetch(window.location.pathname + "?coop_op=respond_block&node_id=" + nodeId + "&requester_id=" + req.requester_id + "&req_id=" + req.req_id + "&block_data=" + encodeURIComponent(blockData));
792
+ }
793
+ }
794
+ } catch (e) {
795
+ console.error("Cooperative polling error", e);
796
+ }
797
+ }, 1000);
798
+
799
+ // Téléchargement du bloc du pair si configuré
800
+ let peerBlock = "";
801
+ if (peerId && peerBlockIdx !== -1) {
802
+ document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
803
+ const reqId = Math.random().toString(36).substring(2);
804
+ await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
805
+
806
+ let attempts = 0;
807
+ while (attempts < 15) {
808
+ const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
809
+ const data = await res.json();
810
+ if (data.status === 'ready') {
811
+ peerBlock = data.block_data;
812
+ break;
813
+ }
814
+ await new Promise(r => setTimeout(r, 1000));
815
+ attempts++;
816
+ }
817
+ if (!peerBlock) {
818
+ document.getElementById('loader').innerText = '⚠️ Peer de sous-réseau injoignable. Validation solo...';
819
+ }
820
+ }
821
+
822
+ document.getElementById('loader').innerText = '⚙️ Génération de la Preuve d\\'Espace...';
823
+ const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
824
+
825
+ window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
491
826
  } catch(e) {
492
827
  document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
493
828
  }
@@ -41,6 +41,8 @@ class SecurityProfiles
41
41
  'subnetScore' => 0.5, // Pénalise les sous-réseaux IP avec une activité suspecte agrégée
42
42
  'botnetClusterScore' => 0.6, // NOUVEAU: Poids pour le clustering botnet
43
43
  'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
44
+ 'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
45
+ 'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
44
46
 
45
47
  ],
46
48
  'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
@@ -87,6 +89,8 @@ class SecurityProfiles
87
89
  'subnetScore' => 0.7, // Poids plus élevé en mode strict
88
90
  'botnetClusterScore' => 0.8, // NOUVEAU: Poids pour le clustering botnet
89
91
  'tcpAnomalyScore' => 1.0, // NEW: Anomalie de pile TCP/IP
92
+ 'quicAnomalyScore' => 1.0, // NEW: Anomalie QUIC
93
+ 'renderingAnomalyScore' => 1.0, // NEW: Anomalie de rendu
90
94
 
91
95
  ],
92
96
  'thresholds' => ['low' => 10, 'medium' => 35, 'high' => 65, 'block' => 90],
@@ -133,6 +137,7 @@ class SecurityProfiles
133
137
  'subnetScore' => 0.8, // Très important pour les API pour détecter les botnets
134
138
  'botnetClusterScore' => 0.7, // NOUVEAU: Poids pour le clustering botnet
135
139
  'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
140
+ 'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
136
141
 
137
142
  ],
138
143
  'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
@@ -181,6 +186,8 @@ class SecurityProfiles
181
186
  'subnetScore' => 0.4, // Utile contre le spam de commentaires coordonné
182
187
  'botnetClusterScore' => 0.5, // NOUVEAU: Poids pour le clustering botnet
183
188
  'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
189
+ 'quicAnomalyScore' => 0.5, // NEW: Anomalie QUIC
190
+ 'renderingAnomalyScore' => 0.5, // NEW: Anomalie de rendu
184
191
 
185
192
  ],
186
193
  'thresholds' => ['low' => 25, 'medium' => 55, 'high' => 80, 'block' => 95],
@@ -228,6 +235,8 @@ class SecurityProfiles
228
235
  'subnetScore' => 0.9, // Crucial contre les attaques de scalping distribuées
229
236
  'botnetClusterScore' => 0.9, // NOUVEAU: Poids pour le clustering botnet
230
237
  'tcpAnomalyScore' => 0.9, // NEW: Anomalie de pile TCP/IP
238
+ 'quicAnomalyScore' => 0.9, // NEW: Anomalie QUIC
239
+ 'renderingAnomalyScore' => 0.9, // NEW: Anomalie de rendu
231
240
 
232
241
  ],
233
242
  'thresholds' => ['low' => 15, 'medium' => 40, 'high' => 70, 'block' => 90],
@@ -30,6 +30,29 @@
30
30
  public function __construct(array $securityConfig)
31
31
  {
32
32
  $this->isProduction = ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production';
33
+
34
+ // Dynamically bind Ed25519 keys if passed via config
35
+ if (isset($securityConfig['ed25519_private_key'])) {
36
+ $_ENV['ED25519_PRIVATE_KEY'] = $securityConfig['ed25519_private_key'];
37
+ }
38
+ if (isset($securityConfig['ed25519_public_key'])) {
39
+ $_ENV['ED25519_PUBLIC_KEY'] = $securityConfig['ed25519_public_key'];
40
+ }
41
+
42
+ // Auto-load optimized config if autotuning savePath is specified
43
+ if (isset($securityConfig['autotuning']['savePath'])) {
44
+ $savePath = $securityConfig['autotuning']['savePath'];
45
+ if (file_exists($savePath)) {
46
+ try {
47
+ $savedConfig = json_decode(file_get_contents($savePath), true);
48
+ if (json_last_error() === JSON_ERROR_NONE && is_array($savedConfig)) {
49
+ $securityConfig = SecurityProfiles::deepMerge($securityConfig, $savedConfig);
50
+ }
51
+ } catch (\Throwable $e) {
52
+ error_log("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
53
+ }
54
+ }
55
+ }
33
56
  $this->securityConfig = $securityConfig;
34
57
  $this->verbose = $securityConfig['verbose'] ?? false;
35
58
  $this->allowlist = $this->buildAllowlist();
@@ -38,6 +61,19 @@
38
61
  $this->validateConfig($securityConfig);
39
62
  }
40
63
 
64
+ /**
65
+ * Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
66
+ * sans nécessiter de redémarrage.
67
+ *
68
+ * @param array $newConfig La nouvelle configuration (partielle ou complète).
69
+ */
70
+ public function updateConfig(array $newConfig): void
71
+ {
72
+ $this->validateConfig($newConfig);
73
+ $this->securityConfig = SecurityProfiles::deepMerge($this->securityConfig, $newConfig);
74
+ $this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
75
+ }
76
+
41
77
  private function validateConfig(array $config): void
42
78
  {
43
79
  if (empty($config)) {
@@ -50,7 +86,8 @@
50
86
  'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
51
87
  'honeypot', 'threatIntel', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger', 'probationaryTtl',
52
88
  'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
53
- 'similarityThreshold', 'summary', 'description'
89
+ 'similarityThreshold', 'summary', 'description',
90
+ 'ed25519_private_key', 'ed25519_public_key'
54
91
  ];
55
92
 
56
93
  if (empty($config['weights'])) {
@@ -443,6 +480,12 @@
443
480
  // Score d'anomalie de pile TCP/IP
444
481
  $tcpAnomaly = RequestUtils::getTcpAnomalyScore($context);
445
482
 
483
+ // Score d'anomalie de flux QUIC/HTTP3
484
+ $quicAnomaly = RequestUtils::getQuicAnomalyScore($context);
485
+
486
+ // Score d'anomalie de rendu d'affichage (V-Sync)
487
+ $renderingAnomaly = RequestUtils::getRenderingAnomalyScore($context);
488
+
446
489
  // Assemblage du vecteur de suspicion final
447
490
  $suspicionVector = array_merge($suspicionVector, [
448
491
  'inconsistencyScore' => $inconsistencyScore,
@@ -462,6 +505,8 @@
462
505
  'subnetScore' => $subnetScore['subnetScore'],
463
506
  'botnetClusterScore' => $botnetCluster['botnetClusterScore'],
464
507
  'tcpAnomalyScore' => $tcpAnomaly['tcpAnomalyScore'],
508
+ 'quicAnomalyScore' => $quicAnomaly['quicAnomalyScore'],
509
+ 'renderingAnomalyScore' => $renderingAnomaly['renderingAnomalyScore'],
465
510
  ]);
466
511
 
467
512
  // Sauvegarder l'état mis à jour de l'appareil dans le store
@@ -589,6 +634,16 @@
589
634
  // Initialiser le vecteur de suspicion pour éviter les erreurs de type.
590
635
  $suspicionVector = [];
591
636
 
637
+ $coopOp = $context->query['coop_op'] ?? null;
638
+ if ($coopOp) {
639
+ $result = ChallengeUtils::handleCooperativeRequest($context->query);
640
+ return [
641
+ 'action' => 'challenge',
642
+ 'status' => 200,
643
+ 'body' => $result
644
+ ];
645
+ }
646
+
592
647
  $this->log('Processing request', ['clientIp' => $context->clientIp, 'path' => $context->path]);
593
648
 
594
649
  // Parse GraphQL query if applicable
@@ -737,7 +792,11 @@
737
792
  // 3. Vérifier un ticket existant
738
793
  $hasValidTicket = false;
739
794
  $powCookie = $context->cookies['pow_clearance'] ?? null;
740
- if (ChallengeUtils::isTicketValid($context->clientIp, $powCookie)) {
795
+ $zkpProof = $context->getHeader('x-zkp-proof') ?? $context->query['pow_zkp'] ?? '';
796
+ $deviceId = $context->cookies['device_id'] ?? '';
797
+ $currentDeviceHash = RequestUtils::getCompositeDeviceHash($context);
798
+ $allowRoaming = $this->securityConfig['allowCrossNetworkRoaming'] ?? false;
799
+ if (ChallengeUtils::isTicketValid($context->clientIp, $powCookie, $deviceId, $currentDeviceHash, $allowRoaming, $zkpProof)) {
741
800
  $hasValidTicket = true;
742
801
  MetricsManager::incrementCounter('tickets_valid_total');
743
802
  // On ne retourne pas tout de suite pour permettre le re-challenge
@@ -38,6 +38,7 @@ class RequestContext
38
38
  public ?string $ja4s = null;
39
39
  public ?string $ja4h = null;
40
40
  public ?string $http2Fingerprint = null;
41
+ public ?string $quicFingerprint = null;
41
42
  public ?string $tcpFingerprint = null;
42
43
 
43
44
  /**
@@ -77,6 +78,7 @@ class RequestContext
77
78
  $this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
78
79
  $this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
79
80
  $this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
81
+ $this->quicFingerprint = $this->headers['x-quic-fp'] ?? null;
80
82
  $this->tlsSessionId = $this->headers['x-tls-session-id'] ?? $this->headers['x-ssl-session-id'] ?? null;
81
83
  }
82
84
  /**