@anonympins/fingerprint 0.7.5 → 0.8.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +1 -1
  3. package/package.json +3 -2
  4. package/public/anonympins-bot-mitigation-pow.zip +0 -0
  5. package/src/js/asn-lookup.js +223 -0
  6. package/src/js/fingerprint.client.js +1784 -1772
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +236 -40
  9. package/src/js/fingerprint.utils.js +252 -2
  10. package/src/js/pow.worker.js +2 -2
  11. package/src/js/tests/asn-lookup.test.js +57 -0
  12. package/src/js/tests/cross_parity.test.js +3 -3
  13. package/src/js/tests/dns-circuit-breaker.test.js +6 -6
  14. package/src/js/tests/fingerprint.builder.test.js +76 -78
  15. package/src/js/tests/fingerprint.client.test.js +16 -17
  16. package/src/js/tests/fingerprint.test.js +19 -12
  17. package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
  18. package/src/js/tests/library.test.js +94 -95
  19. package/src/js/tests/obfuscation.test.js +8 -8
  20. package/src/js/tests/quicFingerprint.test.js +47 -0
  21. package/src/js/tests/tcpFingerprint.test.js +4 -4
  22. package/src/js/upow-model-task.js +155 -112
  23. package/src/php/AsnLookupEngine.php +173 -0
  24. package/src/php/Challenge/ChallengeUtils.php +430 -117
  25. package/src/php/Config/SecurityProfiles.php +75 -62
  26. package/src/php/DirectFingerprint.php +80 -13
  27. package/src/php/FingerprintBuilder.php +13 -13
  28. package/src/php/FingerprintClient.php +6 -6
  29. package/src/php/FingerprintEngine.php +2141 -1837
  30. package/src/php/Ja3AnomalyDetector.php +22 -22
  31. package/src/php/NetworkProfile.php +68 -0
  32. package/src/php/Optimization/FunctionRegistry.php +4 -4
  33. package/src/php/Optimization/Optimization.php +6 -6
  34. package/src/php/ProblemManager.php +12 -2
  35. package/src/php/RequestContext.php +11 -11
  36. package/src/php/Store/InMemoryStore.php +96 -66
  37. package/src/php/Store/MongoDbStore.php +2 -2
  38. package/src/php/Store/RedisStore.php +1 -1
  39. package/src/php/Store/StoreManager.php +48 -35
  40. package/src/php/Tests/AsnLookupTest.php +61 -0
  41. package/src/php/Tests/ChallengeUtilsTest.php +31 -0
  42. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  43. package/src/php/Tests/FingerprintClientTest.php +70 -70
  44. package/src/php/Tests/FingerprintEngineTest.php +4 -2
  45. package/src/php/Tests/PowTest.php +37 -39
  46. package/src/php/Tests/QuicFingerprintTest.php +115 -53
  47. package/src/php/Tests/ThreatIntelTest.php +2 -2
  48. package/src/php/Utils/BigInt.php +202 -202
  49. package/src/php/Utils/BlockList.php +13 -13
  50. package/src/php/Utils/Env.php +7 -2
  51. package/src/php/Utils/Logger.php +19 -1
  52. package/src/php/Utils/MaliciousPatterns.php +6 -6
  53. package/src/php/Utils/MetricsManager.php +209 -209
  54. package/src/php/Utils/RequestUtils.php +348 -18
  55. package/src/php/Utils/TLSClientHelloParser.php +385 -369
  56. package/src/php/WordPress/WpDbStore.php +34 -7
  57. package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1222 -0
  58. package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
  59. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
  60. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
  61. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  62. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
  63. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  64. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
  65. package/src/php/WordPress/package.php +88 -99
  66. package/src/php/WordPress/readme.txt +28 -6
  67. package/src/php/bin/auto-tune.php +1 -1
  68. package/public/fingerprint-anti-bot.zip +0 -0
@@ -5,6 +5,7 @@ import {Worker} from "node:worker_threads";
5
5
  import {getProblemManager, problemManager} from "./problem-manager.js";
6
6
  import {Optimization} from "./library.js";
7
7
  import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
8
+ import {defaultAsnLookup, NetworkProfile} from "./asn-lookup.js";
8
9
  import {DynamicWasmGenerator} from "./dynamic-wasm.js";
9
10
  import {writeFileSync, readFileSync, existsSync, promises as fsPromises} from "node:fs";
10
11
  import {fileURLToPath} from "node:url";
@@ -13,6 +14,9 @@ import {GpuPowSolver} from "./gpu_pow.solver.js";
13
14
  import {
14
15
  verifyZkpProof,
15
16
  sanitizeRedirectPath,
17
+ extractPrivateAccessTokens,
18
+ parsePrivateAccessToken,
19
+ verifyPatSignature,
16
20
  decodePolymorphicFingerprint,
17
21
  deepMerge,
18
22
  getHeaderSignature,
@@ -23,7 +27,8 @@ import {
23
27
  isLoopbackIp,
24
28
  isPrivateIp,
25
29
  parseUserAgent,
26
- safeJsonStringify
30
+ safeJsonStringify,
31
+ generateIssuerPemKeys
27
32
  } from "./fingerprint.utils.js";
28
33
 
29
34
 
@@ -400,6 +405,12 @@ export function getProtocolAnomalyScore(context) {
400
405
  const ua = context.headers?.['user-agent'] || '';
401
406
  const uaParts = parseUserAgent(ua);
402
407
  const browser = uaParts.browser;
408
+ const isChromium = browser?.startsWith('Chrome') || browser?.startsWith('Edge');
409
+ const isFirefox = browser?.startsWith('Firefox');
410
+ const isSafari = browser?.startsWith('Safari');
411
+ const isHumanBrowser = ['Chrome', 'Firefox', 'Safari', 'Edge'].includes(
412
+ browser?.split('/')[0] || null
413
+ );
403
414
 
404
415
  if (browser) {
405
416
  // HTTP/2 Anomaly Logic
@@ -410,9 +421,6 @@ export function getProtocolAnomalyScore(context) {
410
421
  const connWindow = parseInt(parts[1], 10);
411
422
  const streamPriority = parts[2] || '';
412
423
  const headerOrder = parts.length > 3 ? parts[3] : '';
413
- const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
414
- const isFirefox = browser.startsWith('Firefox');
415
- const isSafari = browser.startsWith('Safari');
416
424
 
417
425
  if (isChromium) {
418
426
  if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
@@ -424,6 +432,33 @@ export function getProtocolAnomalyScore(context) {
424
432
  if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
425
433
  }
426
434
 
435
+ // Analyse de la structure des dépendances de flux HTTP/2 (RFC 7540)
436
+ if (streamPriority) {
437
+ const priorityEntries = streamPriority.split(',').map(s => s.trim()).filter(Boolean);
438
+ for (const entry of priorityEntries) {
439
+ const sub = entry.split(':');
440
+ if (sub.length >= 4) {
441
+ const streamId = parseInt(sub[0], 10);
442
+ const exclusive = parseInt(sub[1], 10);
443
+ const depStreamId = parseInt(sub[2], 10);
444
+ const weight = parseInt(sub[3], 10);
445
+
446
+ // Auto-dépendance interdite (RFC 7540 Section 5.3.1)
447
+ if (streamId === depStreamId && streamId > 0) {
448
+ http2Anomaly += 60.0;
449
+ }
450
+ // Poids en dehors de [1, 256] (RFC 7540 Section 5.3.2)
451
+ if (weight < 1 || weight > 256) {
452
+ http2Anomaly += 45.0;
453
+ }
454
+ // Signature scraper : Go net/http2 ou Python h2 par défaut (poids 16 sur flux 0 non-exclusif)
455
+ if (isChromium && depStreamId === 0 && weight === 16 && exclusive === 0) {
456
+ http2Anomaly += 55.0;
457
+ }
458
+ }
459
+ }
460
+ }
461
+
427
462
  // Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
428
463
  if (parts.length >= 5) {
429
464
  const frameCountsStr = parts[4];
@@ -444,6 +479,13 @@ export function getProtocolAnomalyScore(context) {
444
479
  if (priorityCount === 0) http2Anomaly += 25.0;
445
480
  // Chrome est agressif avec les WINDOW_UPDATE
446
481
  if (windowUpdateCount < 2) http2Anomaly += 20.0;
482
+
483
+ // NOUVEAU: Analyse de l'arbre de dépendances HTTP/2.
484
+ // Un arbre plat (peu de dépendances) est suspect pour Chromium.
485
+ const priorityDependencies = streamPriority.split(',').length - 1;
486
+ if (streamPriority.includes(',') && priorityDependencies < 2 && priorityCount > 0) {
487
+ http2Anomaly += 35.0;
488
+ }
447
489
  } else if (isFirefox) {
448
490
  // Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
449
491
  if (priorityCount > 1) http2Anomaly += 20.0;
@@ -471,10 +513,6 @@ export function getProtocolAnomalyScore(context) {
471
513
  const frameOrderRaw = parts[3] || context.headers?.['x-quic-frame-order'] || context.quicFrameOrder || '';
472
514
  const frameOrder = frameOrderRaw.toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
473
515
 
474
- const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
475
- const isFirefox = browser.startsWith('Firefox');
476
- const isSafari = browser.startsWith('Safari');
477
-
478
516
  const maxData = parseInt(params['1'] || params['0x01'] || '0', 10);
479
517
  const maxStreams = parseInt(params['4'] || params['8'] || params['0x08'] || '0', 10);
480
518
  const bidiLocal = parseInt(params['5'] || params['0x05'] || '0', 10);
@@ -484,7 +522,33 @@ export function getProtocolAnomalyScore(context) {
484
522
  // Contrôle de flux global et nombre de flux bidirectionnels
485
523
  if (maxData > 0 && maxData < 1048576) quicAnomaly += 40.0;
486
524
  if (maxStreams > 0 && maxStreams !== 100) quicAnomaly += 30.0;
487
- if (priorityOrder && !priorityOrder.includes('u=')) quicAnomaly += 30.0;
525
+
526
+ // NOUVEAU: Analyse des trames PRIORITY_UPDATE (RFC 9218)
527
+ if (priorityOrder) {
528
+ // Contrôle de l'urgence (u: 0 à 7 strict)
529
+ const uMatch = priorityOrder.match(/\bu=(-?\d+)\b/);
530
+ if (uMatch) {
531
+ const urgency = parseInt(uMatch[1], 10);
532
+ if (urgency < 0 || urgency > 7) {
533
+ quicAnomaly += 50.0;
534
+ }
535
+ } else if (!priorityOrder.includes('u=')) {
536
+ quicAnomaly += 30.0;
537
+ }
538
+
539
+ // Paramètre incrémental invalide ou malformé
540
+ if (/\bi=\d+\b/.test(priorityOrder)) {
541
+ quicAnomaly += 40.0;
542
+ }
543
+ if (/\bw=\d+\b/.test(priorityOrder)) {
544
+ quicAnomaly += 45.0;
545
+ }
546
+ // Un navigateur légitime enverra probablement une mise à jour incrémentale 'i'
547
+ if (priorityOrder.includes('u=') && !priorityOrder.includes('i') && !/\bu=[0-7],i\b/.test(priorityOrder)) {
548
+ quicAnomaly += 40.0; // Urgence envoyée sans drapeau incrémental, suspect pour Chrome
549
+ }
550
+ if (priorityOrder === 'p' || priorityOrder === 'i') quicAnomaly += 50.0; // Champ malformé
551
+ }
488
552
 
489
553
  // Contrôle de flux bidi (Chromium alloue 6MB = 6291456 ou au minimum 512 Ko)
490
554
  // curl-impersonate / quiche alloue 256 Ko (262144) ou 128 Ko (131072)
@@ -506,6 +570,12 @@ export function getProtocolAnomalyScore(context) {
506
570
  if (pIdx !== -1 && sIdx !== -1 && pIdx < sIdx) {
507
571
  quicAnomaly += 60.0;
508
572
  }
573
+
574
+ // RFC 9114: Le flux de contrôle ne doit pas contenir de doublon SETTINGS
575
+ const sCount = frameOrder.filter(f => f === 's' || f === 'settings' || f === '4').length;
576
+ if (sCount > 1) {
577
+ quicAnomaly += 50.0;
578
+ }
509
579
  }
510
580
  } else if (isFirefox) {
511
581
  const maxData = parseInt(params['1'] || '0', 10);
@@ -535,6 +605,55 @@ export function getProtocolAnomalyScore(context) {
535
605
  }
536
606
  }
537
607
  }
608
+
609
+ // NOUVEAU: Analyse du ratio de compression HPACK/QPACK
610
+ const compressionInfo = context.headers?.['x-compression-info'] || null;
611
+ if (compressionInfo) {
612
+ const info = {};
613
+ compressionInfo.split(',').forEach(p => {
614
+ const [key, value] = p.split(':');
615
+ if (key && value) info[key.trim()] = parseFloat(value);
616
+ });
617
+
618
+ const reqCount = info.req_count || 0;
619
+ const hpackRatio = info.hpack_ratio;
620
+ const qpackRatio = info.qpack_ratio;
621
+ const dynamicEntries = info.dynamic_table_entries !== undefined ? info.dynamic_table_entries : info.table_entries;
622
+ const dynamicHits = info.dynamic_hits;
623
+ const dynamicTableSize = info.dynamic_table_size;
624
+
625
+ // Après quelques requêtes, un vrai navigateur doit avoir un bon ratio de compression
626
+ if (reqCount > 3 && isHumanBrowser) {
627
+ if (hpackRatio !== undefined) {
628
+ if (hpackRatio < 0.4) {
629
+ http2Anomaly += (1 - hpackRatio) * 50;
630
+ } else if (hpackRatio > 0.75) {
631
+ http2Anomaly += Math.min(50.0, (hpackRatio - 0.5) * 100);
632
+ }
633
+ }
634
+ if (qpackRatio !== undefined) {
635
+ if (qpackRatio < 0.4) {
636
+ quicAnomaly += (1 - qpackRatio) * 50;
637
+ } else if (qpackRatio > 0.75) {
638
+ quicAnomaly += Math.min(50.0, (qpackRatio - 0.5) * 100);
639
+ }
640
+ }
641
+
642
+ // Détection de réinitialisation de table dynamique
643
+ if (dynamicEntries !== undefined && dynamicEntries === 0) {
644
+ http2Anomaly += 45.0;
645
+ quicAnomaly += 45.0;
646
+ }
647
+ if (dynamicHits !== undefined && dynamicHits === 0) {
648
+ http2Anomaly += 35.0;
649
+ quicAnomaly += 35.0;
650
+ }
651
+ if (dynamicTableSize !== undefined && dynamicTableSize === 0) {
652
+ http2Anomaly += 40.0;
653
+ quicAnomaly += 40.0;
654
+ }
655
+ }
656
+ }
538
657
  }
539
658
 
540
659
  return {
@@ -2369,29 +2488,17 @@ function getBehaviorScore(context) {
2369
2488
  return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
2370
2489
  }
2371
2490
 
2372
- // Traitement direct si un flag binaire client-side (1 = humain, 0 = bot) est envoyé
2373
- if (behaviorHeader === 1 || behaviorHeader === '1') {
2374
- return { behaviorScore: 0 };
2375
- }
2376
- if (behaviorHeader === 0 || behaviorHeader === '0') {
2377
- return { behaviorScore: 100 };
2378
- }
2379
-
2380
2491
  // Traitement direct si un score numérique est envoyé côté client
2381
2492
  if (typeof behaviorHeader === 'number') {
2382
2493
  return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
2383
2494
  }
2384
2495
  if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
2385
2496
  const trimmed = behaviorHeader.trim();
2386
- if (trimmed === '1') return { behaviorScore: 0 };
2387
- if (trimmed === '0') return { behaviorScore: 100 };
2388
2497
  return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
2389
2498
  }
2390
2499
 
2391
2500
  try {
2392
2501
  const metrics = JSON.parse(behaviorHeader);
2393
- if (metrics === 1) return { behaviorScore: 0 };
2394
- if (metrics === 0) return { behaviorScore: 100 };
2395
2502
  if (typeof metrics === 'number') {
2396
2503
  return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
2397
2504
  }
@@ -2713,7 +2820,7 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2713
2820
  }
2714
2821
 
2715
2822
  const { ja3, ja4 } = actualGetTlsFingerprintFn(context) || { ja3: null, ja4: null }; // Defensive check
2716
- const ua = context.headers["user-agent"] || '';
2823
+ const ua = context.headers?.["user-agent"] || '';
2717
2824
  const ja3Raw = context.headers['x-ja3-raw'] || null;
2718
2825
  const httpVersion = context.httpVersion || '';
2719
2826
 
@@ -2735,7 +2842,8 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2735
2842
  score = Math.max(score, 100);
2736
2843
  }
2737
2844
 
2738
- const claimedBrowser = parseUserAgent(ua).browser?.split('/')[0] || null;
2845
+ const uaParts = parseUserAgent(ua);
2846
+ const claimedBrowser = uaParts.browser?.split('/')[0] || null;
2739
2847
  const isHumanBrowser = ['Chrome', 'Firefox', 'Safari', 'Edge'].includes(claimedBrowser);
2740
2848
 
2741
2849
  // --- ANALYSE 2 : CONTRÔLE PROFOND SUR L'EMPREINTE BRUTE (RAW JA3) ---
@@ -2777,8 +2885,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2777
2885
  if (ja4) {
2778
2886
  const parsedJa4 = parseJa4(ja4);
2779
2887
  if (parsedJa4) {
2780
- const uaParts = parseUserAgent(ua);
2781
-
2782
2888
  // Check 1: Incohérence ALPN / HTTP Version
2783
2889
  if (parsedJa4.alpn === 'h2' && (context.httpVersion === '1.1' || context.httpVersion === '1.0')) {
2784
2890
  const hasProxy = context.headers['via'] || context.headers['forwarded'] || context.headers['x-forwarded-proto'] || context.headers['x-forwarded-for'];
@@ -2802,6 +2908,25 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2802
2908
  }
2803
2909
  }
2804
2910
 
2911
+ // Validation TLS 1.3 Encrypted Client Hello (ECH) & Discrépance SNI
2912
+ const outerSni = context.headers?.['x-ech-outer-sni'];
2913
+ const host = (context.headers?.['host'] || '').split(':')[0].trim();
2914
+ const hasEch = context.headers?.['x-ech-present'] === 'true' || context.headers?.['x-has-ech'] === 'true';
2915
+ const echExpected = context.headers?.['x-ech-expected'] === 'true' || Boolean(outerSni);
2916
+
2917
+ if (outerSni && host && outerSni.toLowerCase() !== host.toLowerCase() && !hasEch) {
2918
+ score = Math.max(score, 60.0);
2919
+ }
2920
+
2921
+ const browserVersion = parseInt(uaParts.browser?.split('/')[1] || '0', 10);
2922
+ const isModernEchBrowser = (
2923
+ (claimedBrowser === 'Chrome' || claimedBrowser === 'Edge') && browserVersion >= 119
2924
+ ) || (claimedBrowser === 'Firefox' && browserVersion >= 118);
2925
+
2926
+ if (context.isHttps && isModernEchBrowser && echExpected && !hasEch) {
2927
+ score = Math.max(score, 55.0);
2928
+ }
2929
+
2805
2930
  // 2. If no JA3 hash is available, we cannot perform the consistency check.
2806
2931
  if (ja3 && ua) {
2807
2932
  // Look up the expected browser family (or families) from our database.
@@ -2840,7 +2965,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
2840
2965
  // Create the promise for the async part (Check 4)
2841
2966
  const promise = (async () => {
2842
2967
  let asyncScore = score;
2843
- const uaParts = parseUserAgent(ua);
2844
2968
 
2845
2969
  if (uaParts.browser) {
2846
2970
  const browserFamily = uaParts.browser.split('/')[0];
@@ -3315,18 +3439,26 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3315
3439
  /**
3316
3440
  * Calcule un score d'incohérence analogique lisse plafonnant à une asymptote de 99.9.
3317
3441
  * @param {number} consistencyScore Similarité entre 0 et 1 issue du FingerprintBuilder.
3318
- * @param {number} [inflectionPoint=0.72] Point d'inflexion où la suspicion accélère.
3442
+ * @param {number|object} [inflectionOrProfile=0.72] Point d'inflexion ou profil réseau/IP.
3319
3443
  * @param {number} [steepness=12] Raideur de la transition sigmoïdale.
3320
3444
  * @returns {number} Score de 0 à 99.9 sans saut de palier ni certitude absolue à 100.
3321
3445
  */
3322
- export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPoint = 0.72, steepness = 12) {
3446
+ export function calculateAnalogInconsistencyScore(consistencyScore, inflectionOrProfile = 0.72, steepness = 12) {
3323
3447
  const s = Math.max(0.0, Math.min(1.0, consistencyScore));
3324
3448
  if (s >= 0.98) return 0.0;
3325
3449
 
3450
+ let inflectionPoint = typeof inflectionOrProfile === 'number'
3451
+ ? inflectionOrProfile
3452
+ : (inflectionOrProfile?.inflectionPoint ?? 0.72);
3453
+
3454
+ const k = typeof inflectionOrProfile === 'object' && inflectionOrProfile?.steepness !== undefined
3455
+ ? inflectionOrProfile.steepness
3456
+ : steepness;
3457
+
3326
3458
  const asymptote = 99.9;
3327
- const raw = 1.0 / (1.0 + Math.exp(steepness * (s - inflectionPoint)));
3328
- const minVal = 1.0 / (1.0 + Math.exp(steepness * (1.0 - inflectionPoint)));
3329
- const maxVal = 1.0 / (1.0 + Math.exp(steepness * (0.0 - inflectionPoint)));
3459
+ const raw = 1.0 / (1.0 + Math.exp(k * (s - inflectionPoint)));
3460
+ const minVal = 1.0 / (1.0 + Math.exp(k * (1.0 - inflectionPoint)));
3461
+ const maxVal = 1.0 / (1.0 + Math.exp(k * (0.0 - inflectionPoint)));
3330
3462
  const normalized = ((raw - minVal) / (maxVal - minVal)) * asymptote;
3331
3463
 
3332
3464
  return Math.min(asymptote, Math.round(normalized * 10.0) / 10.0);
@@ -3502,13 +3634,14 @@ async function getBotnetClusterScore(context, stableFpHash) {
3502
3634
  */
3503
3635
  async function getIpReputationScore(ip) {
3504
3636
  const key = `ip-reputation:${ip}`;
3637
+ const netProfile = defaultAsnLookup.lookup(ip);
3505
3638
  const data = await store.get(key);
3506
- if (!data) return 0;
3639
+ if (!data) return netProfile.baseScore;
3507
3640
 
3508
3641
  const now = Date.now();
3509
3642
  const hoursPassed = (now - data.lastUpdate) / (1000 * 60 * 60);
3510
3643
  const decay = Math.floor(hoursPassed * 2); // Decay 2 points per hour of inactivity
3511
- return Math.max(0, data.score - decay);
3644
+ return Math.max(netProfile.baseScore, data.score - decay);
3512
3645
  }
3513
3646
 
3514
3647
  /**
@@ -3839,7 +3972,7 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
3839
3972
  * @param {object} deviceData - The device's activity data.
3840
3973
  * @returns {Promise<{historyScore: number, rotationScore: number}>}
3841
3974
  */
3842
- async function getBehavioralIndicators(context, deviceData) {
3975
+ async function getBehavioralIndicators(context, deviceData, netProfile = null) {
3843
3976
  const rapidChangeThresholdMs = 2000; // 2 secondes
3844
3977
  const maxRapidChanges = 3;
3845
3978
 
@@ -3917,13 +4050,18 @@ async function getBehavioralIndicators(context, deviceData) {
3917
4050
  : MAX_DISTINCT_IPS_PER_DEVICE;
3918
4051
  const freeIpChanges = isSharedIp ? 1 : 3;
3919
4052
 
3920
- const historyScore = Math.min(
4053
+ let historyScore = Math.min(
3921
4054
  100,
3922
4055
  (Math.max(0, deviceData.ips.size - freeIpChanges) /
3923
4056
  (maxIpsForDevice - freeIpChanges)) *
3924
4057
  100,
3925
4058
  );
3926
4059
 
4060
+ // Mobile CGNAT : désactivation/amortissement de la pénalité de rotation rapide d'IP
4061
+ if (netProfile?.toleranceRotation) {
4062
+ historyScore = Math.min(historyScore, 10.0);
4063
+ }
4064
+
3927
4065
  // Score based on rapid identity rotation (0-100)
3928
4066
  const rotationScore = Math.min(
3929
4067
  100,
@@ -4054,6 +4192,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
4054
4192
  // Execute non-interdependent asynchronous operations in parallel
4055
4193
  const zkpProof = (context.getHeader ? context.getHeader('x-zkp-proof') : null) || (context.headers ? context.headers['x-zkp-proof'] : null) || (context.query ? context.query['pow_zkp'] : null) || (context.queryParams ? context.queryParams['pow_zkp'] : null) || '';
4056
4194
  const zkpY = zkpProof ? zkpProof.split(":")[0] : null;
4195
+ const netProfile = defaultAsnLookup.lookup(clientIp);
4057
4196
 
4058
4197
  const [
4059
4198
  behavioral,
@@ -4064,7 +4203,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
4064
4203
  { botnetClusterScore },
4065
4204
  _ // store.set result
4066
4205
  ] = await Promise.all([
4067
- getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
4206
+ getBehavioralIndicators(context, deviceData, netProfile), // This modifies deviceData, so it must be done before saving deviceData
4068
4207
  getThreatIntelScore(context, zkpY, securityConfig), // NOUVEAU: Score de Threat Intelligence Fédéré
4069
4208
  getTlsSpoofingScore(context),
4070
4209
  getSubnetScore(context, deviceId, securityConfig),
@@ -4075,7 +4214,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
4075
4214
 
4076
4215
  // Synchronous calculations
4077
4216
  const { headerAnomalyScore } = getHeaderAnomalies(context);
4078
- const similarityThreshold = securityConfig?.similarityThreshold ?? 0.72;
4217
+ const similarityThreshold = securityConfig?.similarityThreshold ?? netProfile.inflectionPoint;
4079
4218
  const inconsistencyScore = calculateAnalogInconsistencyScore(consistencyScore, similarityThreshold);
4080
4219
 
4081
4220
  const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
@@ -4640,6 +4779,8 @@ export class FingerprintEngine {
4640
4779
  // Auto-generate Ed25519 key pair on load if indicated and keys are not set
4641
4780
  if (securityConfig && (securityConfig.useAsymmetricTickets || securityConfig.ed25519 === 'auto') && !process.env.ED25519_PRIVATE_KEY) {
4642
4781
  const persistentKeyPath = join(configDir, 'ed25519_key.json');
4782
+ const privatePemPath = join(configDir, 'issuer-private.pem');
4783
+ const publicPemPath = join(configDir, 'issuer-public.pem');
4643
4784
  if (existsSync(persistentKeyPath)) {
4644
4785
  try {
4645
4786
  const keys = JSON.parse(readFileSync(persistentKeyPath, 'utf-8'));
@@ -4649,6 +4790,14 @@ export class FingerprintEngine {
4649
4790
  } catch (e) {
4650
4791
  console.error('[Fingerprint] Failed to load persistent Ed25519 keys:', e.message);
4651
4792
  }
4793
+ } else if (existsSync(privatePemPath) && existsSync(publicPemPath)) {
4794
+ try {
4795
+ process.env.ED25519_PRIVATE_KEY = readFileSync(privatePemPath, 'utf-8');
4796
+ process.env.ED25519_PUBLIC_KEY = readFileSync(publicPemPath, 'utf-8');
4797
+ this._log('Persistent Ed25519 PEM keys loaded from disk');
4798
+ } catch (e) {
4799
+ console.error('[Fingerprint] Failed to load persistent Ed25519 PEM keys:', e.message);
4800
+ }
4652
4801
  } else {
4653
4802
  try {
4654
4803
  const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
@@ -4751,6 +4900,7 @@ export class FingerprintEngine {
4751
4900
  'federatedPeers', 'federationSecret', 'filterWhitelist',
4752
4901
  'challengeRateLimit',
4753
4902
  'differentialPrivacy', 'dpEpsilon'
4903
+ ,'pat'
4754
4904
  ]);
4755
4905
 
4756
4906
  // 1. Check for essential keys
@@ -5218,6 +5368,44 @@ export class FingerprintEngine {
5218
5368
  return { action: 'next', score: 0, vector: {} };
5219
5369
  }
5220
5370
 
5371
+ // --- PRIVATE ACCESS TOKENS (PAT / RFC 9578 & Privacy Pass) ZERO-FRICTION CHALLENGE EXEMPTION ---
5372
+ const rawPatTokens = extractPrivateAccessTokens(requestContext.headers);
5373
+ let hasValidPat = false;
5374
+ if (rawPatTokens.length > 0) {
5375
+ // Même avec un PAT valide, une attaque applicative avérée ou un terminal condamné doit être neutralisé
5376
+ if (deviceData?.condemned || this._hasCertainAttack(requestContext)) {
5377
+ return {
5378
+ action: 'block',
5379
+ status: 404,
5380
+ body: 'Forbidden',
5381
+ score: 100,
5382
+ vector: { honeypotScore: 100 }
5383
+ };
5384
+ }
5385
+ const patConfig = this.securityConfig.pat || {};
5386
+ for (const rawToken of rawPatTokens) {
5387
+ const parsedToken = parsePrivateAccessToken(rawToken);
5388
+ if (parsedToken) {
5389
+ const nonceKey = `pat-nonce:${parsedToken.nonce}`;
5390
+ const isReplayed = await store.has(nonceKey);
5391
+ if (!isReplayed) {
5392
+ const isValid = verifyPatSignature(parsedToken, patConfig);
5393
+ if (isValid) {
5394
+ // Anti-replay: cache nonce with 24-hour TTL
5395
+ const nonceTtl = patConfig.nonceTtl || 86400;
5396
+ await store.set(nonceKey, true, nonceTtl);
5397
+ this._log('Private Access Token (PAT) cryptographically verified - challenge exemption enabled', {
5398
+ tokenType: parsedToken.tokenType,
5399
+ tokenKeyId: parsedToken.tokenKeyId
5400
+ });
5401
+ hasValidPat = true;
5402
+ break;
5403
+ }
5404
+ }
5405
+ }
5406
+ }
5407
+ }
5408
+
5221
5409
  // Resolve identity and check for persisted "condemned" status early.
5222
5410
  const currentDeviceHash = getCompositeDeviceHash(requestContext);
5223
5411
  const isNewDevice = !!newCookie;
@@ -5333,6 +5521,9 @@ export class FingerprintEngine {
5333
5521
  // This is a strong signal of spoofing.
5334
5522
  suspicionVector.tlsSpoofingScore = Math.max(suspicionVector.tlsSpoofingScore || 0, 95);
5335
5523
  }
5524
+ if (hasValidPat) {
5525
+ suspicionVector.pat_verified = 100.0;
5526
+ }
5336
5527
  let finalScore = preCalculatedScore !== null ? preCalculatedScore : this.calculateFinalScore(suspicionVector);
5337
5528
 
5338
5529
  this._log('Suspicion vector calculated', {
@@ -5821,7 +6012,7 @@ export class FingerprintEngine {
5821
6012
  // OU
5822
6013
  // 2. La requête est très suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
5823
6014
  const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
5824
- const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
6015
+ const hasValidTicket = (await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof)) || hasValidPat;
5825
6016
  // Correction : Pour éviter une boucle infinie de challenges (qui mène à l'erreur 429),
5826
6017
  // on fait confiance au ticket valide tant qu'il n'a pas expiré.
5827
6018
  const maxIndicatorsCount = Object.values(suspicionVector).filter(val => typeof val === 'number' && val >= 100).length;
@@ -6426,7 +6617,8 @@ function sanitizeProxyHeaders(context, securityConfig) {
6426
6617
  'x-ja4-hash',
6427
6618
  'x-http2-fingerprint',
6428
6619
  'x-tcp-fingerprint',
6429
- 'x-ja3-raw'
6620
+ 'x-ja3-raw',
6621
+ 'x-compression-info'
6430
6622
  ];
6431
6623
 
6432
6624
  if (securityConfig && securityConfig.trustedProxies) {
@@ -7214,6 +7406,8 @@ export const powMiddleware = (securityConfig) => {
7214
7406
  * This is a common pattern to allow mocking of ES module functions.
7215
7407
  */
7216
7408
  export const __internal = {
7409
+ defaultAsnLookup,
7410
+ NetworkProfile,
7217
7411
  get store() { return store; }, // Export the store for testing
7218
7412
  checkChallengeRateLimit,
7219
7413
  getDeviceHash,
@@ -7270,6 +7464,7 @@ export const __internal = {
7270
7464
  findPeerInSubnet,
7271
7465
  handleCooperativeRequest,
7272
7466
  broadcastBannedZkp,
7467
+ generateIssuerPemKeys,
7273
7468
  getMetric,
7274
7469
  incrementCounter,
7275
7470
  observeValue
@@ -7969,4 +8164,5 @@ export async function handleMetricsRequest(req, res, securityConfig) {
7969
8164
  // 3. Si autorisé, servir les métriques.
7970
8165
  res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
7971
8166
  res.send(MetricsManager.getPrometheusMetrics(securityConfig));
7972
- }
8167
+ }
8168
+ export { generateIssuerPemKeys };