@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.
@@ -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
- const [
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 result = await handleCooperativeRequest(query, clientIp);
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
- const outputPtr = wasmModule._malloc(256);
155
- const seedPtr = wasmModule._malloc(seed.length + 1);
156
- for (let s = 0; s < seed.length; s++) {
157
- wasmModule.HEAP8[seedPtr + s] = seed.charCodeAt(s);
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
- }, 20000);
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
- private static function getPowSecret(): string
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
- return ['threatIntelScore' => 0.0];
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
  /**