@anonympins/fingerprint 0.4.6 → 0.5.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 +43 -0
- package/README.md +56 -11
- package/package.json +2 -1
- package/src/js/fingerprint.client.js +360 -23
- package/src/js/fingerprint.js +508 -200
- package/src/js/fingerprint.utils.js +184 -0
- package/src/js/gpu_pow.solver.js +217 -0
- package/src/js/library.js +1 -1
- package/src/js/pow.solver.inline.js +12 -4
- package/src/js/pow.solver.js +12 -4
- package/src/js/tests/fingerprint.client.init.test.js +5 -2
- package/src/js/tests/fingerprint.isMalicious.test.js +234 -116
- package/src/js/tests/fingerprint.test.js +195 -1
- package/src/js/tests/gpu_pow.test.js +118 -0
- package/src/js/tests/ja3AnomalyDetector.test.js +1 -0
- package/src/js/tests/quicFingerprint.test.js +35 -0
- package/src/php/AutoTuner.php +71 -0
- package/src/php/Challenge/ChallengeUtils.php +344 -9
- package/src/php/Config/SecurityProfiles.php +9 -0
- package/src/php/FingerprintEngine.php +61 -2
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +285 -81
- package/src/php/Tests/MaliciousPatternsTest.php +104 -0
- package/src/php/Tests/QuicFingerprintTest.php +54 -0
- package/src/php/Tests/RequestUtilsTest.php +43 -0
- package/src/php/Utils/BigInt.php +202 -144
- package/src/php/Utils/MaliciousPatterns.php +74 -58
- package/src/php/Utils/RequestUtils.php +120 -0
|
@@ -14,6 +14,48 @@ use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
|
14
14
|
*/
|
|
15
15
|
class ChallengeUtils
|
|
16
16
|
{
|
|
17
|
+
private static function fround(float $value): float
|
|
18
|
+
{
|
|
19
|
+
return unpack('f', pack('f', $value))[1];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public static function hashSeedToFloat(string $seed): float
|
|
23
|
+
{
|
|
24
|
+
$hash = 0;
|
|
25
|
+
for ($i = 0; $i < strlen($seed); $i++) {
|
|
26
|
+
$hash = (($hash << 5) - $hash + ord($seed[$i])) & 0xffffffff;
|
|
27
|
+
if ($hash & 0x80000000) {
|
|
28
|
+
$hash = $hash - 0x100000000;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return abs($hash % 1000000) / 1000000;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public static function verifyGpuPow(string $seed, int $iterations, string $solution, array $sampleIndices = [0, 12, 35, 57]): bool
|
|
35
|
+
{
|
|
36
|
+
$values = explode(',', $solution);
|
|
37
|
+
if (count($values) !== 64) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
$numericSeed = self::hashSeedToFloat($seed);
|
|
41
|
+
$r = 3.9999;
|
|
42
|
+
foreach ($sampleIndices as $idx) {
|
|
43
|
+
if ($idx < 0 || $idx >= 64) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
$x = self::fround($numericSeed + $idx * 0.015);
|
|
47
|
+
$rFloat = self::fround($r);
|
|
48
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
49
|
+
$x = self::fround($rFloat * $x * self::fround(1.0 - $x));
|
|
50
|
+
}
|
|
51
|
+
$clientVal = (float)$values[$idx];
|
|
52
|
+
if (abs($clientVal - $x) > 1e-4) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
17
59
|
private const TRAP_URL_TEMPLATES = [
|
|
18
60
|
'/includes/config-{RANDOM}.php',
|
|
19
61
|
'/.env.{RANDOM}',
|
|
@@ -48,6 +90,123 @@ class ChallengeUtils
|
|
|
48
90
|
return $block;
|
|
49
91
|
}
|
|
50
92
|
|
|
93
|
+
public static function registerCooperativeNode(string $clientIp, string $nodeId, string $seed): void
|
|
94
|
+
{
|
|
95
|
+
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
96
|
+
if ($subnet === null) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
$store = StoreManager::getStore();
|
|
100
|
+
$key = "coop-pospace:subnet:{$subnet}";
|
|
101
|
+
$nodes = $store->get($key) ?? [];
|
|
102
|
+
|
|
103
|
+
$now = time();
|
|
104
|
+
// Nettoyage des nœuds expirés (vieux de plus de 2 minutes)
|
|
105
|
+
$nodes = array_filter($nodes, fn($n) => ($now - $n['timestamp']) < 120);
|
|
106
|
+
|
|
107
|
+
$nodes[$nodeId] = [
|
|
108
|
+
'nodeId' => $nodeId,
|
|
109
|
+
'seed' => $seed,
|
|
110
|
+
'timestamp' => $now
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
$store->set($key, $nodes, 120);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public static function findPeerInSubnet(string $clientIp, string $excludeNodeId): ?array
|
|
117
|
+
{
|
|
118
|
+
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
119
|
+
if ($subnet === null) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
$store = StoreManager::getStore();
|
|
123
|
+
$key = "coop-pospace:subnet:{$subnet}";
|
|
124
|
+
$nodes = $store->get($key) ?? [];
|
|
125
|
+
|
|
126
|
+
$now = time();
|
|
127
|
+
$activePeers = [];
|
|
128
|
+
foreach ($nodes as $id => $node) {
|
|
129
|
+
if ($id !== $excludeNodeId && ($now - $node['timestamp']) < 120) {
|
|
130
|
+
$activePeers[] = $node;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (empty($activePeers)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return $activePeers[array_rand($activePeers)];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public static function handleCooperativeRequest(array $params): ?array
|
|
142
|
+
{
|
|
143
|
+
$op = $params['coop_op'] ?? null;
|
|
144
|
+
if (!$op) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$store = StoreManager::getStore();
|
|
149
|
+
$nodeId = $params['node_id'] ?? '';
|
|
150
|
+
if (empty($nodeId)) {
|
|
151
|
+
return ['error' => 'Missing node_id'];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
switch ($op) {
|
|
155
|
+
case 'register':
|
|
156
|
+
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
157
|
+
$seed = $params['seed'] ?? '';
|
|
158
|
+
self::registerCooperativeNode($clientIp, $nodeId, $seed);
|
|
159
|
+
return ['status' => 'registered'];
|
|
160
|
+
|
|
161
|
+
case 'request_peer_block':
|
|
162
|
+
$peerId = $params['peer_id'] ?? '';
|
|
163
|
+
$blockIdx = (int)($params['block_idx'] ?? 0);
|
|
164
|
+
$requestId = $params['req_id'] ?? '';
|
|
165
|
+
if (empty($peerId) || empty($requestId)) {
|
|
166
|
+
return ['error' => 'Invalid parameters'];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
$queueKey = "coop-mailbox:queue:{$peerId}";
|
|
170
|
+
$requests = $store->get($queueKey) ?? [];
|
|
171
|
+
$requests[] = [
|
|
172
|
+
'req_id' => $requestId,
|
|
173
|
+
'requester_id' => $nodeId,
|
|
174
|
+
'block_idx' => $blockIdx
|
|
175
|
+
];
|
|
176
|
+
$store->set($queueKey, $requests, 30);
|
|
177
|
+
return ['status' => 'queued'];
|
|
178
|
+
|
|
179
|
+
case 'poll_requests':
|
|
180
|
+
$queueKey = "coop-mailbox:queue:{$nodeId}";
|
|
181
|
+
$requests = $store->get($queueKey) ?? [];
|
|
182
|
+
$store->delete($queueKey);
|
|
183
|
+
return ['requests' => $requests];
|
|
184
|
+
|
|
185
|
+
case 'respond_block':
|
|
186
|
+
$requesterId = $params['requester_id'] ?? '';
|
|
187
|
+
$requestId = $params['req_id'] ?? '';
|
|
188
|
+
$blockData = $params['block_data'] ?? '';
|
|
189
|
+
if (empty($requesterId) || empty($requestId)) {
|
|
190
|
+
return ['error' => 'Invalid parameters'];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
$responseKey = "coop-mailbox:res:{$requesterId}:{$requestId}";
|
|
194
|
+
$store->set($responseKey, ['block_data' => $blockData], 30);
|
|
195
|
+
return ['status' => 'delivered'];
|
|
196
|
+
|
|
197
|
+
case 'poll_response':
|
|
198
|
+
$requestId = $params['req_id'] ?? '';
|
|
199
|
+
$responseKey = "coop-mailbox:res:{$nodeId}:{$requestId}";
|
|
200
|
+
$data = $store->get($responseKey);
|
|
201
|
+
if ($data) {
|
|
202
|
+
$store->delete($responseKey);
|
|
203
|
+
return ['status' => 'ready', 'block_data' => $data['block_data']];
|
|
204
|
+
}
|
|
205
|
+
return ['status' => 'pending'];
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
51
210
|
public static function generateSpaceChallenge(string $clientIp, string $nonce, float $suspicionFactor, string $originalUrl, array $securityConfig): array
|
|
52
211
|
{
|
|
53
212
|
$pospaceConfig = $securityConfig['pospace'] ?? [];
|
|
@@ -63,13 +222,29 @@ class ChallengeUtils
|
|
|
63
222
|
}
|
|
64
223
|
}
|
|
65
224
|
|
|
66
|
-
|
|
225
|
+
$challenge = [
|
|
67
226
|
'type' => 'pospace',
|
|
68
227
|
'nonce' => $nonce,
|
|
69
228
|
'sizeMb' => $sizeMb,
|
|
70
229
|
'queries' => $queries,
|
|
71
230
|
'path' => $originalUrl
|
|
72
231
|
];
|
|
232
|
+
|
|
233
|
+
// Tentative de couplage coopératif avec un nœud du même sous-réseau
|
|
234
|
+
$peer = self::findPeerInSubnet($clientIp, $nonce);
|
|
235
|
+
if ($peer !== null) {
|
|
236
|
+
$challenge['peerId'] = $peer['nodeId'];
|
|
237
|
+
$challenge['peerBlockIdx'] = random_int(0, $maxBlocks - 1);
|
|
238
|
+
|
|
239
|
+
$store = StoreManager::getStore();
|
|
240
|
+
$store->set("coop-assoc:{$nonce}", [
|
|
241
|
+
'peerNodeId' => $peer['nodeId'],
|
|
242
|
+
'peerSeed' => $peer['seed'],
|
|
243
|
+
'peerBlockIdx' => $challenge['peerBlockIdx']
|
|
244
|
+
], 120);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return $challenge;
|
|
73
248
|
}
|
|
74
249
|
|
|
75
250
|
public static function verifySpacePoW(string $nonce, string $solution, array $queries, string $seed, string $clientSecret): bool
|
|
@@ -78,11 +253,48 @@ class ChallengeUtils
|
|
|
78
253
|
foreach ($queries as $idx) {
|
|
79
254
|
$combined .= self::generateBlock($seed, (int)$idx);
|
|
80
255
|
}
|
|
256
|
+
|
|
257
|
+
// Vérification de la preuve coopérative
|
|
258
|
+
$store = StoreManager::getStore();
|
|
259
|
+
$assoc = $store->get("coop-assoc:{$nonce}");
|
|
260
|
+
if ($assoc !== null) {
|
|
261
|
+
$peerSeed = $assoc['peerSeed'] ?? null;
|
|
262
|
+
$peerBlockIdx = $assoc['peerBlockIdx'] ?? null;
|
|
263
|
+
if ($peerSeed !== null && $peerBlockIdx !== null) {
|
|
264
|
+
$combined .= self::generateBlock($peerSeed, (int)$peerBlockIdx);
|
|
265
|
+
}
|
|
266
|
+
$store->delete("coop-assoc:{$nonce}");
|
|
267
|
+
}
|
|
268
|
+
|
|
81
269
|
$finalBlock = $combined . $nonce . ":" . $clientSecret;
|
|
82
270
|
$hash = hash('sha256', $finalBlock);
|
|
83
271
|
return hash_equals($hash, $solution);
|
|
84
272
|
}
|
|
85
273
|
|
|
274
|
+
public static function verifyZkpProof(string $yStr, string $tStr, string $sStr): bool
|
|
275
|
+
{
|
|
276
|
+
try {
|
|
277
|
+
$p = BigInt::fromHex('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); // secp256k1 prime
|
|
278
|
+
$g = new BigInt(2);
|
|
279
|
+
|
|
280
|
+
$y = BigInt::fromHex($yStr);
|
|
281
|
+
$t = BigInt::fromHex($tStr);
|
|
282
|
+
$s = BigInt::fromHex($sStr);
|
|
283
|
+
|
|
284
|
+
$cStr = (string)$g . (string)$y . (string)$t;
|
|
285
|
+
$cHex = hash('sha256', $cStr);
|
|
286
|
+
$c = BigInt::fromHex($cHex)->mod($p);
|
|
287
|
+
|
|
288
|
+
$left = $g->modPow($s, $p);
|
|
289
|
+
$y_c = $y->modPow($c, $p);
|
|
290
|
+
$right = $t->mul($y_c)->mod($p);
|
|
291
|
+
|
|
292
|
+
return $left->compareTo($right) === 0;
|
|
293
|
+
} catch (\Throwable $e) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
86
298
|
/**
|
|
87
299
|
* Récupère la clé secrète pour les PoW depuis les variables d'environnement.
|
|
88
300
|
*/
|
|
@@ -102,6 +314,22 @@ class ChallengeUtils
|
|
|
102
314
|
*/
|
|
103
315
|
public static function generateStatelessTicket(array $payload): string
|
|
104
316
|
{
|
|
317
|
+
$ed25519Key = $_ENV['ED25519_PRIVATE_KEY'] ?? getenv('ED25519_PRIVATE_KEY');
|
|
318
|
+
if ($ed25519Key) {
|
|
319
|
+
try {
|
|
320
|
+
$serialized = json_encode($payload);
|
|
321
|
+
$privateKey = openssl_pkey_get_private($ed25519Key);
|
|
322
|
+
if ($privateKey && openssl_sign($serialized, $signature, $privateKey, null)) {
|
|
323
|
+
$base64UrlEncode = function ($input) {
|
|
324
|
+
return rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
|
|
325
|
+
};
|
|
326
|
+
return 'ed25519.' . $base64UrlEncode($serialized) . '.' . $base64UrlEncode($signature);
|
|
327
|
+
}
|
|
328
|
+
} catch (\Throwable $e) {
|
|
329
|
+
error_log("[ChallengeUtils] Ed25519 signing failed: " . $e->getMessage());
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
105
333
|
$key = hash('sha256', self::getPowSecret(), true);
|
|
106
334
|
$iv = random_bytes(16);
|
|
107
335
|
$encrypted = openssl_encrypt(json_encode($payload), 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
|
@@ -115,10 +343,40 @@ class ChallengeUtils
|
|
|
115
343
|
/**
|
|
116
344
|
* Décode et valide un ticket stateless chiffré et signé.
|
|
117
345
|
* @param string $ticket
|
|
346
|
+
* @param string $secret
|
|
118
347
|
* @return array|null
|
|
119
348
|
*/
|
|
120
|
-
public static function parseStatelessTicket(string $ticket): ?array
|
|
349
|
+
public static function parseStatelessTicket(string $ticket, string $secret = ''): ?array
|
|
121
350
|
{
|
|
351
|
+
try {
|
|
352
|
+
if (str_starts_with($ticket, 'ed25519.')) {
|
|
353
|
+
$parts = explode('.', $ticket);
|
|
354
|
+
if (count($parts) !== 3) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
$base64UrlDecode = function ($input) {
|
|
358
|
+
return base64_decode(strtr($input, '-_', '+/'));
|
|
359
|
+
};
|
|
360
|
+
$payloadJson = $base64UrlDecode($parts[1]);
|
|
361
|
+
$signature = $base64UrlDecode($parts[2]);
|
|
362
|
+
|
|
363
|
+
$ed25519PubKey = $_ENV['ED25519_PUBLIC_KEY'] ?? getenv('ED25519_PUBLIC_KEY');
|
|
364
|
+
if (!$ed25519PubKey) {
|
|
365
|
+
error_log("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
$publicKey = openssl_pkey_get_public($ed25519PubKey);
|
|
370
|
+
if ($publicKey && openssl_verify($payloadJson, $signature, $publicKey, null) === 1) {
|
|
371
|
+
return json_decode($payloadJson, true);
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
} catch (\Throwable $e) {
|
|
376
|
+
error_log("[ChallengeUtils] Ed25519 verification failed: " . $e->getMessage());
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
122
380
|
$parts = explode('.', $ticket);
|
|
123
381
|
if (count($parts) !== 3) {
|
|
124
382
|
return null;
|
|
@@ -132,7 +390,7 @@ class ChallengeUtils
|
|
|
132
390
|
if (!$iv || !$encrypted || !$signature || strlen($iv) !== 16) {
|
|
133
391
|
return null;
|
|
134
392
|
}
|
|
135
|
-
$key = hash('sha256', self::getPowSecret(), true);
|
|
393
|
+
$key = hash('sha256', !empty($secret) ? $secret : self::getPowSecret(), true);
|
|
136
394
|
$expectedSignature = hash_hmac('sha256', $iv . $encrypted, $key, true);
|
|
137
395
|
if (!hash_equals($expectedSignature, $signature)) {
|
|
138
396
|
return null;
|
|
@@ -143,20 +401,23 @@ class ChallengeUtils
|
|
|
143
401
|
|
|
144
402
|
/**
|
|
145
403
|
* Vérifie si un ticket de passage est valide (supporte les tickets opaques via store et le fallback legacy).
|
|
404
|
+
* Supporte une clé secrète optionnelle passée en paramètre pour la compatibilité avec les tests.
|
|
146
405
|
*/
|
|
147
406
|
public static function isTicketValid(
|
|
148
407
|
?string $ip,
|
|
149
408
|
?string $ticket,
|
|
150
409
|
string $deviceId = '',
|
|
151
410
|
string $deviceHash = '',
|
|
152
|
-
bool $allowCrossNetworkRoaming = false
|
|
411
|
+
bool $allowCrossNetworkRoaming = false,
|
|
412
|
+
string $secret = '',
|
|
413
|
+
string $zkpProof = ''
|
|
153
414
|
): bool {
|
|
154
415
|
if (empty($ip) || empty($ticket)) {
|
|
155
416
|
return false;
|
|
156
417
|
}
|
|
157
418
|
|
|
158
419
|
// Tentative de validation stateless d'abord
|
|
159
|
-
$ticketData = self::parseStatelessTicket($ticket);
|
|
420
|
+
$ticketData = self::parseStatelessTicket($ticket, $secret);
|
|
160
421
|
if ($ticketData !== null) {
|
|
161
422
|
$expiry = $ticketData['expiry'] ?? null;
|
|
162
423
|
$originalIp = $ticketData['originalIp'] ?? null;
|
|
@@ -166,6 +427,18 @@ class ChallengeUtils
|
|
|
166
427
|
if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
|
|
167
428
|
return false;
|
|
168
429
|
}
|
|
430
|
+
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
431
|
+
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
432
|
+
if (!empty($zkpProof)) {
|
|
433
|
+
$zkpParts = explode(':', $zkpProof);
|
|
434
|
+
if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
|
|
435
|
+
if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
169
442
|
if ($ip === $originalIp) {
|
|
170
443
|
return true;
|
|
171
444
|
}
|
|
@@ -193,6 +466,18 @@ class ChallengeUtils
|
|
|
193
466
|
$store->delete("ticket:{$ticket}");
|
|
194
467
|
return false;
|
|
195
468
|
}
|
|
469
|
+
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
470
|
+
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
471
|
+
if (!empty($zkpProof)) {
|
|
472
|
+
$zkpParts = explode(':', $zkpProof);
|
|
473
|
+
if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
|
|
474
|
+
if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
196
481
|
|
|
197
482
|
if ($ip === $originalIp) {
|
|
198
483
|
return true;
|
|
@@ -221,7 +506,7 @@ class ChallengeUtils
|
|
|
221
506
|
return false;
|
|
222
507
|
}
|
|
223
508
|
|
|
224
|
-
$expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", self::getPowSecret());
|
|
509
|
+
$expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", !empty($secret) ? $secret : self::getPowSecret());
|
|
225
510
|
|
|
226
511
|
return hash_equals($expectedSig, $sig);
|
|
227
512
|
}
|
|
@@ -467,6 +752,9 @@ class ChallengeUtils
|
|
|
467
752
|
$sizeMb = $challengeDetails['sizeMb'];
|
|
468
753
|
$queries = $challengeDetails['queries'];
|
|
469
754
|
$path = $challengeDetails['path'];
|
|
755
|
+
$peerId = $challengeDetails['peerId'] ?? '';
|
|
756
|
+
$peerBlockIdx = $challengeDetails['peerBlockIdx'] ?? -1;
|
|
757
|
+
$nodeId = $nonce;
|
|
470
758
|
|
|
471
759
|
$solverCode = self::getPowSolverCode();
|
|
472
760
|
$queriesJson = json_encode($queries);
|
|
@@ -478,16 +766,63 @@ class ChallengeUtils
|
|
|
478
766
|
const clientSecret = "{$clientSecret}";
|
|
479
767
|
const queries = {$queriesJson};
|
|
480
768
|
const sizeMb = {$sizeMb};
|
|
769
|
+
const nodeId = "{$nodeId}";
|
|
770
|
+
const peerId = "{$peerId}";
|
|
771
|
+
const peerBlockIdx = {$peerBlockIdx};
|
|
481
772
|
|
|
482
773
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
483
774
|
await new Promise(r => setTimeout(r, 10));
|
|
484
775
|
|
|
485
776
|
try {
|
|
486
777
|
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
487
|
-
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
488
|
-
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
|
|
489
778
|
|
|
490
|
-
|
|
779
|
+
// Enregistrement coopératif
|
|
780
|
+
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
|
|
781
|
+
|
|
782
|
+
// Écoute des requêtes entrantes de nos pairs suspects
|
|
783
|
+
setInterval(async () => {
|
|
784
|
+
try {
|
|
785
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId);
|
|
786
|
+
const data = await res.json();
|
|
787
|
+
if (data.requests && data.requests.length > 0) {
|
|
788
|
+
for (const req of data.requests) {
|
|
789
|
+
document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
|
|
790
|
+
const blockData = await window.readSpaceBlock(req.block_idx);
|
|
791
|
+
await fetch(window.location.pathname + "?coop_op=respond_block&node_id=" + nodeId + "&requester_id=" + req.requester_id + "&req_id=" + req.req_id + "&block_data=" + encodeURIComponent(blockData));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} catch (e) {
|
|
795
|
+
console.error("Cooperative polling error", e);
|
|
796
|
+
}
|
|
797
|
+
}, 1000);
|
|
798
|
+
|
|
799
|
+
// Téléchargement du bloc du pair si configuré
|
|
800
|
+
let peerBlock = "";
|
|
801
|
+
if (peerId && peerBlockIdx !== -1) {
|
|
802
|
+
document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
|
|
803
|
+
const reqId = Math.random().toString(36).substring(2);
|
|
804
|
+
await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
|
|
805
|
+
|
|
806
|
+
let attempts = 0;
|
|
807
|
+
while (attempts < 15) {
|
|
808
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
|
|
809
|
+
const data = await res.json();
|
|
810
|
+
if (data.status === 'ready') {
|
|
811
|
+
peerBlock = data.block_data;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
815
|
+
attempts++;
|
|
816
|
+
}
|
|
817
|
+
if (!peerBlock) {
|
|
818
|
+
document.getElementById('loader').innerText = '⚠️ Peer de sous-réseau injoignable. Validation solo...';
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
document.getElementById('loader').innerText = '⚙️ Génération de la Preuve d\\'Espace...';
|
|
823
|
+
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
|
|
824
|
+
|
|
825
|
+
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
|
|
491
826
|
} catch(e) {
|
|
492
827
|
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
493
828
|
}
|
|
@@ -41,6 +41,8 @@ class SecurityProfiles
|
|
|
41
41
|
'subnetScore' => 0.5, // Pénalise les sous-réseaux IP avec une activité suspecte agrégée
|
|
42
42
|
'botnetClusterScore' => 0.6, // NOUVEAU: Poids pour le clustering botnet
|
|
43
43
|
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
44
|
+
'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
|
|
45
|
+
'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
|
|
44
46
|
|
|
45
47
|
],
|
|
46
48
|
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
|
|
@@ -87,6 +89,8 @@ class SecurityProfiles
|
|
|
87
89
|
'subnetScore' => 0.7, // Poids plus élevé en mode strict
|
|
88
90
|
'botnetClusterScore' => 0.8, // NOUVEAU: Poids pour le clustering botnet
|
|
89
91
|
'tcpAnomalyScore' => 1.0, // NEW: Anomalie de pile TCP/IP
|
|
92
|
+
'quicAnomalyScore' => 1.0, // NEW: Anomalie QUIC
|
|
93
|
+
'renderingAnomalyScore' => 1.0, // NEW: Anomalie de rendu
|
|
90
94
|
|
|
91
95
|
],
|
|
92
96
|
'thresholds' => ['low' => 10, 'medium' => 35, 'high' => 65, 'block' => 90],
|
|
@@ -133,6 +137,7 @@ class SecurityProfiles
|
|
|
133
137
|
'subnetScore' => 0.8, // Très important pour les API pour détecter les botnets
|
|
134
138
|
'botnetClusterScore' => 0.7, // NOUVEAU: Poids pour le clustering botnet
|
|
135
139
|
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
140
|
+
'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
|
|
136
141
|
|
|
137
142
|
],
|
|
138
143
|
'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
|
|
@@ -181,6 +186,8 @@ class SecurityProfiles
|
|
|
181
186
|
'subnetScore' => 0.4, // Utile contre le spam de commentaires coordonné
|
|
182
187
|
'botnetClusterScore' => 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
183
188
|
'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
|
|
189
|
+
'quicAnomalyScore' => 0.5, // NEW: Anomalie QUIC
|
|
190
|
+
'renderingAnomalyScore' => 0.5, // NEW: Anomalie de rendu
|
|
184
191
|
|
|
185
192
|
],
|
|
186
193
|
'thresholds' => ['low' => 25, 'medium' => 55, 'high' => 80, 'block' => 95],
|
|
@@ -228,6 +235,8 @@ class SecurityProfiles
|
|
|
228
235
|
'subnetScore' => 0.9, // Crucial contre les attaques de scalping distribuées
|
|
229
236
|
'botnetClusterScore' => 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
230
237
|
'tcpAnomalyScore' => 0.9, // NEW: Anomalie de pile TCP/IP
|
|
238
|
+
'quicAnomalyScore' => 0.9, // NEW: Anomalie QUIC
|
|
239
|
+
'renderingAnomalyScore' => 0.9, // NEW: Anomalie de rendu
|
|
231
240
|
|
|
232
241
|
],
|
|
233
242
|
'thresholds' => ['low' => 15, 'medium' => 40, 'high' => 70, 'block' => 90],
|
|
@@ -30,6 +30,29 @@
|
|
|
30
30
|
public function __construct(array $securityConfig)
|
|
31
31
|
{
|
|
32
32
|
$this->isProduction = ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production';
|
|
33
|
+
|
|
34
|
+
// Dynamically bind Ed25519 keys if passed via config
|
|
35
|
+
if (isset($securityConfig['ed25519_private_key'])) {
|
|
36
|
+
$_ENV['ED25519_PRIVATE_KEY'] = $securityConfig['ed25519_private_key'];
|
|
37
|
+
}
|
|
38
|
+
if (isset($securityConfig['ed25519_public_key'])) {
|
|
39
|
+
$_ENV['ED25519_PUBLIC_KEY'] = $securityConfig['ed25519_public_key'];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Auto-load optimized config if autotuning savePath is specified
|
|
43
|
+
if (isset($securityConfig['autotuning']['savePath'])) {
|
|
44
|
+
$savePath = $securityConfig['autotuning']['savePath'];
|
|
45
|
+
if (file_exists($savePath)) {
|
|
46
|
+
try {
|
|
47
|
+
$savedConfig = json_decode(file_get_contents($savePath), true);
|
|
48
|
+
if (json_last_error() === JSON_ERROR_NONE && is_array($savedConfig)) {
|
|
49
|
+
$securityConfig = SecurityProfiles::deepMerge($securityConfig, $savedConfig);
|
|
50
|
+
}
|
|
51
|
+
} catch (\Throwable $e) {
|
|
52
|
+
error_log("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
33
56
|
$this->securityConfig = $securityConfig;
|
|
34
57
|
$this->verbose = $securityConfig['verbose'] ?? false;
|
|
35
58
|
$this->allowlist = $this->buildAllowlist();
|
|
@@ -38,6 +61,19 @@
|
|
|
38
61
|
$this->validateConfig($securityConfig);
|
|
39
62
|
}
|
|
40
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
|
|
66
|
+
* sans nécessiter de redémarrage.
|
|
67
|
+
*
|
|
68
|
+
* @param array $newConfig La nouvelle configuration (partielle ou complète).
|
|
69
|
+
*/
|
|
70
|
+
public function updateConfig(array $newConfig): void
|
|
71
|
+
{
|
|
72
|
+
$this->validateConfig($newConfig);
|
|
73
|
+
$this->securityConfig = SecurityProfiles::deepMerge($this->securityConfig, $newConfig);
|
|
74
|
+
$this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
|
|
75
|
+
}
|
|
76
|
+
|
|
41
77
|
private function validateConfig(array $config): void
|
|
42
78
|
{
|
|
43
79
|
if (empty($config)) {
|
|
@@ -50,7 +86,8 @@
|
|
|
50
86
|
'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
|
|
51
87
|
'honeypot', 'threatIntel', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger', 'probationaryTtl',
|
|
52
88
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
53
|
-
'similarityThreshold', 'summary', 'description'
|
|
89
|
+
'similarityThreshold', 'summary', 'description',
|
|
90
|
+
'ed25519_private_key', 'ed25519_public_key'
|
|
54
91
|
];
|
|
55
92
|
|
|
56
93
|
if (empty($config['weights'])) {
|
|
@@ -443,6 +480,12 @@
|
|
|
443
480
|
// Score d'anomalie de pile TCP/IP
|
|
444
481
|
$tcpAnomaly = RequestUtils::getTcpAnomalyScore($context);
|
|
445
482
|
|
|
483
|
+
// Score d'anomalie de flux QUIC/HTTP3
|
|
484
|
+
$quicAnomaly = RequestUtils::getQuicAnomalyScore($context);
|
|
485
|
+
|
|
486
|
+
// Score d'anomalie de rendu d'affichage (V-Sync)
|
|
487
|
+
$renderingAnomaly = RequestUtils::getRenderingAnomalyScore($context);
|
|
488
|
+
|
|
446
489
|
// Assemblage du vecteur de suspicion final
|
|
447
490
|
$suspicionVector = array_merge($suspicionVector, [
|
|
448
491
|
'inconsistencyScore' => $inconsistencyScore,
|
|
@@ -462,6 +505,8 @@
|
|
|
462
505
|
'subnetScore' => $subnetScore['subnetScore'],
|
|
463
506
|
'botnetClusterScore' => $botnetCluster['botnetClusterScore'],
|
|
464
507
|
'tcpAnomalyScore' => $tcpAnomaly['tcpAnomalyScore'],
|
|
508
|
+
'quicAnomalyScore' => $quicAnomaly['quicAnomalyScore'],
|
|
509
|
+
'renderingAnomalyScore' => $renderingAnomaly['renderingAnomalyScore'],
|
|
465
510
|
]);
|
|
466
511
|
|
|
467
512
|
// Sauvegarder l'état mis à jour de l'appareil dans le store
|
|
@@ -589,6 +634,16 @@
|
|
|
589
634
|
// Initialiser le vecteur de suspicion pour éviter les erreurs de type.
|
|
590
635
|
$suspicionVector = [];
|
|
591
636
|
|
|
637
|
+
$coopOp = $context->query['coop_op'] ?? null;
|
|
638
|
+
if ($coopOp) {
|
|
639
|
+
$result = ChallengeUtils::handleCooperativeRequest($context->query);
|
|
640
|
+
return [
|
|
641
|
+
'action' => 'challenge',
|
|
642
|
+
'status' => 200,
|
|
643
|
+
'body' => $result
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
|
|
592
647
|
$this->log('Processing request', ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
593
648
|
|
|
594
649
|
// Parse GraphQL query if applicable
|
|
@@ -737,7 +792,11 @@
|
|
|
737
792
|
// 3. Vérifier un ticket existant
|
|
738
793
|
$hasValidTicket = false;
|
|
739
794
|
$powCookie = $context->cookies['pow_clearance'] ?? null;
|
|
740
|
-
|
|
795
|
+
$zkpProof = $context->getHeader('x-zkp-proof') ?? $context->query['pow_zkp'] ?? '';
|
|
796
|
+
$deviceId = $context->cookies['device_id'] ?? '';
|
|
797
|
+
$currentDeviceHash = RequestUtils::getCompositeDeviceHash($context);
|
|
798
|
+
$allowRoaming = $this->securityConfig['allowCrossNetworkRoaming'] ?? false;
|
|
799
|
+
if (ChallengeUtils::isTicketValid($context->clientIp, $powCookie, $deviceId, $currentDeviceHash, $allowRoaming, $zkpProof)) {
|
|
741
800
|
$hasValidTicket = true;
|
|
742
801
|
MetricsManager::incrementCounter('tickets_valid_total');
|
|
743
802
|
// On ne retourne pas tout de suite pour permettre le re-challenge
|
|
@@ -38,6 +38,7 @@ class RequestContext
|
|
|
38
38
|
public ?string $ja4s = null;
|
|
39
39
|
public ?string $ja4h = null;
|
|
40
40
|
public ?string $http2Fingerprint = null;
|
|
41
|
+
public ?string $quicFingerprint = null;
|
|
41
42
|
public ?string $tcpFingerprint = null;
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -77,6 +78,7 @@ class RequestContext
|
|
|
77
78
|
$this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
|
|
78
79
|
$this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
|
|
79
80
|
$this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
|
|
81
|
+
$this->quicFingerprint = $this->headers['x-quic-fp'] ?? null;
|
|
80
82
|
$this->tlsSessionId = $this->headers['x-tls-session-id'] ?? $this->headers['x-ssl-session-id'] ?? null;
|
|
81
83
|
}
|
|
82
84
|
/**
|