@anonympins/fingerprint 0.7.3 → 0.7.5

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.
@@ -31,6 +31,7 @@
31
31
  private static ?array $googlebotEntries = null;
32
32
  private static ?array $yandexEntries = null;
33
33
  private static ?array $bingbotEntries = null;
34
+ private static ?array $facebookEntries = null;
34
35
 
35
36
  private static array $dnsCircuitBreaker = [
36
37
  'state' => 'CLOSED',
@@ -265,7 +266,8 @@
265
266
  'similarityThreshold', 'summary', 'description',
266
267
  'ed25519_private_key', 'ed25519_public_key',
267
268
  'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset',
268
- 'filterWhitelist'
269
+ 'filterWhitelist',
270
+ 'differentialPrivacy', 'dpEpsilon'
269
271
  ];
270
272
 
271
273
  if (empty($config['weights'])) {
@@ -347,12 +349,26 @@
347
349
  ];
348
350
  }
349
351
 
352
+ public static function facebook_whitelist(): array
353
+ {
354
+ if (self::$facebookEntries === null) {
355
+ self::$facebookEntries = self::loadBotWhitelist('facebook.json', [
356
+ "31.13.64.0/18", "66.220.144.0/20", "69.63.176.0/20", "157.240.0.0/16"
357
+ ]);
358
+ }
359
+ return [
360
+ 'type' => 'allowlist',
361
+ 'entries' => self::$facebookEntries
362
+ ];
363
+ }
364
+
350
365
  public static function default_whitelist(): array
351
366
  {
352
367
  return [
353
368
  self::googlebot_whitelist(),
354
369
  self::bingbot_whitelist(),
355
370
  self::yandex_whitelist(),
371
+ self::facebook_whitelist(),
356
372
  ['userAgent' => 'Googlebot', 'hostnameSuffix' => '.googlebot.com'],
357
373
  ['userAgent' => 'Google-Extended', 'hostnameSuffix' => '.google.com'],
358
374
  ['userAgent' => 'AdsBot-Google', 'hostnameSuffix' => '.googlebot.com'],
@@ -394,14 +410,30 @@
394
410
 
395
411
  public function calculateFinalScore(array $suspicionVector): float
396
412
  {
397
- $weights = $this->securityConfig['weights'] ?? [];
398
- if (empty($weights)) {
399
- return 0.0;
400
- }
413
+ $baseWeights = $this->securityConfig['weights'] ?? [];
401
414
 
402
415
  $score = 0.0;
403
- foreach ($weights as $key => $weight) {
404
- $score += ($suspicionVector[$key] ?? 0) * $weight;
416
+ // NOUVEAU: Logique d'amplification des poids
417
+ // Si un tunnel est détecté, on augmente le poids des autres signaux suspects.
418
+ $dynamicWeights = $baseWeights;
419
+ if (($suspicionVector['mtuAnomalyScore'] ?? 0.0) > 50.0) {
420
+ $this->log('Tunnel detected, amplifying suspicion weights.', ['mtuScore' => $suspicionVector['mtuAnomalyScore']]);
421
+
422
+ // Augmente le poids des incohérences de bas niveau (difficiles à falsifier)
423
+ $dynamicWeights['tlsSpoofingScore'] = ($baseWeights['tlsSpoofingScore'] ?? 0.8) * 1.25;
424
+ $dynamicWeights['crossLayerInconsistencyScore'] = ($baseWeights['crossLayerInconsistencyScore'] ?? 0.4) * 1.4;
425
+ $dynamicWeights['clientHintsInconsistencyScore'] = ($baseWeights['clientHintsInconsistencyScore'] ?? 0.7) * 1.2;
426
+
427
+ // Augmente le poids des anomalies comportementales (un bot derrière un VPN est plus suspect)
428
+ $dynamicWeights['behaviorScore'] = ($baseWeights['behaviorScore'] ?? 0.7) * 1.15;
429
+ $dynamicWeights['requestPatternScore'] = ($baseWeights['requestPatternScore'] ?? 0.6) * 1.2;
430
+ }
431
+
432
+ foreach ($dynamicWeights as $key => $weight) {
433
+ $metricScore = $suspicionVector[$key] ?? 0.0;
434
+ if ($metricScore > 0) {
435
+ $score += $metricScore * $weight;
436
+ }
405
437
  }
406
438
 
407
439
  return min(100.0, $score);
@@ -519,7 +551,7 @@
519
551
  if ($whitelisted) {
520
552
  $filterWhitelist = $this->securityConfig['filterWhitelist'] ?? false;
521
553
  $bypassWhitelist = false;
522
- if ($filterWhitelist === true) {
554
+ if ($filterWhitelist === true || (is_numeric($filterWhitelist) && $type === 'bot')) {
523
555
  $bypassWhitelist = $this->hasCertainAttack($context);
524
556
  } elseif (is_numeric($filterWhitelist)) {
525
557
  if ($context->preCalculatedScore === null) {
@@ -624,6 +656,21 @@
624
656
  }
625
657
  }
626
658
 
659
+ // Validation de l'ancre matérielle WebAuthn (Secure Enclave / TPM)
660
+ $behaviorHeader = $context->getHeader('x-behavior-metrics');
661
+ if (!empty($behaviorHeader) && is_string($behaviorHeader) && str_starts_with($behaviorHeader, '{')) {
662
+ try {
663
+ $parsedMetrics = json_decode($behaviorHeader, true);
664
+ if (is_array($parsedMetrics) && !empty($parsedMetrics['webauthnAnchor']) && is_array($deviceData)) {
665
+ if ($this->verifyWebAuthnHardwareAnchor($parsedMetrics['webauthnAnchor'], $deviceData)) {
666
+ $deviceData['webauthnVerified'] = true;
667
+ $store->set("device:{$deviceId}", $deviceData);
668
+ }
669
+ }
670
+ } catch (\Throwable $e) {
671
+ }
672
+ }
673
+
627
674
  // Bind the current TLS session ID to the device ID
628
675
  if ($deviceId && $tlsSessionId) {
629
676
  $store->set("tls-session:{$tlsSessionId}", $deviceId, 3600); // 1h cache duration
@@ -950,6 +997,7 @@
950
997
  */
951
998
  public function processRequest(RequestContext $context): array
952
999
  {
1000
+ $store = StoreManager::getStore();
953
1001
  $this->translatePolymorphicHeaders($context);
954
1002
 
955
1003
  // Initialiser le vecteur de suspicion pour éviter les erreurs de type.
@@ -974,6 +1022,64 @@
974
1022
  $context->graphqlOperation = $gqlInfo;
975
1023
  }
976
1024
  }
1025
+
1026
+ // 0. Résoudre l'identité de l'appareil et vérifier l'attestation matérielle WebAuthn
1027
+ $identity = $this->resolveRequestIdentity($context, $suspicionVector);
1028
+ $deviceId = $identity['deviceId'];
1029
+ $deviceData = $identity['deviceData'];
1030
+
1031
+ // Règle Anti-Ferme : Un appareil condamné ou menant une attaque certaine ne bénéficie d'aucun bypass
1032
+ if (($deviceData['condemned'] ?? false) || $this->hasCertainAttack($context)) {
1033
+ if (is_array($deviceData)) {
1034
+ $deviceData['condemned'] = true;
1035
+ $deviceData['webauthnVerified'] = false;
1036
+ $store->set("device:{$deviceId}", $deviceData);
1037
+ }
1038
+ $this->log('Condemned device or attack detected - revoking hardware trust and blocking', ['deviceId' => $deviceId]);
1039
+ $decision = ['action' => 'block', 'status' => 403, 'body' => 'Forbidden', 'score' => 100.0, 'vector' => ['honeypotScore' => 100.0]];
1040
+ if ($this->dryRun) {
1041
+ $this->log("[Dry Run] Intended action: {$decision['action']}", ['score' => $decision['score']]);
1042
+ $decision['intendedAction'] = $decision['action'];
1043
+ $decision['action'] = 'next';
1044
+ unset($decision['status'], $decision['body']);
1045
+ }
1046
+ return $decision;
1047
+ }
1048
+
1049
+ // Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
1050
+ if ($deviceData && !empty($deviceData['webauthnVerified'])) {
1051
+ $this->log('Hardware-anchored device verified (WebAuthn) - full bypass granted', ['deviceId' => $deviceId]);
1052
+ $greenlistTtl = 365 * 86400 * 1000; // 1 an (permanent)
1053
+ $greenlistTicket = ChallengeUtils::generateStatelessTicket([
1054
+ 'expiry' => (int)floor(microtime(true) * 1000) + $greenlistTtl,
1055
+ 'originalIp' => $context->clientIp,
1056
+ 'deviceId' => $deviceId,
1057
+ 'deviceHash' => 'webauthn:greenlist:' . ($deviceData['webauthnCredentialId'] ?? $deviceId),
1058
+ 'greenlist' => true
1059
+ ]);
1060
+
1061
+ $isHttps = !empty($context->isHttps);
1062
+ $decision = [
1063
+ 'action' => 'next',
1064
+ 'score' => 0.0,
1065
+ 'vector' => ['webauthn_verified' => 100.0],
1066
+ 'cookie' => [
1067
+ 'name' => 'pow_clearance',
1068
+ 'value' => $greenlistTicket,
1069
+ 'options' => [
1070
+ 'httponly' => true,
1071
+ 'secure' => $isHttps,
1072
+ 'samesite' => 'Strict',
1073
+ 'expires' => time() + ($greenlistTtl / 1000),
1074
+ 'path' => '/',
1075
+ ]
1076
+ ]
1077
+ ];
1078
+ if (isset($context->newCookieForResponse)) {
1079
+ $decision['newCookieForResponse'] = $context->newCookieForResponse;
1080
+ }
1081
+ return $decision;
1082
+ }
977
1083
 
978
1084
  // 1. Vérifier les listes blanches
979
1085
  if ($this->checkAllowlists($context)) {
@@ -1567,42 +1673,85 @@
1567
1673
  $peers = $this->securityConfig['federatedPeers'] ?? [];
1568
1674
  if (empty($peers)) return;
1569
1675
 
1570
- $timestamp = (int)(microtime(true) * 1000);
1571
- $msg = "{$timestamp}:{$zkpY}";
1572
-
1573
- $signature = '';
1574
- $isAsymmetric = false;
1575
-
1576
- $privateKey = Env::get('ED25519_PRIVATE_KEY');
1577
- if ($privateKey) {
1578
- try {
1579
- $cleanKey = str_replace('\n', "\n", $privateKey);
1580
- $pkeyObj = openssl_pkey_get_private($cleanKey);
1581
- if ($pkeyObj && openssl_sign($msg, $sigBytes, $pkeyObj, null)) {
1582
- $signature = bin2hex($sigBytes);
1583
- $isAsymmetric = true;
1676
+ $dpConfig = $this->securityConfig['differentialPrivacy'] ?? [];
1677
+ $dpEnabled = ($dpConfig['enabled'] ?? true) !== false;
1678
+ $epsilon = isset($dpConfig['epsilon']) && is_numeric($dpConfig['epsilon'])
1679
+ ? (float)$dpConfig['epsilon']
1680
+ : (float)($this->securityConfig['dpEpsilon'] ?? 1.0);
1681
+
1682
+ $sendThreatReport = function (string $targetZkpY, int $ts) use ($peers) {
1683
+ $msg = "{$ts}:{$targetZkpY}";
1684
+ $signature = '';
1685
+ $isAsymmetric = false;
1686
+
1687
+ $privateKey = Env::get('ED25519_PRIVATE_KEY');
1688
+ if ($privateKey) {
1689
+ try {
1690
+ $cleanKey = str_replace('\n', "\n", $privateKey);
1691
+ $pkeyObj = openssl_pkey_get_private($cleanKey);
1692
+ if ($pkeyObj && openssl_sign($msg, $sigBytes, $pkeyObj, null)) {
1693
+ $signature = bin2hex($sigBytes);
1694
+ $isAsymmetric = true;
1695
+ }
1696
+ } catch (\Throwable $e) {
1697
+ self::logError('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
1584
1698
  }
1585
- } catch (\Throwable $e) {
1586
- self::logError('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
1587
1699
  }
1588
- }
1589
1700
 
1590
- if (!$isAsymmetric) {
1591
- $secret = $this->securityConfig['federationSecret'] ?? ChallengeUtils::getPowSecret();
1592
- $signature = hash_hmac('sha256', $msg, $secret);
1593
- }
1701
+ if (!$isAsymmetric) {
1702
+ $secret = $this->securityConfig['federationSecret'] ?? ChallengeUtils::getPowSecret();
1703
+ $signature = hash_hmac('sha256', $msg, $secret);
1704
+ }
1594
1705
 
1595
- foreach ($peers as $peerUrl) {
1596
- $this->asyncPost($peerUrl . '?coop_op=share_threat_intel', [
1597
- 'zkpY' => $zkpY,
1598
- 'signature' => $isAsymmetric ? '' : $signature,
1599
- 'signature_ed25519' => $isAsymmetric ? $signature : '',
1600
- 'timestamp' => $timestamp
1601
- ]);
1706
+ foreach ($peers as $peerUrl) {
1707
+ $this->asyncPost($peerUrl . '?coop_op=share_threat_intel', [
1708
+ 'zkpY' => $targetZkpY,
1709
+ 'signature' => $isAsymmetric ? '' : $signature,
1710
+ 'signature_ed25519' => $isAsymmetric ? $signature : '',
1711
+ 'timestamp' => $ts
1712
+ ]);
1713
+ }
1714
+ };
1715
+
1716
+ $now = (int)(microtime(true) * 1000);
1717
+ $reportTimestamp = $now;
1718
+
1719
+ if ($dpEnabled) {
1720
+ // 1. Differential Privacy : Bruit laplacien sur l'horodatage
1721
+ $deltaT = 5000;
1722
+ $b = $deltaT / max(0.1, $epsilon);
1723
+ $u = (random_int(1, 999999) / 1000000.0) - 0.5;
1724
+ $safeU = abs($u) < 1e-7 ? ($u >= 0 ? 1e-7 : -1e-7) : $u;
1725
+ $laplaceNoise = -$b * ($safeU <=> 0) * log(1.0 - 2.0 * abs($safeU));
1726
+ $clampedNoise = (int)max(-60000, min(60000, round($laplaceNoise)));
1727
+ $reportTimestamp = $now + $clampedNoise;
1728
+ }
1729
+
1730
+ $sendThreatReport($zkpY, $reportTimestamp);
1731
+
1732
+ if ($dpEnabled) {
1733
+ // 2. Differential Privacy : Injection de clés leurres (Decoy ZKP)
1734
+ $decoyProb = isset($dpConfig['dummyRate']) && is_numeric($dpConfig['dummyRate'])
1735
+ ? (float)$dpConfig['dummyRate']
1736
+ : (1.0 / (1.0 + exp($epsilon)));
1737
+
1738
+ $randFloat = random_int(0, 999999) / 1000000.0;
1739
+ if ($randFloat < $decoyProb) {
1740
+ $decoyBytes = random_bytes(32);
1741
+ $decoyZkpY = ltrim(bin2hex($decoyBytes), '0') ?: '1';
1742
+
1743
+ $uDecoy = (random_int(1, 999999) / 1000000.0) - 0.5;
1744
+ $safeUDecoy = abs($uDecoy) < 1e-7 ? ($uDecoy >= 0 ? 1e-7 : -1e-7) : $uDecoy;
1745
+ $b = 5000 / max(0.1, $epsilon);
1746
+ $decoyNoise = (int)max(-60000, min(60000, round(-$b * ($safeUDecoy <=> 0) * log(1.0 - 2.0 * abs($safeUDecoy)))));
1747
+ $decoyTimestamp = $now + $decoyNoise;
1748
+
1749
+ $sendThreatReport($decoyZkpY, $decoyTimestamp);
1750
+ }
1602
1751
  }
1603
1752
  }
1604
1753
 
1605
- private function asyncPost(string $url, array $params): void
1754
+ protected function asyncPost(string $url, array $params): void
1606
1755
  {
1607
1756
  // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url used if available
1608
1757
  $parts = function_exists('wp_parse_url') ? wp_parse_url($url) : parse_url($url);