@anonympins/fingerprint 0.7.4 → 0.8.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/anonympins-bot-mitigation-pow.zip +0 -0
  5. package/src/js/dynamic-wasm.js +218 -218
  6. package/src/js/fingerprint.client.js +1784 -1738
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +489 -99
  9. package/src/js/fingerprint.utils.js +252 -2
  10. package/src/js/pow.worker.js +2 -2
  11. package/src/js/tests/cross_parity.test.js +3 -3
  12. package/src/js/tests/dns-circuit-breaker.test.js +6 -6
  13. package/src/js/tests/dynamic-wasm.test.js +77 -77
  14. package/src/js/tests/fingerprint.builder.test.js +76 -78
  15. package/src/js/tests/fingerprint.client.init.test.js +78 -0
  16. package/src/js/tests/fingerprint.client.test.js +16 -17
  17. package/src/js/tests/fingerprint.test.js +37 -12
  18. package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
  19. package/src/js/tests/library.test.js +94 -95
  20. package/src/js/tests/obfuscation.test.js +8 -8
  21. package/src/js/tests/quicFingerprint.test.js +47 -0
  22. package/src/js/tests/tcpFingerprint.test.js +4 -4
  23. package/src/js/tests/threatIntel.test.js +40 -1
  24. package/src/js/upow-model-task.js +155 -112
  25. package/src/php/Challenge/ChallengeUtils.php +392 -86
  26. package/src/php/Config/SecurityProfiles.php +79 -61
  27. package/src/php/DirectFingerprint.php +55 -8
  28. package/src/php/FingerprintBuilder.php +13 -13
  29. package/src/php/FingerprintClient.php +6 -6
  30. package/src/php/FingerprintEngine.php +2122 -1690
  31. package/src/php/Ja3AnomalyDetector.php +22 -22
  32. package/src/php/Optimization/FunctionRegistry.php +4 -4
  33. package/src/php/Optimization/Optimization.php +6 -6
  34. package/src/php/RequestContext.php +11 -11
  35. package/src/php/Store/InMemoryStore.php +96 -66
  36. package/src/php/Store/MongoDbStore.php +2 -2
  37. package/src/php/Store/RedisStore.php +1 -1
  38. package/src/php/Store/StoreManager.php +48 -35
  39. package/src/php/Tests/ChallengeUtilsTest.php +31 -0
  40. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  41. package/src/php/Tests/FingerprintClientTest.php +70 -70
  42. package/src/php/Tests/PowTest.php +37 -39
  43. package/src/php/Tests/QuicFingerprintTest.php +115 -53
  44. package/src/php/Tests/ThreatIntelTest.php +34 -0
  45. package/src/php/Utils/BigInt.php +202 -202
  46. package/src/php/Utils/BlockList.php +13 -13
  47. package/src/php/Utils/Logger.php +19 -1
  48. package/src/php/Utils/MaliciousPatterns.php +6 -6
  49. package/src/php/Utils/MetricsManager.php +209 -209
  50. package/src/php/Utils/RequestUtils.php +362 -1
  51. package/src/php/Utils/TLSClientHelloParser.php +385 -369
  52. package/src/php/WordPress/WpDbStore.php +7 -7
  53. package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
  54. package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
  55. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
  56. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
  57. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  58. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
  59. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  60. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
  61. package/src/php/WordPress/package.php +80 -101
  62. package/src/php/WordPress/readme.txt +21 -5
  63. package/src/php/bin/auto-tune.php +1 -1
  64. package/public/fingerprint-anti-bot.zip +0 -0
@@ -13,6 +13,9 @@ import {GpuPowSolver} from "./gpu_pow.solver.js";
13
13
  import {
14
14
  verifyZkpProof,
15
15
  sanitizeRedirectPath,
16
+ extractPrivateAccessTokens,
17
+ parsePrivateAccessToken,
18
+ verifyPatSignature,
16
19
  decodePolymorphicFingerprint,
17
20
  deepMerge,
18
21
  getHeaderSignature,
@@ -23,7 +26,8 @@ import {
23
26
  isLoopbackIp,
24
27
  isPrivateIp,
25
28
  parseUserAgent,
26
- safeJsonStringify
29
+ safeJsonStringify,
30
+ generateIssuerPemKeys
27
31
  } from "./fingerprint.utils.js";
28
32
 
29
33
 
@@ -68,7 +72,8 @@ function canAttemptDns() {
68
72
  }
69
73
 
70
74
  /**
71
- * Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante).
75
+ * Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante)
76
+ * avec protection par Anonymat Différentiel (Differential Privacy / Bruit de Laplace & Leurres).
72
77
  * @private
73
78
  * @param {string} zkpY - La clé publique ZKP du terminal banni.
74
79
  * @param {object} config - La configuration de sécurité.
@@ -77,46 +82,87 @@ async function broadcastBannedZkp(zkpY, config) {
77
82
  const peers = config.federatedPeers || [];
78
83
  if (peers.length === 0) return;
79
84
 
80
- const timestamp = Date.now();
81
- const msg = `${timestamp}:${zkpY}`;
82
-
83
- let signature = '';
84
- let isAsymmetric = false;
85
+ const dpConfig = config.differentialPrivacy || {};
86
+ const dpEnabled = dpConfig.enabled !== false;
87
+ const epsilon = typeof dpConfig.epsilon === 'number' ? dpConfig.epsilon : (config.dpEpsilon || 1.0);
85
88
 
86
- if (process.env.ED25519_PRIVATE_KEY) {
87
- try {
88
- const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
89
- const signBuffer = crypto.sign(null, Buffer.from(msg), {
90
- key: cleanKey,
91
- format: 'pem',
92
- type: 'pkcs8'
93
- });
94
- signature = signBuffer.toString('hex');
95
- isAsymmetric = true;
96
- } catch (e) {
97
- console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
89
+ const sendReport = (targetZkpY, ts) => {
90
+ const msg = `${ts}:${targetZkpY}`;
91
+ let signature = '';
92
+ let isAsymmetric = false;
93
+
94
+ if (process.env.ED25519_PRIVATE_KEY) {
95
+ try {
96
+ const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
97
+ const signBuffer = crypto.sign(null, Buffer.from(msg), {
98
+ key: cleanKey,
99
+ format: 'pem',
100
+ type: 'pkcs8'
101
+ });
102
+ signature = signBuffer.toString('hex');
103
+ isAsymmetric = true;
104
+ } catch (e) {
105
+ console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
106
+ }
98
107
  }
99
- }
100
108
 
101
- if (!isAsymmetric) {
102
- const secret = config.federationSecret || getPowSecret();
103
- signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
109
+ if (!isAsymmetric) {
110
+ const secret = config.federationSecret || getPowSecret();
111
+ signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
112
+ }
113
+
114
+ peers.forEach(peerUrl => {
115
+ fetch(peerUrl + '?coop_op=share_threat_intel', {
116
+ method: 'POST',
117
+ headers: {
118
+ 'Content-Type': 'application/json',
119
+ 'X-Federation-Signature': isAsymmetric ? '' : signature,
120
+ 'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
121
+ 'X-Federation-Timestamp': String(ts)
122
+ },
123
+ body: JSON.stringify({ zkpY: targetZkpY })
124
+ }).catch(() => {
125
+ // Échec de propagation silencieux pour ne pas perturber le thread principal
126
+ });
127
+ });
128
+ };
129
+
130
+ let reportTimestamp = Date.now();
131
+
132
+ // 1. Differential Privacy : Perturbation laplacienne de l'horodatage
133
+ if (dpEnabled) {
134
+ const deltaT = 5000; // Sensibilité de 5s
135
+ const b = deltaT / Math.max(0.1, epsilon);
136
+ const u = secureRandomFloat() - 0.5;
137
+ const safeU = Math.abs(u) < 1e-7 ? (u >= 0 ? 1e-7 : -1e-7) : u;
138
+ const laplaceNoise = -b * Math.sign(safeU) * Math.log(1 - 2 * Math.abs(safeU));
139
+ const clampedNoise = Math.max(-60000, Math.min(60000, Math.round(laplaceNoise)));
140
+ reportTimestamp = Date.now() + clampedNoise;
104
141
  }
105
142
 
106
- peers.forEach(peerUrl => {
107
- fetch(peerUrl + '?coop_op=share_threat_intel', {
108
- method: 'POST',
109
- headers: {
110
- 'Content-Type': 'application/json',
111
- 'X-Federation-Signature': isAsymmetric ? '' : signature,
112
- 'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
113
- 'X-Federation-Timestamp': String(timestamp)
114
- },
115
- body: JSON.stringify({ zkpY })
116
- }).catch(() => {
117
- // Échec de propagation silencieux pour ne pas perturber le thread principal
118
- });
119
- });
143
+ sendReport(zkpY, reportTimestamp);
144
+
145
+ // 2. Differential Privacy : Réponse randomisée par injection de leurres (Decoy ZKP)
146
+ if (dpEnabled) {
147
+ const decoyProbability = dpConfig.dummyRate !== undefined
148
+ ? dpConfig.dummyRate
149
+ : (1 / (1 + Math.exp(epsilon)));
150
+
151
+ if (secureRandomFloat() < decoyProbability) {
152
+ const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
153
+ const decoyBytes = crypto.randomBytes(32);
154
+ const decoyInt = (BigInt('0x' + decoyBytes.toString('hex')) % (ZKP_P - 2n)) + 1n;
155
+ const decoyZkpY = decoyInt.toString(16);
156
+
157
+ const uDecoy = secureRandomFloat() - 0.5;
158
+ const safeUDecoy = Math.abs(uDecoy) < 1e-7 ? (uDecoy >= 0 ? 1e-7 : -1e-7) : uDecoy;
159
+ const b = 5000 / Math.max(0.1, epsilon);
160
+ const decoyNoise = Math.max(-60000, Math.min(60000, Math.round(-b * Math.sign(safeUDecoy) * Math.log(1 - 2 * Math.abs(safeUDecoy)))));
161
+ const decoyTimestamp = Date.now() + decoyNoise;
162
+
163
+ sendReport(decoyZkpY, decoyTimestamp);
164
+ }
165
+ }
120
166
  }
121
167
 
122
168
  const withTimeout = (promise, ms) => {
@@ -166,6 +212,9 @@ const bingbotEntries = loadBotWhitelist('bingbot.json', [
166
212
  const yandexEntries = loadBotWhitelist('yandex.json', [
167
213
  ]);
168
214
 
215
+ const facebookEntries = loadBotWhitelist('facebook.json', [
216
+ ]);
217
+
169
218
  function generateSessionMapping() {
170
219
  const randomStr = (len = 6) => Array.from({ length: len }, () => String.fromCharCode(crypto.randomInt(97, 123))).join('');
171
220
  const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
@@ -355,6 +404,12 @@ export function getProtocolAnomalyScore(context) {
355
404
  const ua = context.headers?.['user-agent'] || '';
356
405
  const uaParts = parseUserAgent(ua);
357
406
  const browser = uaParts.browser;
407
+ const isChromium = browser?.startsWith('Chrome') || browser?.startsWith('Edge');
408
+ const isFirefox = browser?.startsWith('Firefox');
409
+ const isSafari = browser?.startsWith('Safari');
410
+ const isHumanBrowser = ['Chrome', 'Firefox', 'Safari', 'Edge'].includes(
411
+ browser?.split('/')[0] || null
412
+ );
358
413
 
359
414
  if (browser) {
360
415
  // HTTP/2 Anomaly Logic
@@ -365,9 +420,6 @@ export function getProtocolAnomalyScore(context) {
365
420
  const connWindow = parseInt(parts[1], 10);
366
421
  const streamPriority = parts[2] || '';
367
422
  const headerOrder = parts.length > 3 ? parts[3] : '';
368
- const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
369
- const isFirefox = browser.startsWith('Firefox');
370
- const isSafari = browser.startsWith('Safari');
371
423
 
372
424
  if (isChromium) {
373
425
  if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
@@ -379,6 +431,33 @@ export function getProtocolAnomalyScore(context) {
379
431
  if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
380
432
  }
381
433
 
434
+ // Analyse de la structure des dépendances de flux HTTP/2 (RFC 7540)
435
+ if (streamPriority) {
436
+ const priorityEntries = streamPriority.split(',').map(s => s.trim()).filter(Boolean);
437
+ for (const entry of priorityEntries) {
438
+ const sub = entry.split(':');
439
+ if (sub.length >= 4) {
440
+ const streamId = parseInt(sub[0], 10);
441
+ const exclusive = parseInt(sub[1], 10);
442
+ const depStreamId = parseInt(sub[2], 10);
443
+ const weight = parseInt(sub[3], 10);
444
+
445
+ // Auto-dépendance interdite (RFC 7540 Section 5.3.1)
446
+ if (streamId === depStreamId && streamId > 0) {
447
+ http2Anomaly += 60.0;
448
+ }
449
+ // Poids en dehors de [1, 256] (RFC 7540 Section 5.3.2)
450
+ if (weight < 1 || weight > 256) {
451
+ http2Anomaly += 45.0;
452
+ }
453
+ // Signature scraper : Go net/http2 ou Python h2 par défaut (poids 16 sur flux 0 non-exclusif)
454
+ if (isChromium && depStreamId === 0 && weight === 16 && exclusive === 0) {
455
+ http2Anomaly += 55.0;
456
+ }
457
+ }
458
+ }
459
+ }
460
+
382
461
  // Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
383
462
  if (parts.length >= 5) {
384
463
  const frameCountsStr = parts[4];
@@ -399,6 +478,13 @@ export function getProtocolAnomalyScore(context) {
399
478
  if (priorityCount === 0) http2Anomaly += 25.0;
400
479
  // Chrome est agressif avec les WINDOW_UPDATE
401
480
  if (windowUpdateCount < 2) http2Anomaly += 20.0;
481
+
482
+ // NOUVEAU: Analyse de l'arbre de dépendances HTTP/2.
483
+ // Un arbre plat (peu de dépendances) est suspect pour Chromium.
484
+ const priorityDependencies = streamPriority.split(',').length - 1;
485
+ if (streamPriority.includes(',') && priorityDependencies < 2 && priorityCount > 0) {
486
+ http2Anomaly += 35.0;
487
+ }
402
488
  } else if (isFirefox) {
403
489
  // Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
404
490
  if (priorityCount > 1) http2Anomaly += 20.0;
@@ -426,10 +512,6 @@ export function getProtocolAnomalyScore(context) {
426
512
  const frameOrderRaw = parts[3] || context.headers?.['x-quic-frame-order'] || context.quicFrameOrder || '';
427
513
  const frameOrder = frameOrderRaw.toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
428
514
 
429
- const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
430
- const isFirefox = browser.startsWith('Firefox');
431
- const isSafari = browser.startsWith('Safari');
432
-
433
515
  const maxData = parseInt(params['1'] || params['0x01'] || '0', 10);
434
516
  const maxStreams = parseInt(params['4'] || params['8'] || params['0x08'] || '0', 10);
435
517
  const bidiLocal = parseInt(params['5'] || params['0x05'] || '0', 10);
@@ -439,7 +521,33 @@ export function getProtocolAnomalyScore(context) {
439
521
  // Contrôle de flux global et nombre de flux bidirectionnels
440
522
  if (maxData > 0 && maxData < 1048576) quicAnomaly += 40.0;
441
523
  if (maxStreams > 0 && maxStreams !== 100) quicAnomaly += 30.0;
442
- if (priorityOrder && !priorityOrder.includes('u=')) quicAnomaly += 30.0;
524
+
525
+ // NOUVEAU: Analyse des trames PRIORITY_UPDATE (RFC 9218)
526
+ if (priorityOrder) {
527
+ // Contrôle de l'urgence (u: 0 à 7 strict)
528
+ const uMatch = priorityOrder.match(/\bu=(-?\d+)\b/);
529
+ if (uMatch) {
530
+ const urgency = parseInt(uMatch[1], 10);
531
+ if (urgency < 0 || urgency > 7) {
532
+ quicAnomaly += 50.0;
533
+ }
534
+ } else if (!priorityOrder.includes('u=')) {
535
+ quicAnomaly += 30.0;
536
+ }
537
+
538
+ // Paramètre incrémental invalide ou malformé
539
+ if (/\bi=\d+\b/.test(priorityOrder)) {
540
+ quicAnomaly += 40.0;
541
+ }
542
+ if (/\bw=\d+\b/.test(priorityOrder)) {
543
+ quicAnomaly += 45.0;
544
+ }
545
+ // Un navigateur légitime enverra probablement une mise à jour incrémentale 'i'
546
+ if (priorityOrder.includes('u=') && !priorityOrder.includes('i') && !/\bu=[0-7],i\b/.test(priorityOrder)) {
547
+ quicAnomaly += 40.0; // Urgence envoyée sans drapeau incrémental, suspect pour Chrome
548
+ }
549
+ if (priorityOrder === 'p' || priorityOrder === 'i') quicAnomaly += 50.0; // Champ malformé
550
+ }
443
551
 
444
552
  // Contrôle de flux bidi (Chromium alloue 6MB = 6291456 ou au minimum 512 Ko)
445
553
  // curl-impersonate / quiche alloue 256 Ko (262144) ou 128 Ko (131072)
@@ -461,6 +569,12 @@ export function getProtocolAnomalyScore(context) {
461
569
  if (pIdx !== -1 && sIdx !== -1 && pIdx < sIdx) {
462
570
  quicAnomaly += 60.0;
463
571
  }
572
+
573
+ // RFC 9114: Le flux de contrôle ne doit pas contenir de doublon SETTINGS
574
+ const sCount = frameOrder.filter(f => f === 's' || f === 'settings' || f === '4').length;
575
+ if (sCount > 1) {
576
+ quicAnomaly += 50.0;
577
+ }
464
578
  }
465
579
  } else if (isFirefox) {
466
580
  const maxData = parseInt(params['1'] || '0', 10);
@@ -490,6 +604,55 @@ export function getProtocolAnomalyScore(context) {
490
604
  }
491
605
  }
492
606
  }
607
+
608
+ // NOUVEAU: Analyse du ratio de compression HPACK/QPACK
609
+ const compressionInfo = context.headers?.['x-compression-info'] || null;
610
+ if (compressionInfo) {
611
+ const info = {};
612
+ compressionInfo.split(',').forEach(p => {
613
+ const [key, value] = p.split(':');
614
+ if (key && value) info[key.trim()] = parseFloat(value);
615
+ });
616
+
617
+ const reqCount = info.req_count || 0;
618
+ const hpackRatio = info.hpack_ratio;
619
+ const qpackRatio = info.qpack_ratio;
620
+ const dynamicEntries = info.dynamic_table_entries !== undefined ? info.dynamic_table_entries : info.table_entries;
621
+ const dynamicHits = info.dynamic_hits;
622
+ const dynamicTableSize = info.dynamic_table_size;
623
+
624
+ // Après quelques requêtes, un vrai navigateur doit avoir un bon ratio de compression
625
+ if (reqCount > 3 && isHumanBrowser) {
626
+ if (hpackRatio !== undefined) {
627
+ if (hpackRatio < 0.4) {
628
+ http2Anomaly += (1 - hpackRatio) * 50;
629
+ } else if (hpackRatio > 0.75) {
630
+ http2Anomaly += Math.min(50.0, (hpackRatio - 0.5) * 100);
631
+ }
632
+ }
633
+ if (qpackRatio !== undefined) {
634
+ if (qpackRatio < 0.4) {
635
+ quicAnomaly += (1 - qpackRatio) * 50;
636
+ } else if (qpackRatio > 0.75) {
637
+ quicAnomaly += Math.min(50.0, (qpackRatio - 0.5) * 100);
638
+ }
639
+ }
640
+
641
+ // Détection de réinitialisation de table dynamique
642
+ if (dynamicEntries !== undefined && dynamicEntries === 0) {
643
+ http2Anomaly += 45.0;
644
+ quicAnomaly += 45.0;
645
+ }
646
+ if (dynamicHits !== undefined && dynamicHits === 0) {
647
+ http2Anomaly += 35.0;
648
+ quicAnomaly += 35.0;
649
+ }
650
+ if (dynamicTableSize !== undefined && dynamicTableSize === 0) {
651
+ http2Anomaly += 40.0;
652
+ quicAnomaly += 40.0;
653
+ }
654
+ }
655
+ }
493
656
  }
494
657
 
495
658
  return {
@@ -706,6 +869,7 @@ const securityProfiles = {
706
869
  http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
707
870
  threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
708
871
  virtualizationScore: 0.8,
872
+ mtuAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie MTU/fragmentation
709
873
  },
710
874
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
711
875
  patterns: {
@@ -754,6 +918,7 @@ const securityProfiles = {
754
918
  renderingAnomalyScore: 1.0,
755
919
  http2AnomalyScore: 1.0,
756
920
  threatIntelScore: 1.0,
921
+ mtuAnomalyScore: 0.9,
757
922
  virtualizationScore: 1.0,
758
923
  },
759
924
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
@@ -805,6 +970,7 @@ const securityProfiles = {
805
970
  renderingAnomalyScore: 0.2,
806
971
  threatIntelScore: 0.6,
807
972
  virtualizationScore: 0.8,
973
+ mtuAnomalyScore: 0.9,
808
974
  },
809
975
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
810
976
  patterns: {
@@ -854,6 +1020,7 @@ const securityProfiles = {
854
1020
  quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
855
1021
  http2AnomalyScore: 0.5,
856
1022
  renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
1023
+ mtuAnomalyScore: 0.9,
857
1024
  virtualizationScore: 0.8,
858
1025
  },
859
1026
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
@@ -902,6 +1069,7 @@ const securityProfiles = {
902
1069
  quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
903
1070
  http2AnomalyScore: 0.9,
904
1071
  renderingAnomalyScore: 0.9,
1072
+ mtuAnomalyScore: 0.9,
905
1073
  virtualizationScore: 0.8,
906
1074
  },
907
1075
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
@@ -1694,6 +1862,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
1694
1862
  if (!expiry || Date.now() > expiry) {
1695
1863
  return false;
1696
1864
  }
1865
+ if (statelessData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
1866
+ if (!deviceId || deviceId === storedDeviceId) {
1867
+ return true;
1868
+ }
1869
+ }
1697
1870
  if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1698
1871
  const expectedY = storedDeviceHash.split(':')[1];
1699
1872
  if (zkpProof) {
@@ -1721,6 +1894,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
1721
1894
  await store.delete(`ticket:${ticket}`);
1722
1895
  return false;
1723
1896
  }
1897
+ if (ticketData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
1898
+ if (!deviceId || deviceId === storedDeviceId) {
1899
+ return true;
1900
+ }
1901
+ }
1724
1902
  if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1725
1903
  const expectedY = storedDeviceHash.split(':')[1];
1726
1904
  if (zkpProof) {
@@ -2309,29 +2487,17 @@ function getBehaviorScore(context) {
2309
2487
  return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
2310
2488
  }
2311
2489
 
2312
- // Traitement direct si un flag binaire client-side (1 = humain, 0 = bot) est envoyé
2313
- if (behaviorHeader === 1 || behaviorHeader === '1') {
2314
- return { behaviorScore: 0 };
2315
- }
2316
- if (behaviorHeader === 0 || behaviorHeader === '0') {
2317
- return { behaviorScore: 100 };
2318
- }
2319
-
2320
2490
  // Traitement direct si un score numérique est envoyé côté client
2321
2491
  if (typeof behaviorHeader === 'number') {
2322
2492
  return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
2323
2493
  }
2324
2494
  if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
2325
2495
  const trimmed = behaviorHeader.trim();
2326
- if (trimmed === '1') return { behaviorScore: 0 };
2327
- if (trimmed === '0') return { behaviorScore: 100 };
2328
2496
  return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
2329
2497
  }
2330
2498
 
2331
2499
  try {
2332
2500
  const metrics = JSON.parse(behaviorHeader);
2333
- if (metrics === 1) return { behaviorScore: 0 };
2334
- if (metrics === 0) return { behaviorScore: 100 };
2335
2501
  if (typeof metrics === 'number') {
2336
2502
  return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
2337
2503
  }
@@ -2653,7 +2819,7 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2653
2819
  }
2654
2820
 
2655
2821
  const { ja3, ja4 } = actualGetTlsFingerprintFn(context) || { ja3: null, ja4: null }; // Defensive check
2656
- const ua = context.headers["user-agent"] || '';
2822
+ const ua = context.headers?.["user-agent"] || '';
2657
2823
  const ja3Raw = context.headers['x-ja3-raw'] || null;
2658
2824
  const httpVersion = context.httpVersion || '';
2659
2825
 
@@ -2675,7 +2841,8 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2675
2841
  score = Math.max(score, 100);
2676
2842
  }
2677
2843
 
2678
- const claimedBrowser = parseUserAgent(ua).browser?.split('/')[0] || null;
2844
+ const uaParts = parseUserAgent(ua);
2845
+ const claimedBrowser = uaParts.browser?.split('/')[0] || null;
2679
2846
  const isHumanBrowser = ['Chrome', 'Firefox', 'Safari', 'Edge'].includes(claimedBrowser);
2680
2847
 
2681
2848
  // --- ANALYSE 2 : CONTRÔLE PROFOND SUR L'EMPREINTE BRUTE (RAW JA3) ---
@@ -2717,8 +2884,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2717
2884
  if (ja4) {
2718
2885
  const parsedJa4 = parseJa4(ja4);
2719
2886
  if (parsedJa4) {
2720
- const uaParts = parseUserAgent(ua);
2721
-
2722
2887
  // Check 1: Incohérence ALPN / HTTP Version
2723
2888
  if (parsedJa4.alpn === 'h2' && (context.httpVersion === '1.1' || context.httpVersion === '1.0')) {
2724
2889
  const hasProxy = context.headers['via'] || context.headers['forwarded'] || context.headers['x-forwarded-proto'] || context.headers['x-forwarded-for'];
@@ -2742,6 +2907,25 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2742
2907
  }
2743
2908
  }
2744
2909
 
2910
+ // Validation TLS 1.3 Encrypted Client Hello (ECH) & Discrépance SNI
2911
+ const outerSni = context.headers?.['x-ech-outer-sni'];
2912
+ const host = (context.headers?.['host'] || '').split(':')[0].trim();
2913
+ const hasEch = context.headers?.['x-ech-present'] === 'true' || context.headers?.['x-has-ech'] === 'true';
2914
+ const echExpected = context.headers?.['x-ech-expected'] === 'true' || Boolean(outerSni);
2915
+
2916
+ if (outerSni && host && outerSni.toLowerCase() !== host.toLowerCase() && !hasEch) {
2917
+ score = Math.max(score, 60.0);
2918
+ }
2919
+
2920
+ const browserVersion = parseInt(uaParts.browser?.split('/')[1] || '0', 10);
2921
+ const isModernEchBrowser = (
2922
+ (claimedBrowser === 'Chrome' || claimedBrowser === 'Edge') && browserVersion >= 119
2923
+ ) || (claimedBrowser === 'Firefox' && browserVersion >= 118);
2924
+
2925
+ if (context.isHttps && isModernEchBrowser && echExpected && !hasEch) {
2926
+ score = Math.max(score, 55.0);
2927
+ }
2928
+
2745
2929
  // 2. If no JA3 hash is available, we cannot perform the consistency check.
2746
2930
  if (ja3 && ua) {
2747
2931
  // Look up the expected browser family (or families) from our database.
@@ -2780,7 +2964,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2780
2964
  // Create the promise for the async part (Check 4)
2781
2965
  const promise = (async () => {
2782
2966
  let asyncScore = score;
2783
- const uaParts = parseUserAgent(ua);
2784
2967
 
2785
2968
  if (uaParts.browser) {
2786
2969
  const browserFamily = uaParts.browser.split('/')[0];
@@ -3912,6 +4095,52 @@ function getVirtualizationAnomalyScore(context) {
3912
4095
  return Math.min(100.0, score);
3913
4096
  }
3914
4097
 
4098
+ /**
4099
+ * @private
4100
+ * Analyzes TCP MTU and fragmentation flags to detect network tunnels (VPN/Proxy).
4101
+ * @param {object} context - The request context.
4102
+ * @returns {{mtuAnomalyScore: number}}
4103
+ */
4104
+ function getMtuAnomalyScore(context) {
4105
+ const mtuHeader = context.headers?.['x-tcp-mtu-info'];
4106
+ if (!mtuHeader || typeof mtuHeader !== 'string') {
4107
+ return { mtuAnomalyScore: 0.0 };
4108
+ }
4109
+
4110
+ const parts = mtuHeader.split(':');
4111
+ if (parts.length < 2) {
4112
+ return { mtuAnomalyScore: 0.0 };
4113
+ }
4114
+
4115
+ const mtu = parseInt(parts[0], 10);
4116
+ const df = parseInt(parts[1], 10); // Don't Fragment bit (1 or 0)
4117
+
4118
+ if (isNaN(mtu) || isNaN(df)) {
4119
+ return { mtuAnomalyScore: 0.0 };
4120
+ }
4121
+
4122
+ let score = 0.0;
4123
+
4124
+ // Pénalité modérée pour les MTU typiques des VPNs/tunnels.
4125
+ if (mtu > 1200 && mtu <= 1420) {
4126
+ score += 35.0;
4127
+ } else if (mtu > 1420 && mtu < 1492) {
4128
+ score += 20.0;
4129
+ }
4130
+
4131
+ const ua = context.headers?.['user-agent'] || '';
4132
+ const uaParts = parseUserAgent(ua);
4133
+ const os = uaParts.os;
4134
+
4135
+ // Pénalités additionnelles en cas d'incohérence OS vs signature réseau.
4136
+ if (os) {
4137
+ if (os.startsWith('Windows') && mtu < 1492) score += 20.0;
4138
+ if ((os.startsWith('Android') || os.startsWith('iOS')) && mtu < 1480) score += 15.0;
4139
+ if (df === 0 && (os.startsWith('Windows') || os.startsWith('Mac') || os.startsWith('Linux'))) score += 40.0;
4140
+ }
4141
+
4142
+ return { mtuAnomalyScore: Math.min(100.0, score) };
4143
+ }
3915
4144
 
3916
4145
  /**
3917
4146
  * Returns a vector of raw (unweighted) suspicion scores.
@@ -4002,6 +4231,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
4002
4231
  const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
4003
4232
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
4004
4233
  const virtualizationScore = getVirtualizationAnomalyScore(context);
4234
+ const { mtuAnomalyScore } = getMtuAnomalyScore(context);
4005
4235
 
4006
4236
  // Save the updated device state to the store
4007
4237
  // Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
@@ -4013,7 +4243,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
4013
4243
  deviceData.ips = new Set(deviceData.ips);
4014
4244
  }
4015
4245
  // Le vecteur de suspicion est maintenant complet.
4016
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
4246
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore, mtuAnomalyScore };
4017
4247
  };
4018
4248
 
4019
4249
  // A residential user can change networks (home, 4G, public wifi).
@@ -4232,6 +4462,24 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
4232
4462
  </body></html>`;
4233
4463
  }
4234
4464
 
4465
+ /**
4466
+ * Generates closed-shadow DOM honeypot markup for server-rendered challenge pages.
4467
+ * Utilizes Declarative Shadow DOM with mode="closed" to prevent screen reader false positives
4468
+ * while capturing bot web scrapers and crawlers.
4469
+ * @param {string[]} trapUrls
4470
+ * @returns {string}
4471
+ */
4472
+ function generateClosedShadowTraps(trapUrls = []) {
4473
+ const list = Array.isArray(trapUrls) ? trapUrls : [];
4474
+ if (list.length === 0) return '';
4475
+
4476
+ const trapLinksHtml = list.map((url, idx) => {
4477
+ return `<a href="${url}" rel="nofollow" tabindex="-1"><span>&gt; ${idx + 1}</span></a>`;
4478
+ }).join(' ');
4479
+
4480
+ return `<div style="position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;" aria-hidden="true"><template shadowrootmode="closed" shadowroot="closed"><style>:host { position: absolute; left: -9999px; top: -9999px; width: 0; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; } a { color: transparent; text-decoration: none; }</style>${trapLinksHtml}</template></div>`;
4481
+ }
4482
+
4235
4483
  /**
4236
4484
  * Generates the HTML content for a combined CPU + Memory PoW challenge.
4237
4485
  * @param {object} cpuChallengeDetails - Details from generateCpuTargetChallenge.
@@ -4248,6 +4496,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
4248
4496
  const fingerprint = originalFingerprint;
4249
4497
  const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp, tlsSessionId);
4250
4498
  const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
4499
+ const trapContainerHtml = generateClosedShadowTraps(trapUrls);
4251
4500
 
4252
4501
  // Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
4253
4502
  const clientInitConfig = {
@@ -4315,12 +4564,24 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
4315
4564
  }
4316
4565
 
4317
4566
  if (!htmlTemplate) {
4318
- htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script></body></html>`; // eslint-disable-line max-len
4567
+ htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>`; // eslint-disable-line max-len
4319
4568
  }
4320
4569
 
4321
- return htmlTemplate
4570
+ let renderedHtml = htmlTemplate
4322
4571
  .replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
4323
4572
  .replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
4573
+
4574
+ if (renderedHtml.includes('<!-- FINGERPRINT_TRAPS -->')) {
4575
+ renderedHtml = renderedHtml.replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
4576
+ } else if (trapContainerHtml) {
4577
+ if (renderedHtml.includes('</body>')) {
4578
+ renderedHtml = renderedHtml.replace('</body>', `${trapContainerHtml}</body>`);
4579
+ } else {
4580
+ renderedHtml += trapContainerHtml;
4581
+ }
4582
+ }
4583
+
4584
+ return renderedHtml;
4324
4585
  }
4325
4586
 
4326
4587
  /**
@@ -4502,6 +4763,8 @@ export class FingerprintEngine {
4502
4763
  // Auto-generate Ed25519 key pair on load if indicated and keys are not set
4503
4764
  if (securityConfig && (securityConfig.useAsymmetricTickets || securityConfig.ed25519 === 'auto') && !process.env.ED25519_PRIVATE_KEY) {
4504
4765
  const persistentKeyPath = join(configDir, 'ed25519_key.json');
4766
+ const privatePemPath = join(configDir, 'issuer-private.pem');
4767
+ const publicPemPath = join(configDir, 'issuer-public.pem');
4505
4768
  if (existsSync(persistentKeyPath)) {
4506
4769
  try {
4507
4770
  const keys = JSON.parse(readFileSync(persistentKeyPath, 'utf-8'));
@@ -4511,6 +4774,14 @@ export class FingerprintEngine {
4511
4774
  } catch (e) {
4512
4775
  console.error('[Fingerprint] Failed to load persistent Ed25519 keys:', e.message);
4513
4776
  }
4777
+ } else if (existsSync(privatePemPath) && existsSync(publicPemPath)) {
4778
+ try {
4779
+ process.env.ED25519_PRIVATE_KEY = readFileSync(privatePemPath, 'utf-8');
4780
+ process.env.ED25519_PUBLIC_KEY = readFileSync(publicPemPath, 'utf-8');
4781
+ this._log('Persistent Ed25519 PEM keys loaded from disk');
4782
+ } catch (e) {
4783
+ console.error('[Fingerprint] Failed to load persistent Ed25519 PEM keys:', e.message);
4784
+ }
4514
4785
  } else {
4515
4786
  try {
4516
4787
  const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
@@ -4611,7 +4882,9 @@ export class FingerprintEngine {
4611
4882
  'similarityThreshold', 'reset',
4612
4883
  'ed25519_private_key', 'ed25519_public_key', 'upowModel',
4613
4884
  'federatedPeers', 'federationSecret', 'filterWhitelist',
4614
- 'challengeRateLimit'
4885
+ 'challengeRateLimit',
4886
+ 'differentialPrivacy', 'dpEpsilon'
4887
+ ,'pat'
4615
4888
  ]);
4616
4889
 
4617
4890
  // 1. Check for essential keys
@@ -4649,35 +4922,31 @@ export class FingerprintEngine {
4649
4922
  }
4650
4923
  }
4651
4924
  calculateFinalScore(suspicionVector) {
4652
- const { weights } = this.securityConfig;
4653
- if (!weights) return 0;
4654
-
4655
- const score =
4656
- (suspicionVector.historyScore || 0) * (weights.historyScore || 0) +
4657
- (suspicionVector.rotationScore || 0) * (weights.rotationScore || 0) +
4658
- (suspicionVector.headerAnomalyScore || 0) * (weights.headerAnomalyScore || 0) +
4659
- (suspicionVector.requestPatternScore || 0) * (weights.requestPatternScore || 0) +
4660
- (suspicionVector.inconsistencyScore || 0) * (weights.inconsistencyScore || 0) +
4661
- (suspicionVector.honeypotScore || 0) * (weights.honeypotScore || 0) +
4662
- (suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
4663
- (suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
4664
- (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
4665
- (suspicionVector.botnetClusterScore || 0) * (weights.botnetClusterScore || 0) +
4666
- (suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
4667
- (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
4668
- (suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0) +
4669
- (suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
4670
- (suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
4671
- (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
4672
- (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
4673
- (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
4674
- (suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) +
4675
- (suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
4676
- (suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
4677
- (suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
4678
- (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
4679
-
4680
- return Math.min(100, score);
4925
+ const baseWeights = this.securityConfig.weights;
4926
+ if (!baseWeights) return 0;
4927
+
4928
+ const dynamicWeights = { ...baseWeights };
4929
+ if ((suspicionVector.mtuAnomalyScore || 0) > 50.0) {
4930
+ this._log('Tunnel detected, amplifying suspicion weights.', { mtuScore: suspicionVector.mtuAnomalyScore });
4931
+
4932
+ // Augmente le poids des incohérences de bas niveau (difficiles à falsifier)
4933
+ dynamicWeights.tlsSpoofingScore = (baseWeights.tlsSpoofingScore || 0.8) * 1.25;
4934
+ dynamicWeights.crossLayerInconsistencyScore = (baseWeights.crossLayerInconsistencyScore || 0.4) * 1.4;
4935
+ dynamicWeights.clientHintsInconsistencyScore = (baseWeights.clientHintsInconsistencyScore || 0.7) * 1.2;
4936
+
4937
+ // Augmente le poids des anomalies comportementales (un bot derrière un VPN est plus suspect)
4938
+ dynamicWeights.behaviorScore = (baseWeights.behaviorScore || 0.7) * 1.15;
4939
+ dynamicWeights.requestPatternScore = (baseWeights.requestPatternScore || 0.6) * 1.2;
4940
+ }
4941
+
4942
+ let score = 0;
4943
+ for (const key in dynamicWeights) {
4944
+ const metricScore = suspicionVector[key] || 0.0;
4945
+ if (metricScore > 0) {
4946
+ score += metricScore * dynamicWeights[key];
4947
+ }
4948
+ }
4949
+ return Math.min(100.0, score);
4681
4950
  }
4682
4951
 
4683
4952
 
@@ -4998,9 +5267,84 @@ export class FingerprintEngine {
4998
5267
 
4999
5268
  // Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
5000
5269
  const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
5270
+
5271
+ // Règle Anti-Ferme : Même avec du matériel sécurisé, un appareil condamné ou menant une attaque certaine est révoqué et bloqué
5272
+ if (deviceData?.condemned || this._hasCertainAttack(requestContext)) {
5273
+ if (deviceData) {
5274
+ deviceData.condemned = true;
5275
+ deviceData.webauthnVerified = false;
5276
+ await store.set(`device:${deviceId}`, deviceData);
5277
+ }
5278
+ this._log('Condemned device or attack detected - revoking hardware trust and blocking', { deviceId });
5279
+ const decision = { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
5280
+ if (this.dryRun) {
5281
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
5282
+ decision.intendedAction = decision.action;
5283
+ decision.action = 'next';
5284
+ delete decision.status;
5285
+ delete decision.body;
5286
+ if (requestContext._newCookies) {
5287
+ const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
5288
+ if (deviceCookie) decision.newCookieForResponse = deviceCookie;
5289
+ }
5290
+ }
5291
+ return decision;
5292
+ }
5293
+
5294
+ // Validation immédiate de l'attestation matérielle WebAuthn si présente dans la requête
5295
+ const behaviorHeader = requestContext.headers?.['x-behavior-metrics'];
5296
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
5297
+ try {
5298
+ const parsedMetrics = JSON.parse(behaviorHeader);
5299
+ if (parsedMetrics && parsedMetrics.webauthnAnchor && deviceData) {
5300
+ if (verifyWebAuthnHardwareAnchor(parsedMetrics.webauthnAnchor, deviceData)) {
5301
+ deviceData.webauthnVerified = true;
5302
+ await store.set(`device:${deviceId}`, deviceData);
5303
+ }
5304
+ }
5305
+ } catch (e) {}
5306
+ }
5307
+
5001
5308
  if (deviceData && deviceData.webauthnVerified) {
5002
5309
  this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
5003
- return { action: 'next', score: 0, vector: { webauthn_verified: 100 } };
5310
+ const greenlistTtl = 365 * 86400 * 1000; // 1 an (permanent)
5311
+ const greenlistTicket = generateStatelessTicket({
5312
+ expiry: Date.now() + greenlistTtl,
5313
+ originalIp: clientIp,
5314
+ deviceId,
5315
+ deviceHash: 'webauthn:greenlist:' + (deviceData.webauthnCredentialId || deviceId),
5316
+ greenlist: true
5317
+ });
5318
+ const isHttps = requestContext.headers?.['x-forwarded-proto'] === 'https' ||
5319
+ requestContext.rawReq?.secure ||
5320
+ requestContext.rawReq?.protocol === 'https' ||
5321
+ requestContext.rawReq?.connection?.encrypted;
5322
+ const secureOption = isHttps || this.isProduction;
5323
+
5324
+ const decision = {
5325
+ action: 'next',
5326
+ score: 0,
5327
+ vector: { webauthn_verified: 100 },
5328
+ cookie: {
5329
+ name: 'pow_clearance',
5330
+ value: greenlistTicket,
5331
+ options: {
5332
+ httpOnly: true,
5333
+ secure: secureOption,
5334
+ sameSite: 'strict',
5335
+ ...(secureOption && { partitioned: true }),
5336
+ maxAge: greenlistTtl,
5337
+ path: '/'
5338
+ }
5339
+ }
5340
+ };
5341
+ if (requestContext._newCookies) {
5342
+ const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
5343
+ if (deviceCookie) {
5344
+ decision.newCookieForResponse = deviceCookie;
5345
+ }
5346
+ }
5347
+ return decision;
5004
5348
  }
5005
5349
 
5006
5350
  if (isStatic) {
@@ -5008,6 +5352,39 @@ export class FingerprintEngine {
5008
5352
  return { action: 'next', score: 0, vector: {} };
5009
5353
  }
5010
5354
 
5355
+ // --- PRIVATE ACCESS TOKENS (PAT / RFC 9505 & Privacy Pass) ZERO-FRICTION BYPASS ---
5356
+ const rawPatTokens = extractPrivateAccessTokens(requestContext.headers);
5357
+ if (rawPatTokens.length > 0) {
5358
+ const patConfig = this.securityConfig.pat || {};
5359
+ for (const rawToken of rawPatTokens) {
5360
+ const parsedToken = parsePrivateAccessToken(rawToken);
5361
+ if (parsedToken) {
5362
+ const nonceKey = `pat-nonce:${parsedToken.nonce}`;
5363
+ const isReplayed = await store.has(nonceKey);
5364
+ if (!isReplayed) {
5365
+ const isValid = verifyPatSignature(parsedToken, patConfig);
5366
+ if (isValid) {
5367
+ // Anti-replay: cache nonce with 24-hour TTL
5368
+ const nonceTtl = patConfig.nonceTtl || 86400;
5369
+ await store.set(nonceKey, true, nonceTtl);
5370
+ this._log('Private Access Token (PAT) cryptographically verified - granting zero-friction bypass', {
5371
+ tokenType: parsedToken.tokenType,
5372
+ tokenKeyId: parsedToken.tokenKeyId
5373
+ });
5374
+ const decision = {
5375
+ action: 'next',
5376
+ score: 0.0,
5377
+ vector: { pat_verified: 100.0, privacy_pass: 100.0 },
5378
+ intendedAction: 'next'
5379
+ };
5380
+ if (newCookie) decision.newCookieForResponse = newCookie;
5381
+ return decision;
5382
+ }
5383
+ }
5384
+ }
5385
+ }
5386
+ }
5387
+
5011
5388
  // Resolve identity and check for persisted "condemned" status early.
5012
5389
  const currentDeviceHash = getCompositeDeviceHash(requestContext);
5013
5390
  const isNewDevice = !!newCookie;
@@ -5043,7 +5420,7 @@ export class FingerprintEngine {
5043
5420
  if (filterWhitelist === true) {
5044
5421
  bypassWhitelist = this._hasCertainAttack(requestContext);
5045
5422
  } else if (typeof filterWhitelist === 'number') {
5046
- const res = await getScoreAndVector();
5423
+ const res = await getScoreAndVector();
5047
5424
  if (res.score > filterWhitelist) {
5048
5425
  bypassWhitelist = true;
5049
5426
  }
@@ -6216,7 +6593,8 @@ function sanitizeProxyHeaders(context, securityConfig) {
6216
6593
  'x-ja4-hash',
6217
6594
  'x-http2-fingerprint',
6218
6595
  'x-tcp-fingerprint',
6219
- 'x-ja3-raw'
6596
+ 'x-ja3-raw',
6597
+ 'x-compression-info'
6220
6598
  ];
6221
6599
 
6222
6600
  if (securityConfig && securityConfig.trustedProxies) {
@@ -6633,6 +7011,7 @@ export const default_whitelist = () => [
6633
7011
  googlebot_whitelist(),
6634
7012
  bingbot_whitelist(),
6635
7013
  yandex_whitelist(),
7014
+ facebook_whitelist(),
6636
7015
  // === Moteurs de recherche majeurs ===
6637
7016
  { userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
6638
7017
  { userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
@@ -6750,6 +7129,11 @@ export const bingbot_whitelist = () => ({
6750
7129
  entries: bingbotEntries
6751
7130
  });
6752
7131
 
7132
+ export const facebook_whitelist = () => ({
7133
+ type: 'allowlist',
7134
+ entries: facebookEntries
7135
+ });
7136
+
6753
7137
 
6754
7138
  /**
6755
7139
  * Extracts the TLS Session ID or ticket hash from the request context.
@@ -6983,6 +7367,9 @@ export const powMiddleware = (securityConfig) => {
6983
7367
 
6984
7368
  case 'next':
6985
7369
  default:
7370
+ if (decision.cookie && res && typeof res.cookie === 'function') {
7371
+ res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
7372
+ }
6986
7373
  return next();
6987
7374
  }
6988
7375
  };
@@ -6999,6 +7386,7 @@ export const __internal = {
6999
7386
  checkChallengeRateLimit,
7000
7387
  getDeviceHash,
7001
7388
  getCompositeDeviceHash,
7389
+ generateClosedShadowTraps,
7002
7390
  getSuspicionVector,
7003
7391
  getTlsSessionId,
7004
7392
  pruneTrafficData,
@@ -7050,6 +7438,7 @@ export const __internal = {
7050
7438
  findPeerInSubnet,
7051
7439
  handleCooperativeRequest,
7052
7440
  broadcastBannedZkp,
7441
+ generateIssuerPemKeys,
7053
7442
  getMetric,
7054
7443
  incrementCounter,
7055
7444
  observeValue
@@ -7749,4 +8138,5 @@ export async function handleMetricsRequest(req, res, securityConfig) {
7749
8138
  // 3. Si autorisé, servir les métriques.
7750
8139
  res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
7751
8140
  res.send(MetricsManager.getPrometheusMetrics(securityConfig));
7752
- }
8141
+ }
8142
+ export { generateIssuerPemKeys };