@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.
- package/CHANGELOG.md +59 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +1784 -1738
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +489 -99
- 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/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.init.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +37 -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/tests/threatIntel.test.js +40 -1
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +392 -86
- package/src/php/Config/SecurityProfiles.php +79 -61
- 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 +2122 -1690
- 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 +34 -0
- 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 +362 -1
- 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
|
|
|
@@ -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
|
|
81
|
-
const
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
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>> ${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
|
|
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
|
-
|
|
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
|
|
4653
|
-
if (!
|
|
4654
|
-
|
|
4655
|
-
const
|
|
4656
|
-
|
|
4657
|
-
(
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
(
|
|
4661
|
-
(
|
|
4662
|
-
(
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
(
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|