@anonympins/fingerprint 0.7.2 → 0.7.4

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +36 -9
  2. package/README.md +1 -1
  3. package/package.json +109 -107
  4. package/public/fingerprint-anti-bot.zip +0 -0
  5. package/src/js/fingerprint.client.js +252 -45
  6. package/src/js/fingerprint.client.obfuscated.js +1 -1
  7. package/src/js/fingerprint.js +265 -185
  8. package/src/js/fingerprint.utils.js +32 -5
  9. package/src/js/fingerprint.worker.js +81 -0
  10. package/src/js/library.js +6 -2
  11. package/src/js/pow.solver.js +2 -3
  12. package/src/js/problem-manager.js +113 -113
  13. package/src/js/tests/fingerprint.client.init.test.js +30 -0
  14. package/src/js/tests/fingerprint.test.js +136 -4
  15. package/src/js/tests/threatIntel.test.js +76 -0
  16. package/src/php/AutoTuner.php +36 -18
  17. package/src/php/Challenge/ChallengeUtils.php +121 -240
  18. package/src/php/DirectFingerprint.php +16 -4
  19. package/src/php/FingerprintClient.php +17 -19
  20. package/src/php/FingerprintEngine.php +77 -39
  21. package/src/php/Ja3AnomalyDetector.php +20 -12
  22. package/src/php/Optimization/Optimization.php +5 -1
  23. package/src/php/Optimization/OptimizationOperators.php +5 -1
  24. package/src/php/Optimization/ProblemInitializers.php +16 -4
  25. package/src/php/ProblemManager.php +20 -14
  26. package/src/php/Tests/ChallengeUtilsTest.php +469 -453
  27. package/src/php/Tests/FingerprintEngineTest.php +6 -1
  28. package/src/php/Tests/RequestUtilsTest.php +1 -1
  29. package/src/php/Tests/ThreatIntelTest.php +64 -0
  30. package/src/php/Utils/Env.php +51 -49
  31. package/src/php/Utils/MaliciousPatterns.php +74 -74
  32. package/src/php/Utils/RequestUtils.php +231 -132
  33. package/src/php/WordPress/WpDbStore.php +4 -0
  34. package/src/php/WordPress/{fingerprint-wordpress.php → fingerprint-anti-bot.php} +131 -126
  35. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  36. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
  37. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  38. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
  39. package/src/php/WordPress/package.php +6 -3
  40. package/src/php/WordPress/readme.txt +36 -0
  41. package/public/fingerprint-wordpress.zip +0 -0
@@ -1,454 +1,470 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\Challenge\ChallengeUtils;
8
- use Anonympins\Fingerprint\Store\InMemoryStore;
9
- use Anonympins\Fingerprint\Store\StoreManager;
10
- use Anonympins\Fingerprint\Utils\BigInt;
11
- use Anonympins\Fingerprint\Utils\Env;
12
- use PHPUnit\Framework\TestCase;
13
-
14
- class ChallengeUtilsTest extends TestCase
15
- {
16
- public function testGpuPowDeterministicHashing(): void
17
- {
18
- $seed = 'test-seed-determinism';
19
- $f1 = ChallengeUtils::hashSeedToFloat($seed);
20
- $f2 = ChallengeUtils::hashSeedToFloat($seed);
21
- $f3 = ChallengeUtils::hashSeedToFloat('different-seed');
22
-
23
- $this->assertEquals($f1, $f2, 'Seed hashing must be deterministic.');
24
- $this->assertGreaterThanOrEqual(0, $f1);
25
- $this->assertLessThan(1, $f1);
26
- $this->assertNotEquals($f1, $f3);
27
- }
28
-
29
- public function testGpuPowVerificationRejectsTamperedSolution(): void
30
- {
31
- $seed = 'php-verification-seed';
32
- $iterations = 100;
33
-
34
- // Generate correct solutions mimicking the GPU solver trajectory
35
- $numericSeed = ChallengeUtils::hashSeedToFloat($seed);
36
- $r = 3.9999;
37
- $solutions = [];
38
- $fround = function (float $value): float {
39
- return unpack('f', pack('f', $value))[1];
40
- };
41
-
42
- for ($idx = 0; $idx < 64; $idx++) {
43
- $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
44
- $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
45
- $rFloat = $fround($r);
46
- for ($i = 0; $i < $iterations; $i++) {
47
- $x = $fround($rFloat * $x * $fround(1.0 - $x));
48
- }
49
- $solutions[] = number_format($x, 6, '.', '');
50
- }
51
- $validSolution = implode(',', $solutions);
52
-
53
- $this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $validSolution));
54
-
55
- // Tamper with value on verified sample index 12
56
- $tampered = $solutions;
57
- $sampleIndices = ChallengeUtils::deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
58
- $tamperedIndex = $sampleIndices[0];
59
- $tampered[$tamperedIndex] = (string)((float)$tampered[$tamperedIndex] + 0.0002); // Out of tolerance
60
- $this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tampered)));
61
- }
62
-
63
- public function testGpuPowVerification(): void
64
- {
65
- $seed = 'php-gpu-test-seed';
66
- $iterations = 50;
67
-
68
- $hash = 0;
69
- for ($i = 0; $i < strlen($seed); $i++) {
70
- $hash = (($hash << 5) - $hash + ord($seed[$i])) & 0xffffffff;
71
- if ($hash & 0x80000000) {
72
- $hash = $hash - 0x100000000;
73
- }
74
- }
75
- $numericSeed = abs($hash % 1000000) / 1000000;
76
- $r = 3.9999;
77
- $solutions = [];
78
- $fround = function (float $value): float {
79
- return unpack('f', pack('f', $value))[1];
80
- };
81
- for ($idx = 0; $idx < 64; $idx++) {
82
- $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
83
- $rFloat = $fround($r);
84
- for ($i = 0; $i < $iterations; $i++) {
85
- $x = $fround($rFloat * $x * $fround(1.0 - $x));
86
- }
87
- $solutions[] = number_format($x, 6, '.', '');
88
- }
89
- $solutionString = implode(',', $solutions);
90
- $this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $solutionString));
91
- $sampleIndices = ChallengeUtils::deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
92
- $tamperedIndex = $sampleIndices[0];
93
- $tamperedSolutions = $solutions;
94
- $tamperedSolutions[$tamperedIndex] = (string)((float)$tamperedSolutions[$tamperedIndex] + 0.1);
95
- $this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tamperedSolutions)));
96
- }
97
-
98
- protected function setUp(): void
99
- {
100
- // Définir une clé secrète pour les tests
101
- $store = new InMemoryStore();
102
- StoreManager::configureStore($store);
103
- Env::set('POW_SECRET', 'test-secret-key-that-is-long-enough-for-hmac');
104
- }
105
-
106
- public function testZkpProofValidation(): void
107
- {
108
- $fp = 'cvs:12345|gpu:67890';
109
- $p = BigInt::fromHex('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f');
110
- $g = new BigInt(2);
111
-
112
- $x = BigInt::fromHex(hash('sha256', $fp))->mod($p);
113
- $y = $g->modPow($x, $p);
114
- $v = new BigInt(987654321);
115
- $t = $g->modPow($v, $p);
116
-
117
- $cStr = (string)$g . (string)$y . (string)$t;
118
- $c = BigInt::fromHex(hash('sha256', $cStr))->mod($p);
119
- $s = $v->add($c->mul($x))->mod($p->sub(new BigInt(1)));
120
-
121
- $zkpProof = $y->toHex() . ':' . $t->toHex() . ':' . $s->toHex();
122
- $this->assertTrue(ChallengeUtils::verifyZkpProof($y->toHex(), $t->toHex(), $s->toHex()));
123
-
124
- $ticket = ChallengeUtils::generateStatelessTicket([
125
- 'expiry' => (time() + 3600) * 1000,
126
- 'originalIp' => '127.0.0.1',
127
- 'deviceId' => 'dev-zkp',
128
- 'deviceHash' => 'zkp:' . $y->toHex()
129
- ]);
130
-
131
- $this->assertTrue(ChallengeUtils::isTicketValid('127.0.0.1', $ticket, 'dev-zkp', '', false, '', $zkpProof));
132
- }
133
-
134
- public function testIsTicketValid(): void
135
- {
136
- $ip = '127.0.0.1';
137
- $expiry = (int)floor(microtime(true) * 1000) + 3600000; // 1 heure
138
- $signature = hash_hmac('sha256', "{$ip}:{$expiry}", $_ENV['POW_SECRET']);
139
- $validTicket = "{$expiry}:{$signature}";
140
-
141
- $this->assertTrue(ChallengeUtils::isTicketValid($ip, $validTicket));
142
- $this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $validTicket), "Le ticket ne doit pas être valide pour une autre IP.");
143
-
144
- $expiredExpiry = (int)floor(microtime(true) * 1000) - 1000;
145
- $expiredSignature = hash_hmac('sha256', "{$ip}:{$expiredExpiry}", $_ENV['POW_SECRET']);
146
- $expiredTicket = "{$expiredExpiry}:{$expiredSignature}";
147
- $this->assertFalse(ChallengeUtils::isTicketValid($ip, $expiredTicket), "Un ticket expiré doit être invalide.");
148
-
149
- $this->assertFalse(ChallengeUtils::isTicketValid($ip, 'invalid-ticket-format'), "Un format de ticket invalide doit être rejeté.");
150
- }
151
-
152
- public function testCpuTargetCalculation(): void
153
- {
154
- $config = ['cpu' => ['minDifficultyBits' => 8, 'maxDifficultyBits' => 24]];
155
-
156
- // Low suspicion -> minimum difficulty
157
- $targetLow = ChallengeUtils::calculateCpuTarget(0.0, $config);
158
- $expectedTargetLow = (new BigInt(1))->shiftLeft(256 - 8);
159
- $this->assertEquals(0, BigInt::fromHex($targetLow)->compareTo($expectedTargetLow), "Target for 0.0 suspicion should correspond to 8 bits of difficulty.");
160
-
161
- // High suspicion -> maximum difficulty
162
- $targetHigh = ChallengeUtils::calculateCpuTarget(1.0, $config);
163
- $expectedTargetHigh = (new BigInt(1))->shiftLeft(256 - 24);
164
- $this->assertEquals(0, BigInt::fromHex($targetHigh)->compareTo($expectedTargetHigh), "Target for 1.0 suspicion should correspond to 24 bits of difficulty.");
165
-
166
- // Medium suspicion -> intermediate difficulty
167
- $targetMid = ChallengeUtils::calculateCpuTarget(0.5, $config);
168
- $expectedBits = 8 + 0.5 * (24 - 8); // 16 bits
169
- $expectedTargetMid = (new BigInt(1))->shiftLeft(256 - (int)$expectedBits);
170
- $this->assertEquals(0, BigInt::fromHex($targetMid)->compareTo($expectedTargetMid), "Target for 0.5 suspicion should correspond to 16 bits of difficulty.");
171
- }
172
-
173
- public function testVerifyCpuTargetPoW(): void
174
- {
175
- $ip = '127.0.0.1';
176
- $nonce = 'test-nonce';
177
- $fingerprint = 'ua:test-fp';
178
- $clientSecret = 'test-secret';
179
- $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $fingerprint);
180
-
181
- // Cible facile pour un test rapide (ex: 4 zéros hexadécimaux -> 16 bits)
182
- $targetHex = '0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
183
- $challengeContext = ['cpuTarget' => $targetHex, 'baseBlock' => $baseBlock];
184
-
185
- // Trouver une solution valide
186
- $solution = 0;
187
- while (true) {
188
- $hash = hash('sha256', $baseBlock . $solution);
189
- if (strcmp($hash, $targetHex) < 0) break;
190
- $solution++;
191
- }
192
-
193
- $ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket($ip, 3600, $nonce, (string)$solution, $challengeContext);
194
- $this->assertNotNull($ticket, "Un ticket valide aurait dû être généré.");
195
- $this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket));
196
- }
197
-
198
- public function testEd25519TicketValidation(): void
199
- {
200
- if (!extension_loaded('openssl')) {
201
- $this->markTestSkipped('openssl extension is not loaded.');
202
- }
203
-
204
- if (!defined('OPENSSL_KEYTYPE_ED25519')) {
205
- $this->markTestSkipped('Ed25519 is not supported or constant OPENSSL_KEYTYPE_ED25519 is undefined in this PHP/OpenSSL environment.');
206
- }
207
-
208
- $pkey = @openssl_pkey_new(["private_key_type" => constant('OPENSSL_KEYTYPE_ED25519')]);
209
- if (!$pkey) {
210
- $this->markTestSkipped('Ed25519 is not supported in this PHP/OpenSSL environment.');
211
- }
212
-
213
- openssl_pkey_export($pkey, $privateKeyPem);
214
- $details = openssl_pkey_get_details($pkey);
215
- $publicKeyPem = $details['key'];
216
-
217
- // Test if openssl_sign supports null algorithm for Ed25519 (PHP 8.0.0 bug)
218
- $testSig = '';
219
- $testPriv = @openssl_pkey_get_private($privateKeyPem);
220
- try {
221
- if (!$testPriv || !@openssl_sign('test', $testSig, $testPriv, null)) {
222
- $this->markTestSkipped('Ed25519 signing is not fully supported or buggy in this PHP/OpenSSL environment.');
223
- }
224
- } catch (\TypeError $e) {
225
- $this->markTestSkipped('Ed25519 signing is not supported due to PHP 8.0.0 openssl_sign() null algorithm bug.');
226
- }
227
-
228
- Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
229
- Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
230
-
231
- $ip = '127.0.0.1';
232
- $expiry = (int)floor(microtime(true) * 1000) + 3600000;
233
- $payload = [
234
- 'expiry' => $expiry,
235
- 'originalIp' => $ip,
236
- 'deviceId' => 'device-123',
237
- 'deviceHash' => 'hash-abc'
238
- ];
239
-
240
- $ticket = ChallengeUtils::generateStatelessTicket($payload);
241
- $this->assertStringStartsWith('ed25519.', $ticket);
242
-
243
- $this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket, 'device-123', 'hash-abc'));
244
- $this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $ticket, 'device-123', 'hash-abc'));
245
-
246
- Env::clear('ED25519_PRIVATE_KEY');
247
- Env::clear('ED25519_PUBLIC_KEY');
248
- }
249
-
250
- public function testCooperativePoSpaceWorkflow(): void
251
- {
252
- $clientIp = '127.0.0.1';
253
- $nodeIdA = 'node-a';
254
- $seedA = 'seed-a';
255
-
256
- // Configurer les secrets dans le store pour passer la validation de signature
257
- $store = StoreManager::getStore();
258
- $store->set("secret:node-a", ['clientSecret' => 'secret-a'], 300);
259
- $store->set("secret:node-b", ['clientSecret' => 'secret-b'], 300);
260
-
261
- // 1. Enregistrement du nœud A (pair)
262
- ChallengeUtils::registerCooperativeNode($clientIp, $nodeIdA, $seedA);
263
-
264
- // 2. Recherche de pair pour le nœud B (dans le même sous-réseau)
265
- $peer = ChallengeUtils::findPeerInSubnet($clientIp, 'node-b');
266
- $this->assertNotNull($peer);
267
- $this->assertEquals($nodeIdA, $peer['nodeId']);
268
- $this->assertEquals($seedA, $peer['seed']);
269
-
270
- // 3. Demande de bloc du nœud B vers le nœud A
271
- $paramsReq = [
272
- 'coop_op' => 'request_peer_block',
273
- 'node_id' => 'node-b',
274
- 'peer_id' => 'node-a',
275
- 'block_idx' => '42',
276
- 'req_id' => 'req-123',
277
- 'coop_sig' => hash('sha256', 'secret-b:request_peer_block:node-b:node-a:42:req-123')
278
- ];
279
- $resReq = ChallengeUtils::handleCooperativeRequest($paramsReq);
280
- $this->assertEquals('queued', $resReq['status']);
281
-
282
- // 4. Récupération de la demande par le nœud A
283
- $paramsPoll = [
284
- 'coop_op' => 'poll_requests',
285
- 'node_id' => 'node-a',
286
- 'coop_sig' => hash('sha256', 'secret-a:poll_requests:node-a')
287
- ];
288
- $resPoll = ChallengeUtils::handleCooperativeRequest($paramsPoll);
289
- $this->assertCount(1, $resPoll['requests']);
290
- $this->assertEquals('req-123', $resPoll['requests'][0]['req_id']);
291
- $this->assertEquals(42, $resPoll['requests'][0]['block_idx']);
292
-
293
- // 5. Réponse avec la donnée de bloc par le nœud A
294
- $paramsResp = [
295
- 'coop_op' => 'respond_block',
296
- 'node_id' => 'node-a',
297
- 'requester_id' => 'node-b',
298
- 'req_id' => 'req-123',
299
- 'block_data' => 'dummy-block-data-xyz',
300
- 'coop_sig' => hash('sha256', 'secret-a:respond_block:node-a:node-b:req-123:dummy-block-data-xyz')
301
- ];
302
- $resResp = ChallengeUtils::handleCooperativeRequest($paramsResp);
303
- $this->assertEquals('delivered', $resResp['status']);
304
- }
305
-
306
- public function testCooperativeRequestHandlingWithSignatures(): void
307
- {
308
- $store = StoreManager::getStore();
309
-
310
- // 1. Setup: Define test data
311
- $nodeIdA = 'node-alpha';
312
- $nodeIdB = 'node-beta';
313
- $clientSecretA = 'secret-alpha-for-node-a';
314
- $clientSecretB = 'secret-beta-for-node-b';
315
- $seedA = 'seed-for-alpha';
316
- $seedB = 'seed-for-beta';
317
- $blockIdx = 42;
318
- $reqId = 'req-12345';
319
- $blockData = '0102030405060708090a0b0c0d0e0f'; // Hex string representing block data
320
-
321
- // Mock $_SERVER['REMOTE_ADDR'] for register operation
322
- $_SERVER['REMOTE_ADDR'] = '192.168.1.100';
323
-
324
- // Store challenge contexts for both nodes (crucial for signature verification)
325
- $store->set("secret:{$nodeIdA}", ['clientSecret' => $clientSecretA], 300);
326
- $store->set("secret:{$nodeIdB}", ['clientSecret' => $clientSecretB], 300);
327
-
328
- // Helper to generate valid signatures (mimics client-side logic)
329
- $generateCoopSig = function (string $op, string $nodeId, string $clientSecret, ...$extraParams) {
330
- $msg = "{$clientSecret}:{$op}:{$nodeId}";
331
- foreach ($extraParams as $param) {
332
- $msg .= ":{$param}";
333
- }
334
- return hash('sha256', $msg);
335
- };
336
-
337
- // --- Test 1: Successful Registration (Node A) ---
338
- $sigA_register = $generateCoopSig('register', $nodeIdA, $clientSecretA, $seedA);
339
- $paramsRegisterA = [
340
- 'coop_op' => 'register',
341
- 'node_id' => $nodeIdA,
342
- 'seed' => $seedA,
343
- 'coop_sig' => $sigA_register
344
- ];
345
- $resRegisterA = ChallengeUtils::handleCooperativeRequest($paramsRegisterA);
346
- $this->assertEquals(['status' => 'registered'], $resRegisterA, 'Node A should register successfully.');
347
-
348
- // --- Test 2: Successful Registration (Node B) ---
349
- $sigB_register = $generateCoopSig('register', $nodeIdB, $clientSecretB, $seedB);
350
- $paramsRegisterB = [
351
- 'coop_op' => 'register',
352
- 'node_id' => $nodeIdB,
353
- 'seed' => $seedB,
354
- 'coop_sig' => $sigB_register
355
- ];
356
- $resRegisterB = ChallengeUtils::handleCooperativeRequest($paramsRegisterB);
357
- $this->assertEquals(['status' => 'registered'], $resRegisterB, 'Node B should register successfully.');
358
-
359
- // --- Test 3: Failed Registration (Invalid Signature) ---
360
- $paramsInvalidSig = $paramsRegisterA;
361
- $paramsInvalidSig['coop_sig'] = 'invalid-signature';
362
- $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
363
- $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Registration with invalid signature should fail.');
364
-
365
- // --- Test 4: Successful Block Request (Node A requests from Node B) ---
366
- $sigA_requestBlock = $generateCoopSig('request_peer_block', $nodeIdA, $clientSecretA, $nodeIdB, (string)$blockIdx, $reqId);
367
- $paramsRequestBlock = [
368
- 'coop_op' => 'request_peer_block',
369
- 'node_id' => $nodeIdA,
370
- 'peer_id' => $nodeIdB,
371
- 'block_idx' => (string)$blockIdx,
372
- 'req_id' => $reqId,
373
- 'coop_sig' => $sigA_requestBlock
374
- ];
375
- $resRequestBlock = ChallengeUtils::handleCooperativeRequest($paramsRequestBlock);
376
- $this->assertEquals(['status' => 'queued'], $resRequestBlock, 'Block request should be queued.');
377
-
378
- // --- Test 5: Failed Block Request (Invalid Signature) ---
379
- $paramsInvalidSig = $paramsRequestBlock;
380
- $paramsInvalidSig['coop_sig'] = 'invalid-signature';
381
- $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
382
- $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Block request with invalid signature should fail.');
383
-
384
- // --- Test 6: Successful Poll Requests (Node B polls for requests) ---
385
- $sigB_pollRequests = $generateCoopSig('poll_requests', $nodeIdB, $clientSecretB);
386
- $paramsPollRequests = [
387
- 'coop_op' => 'poll_requests',
388
- 'node_id' => $nodeIdB,
389
- 'coop_sig' => $sigB_pollRequests
390
- ];
391
- $resPollRequests = ChallengeUtils::handleCooperativeRequest($paramsPollRequests);
392
- $this->assertCount(1, $resPollRequests['requests'], 'Node B should receive 1 request.');
393
- $this->assertEquals($reqId, $resPollRequests['requests'][0]['req_id']);
394
- $this->assertEquals($nodeIdA, $resPollRequests['requests'][0]['requester_id']);
395
- $this->assertEquals($blockIdx, $resPollRequests['requests'][0]['block_idx']);
396
-
397
- // --- Test 7: Failed Poll Requests (Invalid Signature) ---
398
- $paramsInvalidSig = $paramsPollRequests;
399
- $paramsInvalidSig['coop_sig'] = 'invalid-signature';
400
- $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
401
- $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Poll requests with invalid signature should fail.');
402
-
403
- // --- Test 8: Successful Respond Block (Node B responds to Node A) ---
404
- $sigB_respondBlock = $generateCoopSig('respond_block', $nodeIdB, $clientSecretB, $nodeIdA, $reqId, $blockData);
405
- $paramsRespondBlock = [
406
- 'coop_op' => 'respond_block',
407
- 'node_id' => $nodeIdB,
408
- 'requester_id' => $nodeIdA,
409
- 'req_id' => $reqId,
410
- 'block_data' => $blockData,
411
- 'coop_sig' => $sigB_respondBlock
412
- ];
413
- $resRespondBlock = ChallengeUtils::handleCooperativeRequest($paramsRespondBlock);
414
- $this->assertEquals(['status' => 'delivered'], $resRespondBlock, 'Block response should be delivered.');
415
-
416
- // --- Test 9: Failed Respond Block (Invalid Signature) ---
417
- $paramsInvalidSig = $paramsRespondBlock;
418
- $paramsInvalidSig['coop_sig'] = 'invalid-signature';
419
- $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
420
- $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Respond block with invalid signature should fail.');
421
-
422
- // --- Test 10: Successful Poll Response (Node A polls for response) ---
423
- $sigA_pollResponse = $generateCoopSig('poll_response', $nodeIdA, $clientSecretA, $reqId);
424
- $paramsPollResponse = [
425
- 'coop_op' => 'poll_response',
426
- 'node_id' => $nodeIdA,
427
- 'req_id' => $reqId,
428
- 'coop_sig' => $sigA_pollResponse
429
- ];
430
- $resPollResponse = ChallengeUtils::handleCooperativeRequest($paramsPollResponse);
431
- $this->assertEquals(['status' => 'ready', 'block_data' => $blockData], $resPollResponse, 'Node A should receive the block data.');
432
-
433
- // --- Test 11: Failed Poll Response (Invalid Signature) ---
434
- $paramsInvalidSig = $paramsPollResponse;
435
- $paramsInvalidSig['coop_sig'] = 'invalid-signature';
436
- $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
437
- $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Poll response with invalid signature should fail.');
438
-
439
- // --- Test 12: Invalid/Expired node_id ---
440
- $paramsExpiredNode = ['coop_op' => 'register', 'node_id' => 'non-existent-node', 'coop_sig' => 'any-sig'];
441
- $resExpiredNode = ChallengeUtils::handleCooperativeRequest($paramsExpiredNode);
442
- $this->assertEquals(['error' => 'Invalid or expired node_id'], $resExpiredNode, 'Request with non-existent node_id should fail.');
443
-
444
- // --- Test 13: Missing node_id ---
445
- $paramsMissingNodeId = ['coop_op' => 'register', 'coop_sig' => 'any-sig'];
446
- $resMissingNodeId = ChallengeUtils::handleCooperativeRequest($paramsMissingNodeId);
447
- $this->assertEquals(['error' => 'Missing node_id'], $resMissingNodeId, 'Request without node_id should fail.');
448
-
449
- // --- Test 14: Invalid cooperative operation ---
450
- $paramsInvalidOp = ['coop_op' => 'unknown_op', 'node_id' => $nodeIdA, 'coop_sig' => 'any-sig'];
451
- $resInvalidOp = ChallengeUtils::handleCooperativeRequest($paramsInvalidOp);
452
- $this->assertEquals(['error' => 'Invalid cooperative operation'], $resInvalidOp, 'Request with unknown operation should fail.');
453
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\Challenge\ChallengeUtils;
8
+ use Anonympins\Fingerprint\Store\InMemoryStore;
9
+ use Anonympins\Fingerprint\Store\StoreManager;
10
+ use Anonympins\Fingerprint\Utils\BigInt;
11
+ use Anonympins\Fingerprint\Utils\Env;
12
+ use PHPUnit\Framework\TestCase;
13
+
14
+ class ChallengeUtilsTest extends TestCase
15
+ {
16
+ public function testGpuPowDeterministicHashing(): void
17
+ {
18
+ $seed = 'test-seed-determinism';
19
+ $f1 = ChallengeUtils::hashSeedToFloat($seed);
20
+ $f2 = ChallengeUtils::hashSeedToFloat($seed);
21
+ $f3 = ChallengeUtils::hashSeedToFloat('different-seed');
22
+
23
+ $this->assertEquals($f1, $f2, 'Seed hashing must be deterministic.');
24
+ $this->assertGreaterThanOrEqual(0, $f1);
25
+ $this->assertLessThan(1, $f1);
26
+ $this->assertNotEquals($f1, $f3);
27
+ }
28
+
29
+ public function testGpuPowVerificationRejectsTamperedSolution(): void
30
+ {
31
+ $seed = 'php-verification-seed';
32
+ $iterations = 100;
33
+
34
+ // Generate correct solutions mimicking the GPU solver trajectory
35
+ $numericSeed = ChallengeUtils::hashSeedToFloat($seed);
36
+ $r = 3.9999;
37
+ $solutions = [];
38
+ $fround = function (float $value): float {
39
+ return unpack('f', pack('f', $value))[1];
40
+ };
41
+
42
+ for ($idx = 0; $idx < 64; $idx++) {
43
+ $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
44
+ $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
45
+ $rFloat = $fround($r);
46
+ for ($i = 0; $i < $iterations; $i++) {
47
+ $x = $fround($rFloat * $x * $fround(1.0 - $x));
48
+ }
49
+ $solutions[] = number_format($x, 6, '.', '');
50
+ }
51
+ $validSolution = implode(',', $solutions);
52
+
53
+ $this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $validSolution));
54
+
55
+ // Tamper with value on verified sample index 12
56
+ $tampered = $solutions;
57
+ $sampleIndices = ChallengeUtils::deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
58
+ $tamperedIndex = $sampleIndices[0];
59
+ $tampered[$tamperedIndex] = (string)((float)$tampered[$tamperedIndex] + 0.0002); // Out of tolerance
60
+ $this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tampered)));
61
+ }
62
+
63
+ public function testGpuPowVerification(): void
64
+ {
65
+ $seed = 'php-gpu-test-seed';
66
+ $iterations = 50;
67
+
68
+ $hash = 0;
69
+ for ($i = 0; $i < strlen($seed); $i++) {
70
+ $hash = (($hash << 5) - $hash + ord($seed[$i])) & 0xffffffff;
71
+ if ($hash & 0x80000000) {
72
+ $hash = $hash - 0x100000000;
73
+ }
74
+ }
75
+ $numericSeed = abs($hash % 1000000) / 1000000;
76
+ $r = 3.9999;
77
+ $solutions = [];
78
+ $fround = function (float $value): float {
79
+ return unpack('f', pack('f', $value))[1];
80
+ };
81
+ for ($idx = 0; $idx < 64; $idx++) {
82
+ $x = $fround(fmod($numericSeed + $idx * 0.015, 1.0));
83
+ $rFloat = $fround($r);
84
+ for ($i = 0; $i < $iterations; $i++) {
85
+ $x = $fround($rFloat * $x * $fround(1.0 - $x));
86
+ }
87
+ $solutions[] = number_format($x, 6, '.', '');
88
+ }
89
+ $solutionString = implode(',', $solutions);
90
+ $this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $solutionString));
91
+ $sampleIndices = ChallengeUtils::deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
92
+ $tamperedIndex = $sampleIndices[0];
93
+ $tamperedSolutions = $solutions;
94
+ $tamperedSolutions[$tamperedIndex] = (string)((float)$tamperedSolutions[$tamperedIndex] + 0.1);
95
+ $this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tamperedSolutions)));
96
+ }
97
+
98
+ protected function setUp(): void
99
+ {
100
+ // Définir une clé secrète pour les tests
101
+ $store = new InMemoryStore();
102
+ StoreManager::configureStore($store);
103
+ Env::set('POW_SECRET', 'test-secret-key-that-is-long-enough-for-hmac');
104
+ }
105
+
106
+ public function testZkpProofValidation(): void
107
+ {
108
+ $fp = 'cvs:12345|gpu:67890';
109
+ $p = BigInt::fromHex('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f');
110
+ $g = new BigInt(2);
111
+
112
+ $x = BigInt::fromHex(hash('sha256', $fp))->mod($p);
113
+ $y = $g->modPow($x, $p);
114
+ $v = new BigInt(987654321);
115
+ $t = $g->modPow($v, $p);
116
+
117
+ $cStr = (string)$g . (string)$y . (string)$t;
118
+ $c = BigInt::fromHex(hash('sha256', $cStr))->mod($p);
119
+ $s = $v->add($c->mul($x))->mod($p->sub(new BigInt(1)));
120
+
121
+ $zkpProof = $y->toHex() . ':' . $t->toHex() . ':' . $s->toHex();
122
+ $this->assertTrue(ChallengeUtils::verifyZkpProof($y->toHex(), $t->toHex(), $s->toHex()));
123
+
124
+ $ticket = ChallengeUtils::generateStatelessTicket([
125
+ 'expiry' => (time() + 3600) * 1000,
126
+ 'originalIp' => '127.0.0.1',
127
+ 'deviceId' => 'dev-zkp',
128
+ 'deviceHash' => 'zkp:' . $y->toHex()
129
+ ]);
130
+
131
+ $this->assertTrue(ChallengeUtils::isTicketValid('127.0.0.1', $ticket, 'dev-zkp', '', false, '', $zkpProof));
132
+ }
133
+
134
+ public function testIsTicketValid(): void
135
+ {
136
+ $ip = '127.0.0.1';
137
+ $expiry = (int)floor(microtime(true) * 1000) + 3600000; // 1 heure
138
+ $signature = hash_hmac('sha256', "{$ip}:{$expiry}", $_ENV['POW_SECRET']);
139
+ $validTicket = "{$expiry}:{$signature}";
140
+
141
+ $this->assertTrue(ChallengeUtils::isTicketValid($ip, $validTicket));
142
+ $this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $validTicket), "Le ticket ne doit pas être valide pour une autre IP.");
143
+
144
+ $expiredExpiry = (int)floor(microtime(true) * 1000) - 1000;
145
+ $expiredSignature = hash_hmac('sha256', "{$ip}:{$expiredExpiry}", $_ENV['POW_SECRET']);
146
+ $expiredTicket = "{$expiredExpiry}:{$expiredSignature}";
147
+ $this->assertFalse(ChallengeUtils::isTicketValid($ip, $expiredTicket), "Un ticket expiré doit être invalide.");
148
+
149
+ $this->assertFalse(ChallengeUtils::isTicketValid($ip, 'invalid-ticket-format'), "Un format de ticket invalide doit être rejeté.");
150
+ }
151
+
152
+ public function testCheckChallengeRateLimit(): void
153
+ {
154
+ $ip = '192.168.1.100';
155
+
156
+ // 5 premières demandes autorisées avec la capacité par défaut (5.0)
157
+ for ($i = 0; $i < 5; $i++) {
158
+ $this->assertTrue(ChallengeUtils::checkChallengeRateLimit($ip));
159
+ }
160
+
161
+ // La 6e demande consécutive doit être rejetée (tokens épuisés)
162
+ $this->assertFalse(ChallengeUtils::checkChallengeRateLimit($ip));
163
+
164
+ // Une autre adresse IP sur un autre sous-réseau doit disposer de son propre réservoir
165
+ $this->assertTrue(ChallengeUtils::checkChallengeRateLimit('10.0.0.1'));
166
+ }
167
+
168
+ public function testCpuTargetCalculation(): void
169
+ {
170
+ $config = ['cpu' => ['minDifficultyBits' => 8, 'maxDifficultyBits' => 24]];
171
+
172
+ // Low suspicion -> minimum difficulty
173
+ $targetLow = ChallengeUtils::calculateCpuTarget(0.0, $config);
174
+ $expectedTargetLow = (new BigInt(1))->shiftLeft(256 - 8);
175
+ $this->assertEquals(0, BigInt::fromHex($targetLow)->compareTo($expectedTargetLow), "Target for 0.0 suspicion should correspond to 8 bits of difficulty.");
176
+
177
+ // High suspicion -> maximum difficulty
178
+ $targetHigh = ChallengeUtils::calculateCpuTarget(1.0, $config);
179
+ $expectedTargetHigh = (new BigInt(1))->shiftLeft(256 - 24);
180
+ $this->assertEquals(0, BigInt::fromHex($targetHigh)->compareTo($expectedTargetHigh), "Target for 1.0 suspicion should correspond to 24 bits of difficulty.");
181
+
182
+ // Medium suspicion -> intermediate difficulty
183
+ $targetMid = ChallengeUtils::calculateCpuTarget(0.5, $config);
184
+ $expectedBits = 8 + 0.5 * (24 - 8); // 16 bits
185
+ $expectedTargetMid = (new BigInt(1))->shiftLeft(256 - (int)$expectedBits);
186
+ $this->assertEquals(0, BigInt::fromHex($targetMid)->compareTo($expectedTargetMid), "Target for 0.5 suspicion should correspond to 16 bits of difficulty.");
187
+ }
188
+
189
+ public function testVerifyCpuTargetPoW(): void
190
+ {
191
+ $ip = '127.0.0.1';
192
+ $nonce = 'test-nonce';
193
+ $fingerprint = 'ua:test-fp';
194
+ $clientSecret = 'test-secret';
195
+ $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $fingerprint);
196
+
197
+ // Cible facile pour un test rapide (ex: 4 zéros hexadécimaux -> 16 bits)
198
+ $targetHex = '0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
199
+ $challengeContext = ['cpuTarget' => $targetHex, 'baseBlock' => $baseBlock];
200
+
201
+ // Trouver une solution valide
202
+ $solution = 0;
203
+ while (true) {
204
+ $hash = hash('sha256', $baseBlock . $solution);
205
+ if (strcmp($hash, $targetHex) < 0) break;
206
+ $solution++;
207
+ }
208
+
209
+ $ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket($ip, 3600, $nonce, (string)$solution, $challengeContext);
210
+ $this->assertNotNull($ticket, "Un ticket valide aurait dû être généré.");
211
+ $this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket));
212
+ }
213
+
214
+ public function testEd25519TicketValidation(): void
215
+ {
216
+ if (!extension_loaded('openssl')) {
217
+ $this->markTestSkipped('openssl extension is not loaded.');
218
+ }
219
+
220
+ if (!defined('OPENSSL_KEYTYPE_ED25519')) {
221
+ $this->markTestSkipped('Ed25519 is not supported or constant OPENSSL_KEYTYPE_ED25519 is undefined in this PHP/OpenSSL environment.');
222
+ }
223
+
224
+ $pkey = @openssl_pkey_new(["private_key_type" => constant('OPENSSL_KEYTYPE_ED25519')]);
225
+ if (!$pkey) {
226
+ $this->markTestSkipped('Ed25519 is not supported in this PHP/OpenSSL environment.');
227
+ }
228
+
229
+ openssl_pkey_export($pkey, $privateKeyPem);
230
+ $details = openssl_pkey_get_details($pkey);
231
+ $publicKeyPem = $details['key'];
232
+
233
+ // Test if openssl_sign supports null algorithm for Ed25519 (PHP 8.0.0 bug)
234
+ $testSig = '';
235
+ $testPriv = @openssl_pkey_get_private($privateKeyPem);
236
+ try {
237
+ if (!$testPriv || !@openssl_sign('test', $testSig, $testPriv, null)) {
238
+ $this->markTestSkipped('Ed25519 signing is not fully supported or buggy in this PHP/OpenSSL environment.');
239
+ }
240
+ } catch (\TypeError $e) {
241
+ $this->markTestSkipped('Ed25519 signing is not supported due to PHP 8.0.0 openssl_sign() null algorithm bug.');
242
+ }
243
+
244
+ Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
245
+ Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
246
+
247
+ $ip = '127.0.0.1';
248
+ $expiry = (int)floor(microtime(true) * 1000) + 3600000;
249
+ $payload = [
250
+ 'expiry' => $expiry,
251
+ 'originalIp' => $ip,
252
+ 'deviceId' => 'device-123',
253
+ 'deviceHash' => 'hash-abc'
254
+ ];
255
+
256
+ $ticket = ChallengeUtils::generateStatelessTicket($payload);
257
+ $this->assertStringStartsWith('ed25519.', $ticket);
258
+
259
+ $this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket, 'device-123', 'hash-abc'));
260
+ $this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $ticket, 'device-123', 'hash-abc'));
261
+
262
+ Env::clear('ED25519_PRIVATE_KEY');
263
+ Env::clear('ED25519_PUBLIC_KEY');
264
+ }
265
+
266
+ public function testCooperativePoSpaceWorkflow(): void
267
+ {
268
+ $clientIp = '127.0.0.1';
269
+ $nodeIdA = 'node-a';
270
+ $seedA = 'seed-a';
271
+
272
+ // Configurer les secrets dans le store pour passer la validation de signature
273
+ $store = StoreManager::getStore();
274
+ $store->set("secret:node-a", ['clientSecret' => 'secret-a'], 300);
275
+ $store->set("secret:node-b", ['clientSecret' => 'secret-b'], 300);
276
+
277
+ // 1. Enregistrement du nœud A (pair)
278
+ ChallengeUtils::registerCooperativeNode($clientIp, $nodeIdA, $seedA);
279
+
280
+ // 2. Recherche de pair pour le nœud B (dans le même sous-réseau)
281
+ $peer = ChallengeUtils::findPeerInSubnet($clientIp, 'node-b');
282
+ $this->assertNotNull($peer);
283
+ $this->assertEquals($nodeIdA, $peer['nodeId']);
284
+ $this->assertEquals($seedA, $peer['seed']);
285
+
286
+ // 3. Demande de bloc du nœud B vers le nœud A
287
+ $paramsReq = [
288
+ 'coop_op' => 'request_peer_block',
289
+ 'node_id' => 'node-b',
290
+ 'peer_id' => 'node-a',
291
+ 'block_idx' => '42',
292
+ 'req_id' => 'req-123',
293
+ 'coop_sig' => hash('sha256', 'secret-b:request_peer_block:node-b:node-a:42:req-123')
294
+ ];
295
+ $resReq = ChallengeUtils::handleCooperativeRequest($paramsReq);
296
+ $this->assertEquals('queued', $resReq['status']);
297
+
298
+ // 4. Récupération de la demande par le nœud A
299
+ $paramsPoll = [
300
+ 'coop_op' => 'poll_requests',
301
+ 'node_id' => 'node-a',
302
+ 'coop_sig' => hash('sha256', 'secret-a:poll_requests:node-a')
303
+ ];
304
+ $resPoll = ChallengeUtils::handleCooperativeRequest($paramsPoll);
305
+ $this->assertCount(1, $resPoll['requests']);
306
+ $this->assertEquals('req-123', $resPoll['requests'][0]['req_id']);
307
+ $this->assertEquals(42, $resPoll['requests'][0]['block_idx']);
308
+
309
+ // 5. Réponse avec la donnée de bloc par le nœud A
310
+ $paramsResp = [
311
+ 'coop_op' => 'respond_block',
312
+ 'node_id' => 'node-a',
313
+ 'requester_id' => 'node-b',
314
+ 'req_id' => 'req-123',
315
+ 'block_data' => 'dummy-block-data-xyz',
316
+ 'coop_sig' => hash('sha256', 'secret-a:respond_block:node-a:node-b:req-123:dummy-block-data-xyz')
317
+ ];
318
+ $resResp = ChallengeUtils::handleCooperativeRequest($paramsResp);
319
+ $this->assertEquals('delivered', $resResp['status']);
320
+ }
321
+
322
+ public function testCooperativeRequestHandlingWithSignatures(): void
323
+ {
324
+ $store = StoreManager::getStore();
325
+
326
+ // 1. Setup: Define test data
327
+ $nodeIdA = 'node-alpha';
328
+ $nodeIdB = 'node-beta';
329
+ $clientSecretA = 'secret-alpha-for-node-a';
330
+ $clientSecretB = 'secret-beta-for-node-b';
331
+ $seedA = 'seed-for-alpha';
332
+ $seedB = 'seed-for-beta';
333
+ $blockIdx = 42;
334
+ $reqId = 'req-12345';
335
+ $blockData = '0102030405060708090a0b0c0d0e0f'; // Hex string representing block data
336
+
337
+ // Mock $_SERVER['REMOTE_ADDR'] for register operation
338
+ $_SERVER['REMOTE_ADDR'] = '192.168.1.100';
339
+
340
+ // Store challenge contexts for both nodes (crucial for signature verification)
341
+ $store->set("secret:{$nodeIdA}", ['clientSecret' => $clientSecretA], 300);
342
+ $store->set("secret:{$nodeIdB}", ['clientSecret' => $clientSecretB], 300);
343
+
344
+ // Helper to generate valid signatures (mimics client-side logic)
345
+ $generateCoopSig = function (string $op, string $nodeId, string $clientSecret, ...$extraParams) {
346
+ $msg = "{$clientSecret}:{$op}:{$nodeId}";
347
+ foreach ($extraParams as $param) {
348
+ $msg .= ":{$param}";
349
+ }
350
+ return hash('sha256', $msg);
351
+ };
352
+
353
+ // --- Test 1: Successful Registration (Node A) ---
354
+ $sigA_register = $generateCoopSig('register', $nodeIdA, $clientSecretA, $seedA);
355
+ $paramsRegisterA = [
356
+ 'coop_op' => 'register',
357
+ 'node_id' => $nodeIdA,
358
+ 'seed' => $seedA,
359
+ 'coop_sig' => $sigA_register
360
+ ];
361
+ $resRegisterA = ChallengeUtils::handleCooperativeRequest($paramsRegisterA);
362
+ $this->assertEquals(['status' => 'registered'], $resRegisterA, 'Node A should register successfully.');
363
+
364
+ // --- Test 2: Successful Registration (Node B) ---
365
+ $sigB_register = $generateCoopSig('register', $nodeIdB, $clientSecretB, $seedB);
366
+ $paramsRegisterB = [
367
+ 'coop_op' => 'register',
368
+ 'node_id' => $nodeIdB,
369
+ 'seed' => $seedB,
370
+ 'coop_sig' => $sigB_register
371
+ ];
372
+ $resRegisterB = ChallengeUtils::handleCooperativeRequest($paramsRegisterB);
373
+ $this->assertEquals(['status' => 'registered'], $resRegisterB, 'Node B should register successfully.');
374
+
375
+ // --- Test 3: Failed Registration (Invalid Signature) ---
376
+ $paramsInvalidSig = $paramsRegisterA;
377
+ $paramsInvalidSig['coop_sig'] = 'invalid-signature';
378
+ $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
379
+ $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Registration with invalid signature should fail.');
380
+
381
+ // --- Test 4: Successful Block Request (Node A requests from Node B) ---
382
+ $sigA_requestBlock = $generateCoopSig('request_peer_block', $nodeIdA, $clientSecretA, $nodeIdB, (string)$blockIdx, $reqId);
383
+ $paramsRequestBlock = [
384
+ 'coop_op' => 'request_peer_block',
385
+ 'node_id' => $nodeIdA,
386
+ 'peer_id' => $nodeIdB,
387
+ 'block_idx' => (string)$blockIdx,
388
+ 'req_id' => $reqId,
389
+ 'coop_sig' => $sigA_requestBlock
390
+ ];
391
+ $resRequestBlock = ChallengeUtils::handleCooperativeRequest($paramsRequestBlock);
392
+ $this->assertEquals(['status' => 'queued'], $resRequestBlock, 'Block request should be queued.');
393
+
394
+ // --- Test 5: Failed Block Request (Invalid Signature) ---
395
+ $paramsInvalidSig = $paramsRequestBlock;
396
+ $paramsInvalidSig['coop_sig'] = 'invalid-signature';
397
+ $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
398
+ $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Block request with invalid signature should fail.');
399
+
400
+ // --- Test 6: Successful Poll Requests (Node B polls for requests) ---
401
+ $sigB_pollRequests = $generateCoopSig('poll_requests', $nodeIdB, $clientSecretB);
402
+ $paramsPollRequests = [
403
+ 'coop_op' => 'poll_requests',
404
+ 'node_id' => $nodeIdB,
405
+ 'coop_sig' => $sigB_pollRequests
406
+ ];
407
+ $resPollRequests = ChallengeUtils::handleCooperativeRequest($paramsPollRequests);
408
+ $this->assertCount(1, $resPollRequests['requests'], 'Node B should receive 1 request.');
409
+ $this->assertEquals($reqId, $resPollRequests['requests'][0]['req_id']);
410
+ $this->assertEquals($nodeIdA, $resPollRequests['requests'][0]['requester_id']);
411
+ $this->assertEquals($blockIdx, $resPollRequests['requests'][0]['block_idx']);
412
+
413
+ // --- Test 7: Failed Poll Requests (Invalid Signature) ---
414
+ $paramsInvalidSig = $paramsPollRequests;
415
+ $paramsInvalidSig['coop_sig'] = 'invalid-signature';
416
+ $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
417
+ $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Poll requests with invalid signature should fail.');
418
+
419
+ // --- Test 8: Successful Respond Block (Node B responds to Node A) ---
420
+ $sigB_respondBlock = $generateCoopSig('respond_block', $nodeIdB, $clientSecretB, $nodeIdA, $reqId, $blockData);
421
+ $paramsRespondBlock = [
422
+ 'coop_op' => 'respond_block',
423
+ 'node_id' => $nodeIdB,
424
+ 'requester_id' => $nodeIdA,
425
+ 'req_id' => $reqId,
426
+ 'block_data' => $blockData,
427
+ 'coop_sig' => $sigB_respondBlock
428
+ ];
429
+ $resRespondBlock = ChallengeUtils::handleCooperativeRequest($paramsRespondBlock);
430
+ $this->assertEquals(['status' => 'delivered'], $resRespondBlock, 'Block response should be delivered.');
431
+
432
+ // --- Test 9: Failed Respond Block (Invalid Signature) ---
433
+ $paramsInvalidSig = $paramsRespondBlock;
434
+ $paramsInvalidSig['coop_sig'] = 'invalid-signature';
435
+ $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
436
+ $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Respond block with invalid signature should fail.');
437
+
438
+ // --- Test 10: Successful Poll Response (Node A polls for response) ---
439
+ $sigA_pollResponse = $generateCoopSig('poll_response', $nodeIdA, $clientSecretA, $reqId);
440
+ $paramsPollResponse = [
441
+ 'coop_op' => 'poll_response',
442
+ 'node_id' => $nodeIdA,
443
+ 'req_id' => $reqId,
444
+ 'coop_sig' => $sigA_pollResponse
445
+ ];
446
+ $resPollResponse = ChallengeUtils::handleCooperativeRequest($paramsPollResponse);
447
+ $this->assertEquals(['status' => 'ready', 'block_data' => $blockData], $resPollResponse, 'Node A should receive the block data.');
448
+
449
+ // --- Test 11: Failed Poll Response (Invalid Signature) ---
450
+ $paramsInvalidSig = $paramsPollResponse;
451
+ $paramsInvalidSig['coop_sig'] = 'invalid-signature';
452
+ $resInvalidSig = ChallengeUtils::handleCooperativeRequest($paramsInvalidSig);
453
+ $this->assertEquals(['error' => 'Invalid cooperative signature'], $resInvalidSig, 'Poll response with invalid signature should fail.');
454
+
455
+ // --- Test 12: Invalid/Expired node_id ---
456
+ $paramsExpiredNode = ['coop_op' => 'register', 'node_id' => 'non-existent-node', 'coop_sig' => 'any-sig'];
457
+ $resExpiredNode = ChallengeUtils::handleCooperativeRequest($paramsExpiredNode);
458
+ $this->assertEquals(['error' => 'Invalid or expired node_id'], $resExpiredNode, 'Request with non-existent node_id should fail.');
459
+
460
+ // --- Test 13: Missing node_id ---
461
+ $paramsMissingNodeId = ['coop_op' => 'register', 'coop_sig' => 'any-sig'];
462
+ $resMissingNodeId = ChallengeUtils::handleCooperativeRequest($paramsMissingNodeId);
463
+ $this->assertEquals(['error' => 'Missing node_id'], $resMissingNodeId, 'Request without node_id should fail.');
464
+
465
+ // --- Test 14: Invalid cooperative operation ---
466
+ $paramsInvalidOp = ['coop_op' => 'unknown_op', 'node_id' => $nodeIdA, 'coop_sig' => 'any-sig'];
467
+ $resInvalidOp = ChallengeUtils::handleCooperativeRequest($paramsInvalidOp);
468
+ $this->assertEquals(['error' => 'Invalid cooperative operation'], $resInvalidOp, 'Request with unknown operation should fail.');
469
+ }
454
470
  }