@anonympins/fingerprint 0.5.4 → 0.6.0

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