@anonympins/fingerprint 0.6.0 → 0.6.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.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/fingerprint.client.js +2 -0
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +201 -11
- package/src/js/tests/cross_parity.test.js +17 -11
- package/src/php/Challenge/ChallengeUtils.php +63 -2
- package/src/php/FingerprintEngine.php +99 -3
- package/src/php/RequestContext.php +2 -0
- package/src/php/Utils/RequestUtils.php +7 -1
package/src/js/fingerprint.js
CHANGED
|
@@ -63,6 +63,58 @@ function canAttemptDns() {
|
|
|
63
63
|
return true; // HALF-OPEN
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante).
|
|
68
|
+
* @private
|
|
69
|
+
* @param {string} zkpY - La clé publique ZKP du terminal banni.
|
|
70
|
+
* @param {object} config - La configuration de sécurité.
|
|
71
|
+
*/
|
|
72
|
+
async function broadcastBannedZkp(zkpY, config) {
|
|
73
|
+
const peers = config.federatedPeers || [];
|
|
74
|
+
if (peers.length === 0) return;
|
|
75
|
+
|
|
76
|
+
const timestamp = Date.now();
|
|
77
|
+
const msg = `${timestamp}:${zkpY}`;
|
|
78
|
+
|
|
79
|
+
let signature = '';
|
|
80
|
+
let isAsymmetric = false;
|
|
81
|
+
|
|
82
|
+
if (process.env.ED25519_PRIVATE_KEY) {
|
|
83
|
+
try {
|
|
84
|
+
const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
|
|
85
|
+
const signBuffer = crypto.sign(null, Buffer.from(msg), {
|
|
86
|
+
key: cleanKey,
|
|
87
|
+
format: 'pem',
|
|
88
|
+
type: 'pkcs8'
|
|
89
|
+
});
|
|
90
|
+
signature = signBuffer.toString('hex');
|
|
91
|
+
isAsymmetric = true;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!isAsymmetric) {
|
|
98
|
+
const secret = config.federationSecret || getPowSecret();
|
|
99
|
+
signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
peers.forEach(peerUrl => {
|
|
103
|
+
fetch(peerUrl + '?coop_op=share_threat_intel', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
'X-Federation-Signature': isAsymmetric ? '' : signature,
|
|
108
|
+
'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
|
|
109
|
+
'X-Federation-Timestamp': String(timestamp)
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({ zkpY })
|
|
112
|
+
}).catch(() => {
|
|
113
|
+
// Échec de propagation silencieux pour ne pas perturber le thread principal
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
66
118
|
const withTimeout = (promise, ms) => {
|
|
67
119
|
let timeoutId;
|
|
68
120
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -494,7 +546,8 @@ const securityProfiles = {
|
|
|
494
546
|
botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
|
|
495
547
|
tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
|
|
496
548
|
quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
497
|
-
renderingAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
549
|
+
renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
550
|
+
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
498
551
|
},
|
|
499
552
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
500
553
|
patterns: {
|
|
@@ -511,6 +564,7 @@ const securityProfiles = {
|
|
|
511
564
|
},
|
|
512
565
|
allowCrossNetworkRoaming: true, // Profil balancé : tolérant par défaut
|
|
513
566
|
wasm: true,
|
|
567
|
+
useAsymmetricTickets: true,
|
|
514
568
|
},
|
|
515
569
|
/**
|
|
516
570
|
* @summary **Strict Profile**
|
|
@@ -531,7 +585,8 @@ const securityProfiles = {
|
|
|
531
585
|
subnetScore: 0.5,
|
|
532
586
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
533
587
|
botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
|
|
534
|
-
renderingAnomalyScore: 1.0 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
588
|
+
renderingAnomalyScore: 1.0, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
589
|
+
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
535
590
|
},
|
|
536
591
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
537
592
|
patterns: {
|
|
@@ -549,6 +604,7 @@ const securityProfiles = {
|
|
|
549
604
|
challengeNewDevices: true, // Challenge all new devices
|
|
550
605
|
allowCrossNetworkRoaming: false, // Strict : interdiction de changer complètement de réseau sans re-challenge
|
|
551
606
|
wasm: true,
|
|
607
|
+
useAsymmetricTickets: true,
|
|
552
608
|
},
|
|
553
609
|
/**
|
|
554
610
|
* @summary **API Profile**
|
|
@@ -588,6 +644,7 @@ const securityProfiles = {
|
|
|
588
644
|
isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json'),
|
|
589
645
|
allowCrossNetworkRoaming: false, // Les API ne doivent pas subir de roaming inter-IP suspect
|
|
590
646
|
wasm: true,
|
|
647
|
+
useAsymmetricTickets: true,
|
|
591
648
|
}
|
|
592
649
|
,
|
|
593
650
|
/**
|
|
@@ -611,7 +668,8 @@ const securityProfiles = {
|
|
|
611
668
|
botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
612
669
|
tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
|
|
613
670
|
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
614
|
-
renderingAnomalyScore: 0.5 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
671
|
+
renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
672
|
+
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
615
673
|
},
|
|
616
674
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
617
675
|
patterns: {
|
|
@@ -628,6 +686,7 @@ const securityProfiles = {
|
|
|
628
686
|
},
|
|
629
687
|
allowCrossNetworkRoaming: true,
|
|
630
688
|
wasm: true,
|
|
689
|
+
useAsymmetricTickets: true,
|
|
631
690
|
},
|
|
632
691
|
/**
|
|
633
692
|
* @summary **E-commerce Profile**
|
|
@@ -651,7 +710,8 @@ const securityProfiles = {
|
|
|
651
710
|
botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
652
711
|
tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
|
|
653
712
|
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
654
|
-
renderingAnomalyScore: 0.9 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
713
|
+
renderingAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie de rendu,threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
714
|
+
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
655
715
|
},
|
|
656
716
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
657
717
|
patterns: {
|
|
@@ -670,6 +730,7 @@ const securityProfiles = {
|
|
|
670
730
|
isApiRequest: (req) => req.path.startsWith('/api/cart') || req.path.startsWith('/api/stock') || req.path.startsWith('/api/checkout'),
|
|
671
731
|
allowCrossNetworkRoaming: false, // E-commerce : interdiction de changer de réseau sans re-challenge
|
|
672
732
|
wasm: true,
|
|
733
|
+
useAsymmetricTickets: true,
|
|
673
734
|
}
|
|
674
735
|
};
|
|
675
736
|
|
|
@@ -3294,15 +3355,20 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3294
3355
|
const stableFpHash = cyrb53(stableFp).toString();
|
|
3295
3356
|
|
|
3296
3357
|
// Execute non-interdependent asynchronous operations in parallel
|
|
3297
|
-
|
|
3358
|
+
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) || '';
|
|
3359
|
+
const zkpY = zkpProof ? zkpProof.split(":")[0] : null;
|
|
3360
|
+
|
|
3361
|
+
const [
|
|
3298
3362
|
behavioral,
|
|
3363
|
+
{ threatIntelScore },
|
|
3299
3364
|
{ tlsSpoofingScore },
|
|
3300
3365
|
{ subnetScore },
|
|
3301
3366
|
ipReputationScore,
|
|
3302
3367
|
{ botnetClusterScore },
|
|
3303
3368
|
_ // store.set result
|
|
3304
3369
|
] = await Promise.all([
|
|
3305
|
-
getBehavioralIndicators(context, deviceData),
|
|
3370
|
+
getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
|
|
3371
|
+
getThreatIntelScore(zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3306
3372
|
getTlsSpoofingScore(context),
|
|
3307
3373
|
getSubnetScore(context, deviceId),
|
|
3308
3374
|
getIpReputationScore(clientIp),
|
|
@@ -3354,7 +3420,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3354
3420
|
deviceData.ips = new Set(deviceData.ips);
|
|
3355
3421
|
}
|
|
3356
3422
|
// Le vecteur de suspicion est maintenant complet.
|
|
3357
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore };
|
|
3423
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore };
|
|
3358
3424
|
};
|
|
3359
3425
|
|
|
3360
3426
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -3729,6 +3795,22 @@ function parseGraphQLQuery(body) {
|
|
|
3729
3795
|
}
|
|
3730
3796
|
return null;
|
|
3731
3797
|
}
|
|
3798
|
+
|
|
3799
|
+
/**
|
|
3800
|
+
* Calculates a score based on whether the client's ZKP public key (y) is found in a banned list.
|
|
3801
|
+
* @param {string} zkpY - The 'y' component of the ZKP proof (public key).
|
|
3802
|
+
* @returns {Promise<{threatIntelScore: number}>}
|
|
3803
|
+
*/
|
|
3804
|
+
async function getThreatIntelScore(zkpY) {
|
|
3805
|
+
if (!zkpY) return { threatIntelScore: 0 };
|
|
3806
|
+
|
|
3807
|
+
const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
|
|
3808
|
+
if (isBanned) {
|
|
3809
|
+
return { threatIntelScore: 100 };
|
|
3810
|
+
}
|
|
3811
|
+
return { threatIntelScore: 0 };
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3732
3814
|
export class FingerprintEngine {
|
|
3733
3815
|
constructor(securityConfig) {
|
|
3734
3816
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
@@ -3741,6 +3823,20 @@ export class FingerprintEngine {
|
|
|
3741
3823
|
process.env.ED25519_PUBLIC_KEY = securityConfig.ed25519_public_key;
|
|
3742
3824
|
}
|
|
3743
3825
|
|
|
3826
|
+
// Auto-generate Ed25519 key pair on load if indicated and keys are not set
|
|
3827
|
+
if (securityConfig && (securityConfig.useAsymmetricTickets || securityConfig.ed25519 === 'auto') && !process.env.ED25519_PRIVATE_KEY) {
|
|
3828
|
+
try {
|
|
3829
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
|
|
3830
|
+
privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
|
|
3831
|
+
publicKeyEncoding: { format: 'pem', type: 'spki' }
|
|
3832
|
+
});
|
|
3833
|
+
process.env.ED25519_PRIVATE_KEY = privateKey;
|
|
3834
|
+
process.env.ED25519_PUBLIC_KEY = publicKey;
|
|
3835
|
+
} catch (e) {
|
|
3836
|
+
console.error('[Fingerprint] Native Ed25519 key generation failed:', e.message);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3744
3840
|
let finalConfig = securityConfig;
|
|
3745
3841
|
if (securityConfig && securityConfig.autotuning && securityConfig.autotuning.savePath) {
|
|
3746
3842
|
const sPath = securityConfig.autotuning.savePath;
|
|
@@ -3792,7 +3888,8 @@ export class FingerprintEngine {
|
|
|
3792
3888
|
'trustedProxies',
|
|
3793
3889
|
'wasm',
|
|
3794
3890
|
'similarityThreshold',
|
|
3795
|
-
'ed25519_private_key', 'ed25519_public_key'
|
|
3891
|
+
'ed25519_private_key', 'ed25519_public_key',
|
|
3892
|
+
'federatedPeers', 'federationSecret'
|
|
3796
3893
|
]);
|
|
3797
3894
|
|
|
3798
3895
|
// 1. Check for essential keys
|
|
@@ -3839,10 +3936,13 @@ export class FingerprintEngine {
|
|
|
3839
3936
|
(suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
|
|
3840
3937
|
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
|
|
3841
3938
|
(suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
3939
|
+
(suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
3842
3940
|
(suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
|
|
3843
3941
|
|
|
3844
3942
|
return Math.min(100, score);
|
|
3845
3943
|
}
|
|
3944
|
+
|
|
3945
|
+
|
|
3846
3946
|
/**
|
|
3847
3947
|
* Checks if an IP address is in the static allowlist (IPs or CIDR ranges).
|
|
3848
3948
|
* This is the fastest check and should be performed first.
|
|
@@ -4107,7 +4207,13 @@ export class FingerprintEngine {
|
|
|
4107
4207
|
const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
4108
4208
|
|
|
4109
4209
|
if (query.coop_op) {
|
|
4110
|
-
const
|
|
4210
|
+
const coopParams = {
|
|
4211
|
+
...query,
|
|
4212
|
+
signature: requestContext.headers?.['x-federation-signature'] || query.signature,
|
|
4213
|
+
signature_ed25519: requestContext.headers?.['x-federation-signature-ed25519'] || query.signature_ed25519,
|
|
4214
|
+
timestamp: requestContext.headers?.['x-federation-timestamp'] || query.timestamp
|
|
4215
|
+
};
|
|
4216
|
+
const result = await handleCooperativeRequest(coopParams, clientIp, this.securityConfig);
|
|
4111
4217
|
return {
|
|
4112
4218
|
action: 'challenge',
|
|
4113
4219
|
status: 200,
|
|
@@ -4550,6 +4656,22 @@ export class FingerprintEngine {
|
|
|
4550
4656
|
|
|
4551
4657
|
const isBlocked = finalScore >= blockThreshold;
|
|
4552
4658
|
|
|
4659
|
+
// NOUVEAU: Si l'action est de bloquer, on enregistre le ZKP du client dans la liste des bannis.
|
|
4660
|
+
if (isBlocked) {
|
|
4661
|
+
const zkpProof = requestContext.headers['x-zkp-proof'] || requestContext.query.pow_zkp || '';
|
|
4662
|
+
const parts = zkpProof.split(':');
|
|
4663
|
+
if (parts.length === 3) {
|
|
4664
|
+
const [zkpY, zkpT, zkpS] = parts;
|
|
4665
|
+
// 1. Valider cryptographiquement la preuve avant de bannir/diffuser
|
|
4666
|
+
if (verifyZkpProof(zkpY, zkpT, zkpS)) {
|
|
4667
|
+
// 2. Dédoublonner : Ne diffuser que si la clé n'est pas déjà connue comme bannie
|
|
4668
|
+
if (!(await store.has(`banned-zkp-y:${zkpY}`))) {
|
|
4669
|
+
await store.set(`banned-zkp-y:${zkpY}`, true, 86400 * 30); // Banni pour 30 jours
|
|
4670
|
+
broadcastBannedZkp(zkpY, this.securityConfig).catch(() => {});
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4553
4675
|
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked && finalScore > 0;
|
|
4554
4676
|
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
4555
4677
|
const isSuspicious = finalScore >= thresholds.low;
|
|
@@ -4970,10 +5092,77 @@ export async function findPeerInSubnet(clientIp, excludeNodeId) {
|
|
|
4970
5092
|
return activePeers[randomIndex];
|
|
4971
5093
|
}
|
|
4972
5094
|
|
|
4973
|
-
export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
|
|
5095
|
+
export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', config = {}) {
|
|
4974
5096
|
const op = params.coop_op;
|
|
4975
5097
|
if (!op) return null;
|
|
4976
5098
|
|
|
5099
|
+
if (op === 'share_threat_intel') {
|
|
5100
|
+
const peers = config.federatedPeers || [];
|
|
5101
|
+
if (peers.length > 0) {
|
|
5102
|
+
const allowedIPsAndHosts = peers.map(url => {
|
|
5103
|
+
try { return new URL(url).hostname; } catch(e) { return url; }
|
|
5104
|
+
});
|
|
5105
|
+
if (!allowedIPsAndHosts.includes(clientIp)) {
|
|
5106
|
+
return { error: 'Unauthorized federation sender IP' };
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
const zkpY = params.zkpY || '';
|
|
5110
|
+
const timestamp = Number(params.timestamp || 0);
|
|
5111
|
+
|
|
5112
|
+
if (!zkpY || !timestamp) {
|
|
5113
|
+
return { error: 'Missing threat intel parameters' };
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
// Vérification de la fraîcheur du message pour éviter les attaques par rejeu
|
|
5117
|
+
if (Math.abs(Date.now() - timestamp) > 300000) { // 5 minutes max skew
|
|
5118
|
+
return { error: 'Message expired or clock skew too high' };
|
|
5119
|
+
}
|
|
5120
|
+
|
|
5121
|
+
const msg = `${timestamp}:${zkpY}`;
|
|
5122
|
+
const sigEd25519 = params.signature_ed25519 || '';
|
|
5123
|
+
const sigHmac = params.signature || '';
|
|
5124
|
+
|
|
5125
|
+
if (sigEd25519) {
|
|
5126
|
+
const publicKey = config.ed25519_public_key || process.env.ED25519_PUBLIC_KEY;
|
|
5127
|
+
if (!publicKey) {
|
|
5128
|
+
return { error: 'Missing public key for asymmetric verification' };
|
|
5129
|
+
}
|
|
5130
|
+
try {
|
|
5131
|
+
const cleanKey = publicKey.replace(/\\n/g, '\n');
|
|
5132
|
+
const isVerified = crypto.verify(
|
|
5133
|
+
null,
|
|
5134
|
+
Buffer.from(msg),
|
|
5135
|
+
{ key: cleanKey, format: 'pem', type: 'spki' },
|
|
5136
|
+
Buffer.from(sigEd25519, 'hex')
|
|
5137
|
+
);
|
|
5138
|
+
if (!isVerified) {
|
|
5139
|
+
return { error: 'Invalid asymmetric federation signature' };
|
|
5140
|
+
}
|
|
5141
|
+
} catch (e) {
|
|
5142
|
+
return { error: 'Asymmetric signature verification failed' };
|
|
5143
|
+
}
|
|
5144
|
+
} else if (sigHmac) {
|
|
5145
|
+
const secret = config.federationSecret || getPowSecret();
|
|
5146
|
+
const expectedSig = crypto.createHmac('sha256', secret).update(msg).digest('hex');
|
|
5147
|
+
try {
|
|
5148
|
+
const isSigValid = crypto.timingSafeEqual(
|
|
5149
|
+
Buffer.from(sigHmac, 'hex'),
|
|
5150
|
+
Buffer.from(expectedSig, 'hex')
|
|
5151
|
+
);
|
|
5152
|
+
if (!isSigValid) {
|
|
5153
|
+
return { error: 'Invalid federation signature' };
|
|
5154
|
+
}
|
|
5155
|
+
} catch (e) {
|
|
5156
|
+
return { error: 'Invalid signature verification' };
|
|
5157
|
+
}
|
|
5158
|
+
} else {
|
|
5159
|
+
return { error: 'Missing signature' };
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
await store.set(`banned-zkp-y:${zkpY}`, true, 86400 * 30); // 30 jours
|
|
5163
|
+
return { status: 'synchronized' };
|
|
5164
|
+
}
|
|
5165
|
+
|
|
4977
5166
|
const nodeId = params.node_id || '';
|
|
4978
5167
|
if (!nodeId) {
|
|
4979
5168
|
return { error: 'Missing node_id' };
|
|
@@ -5916,7 +6105,8 @@ export const __internal = {
|
|
|
5916
6105
|
canAttemptDns,
|
|
5917
6106
|
registerCooperativeNode,
|
|
5918
6107
|
findPeerInSubnet,
|
|
5919
|
-
handleCooperativeRequest
|
|
6108
|
+
handleCooperativeRequest,
|
|
6109
|
+
broadcastBannedZkp
|
|
5920
6110
|
};
|
|
5921
6111
|
|
|
5922
6112
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -140,21 +140,27 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
140
140
|
it('should verify bit-to-bit convergence across 1,000 different seeds', () => {
|
|
141
141
|
const iterations = 100;
|
|
142
142
|
for (let i = 0; i < 50; i++) {
|
|
143
|
-
const seed = `seed_parity_val_${i}`;
|
|
144
|
-
const jsTrajectory = jsGenerateTrajectory(seed, iterations);
|
|
145
|
-
const phpTrajectory = runPhpTrajectory(seed, iterations);
|
|
143
|
+
const seed = `seed_parity_val_${i}`; // Use a unique seed for each iteration
|
|
144
|
+
const jsTrajectory = jsGenerateTrajectory(seed, iterations); // Generate trajectory using JS
|
|
145
|
+
const phpTrajectory = runPhpTrajectory(seed, iterations); // Generate trajectory using PHP
|
|
146
146
|
|
|
147
147
|
for (let idx = 0; idx < 64; idx++) {
|
|
148
148
|
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
149
149
|
const phpVal = Math.fround(phpTrajectory[idx]);
|
|
150
|
-
expect(jsVal).toBe(phpVal);
|
|
150
|
+
expect(jsVal).toBe(phpVal, `PHP and JS trajectories mismatch at index ${idx} for seed "${seed}"`);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
if (wasmModule && typeof wasmModule._generate_gpu_pow_trajectory === 'function') {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
// Ensure _malloc and HEAP8 are available on the wasmModule
|
|
155
|
+
if (typeof wasmModule._malloc !== 'function' || wasmModule.HEAP8 == null) {
|
|
156
|
+
console.warn("WASM module is missing _malloc or HEAP8. Skipping WASM parity test.");
|
|
157
|
+
continue; // Skip WASM part of the test if prerequisites are not met
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const outputPtr = wasmModule._malloc(256); // Allocate memory for 64 floats (64 * 4 bytes)
|
|
161
|
+
const seedPtr = wasmModule._malloc(seed.length + 1); // Allocate memory for seed string + null terminator
|
|
162
|
+
for (let s = 0; s < seed.length; s++) { // Copy seed string to WASM memory
|
|
163
|
+
wasmModule.HEAP8[seedPtr + s] = seed.charCodeAt(s); // HEAP8 is Int8Array
|
|
158
164
|
}
|
|
159
165
|
wasmModule.HEAP8[seedPtr + seed.length] = 0;
|
|
160
166
|
|
|
@@ -163,13 +169,13 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
163
169
|
const wasmOutput = new Float32Array(wasmModule.HEAPF32.buffer, outputPtr, 64);
|
|
164
170
|
for (let idx = 0; idx < 64; idx++) {
|
|
165
171
|
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
166
|
-
const wasmVal = Math.fround(wasmOutput[idx]);
|
|
167
|
-
expect(jsVal).toBe(wasmVal);
|
|
172
|
+
const wasmVal = Math.fround(wasmOutput[idx]); // Ensure float32 precision for comparison
|
|
173
|
+
expect(jsVal).toBe(wasmVal, `WASM and JS trajectories mismatch at index ${idx} for seed "${seed}"`);
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
wasmModule._free(outputPtr);
|
|
171
177
|
wasmModule._free(seedPtr);
|
|
172
178
|
}
|
|
173
179
|
}
|
|
174
|
-
},
|
|
180
|
+
}, 30000); // Increased timeout for potentially longer WASM compilation/execution
|
|
175
181
|
});
|
|
@@ -138,7 +138,7 @@ class ChallengeUtils
|
|
|
138
138
|
return $activePeers[array_rand($activePeers)];
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
public static function handleCooperativeRequest(array $params): ?array
|
|
141
|
+
public static function handleCooperativeRequest(array $params, string $clientIp = '127.0.0.1', array $config = []): ?array
|
|
142
142
|
{
|
|
143
143
|
$op = $params['coop_op'] ?? null;
|
|
144
144
|
if (!$op) {
|
|
@@ -146,6 +146,67 @@ class ChallengeUtils
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
$store = StoreManager::getStore();
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if ($op === 'share_threat_intel') {
|
|
152
|
+
$peers = $config['federatedPeers'] ?? [];
|
|
153
|
+
if (!empty($peers)) {
|
|
154
|
+
$allowedHosts = array_map(function ($url) {
|
|
155
|
+
$host = parse_url($url, PHP_URL_HOST);
|
|
156
|
+
return !empty($host) ? $host : $url;
|
|
157
|
+
}, $peers);
|
|
158
|
+
|
|
159
|
+
if (!in_array($clientIp, $allowedHosts, true)) {
|
|
160
|
+
return ['error' => 'Unauthorized federation sender IP'];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
$zkpY = $params['zkpY'] ?? '';
|
|
164
|
+
$headers = function_exists('getallheaders') ? array_change_key_case(getallheaders(), CASE_LOWER) : [];
|
|
165
|
+
$sigEd25519 = $params['signature_ed25519'] ?? $headers['x-federation-signature-ed25519'] ?? $_SERVER['HTTP_X_FEDERATION_SIGNATURE_ED25519'] ?? '';
|
|
166
|
+
$sigHmac = $params['signature'] ?? $headers['x-federation-signature'] ?? $_SERVER['HTTP_X_FEDERATION_SIGNATURE'] ?? '';
|
|
167
|
+
$timestamp = (int)($params['timestamp'] ?? $headers['x-federation-timestamp'] ?? $_SERVER['HTTP_X_FEDERATION_TIMESTAMP'] ?? 0);
|
|
168
|
+
|
|
169
|
+
if (empty($zkpY) || empty($timestamp)) {
|
|
170
|
+
return ['error' => 'Missing threat intel parameters'];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Time window check (5 minutes anti-replay)
|
|
174
|
+
$now = (int)(microtime(true) * 1000);
|
|
175
|
+
if (abs($now - $timestamp) > 300000) {
|
|
176
|
+
return ['error' => 'Message expired or clock skew too high'];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
$msg = "{$timestamp}:{$zkpY}";
|
|
180
|
+
|
|
181
|
+
if (!empty($sigEd25519)) {
|
|
182
|
+
$publicKey = $config['ed25519_public_key'] ?? $_ENV['ED25519_PUBLIC_KEY'] ?? getenv('ED25519_PUBLIC_KEY');
|
|
183
|
+
if (!$publicKey) {
|
|
184
|
+
return ['error' => 'Missing public key for asymmetric verification'];
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
$cleanKey = str_replace('\n', "\n", $publicKey);
|
|
188
|
+
$pubKeyObj = openssl_pkey_get_public($cleanKey);
|
|
189
|
+
if (!$pubKeyObj || openssl_verify($msg, hex2bin($sigEd25519), $pubKeyObj, null) !== 1) {
|
|
190
|
+
return ['error' => 'Invalid asymmetric federation signature'];
|
|
191
|
+
}
|
|
192
|
+
} catch (\Throwable $e) {
|
|
193
|
+
return ['error' => 'Asymmetric signature verification failed'];
|
|
194
|
+
}
|
|
195
|
+
} elseif (!empty($sigHmac)) {
|
|
196
|
+
$secret = $params['federationSecret'] ?? $config['federationSecret'] ?? self::getPowSecret();
|
|
197
|
+
$expectedSig = hash_hmac('sha256', $msg, $secret);
|
|
198
|
+
if (!hash_equals($expectedSig, $sigHmac)) {
|
|
199
|
+
return ['error' => 'Invalid federation signature'];
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
return ['error' => 'Missing signature'];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Ban the ZKP public key for 30 days
|
|
206
|
+
$store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
|
|
207
|
+
return ['status' => 'synchronized'];
|
|
208
|
+
}
|
|
209
|
+
|
|
149
210
|
$nodeId = $params['node_id'] ?? '';
|
|
150
211
|
if (empty($nodeId)) {
|
|
151
212
|
return ['error' => 'Missing node_id'];
|
|
@@ -334,7 +395,7 @@ class ChallengeUtils
|
|
|
334
395
|
/**
|
|
335
396
|
* Récupère la clé secrète pour les PoW depuis les variables d'environnement.
|
|
336
397
|
*/
|
|
337
|
-
|
|
398
|
+
public static function getPowSecret(): string
|
|
338
399
|
{
|
|
339
400
|
$secret = $_ENV['POW_SECRET'] ?? getenv('POW_SECRET');
|
|
340
401
|
if (!$secret && ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production') {
|
|
@@ -662,13 +662,13 @@
|
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
// Check 4: La stagnation (Lack of Entropy / Genericity)
|
|
665
|
-
if ($uaParts['browser']) {
|
|
665
|
+
if (is_string($uaParts['browser'])) {
|
|
666
666
|
$ja4Key = "ja4-browsers:{$ja4}";
|
|
667
667
|
$seenBrowsers = $store->get($ja4Key) ?: [];
|
|
668
668
|
if (!is_array($seenBrowsers)) {
|
|
669
669
|
$seenBrowsers = [];
|
|
670
670
|
}
|
|
671
|
-
$browserFamily = explode('/', $uaParts['browser'])[0] ?? null;
|
|
671
|
+
$browserFamily = explode('/', (string)$uaParts['browser'])[0] ?? null;
|
|
672
672
|
if ($browserFamily && !in_array($browserFamily, $seenBrowsers, true)) {
|
|
673
673
|
$seenBrowsers[] = $browserFamily;
|
|
674
674
|
$store->set($ja4Key, $seenBrowsers, 86400);
|
|
@@ -874,7 +874,7 @@
|
|
|
874
874
|
|
|
875
875
|
$coopOp = $context->query['coop_op'] ?? null;
|
|
876
876
|
if ($coopOp) {
|
|
877
|
-
$result = ChallengeUtils::handleCooperativeRequest($context->query);
|
|
877
|
+
$result = ChallengeUtils::handleCooperativeRequest($context->query, $context->clientIp, $this->securityConfig);
|
|
878
878
|
return [
|
|
879
879
|
'action' => 'challenge',
|
|
880
880
|
'status' => 200,
|
|
@@ -1125,6 +1125,24 @@
|
|
|
1125
1125
|
MetricsManager::incrementCounter('requests_total', ['status' => 'blocked']);
|
|
1126
1126
|
$this->logger->log('info', 'request_blocked', ['deviceId' => $deviceId, 'score' => $finalScore, 'vector' => $suspicionVector]);
|
|
1127
1127
|
}
|
|
1128
|
+
|
|
1129
|
+
// Federated Threat Intelligence & ZKP Synchronization
|
|
1130
|
+
$zkpProof = $context->getHeader('x-zkp-proof') ?? $context->query['pow_zkp'] ?? '';
|
|
1131
|
+
$parts = explode(':', $zkpProof);
|
|
1132
|
+
if (count($parts) === 3) {
|
|
1133
|
+
$zkpY = $parts[0];
|
|
1134
|
+
$zkpT = $parts[1];
|
|
1135
|
+
$zkpS = $parts[2];
|
|
1136
|
+
// 1. Valider cryptographiquement la preuve avant de bannir/diffuser
|
|
1137
|
+
if (ChallengeUtils::verifyZkpProof($zkpY, $zkpT, $zkpS)) {
|
|
1138
|
+
// 2. Dédoublonner : Ne diffuser que si la clé n'est pas déjà bannie
|
|
1139
|
+
if (!$store->has("banned-zkp-y:{$zkpY}")) {
|
|
1140
|
+
$store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
|
|
1141
|
+
$this->broadcastBannedZkp($zkpY);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1128
1146
|
$decision = ['action' => 'block', 'status' => 403, 'body' => 'Forbidden', 'score' => $finalScore, 'vector' => $suspicionVector];
|
|
1129
1147
|
if ($this->dryRun) {
|
|
1130
1148
|
$this->log("[Dry Run] Intended action: {$decision['action']}", ['score' => $decision['score']]);
|
|
@@ -1407,6 +1425,84 @@
|
|
|
1407
1425
|
return false;
|
|
1408
1426
|
}
|
|
1409
1427
|
|
|
1428
|
+
|
|
1429
|
+
private function broadcastBannedZkp(string $zkpY): void
|
|
1430
|
+
{
|
|
1431
|
+
$peers = $this->securityConfig['federatedPeers'] ?? [];
|
|
1432
|
+
if (empty($peers)) return;
|
|
1433
|
+
|
|
1434
|
+
$timestamp = (int)(microtime(true) * 1000);
|
|
1435
|
+
$msg = "{$timestamp}:{$zkpY}";
|
|
1436
|
+
|
|
1437
|
+
$signature = '';
|
|
1438
|
+
$isAsymmetric = false;
|
|
1439
|
+
|
|
1440
|
+
$privateKey = $_ENV['ED25519_PRIVATE_KEY'] ?? getenv('ED25519_PRIVATE_KEY');
|
|
1441
|
+
if ($privateKey) {
|
|
1442
|
+
try {
|
|
1443
|
+
$cleanKey = str_replace('\n', "\n", $privateKey);
|
|
1444
|
+
$pkeyObj = openssl_pkey_get_private($cleanKey);
|
|
1445
|
+
if ($pkeyObj && openssl_sign($msg, $sigBytes, $pkeyObj, null)) {
|
|
1446
|
+
$signature = bin2hex($sigBytes);
|
|
1447
|
+
$isAsymmetric = true;
|
|
1448
|
+
}
|
|
1449
|
+
} catch (\Throwable $e) {
|
|
1450
|
+
error_log('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (!$isAsymmetric) {
|
|
1455
|
+
$secret = $this->securityConfig['federationSecret'] ?? ChallengeUtils::getPowSecret();
|
|
1456
|
+
$signature = hash_hmac('sha256', $msg, $secret);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
foreach ($peers as $peerUrl) {
|
|
1460
|
+
$this->asyncPost($peerUrl . '?coop_op=share_threat_intel', [
|
|
1461
|
+
'zkpY' => $zkpY,
|
|
1462
|
+
'signature' => $isAsymmetric ? '' : $signature,
|
|
1463
|
+
'signature_ed25519' => $isAsymmetric ? $signature : '',
|
|
1464
|
+
'timestamp' => $timestamp
|
|
1465
|
+
]);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
private function asyncPost(string $url, array $params): void
|
|
1470
|
+
{
|
|
1471
|
+
$parts = parse_url($url);
|
|
1472
|
+
if ($parts === false) return;
|
|
1473
|
+
|
|
1474
|
+
$host = $parts['host'];
|
|
1475
|
+
$port = $parts['port'] ?? ($parts['scheme'] === 'https' ? 443 : 80);
|
|
1476
|
+
$path = ($parts['path'] ?? '/') . (isset($parts['query']) ? '?' . $parts['query'] : '');
|
|
1477
|
+
$scheme = $parts['scheme'] === 'https' ? 'ssl://' : '';
|
|
1478
|
+
|
|
1479
|
+
$postData = json_encode($params);
|
|
1480
|
+
|
|
1481
|
+
$fp = @stream_socket_client(
|
|
1482
|
+
"{$scheme}{$host}:{$port}",
|
|
1483
|
+
$errno,
|
|
1484
|
+
$errstr,
|
|
1485
|
+
0.5,
|
|
1486
|
+
STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT
|
|
1487
|
+
);
|
|
1488
|
+
|
|
1489
|
+
if ($fp) {
|
|
1490
|
+
stream_set_blocking($fp, false);
|
|
1491
|
+
$out = "POST {$path} HTTP/1.1\r\n";
|
|
1492
|
+
$out .= "Host: {$host}\r\n";
|
|
1493
|
+
$out .= "Content-Type: application/json\r\n";
|
|
1494
|
+
$out .= "Content-Length: " . strlen($postData) . "\r\n";
|
|
1495
|
+
$out .= "X-Federation-Signature: " . ($params['signature'] ?? '') . "\r\n";
|
|
1496
|
+
$out .= "X-Federation-Signature-Ed25519: " . ($params['signature_ed25519'] ?? '') . "\r\n";
|
|
1497
|
+
$out .= "X-Federation-Timestamp: " . ($params['timestamp'] ?? '') . "\r\n";
|
|
1498
|
+
$out .= "Connection: Close\r\n\r\n";
|
|
1499
|
+
$out .= $postData;
|
|
1500
|
+
|
|
1501
|
+
@fwrite($fp, $out);
|
|
1502
|
+
@fclose($fp);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1410
1506
|
/**
|
|
1411
1507
|
* Vérifie si une requête provient d'un bot légitime et whitelisté (ex: Googlebot)
|
|
1412
1508
|
* en utilisant des recherches DNS inversées et directes. Le résultat est mis en cache.
|
|
@@ -40,6 +40,7 @@ class RequestContext
|
|
|
40
40
|
public ?string $http2Fingerprint = null;
|
|
41
41
|
public ?string $quicFingerprint = null;
|
|
42
42
|
public ?string $tcpFingerprint = null;
|
|
43
|
+
public ?string $zkpY = null; // NOUVEAU: Clé publique ZKP du client
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* @param string $clientIp
|
|
@@ -79,6 +80,7 @@ class RequestContext
|
|
|
79
80
|
$this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
|
|
80
81
|
$this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
|
|
81
82
|
$this->quicFingerprint = $this->headers['x-quic-fp'] ?? null;
|
|
83
|
+
$this->zkpY = isset($this->headers['x-zkp-proof']) ? explode(':', $this->headers['x-zkp-proof'])[0] : null;
|
|
82
84
|
$this->tlsSessionId = $this->headers['x-tls-session-id'] ?? $this->headers['x-ssl-session-id'] ?? null;
|
|
83
85
|
}
|
|
84
86
|
/**
|
|
@@ -1025,7 +1025,13 @@ class RequestUtils
|
|
|
1025
1025
|
|
|
1026
1026
|
public static function getThreatIntelScore(RequestContext $context, array $threatIntelConfig): array
|
|
1027
1027
|
{
|
|
1028
|
-
|
|
1028
|
+
$zkpY = $context->zkpY;
|
|
1029
|
+
if (empty($zkpY)) {
|
|
1030
|
+
return ['threatIntelScore' => 0.0];
|
|
1031
|
+
}
|
|
1032
|
+
$store = StoreManager::getStore();
|
|
1033
|
+
$isBanned = $store->has("banned-zkp-y:{$zkpY}");
|
|
1034
|
+
return ['threatIntelScore' => $isBanned ? 100.0 : 0.0];
|
|
1029
1035
|
}
|
|
1030
1036
|
|
|
1031
1037
|
/**
|