@anonympins/fingerprint 0.7.5 → 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.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/fingerprint.client.js +1784 -1772
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +198 -28
- package/src/js/fingerprint.utils.js +252 -2
- package/src/js/pow.worker.js +2 -2
- package/src/js/tests/cross_parity.test.js +3 -3
- package/src/js/tests/dns-circuit-breaker.test.js +6 -6
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +19 -12
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
- package/src/js/tests/library.test.js +94 -95
- package/src/js/tests/obfuscation.test.js +8 -8
- package/src/js/tests/quicFingerprint.test.js +47 -0
- package/src/js/tests/tcpFingerprint.test.js +4 -4
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +382 -86
- package/src/php/Config/SecurityProfiles.php +75 -62
- package/src/php/DirectFingerprint.php +55 -8
- package/src/php/FingerprintBuilder.php +13 -13
- package/src/php/FingerprintClient.php +6 -6
- package/src/php/FingerprintEngine.php +2120 -1837
- package/src/php/Ja3AnomalyDetector.php +22 -22
- package/src/php/Optimization/FunctionRegistry.php +4 -4
- package/src/php/Optimization/Optimization.php +6 -6
- package/src/php/RequestContext.php +11 -11
- package/src/php/Store/InMemoryStore.php +96 -66
- package/src/php/Store/MongoDbStore.php +2 -2
- package/src/php/Store/RedisStore.php +1 -1
- package/src/php/Store/StoreManager.php +48 -35
- package/src/php/Tests/ChallengeUtilsTest.php +31 -0
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintClientTest.php +70 -70
- package/src/php/Tests/PowTest.php +37 -39
- package/src/php/Tests/QuicFingerprintTest.php +115 -53
- package/src/php/Tests/ThreatIntelTest.php +2 -2
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/BlockList.php +13 -13
- package/src/php/Utils/Logger.php +19 -1
- package/src/php/Utils/MaliciousPatterns.php +6 -6
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +327 -14
- package/src/php/Utils/TLSClientHelloParser.php +385 -369
- package/src/php/WordPress/WpDbStore.php +7 -7
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
- package/src/php/WordPress/package.php +80 -101
- package/src/php/WordPress/readme.txt +21 -5
- package/src/php/bin/auto-tune.php +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
package/src/js/fingerprint.js
CHANGED
|
@@ -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
|
|
|
@@ -400,6 +404,12 @@ export function getProtocolAnomalyScore(context) {
|
|
|
400
404
|
const ua = context.headers?.['user-agent'] || '';
|
|
401
405
|
const uaParts = parseUserAgent(ua);
|
|
402
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
|
+
);
|
|
403
413
|
|
|
404
414
|
if (browser) {
|
|
405
415
|
// HTTP/2 Anomaly Logic
|
|
@@ -410,9 +420,6 @@ export function getProtocolAnomalyScore(context) {
|
|
|
410
420
|
const connWindow = parseInt(parts[1], 10);
|
|
411
421
|
const streamPriority = parts[2] || '';
|
|
412
422
|
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
423
|
|
|
417
424
|
if (isChromium) {
|
|
418
425
|
if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
|
|
@@ -424,6 +431,33 @@ export function getProtocolAnomalyScore(context) {
|
|
|
424
431
|
if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
|
|
425
432
|
}
|
|
426
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
|
+
|
|
427
461
|
// Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
|
|
428
462
|
if (parts.length >= 5) {
|
|
429
463
|
const frameCountsStr = parts[4];
|
|
@@ -444,6 +478,13 @@ export function getProtocolAnomalyScore(context) {
|
|
|
444
478
|
if (priorityCount === 0) http2Anomaly += 25.0;
|
|
445
479
|
// Chrome est agressif avec les WINDOW_UPDATE
|
|
446
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
|
+
}
|
|
447
488
|
} else if (isFirefox) {
|
|
448
489
|
// Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
|
|
449
490
|
if (priorityCount > 1) http2Anomaly += 20.0;
|
|
@@ -471,10 +512,6 @@ export function getProtocolAnomalyScore(context) {
|
|
|
471
512
|
const frameOrderRaw = parts[3] || context.headers?.['x-quic-frame-order'] || context.quicFrameOrder || '';
|
|
472
513
|
const frameOrder = frameOrderRaw.toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
|
|
473
514
|
|
|
474
|
-
const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
|
|
475
|
-
const isFirefox = browser.startsWith('Firefox');
|
|
476
|
-
const isSafari = browser.startsWith('Safari');
|
|
477
|
-
|
|
478
515
|
const maxData = parseInt(params['1'] || params['0x01'] || '0', 10);
|
|
479
516
|
const maxStreams = parseInt(params['4'] || params['8'] || params['0x08'] || '0', 10);
|
|
480
517
|
const bidiLocal = parseInt(params['5'] || params['0x05'] || '0', 10);
|
|
@@ -484,7 +521,33 @@ export function getProtocolAnomalyScore(context) {
|
|
|
484
521
|
// Contrôle de flux global et nombre de flux bidirectionnels
|
|
485
522
|
if (maxData > 0 && maxData < 1048576) quicAnomaly += 40.0;
|
|
486
523
|
if (maxStreams > 0 && maxStreams !== 100) quicAnomaly += 30.0;
|
|
487
|
-
|
|
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
|
+
}
|
|
488
551
|
|
|
489
552
|
// Contrôle de flux bidi (Chromium alloue 6MB = 6291456 ou au minimum 512 Ko)
|
|
490
553
|
// curl-impersonate / quiche alloue 256 Ko (262144) ou 128 Ko (131072)
|
|
@@ -506,6 +569,12 @@ export function getProtocolAnomalyScore(context) {
|
|
|
506
569
|
if (pIdx !== -1 && sIdx !== -1 && pIdx < sIdx) {
|
|
507
570
|
quicAnomaly += 60.0;
|
|
508
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
|
+
}
|
|
509
578
|
}
|
|
510
579
|
} else if (isFirefox) {
|
|
511
580
|
const maxData = parseInt(params['1'] || '0', 10);
|
|
@@ -535,6 +604,55 @@ export function getProtocolAnomalyScore(context) {
|
|
|
535
604
|
}
|
|
536
605
|
}
|
|
537
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
|
+
}
|
|
538
656
|
}
|
|
539
657
|
|
|
540
658
|
return {
|
|
@@ -2369,29 +2487,17 @@ function getBehaviorScore(context) {
|
|
|
2369
2487
|
return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
|
|
2370
2488
|
}
|
|
2371
2489
|
|
|
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
2490
|
// Traitement direct si un score numérique est envoyé côté client
|
|
2381
2491
|
if (typeof behaviorHeader === 'number') {
|
|
2382
2492
|
return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
|
|
2383
2493
|
}
|
|
2384
2494
|
if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
|
|
2385
2495
|
const trimmed = behaviorHeader.trim();
|
|
2386
|
-
if (trimmed === '1') return { behaviorScore: 0 };
|
|
2387
|
-
if (trimmed === '0') return { behaviorScore: 100 };
|
|
2388
2496
|
return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
|
|
2389
2497
|
}
|
|
2390
2498
|
|
|
2391
2499
|
try {
|
|
2392
2500
|
const metrics = JSON.parse(behaviorHeader);
|
|
2393
|
-
if (metrics === 1) return { behaviorScore: 0 };
|
|
2394
|
-
if (metrics === 0) return { behaviorScore: 100 };
|
|
2395
2501
|
if (typeof metrics === 'number') {
|
|
2396
2502
|
return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
|
|
2397
2503
|
}
|
|
@@ -2713,7 +2819,7 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2713
2819
|
}
|
|
2714
2820
|
|
|
2715
2821
|
const { ja3, ja4 } = actualGetTlsFingerprintFn(context) || { ja3: null, ja4: null }; // Defensive check
|
|
2716
|
-
const ua = context.headers["user-agent"] || '';
|
|
2822
|
+
const ua = context.headers?.["user-agent"] || '';
|
|
2717
2823
|
const ja3Raw = context.headers['x-ja3-raw'] || null;
|
|
2718
2824
|
const httpVersion = context.httpVersion || '';
|
|
2719
2825
|
|
|
@@ -2735,7 +2841,8 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2735
2841
|
score = Math.max(score, 100);
|
|
2736
2842
|
}
|
|
2737
2843
|
|
|
2738
|
-
const
|
|
2844
|
+
const uaParts = parseUserAgent(ua);
|
|
2845
|
+
const claimedBrowser = uaParts.browser?.split('/')[0] || null;
|
|
2739
2846
|
const isHumanBrowser = ['Chrome', 'Firefox', 'Safari', 'Edge'].includes(claimedBrowser);
|
|
2740
2847
|
|
|
2741
2848
|
// --- ANALYSE 2 : CONTRÔLE PROFOND SUR L'EMPREINTE BRUTE (RAW JA3) ---
|
|
@@ -2777,8 +2884,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2777
2884
|
if (ja4) {
|
|
2778
2885
|
const parsedJa4 = parseJa4(ja4);
|
|
2779
2886
|
if (parsedJa4) {
|
|
2780
|
-
const uaParts = parseUserAgent(ua);
|
|
2781
|
-
|
|
2782
2887
|
// Check 1: Incohérence ALPN / HTTP Version
|
|
2783
2888
|
if (parsedJa4.alpn === 'h2' && (context.httpVersion === '1.1' || context.httpVersion === '1.0')) {
|
|
2784
2889
|
const hasProxy = context.headers['via'] || context.headers['forwarded'] || context.headers['x-forwarded-proto'] || context.headers['x-forwarded-for'];
|
|
@@ -2802,6 +2907,25 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2802
2907
|
}
|
|
2803
2908
|
}
|
|
2804
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
|
+
|
|
2805
2929
|
// 2. If no JA3 hash is available, we cannot perform the consistency check.
|
|
2806
2930
|
if (ja3 && ua) {
|
|
2807
2931
|
// Look up the expected browser family (or families) from our database.
|
|
@@ -2840,7 +2964,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2840
2964
|
// Create the promise for the async part (Check 4)
|
|
2841
2965
|
const promise = (async () => {
|
|
2842
2966
|
let asyncScore = score;
|
|
2843
|
-
const uaParts = parseUserAgent(ua);
|
|
2844
2967
|
|
|
2845
2968
|
if (uaParts.browser) {
|
|
2846
2969
|
const browserFamily = uaParts.browser.split('/')[0];
|
|
@@ -4640,6 +4763,8 @@ export class FingerprintEngine {
|
|
|
4640
4763
|
// Auto-generate Ed25519 key pair on load if indicated and keys are not set
|
|
4641
4764
|
if (securityConfig && (securityConfig.useAsymmetricTickets || securityConfig.ed25519 === 'auto') && !process.env.ED25519_PRIVATE_KEY) {
|
|
4642
4765
|
const persistentKeyPath = join(configDir, 'ed25519_key.json');
|
|
4766
|
+
const privatePemPath = join(configDir, 'issuer-private.pem');
|
|
4767
|
+
const publicPemPath = join(configDir, 'issuer-public.pem');
|
|
4643
4768
|
if (existsSync(persistentKeyPath)) {
|
|
4644
4769
|
try {
|
|
4645
4770
|
const keys = JSON.parse(readFileSync(persistentKeyPath, 'utf-8'));
|
|
@@ -4649,6 +4774,14 @@ export class FingerprintEngine {
|
|
|
4649
4774
|
} catch (e) {
|
|
4650
4775
|
console.error('[Fingerprint] Failed to load persistent Ed25519 keys:', e.message);
|
|
4651
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
|
+
}
|
|
4652
4785
|
} else {
|
|
4653
4786
|
try {
|
|
4654
4787
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
|
|
@@ -4751,6 +4884,7 @@ export class FingerprintEngine {
|
|
|
4751
4884
|
'federatedPeers', 'federationSecret', 'filterWhitelist',
|
|
4752
4885
|
'challengeRateLimit',
|
|
4753
4886
|
'differentialPrivacy', 'dpEpsilon'
|
|
4887
|
+
,'pat'
|
|
4754
4888
|
]);
|
|
4755
4889
|
|
|
4756
4890
|
// 1. Check for essential keys
|
|
@@ -5218,6 +5352,39 @@ export class FingerprintEngine {
|
|
|
5218
5352
|
return { action: 'next', score: 0, vector: {} };
|
|
5219
5353
|
}
|
|
5220
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
|
+
|
|
5221
5388
|
// Resolve identity and check for persisted "condemned" status early.
|
|
5222
5389
|
const currentDeviceHash = getCompositeDeviceHash(requestContext);
|
|
5223
5390
|
const isNewDevice = !!newCookie;
|
|
@@ -6426,7 +6593,8 @@ function sanitizeProxyHeaders(context, securityConfig) {
|
|
|
6426
6593
|
'x-ja4-hash',
|
|
6427
6594
|
'x-http2-fingerprint',
|
|
6428
6595
|
'x-tcp-fingerprint',
|
|
6429
|
-
'x-ja3-raw'
|
|
6596
|
+
'x-ja3-raw',
|
|
6597
|
+
'x-compression-info'
|
|
6430
6598
|
];
|
|
6431
6599
|
|
|
6432
6600
|
if (securityConfig && securityConfig.trustedProxies) {
|
|
@@ -7270,6 +7438,7 @@ export const __internal = {
|
|
|
7270
7438
|
findPeerInSubnet,
|
|
7271
7439
|
handleCooperativeRequest,
|
|
7272
7440
|
broadcastBannedZkp,
|
|
7441
|
+
generateIssuerPemKeys,
|
|
7273
7442
|
getMetric,
|
|
7274
7443
|
incrementCounter,
|
|
7275
7444
|
observeValue
|
|
@@ -7969,4 +8138,5 @@ export async function handleMetricsRequest(req, res, securityConfig) {
|
|
|
7969
8138
|
// 3. Si autorisé, servir les métriques.
|
|
7970
8139
|
res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
7971
8140
|
res.send(MetricsManager.getPrometheusMetrics(securityConfig));
|
|
7972
|
-
}
|
|
8141
|
+
}
|
|
8142
|
+
export { generateIssuerPemKeys };
|