@anonympins/fingerprint 0.3.8 → 0.4.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +331 -256
  2. package/README.md +62 -53
  3. package/composer.json +38 -38
  4. package/index.js +4 -4
  5. package/package.json +103 -103
  6. package/phpunit.xml +20 -20
  7. package/public/fp.js +1 -1
  8. package/public/fp.wasm +0 -0
  9. package/src/js/build-client.js +1 -1
  10. package/src/js/fingerprint.client.js +2 -0
  11. package/src/js/fingerprint.js +4429 -4381
  12. package/src/js/mongodb-store.js +79 -79
  13. package/src/js/pow.solver.inline.js +31 -0
  14. package/src/js/pow.solver.js +31 -0
  15. package/src/js/tests/fingerprint.builder.test.js +79 -0
  16. package/src/js/tests/fingerprint.client.init.test.js +120 -0
  17. package/src/js/tests/fingerprint.client.test.js +105 -0
  18. package/src/js/tests/fingerprint.engine.test.js +371 -0
  19. package/src/js/tests/fingerprint.isMalicious.test.js +117 -0
  20. package/src/js/tests/fingerprint.test.js +2319 -0
  21. package/src/js/tests/ip-reputation.test.js +132 -0
  22. package/src/js/tests/ja3AnomalyDetector.test.js +135 -0
  23. package/src/js/tests/library.test.js +96 -0
  24. package/src/js/tests/metrics.test.js +104 -0
  25. package/src/js/tests/pow.solver.test.js +198 -0
  26. package/src/js/tests/problem-manager.test.js +323 -0
  27. package/src/js/tests/stores.test.js +118 -0
  28. package/src/php/Challenge/ChallengeUtils.php +361 -361
  29. package/src/php/Config/SecurityProfiles.php +271 -266
  30. package/src/php/FingerprintBuilder.php +185 -185
  31. package/src/php/FingerprintClient.php +131 -131
  32. package/src/php/FingerprintEngine.php +1006 -1006
  33. package/src/php/Ja3AnomalyDetector.php +227 -227
  34. package/src/php/Optimization/FunctionRegistry.php +62 -62
  35. package/src/php/Optimization/Optimization.php +255 -255
  36. package/src/php/Optimization/OptimizationOperators.php +304 -304
  37. package/src/php/Store/InMemoryStore.php +66 -66
  38. package/src/php/Store/MongoDbStore.php +104 -104
  39. package/src/php/Store/RedisStore.php +53 -53
  40. package/src/php/Tests/ChallengeUtilsTest.php +81 -81
  41. package/src/php/Tests/FingerprintBuilderTest.php +57 -57
  42. package/src/php/Tests/FingerprintClientTest.php +71 -0
  43. package/src/php/Tests/FingerprintEngineTest.php +299 -299
  44. package/src/php/Tests/IpReputationTest.php +156 -156
  45. package/src/php/Tests/Ja3AnomalyDetectorTest.php +179 -179
  46. package/src/php/Tests/MetricsTest.php +45 -45
  47. package/src/php/Tests/PowTest.php +39 -39
  48. package/src/php/Tests/ProblemManagerTest.php +296 -296
  49. package/src/php/Tests/RequestUtilsTest.php +253 -145
  50. package/src/php/Tests/TLSClientHelloParserTest.php +118 -0
  51. package/src/php/Tests/problems.config.json +8 -8
  52. package/src/php/Utils/BigInt.php +144 -144
  53. package/src/php/Utils/Logger.php +29 -29
  54. package/src/php/Utils/MaliciousPatterns.php +58 -58
  55. package/src/php/Utils/MetricsManager.php +166 -166
  56. package/src/php/Utils/RequestUtils.php +1169 -1169
  57. package/src/php/Utils/TLSClientHelloParser.php +117 -0
  58. package/src/php/bin/auto-tune.php +117 -117
@@ -1,146 +1,254 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\RequestContext;
8
- use Anonympins\Fingerprint\Utils\RequestUtils;
9
- use PHPUnit\Framework\TestCase;
10
-
11
- class RequestUtilsTest extends TestCase
12
- {
13
- private function createRequestContext(array $overrides = []): RequestContext
14
- {
15
- $defaults = [
16
- 'clientIp' => '127.0.0.1',
17
- 'path' => '/',
18
- 'headers' => ['user-agent' => 'Test UA'],
19
- 'query' => [],
20
- 'body' => null,
21
- 'cookies' => [],
22
- 'httpVersion' => '1.1',
23
- ];
24
- $params = array_merge($defaults, $overrides);
25
- return new RequestContext(
26
- $params['clientIp'], $params['path'], $params['headers'],
27
- $params['query'], $params['body'], $params['cookies'], $params['httpVersion']
28
- );
29
- }
30
-
31
- public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
32
- {
33
- $context = $this->createRequestContext([
34
- 'headers' => ['x-behavior-metrics' => json_encode([])]
35
- ]);
36
- $result = RequestUtils::getClickVarianceScore($context);
37
- $this->assertEquals(0.0, $result['clickVarianceScore']);
38
- }
39
-
40
- public function testGetClickVarianceScoreReturnsZeroForInsufficientClicks(): void
41
- {
42
- $metrics = [
43
- 'clicksHistory' => [
44
- ['x' => 10, 'y' => 10, 'targetId' => 'hash1'],
45
- ['x' => 11, 'y' => 11, 'targetId' => 'hash1'],
46
- ['x' => 100, 'y' => 100, 'targetId' => 'hash2']
47
- ]
48
- ];
49
- $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
50
- $result = RequestUtils::getClickVarianceScore($context);
51
- $this->assertEquals(0.0, $result['clickVarianceScore']);
52
- }
53
-
54
- public function testGetClickVarianceScoreReturnsHighScoreForLowVariance(): void
55
- {
56
- $metrics = [
57
- 'clicksHistory' => [
58
- ['x' => 100, 'y' => 100, 'targetId' => 'hash1'],
59
- ['x' => 100.1, 'y' => 100.2, 'targetId' => 'hash1'],
60
- ['x' => 99.9, 'y' => 99.8, 'targetId' => 'hash1']
61
- ]
62
- ];
63
- $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
64
- $result = RequestUtils::getClickVarianceScore($context);
65
- $this->assertGreaterThan(90, $result['clickVarianceScore']);
66
- }
67
-
68
- public function testGetClickVarianceScoreReturnsLowScoreForHighVariance(): void
69
- {
70
- $metrics = [
71
- 'clicksHistory' => [
72
- ['x' => 105, 'y' => 110, 'targetId' => 'hash1'],
73
- ['x' => 98, 'y' => 102, 'targetId' => 'hash1'],
74
- ['x' => 112, 'y' => 95, 'targetId' => 'hash1']
75
- ]
76
- ];
77
- $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
78
- $result = RequestUtils::getClickVarianceScore($context);
79
- $this->assertEquals(0.0, $result['clickVarianceScore']);
80
- }
81
-
82
- public function testSanitizeTrafficDataFiltersSybilAttacks(): void
83
- {
84
- $trafficData = [];
85
- // Ajout de 100 logs provenant d'un attaquant (Sybil)
86
- for ($i = 0; $i < 100; $i++) {
87
- $trafficData[] = ['deviceId' => 'attacker_device', 'type' => 'trap_triggered'];
88
- }
89
- // Ajout de 10 logs d'utilisateurs légitimes distincts
90
- for ($i = 0; $i < 10; $i++) {
91
- $trafficData[] = ['deviceId' => "legit_device_{$i}", 'type' => 'challenge_solved'];
92
- }
93
-
94
- $sanitized = RequestUtils::sanitizeTrafficData($trafficData);
95
-
96
- $attackerLogs = array_filter($sanitized, fn($log) => $log['deviceId'] === 'attacker_device');
97
-
98
- // Total de 110 logs. 2% de 110 est 2.2 -> max(3, 2) = 3 logs maximum autorisés pour l'attaquant.
99
- $this->assertLessThanOrEqual(3, count($attackerLogs));
100
- }
101
-
102
- public function testChallengePayloadSigningAndVerification(): void
103
- {
104
- $secret = 'test-fallback-dev-secret-32-chars-minimum';
105
- $clientIp = '203.0.113.42';
106
- $payload = [
107
- 'clientSecret' => 'some_client_secret',
108
- 'cpuTarget' => '00000000ffffffff',
109
- 'fingerprint' => 'os:hash|gpu:hash2',
110
- 'memDifficulty' => '16',
111
- 'originalPath' => '/submit',
112
- ];
113
-
114
- // 1. Cas nominal : Signature et vérification réussies
115
- $signature = RequestUtils::signChallengePayload($secret, $payload, $clientIp);
116
- $this->assertNotEmpty($signature);
117
-
118
- $payloadWithSig = $payload;
119
- $payloadWithSig['signature'] = $signature;
120
-
121
- $isValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, $clientIp);
122
- $this->assertTrue($isValid, "La signature valide doit être acceptée.");
123
-
124
- // 2. Détection de modification (tampering) sur cpuTarget
125
- $tamperedPayload = $payloadWithSig;
126
- $tamperedPayload['cpuTarget'] = 'ffffffffffffffff'; // Tentative de baisse de la difficulté
127
-
128
- $isTamperedValid = RequestUtils::verifyChallengePayload($secret, $tamperedPayload, $clientIp);
129
- $this->assertFalse($isTamperedValid, "Un payload modifié doit être rejeté.");
130
-
131
- // 3. Détection de modification sur le fingerprint
132
- $tamperedFpPayload = $payloadWithSig;
133
- $tamperedFpPayload['fingerprint'] = 'os:another_hash|gpu:hash2';
134
-
135
- $isTamperedFpValid = RequestUtils::verifyChallengePayload($secret, $tamperedFpPayload, $clientIp);
136
- $this->assertFalse($isTamperedFpValid, "Un fingerprint modifié doit être rejeté.");
137
-
138
- // 4. Détection d'usurpation d'adresse IP (IP mismatch)
139
- $isIpMismatchValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, '198.51.100.1');
140
- $this->assertFalse($isIpMismatchValid, "Le payload ne doit pas être valide pour une autre adresse IP.");
141
-
142
- // 5. Absence de signature
143
- $isMissingSigValid = RequestUtils::verifyChallengePayload($secret, $payload, $clientIp);
144
- $this->assertFalse($isMissingSigValid, "Un payload sans signature doit être rejeté.");
145
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\FingerprintBuilder;
8
+ use Anonympins\Fingerprint\RequestContext;
9
+ use Anonympins\Fingerprint\Utils\RequestUtils;
10
+ use PHPUnit\Framework\TestCase;
11
+
12
+ class RequestUtilsTest extends TestCase
13
+ {
14
+ private function createRequestContext(array $overrides = []): RequestContext
15
+ {
16
+ $defaults = [
17
+ 'clientIp' => '127.0.0.1',
18
+ 'path' => '/',
19
+ 'headers' => ['user-agent' => 'Test UA'],
20
+ 'query' => [],
21
+ 'body' => null,
22
+ 'cookies' => [],
23
+ 'httpVersion' => '1.1',
24
+ ];
25
+ $params = array_merge($defaults, $overrides);
26
+ return new RequestContext(
27
+ $params['clientIp'], $params['path'], $params['headers'],
28
+ $params['query'], $params['body'], $params['cookies'], $params['httpVersion']
29
+ );
30
+ }
31
+
32
+ public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
33
+ {
34
+ $context = $this->createRequestContext([
35
+ 'headers' => ['x-behavior-metrics' => json_encode([])]
36
+ ]);
37
+ $result = RequestUtils::getClickVarianceScore($context);
38
+ $this->assertEquals(0.0, $result['clickVarianceScore']);
39
+ }
40
+
41
+ public function testGetClickVarianceScoreReturnsZeroForInsufficientClicks(): void
42
+ {
43
+ $metrics = [
44
+ 'clicksHistory' => [
45
+ ['x' => 10, 'y' => 10, 'targetId' => 'hash1'],
46
+ ['x' => 11, 'y' => 11, 'targetId' => 'hash1'],
47
+ ['x' => 100, 'y' => 100, 'targetId' => 'hash2']
48
+ ]
49
+ ];
50
+ $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
51
+ $result = RequestUtils::getClickVarianceScore($context);
52
+ $this->assertEquals(0.0, $result['clickVarianceScore']);
53
+ }
54
+
55
+ public function testGetClickVarianceScoreReturnsHighScoreForLowVariance(): void
56
+ {
57
+ $metrics = [
58
+ 'clicksHistory' => [
59
+ ['x' => 100, 'y' => 100, 'targetId' => 'hash1'],
60
+ ['x' => 100.1, 'y' => 100.2, 'targetId' => 'hash1'],
61
+ ['x' => 99.9, 'y' => 99.8, 'targetId' => 'hash1']
62
+ ]
63
+ ];
64
+ $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
65
+ $result = RequestUtils::getClickVarianceScore($context);
66
+ $this->assertGreaterThan(90, $result['clickVarianceScore']);
67
+ }
68
+
69
+ public function testGetClickVarianceScoreReturnsLowScoreForHighVariance(): void
70
+ {
71
+ $metrics = [
72
+ 'clicksHistory' => [
73
+ ['x' => 105, 'y' => 110, 'targetId' => 'hash1'],
74
+ ['x' => 98, 'y' => 102, 'targetId' => 'hash1'],
75
+ ['x' => 112, 'y' => 95, 'targetId' => 'hash1']
76
+ ]
77
+ ];
78
+ $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
79
+ $result = RequestUtils::getClickVarianceScore($context);
80
+ $this->assertEquals(0.0, $result['clickVarianceScore']);
81
+ }
82
+
83
+ public function testSanitizeTrafficDataFiltersSybilAttacks(): void
84
+ {
85
+ $trafficData = [];
86
+ // Ajout de 100 logs provenant d'un attaquant (Sybil)
87
+ for ($i = 0; $i < 100; $i++) {
88
+ $trafficData[] = ['deviceId' => 'attacker_device', 'type' => 'trap_triggered'];
89
+ }
90
+ // Ajout de 10 logs d'utilisateurs légitimes distincts
91
+ for ($i = 0; $i < 10; $i++) {
92
+ $trafficData[] = ['deviceId' => "legit_device_{$i}", 'type' => 'challenge_solved'];
93
+ }
94
+
95
+ $sanitized = RequestUtils::sanitizeTrafficData($trafficData);
96
+
97
+ $attackerLogs = array_filter($sanitized, fn($log) => $log['deviceId'] === 'attacker_device');
98
+
99
+ // Total de 110 logs. 2% de 110 est 2.2 -> max(3, 2) = 3 logs maximum autorisés pour l'attaquant.
100
+ $this->assertLessThanOrEqual(3, count($attackerLogs));
101
+ }
102
+
103
+ public function testChallengePayloadSigningAndVerification(): void
104
+ {
105
+ $secret = 'test-fallback-dev-secret-32-chars-minimum';
106
+ $clientIp = '203.0.113.42';
107
+ $payload = [
108
+ 'clientSecret' => 'some_client_secret',
109
+ 'cpuTarget' => '00000000ffffffff',
110
+ 'fingerprint' => 'os:hash|gpu:hash2',
111
+ 'memDifficulty' => '16',
112
+ 'originalPath' => '/submit',
113
+ ];
114
+
115
+ // 1. Cas nominal : Signature et vérification réussies
116
+ $signature = RequestUtils::signChallengePayload($secret, $payload, $clientIp);
117
+ $this->assertNotEmpty($signature);
118
+
119
+ $payloadWithSig = $payload;
120
+ $payloadWithSig['signature'] = $signature;
121
+
122
+ $isValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, $clientIp);
123
+ $this->assertTrue($isValid, "La signature valide doit être acceptée.");
124
+
125
+ // 2. Détection de modification (tampering) sur cpuTarget
126
+ $tamperedPayload = $payloadWithSig;
127
+ $tamperedPayload['cpuTarget'] = 'ffffffffffffffff'; // Tentative de baisse de la difficulté
128
+
129
+ $isTamperedValid = RequestUtils::verifyChallengePayload($secret, $tamperedPayload, $clientIp);
130
+ $this->assertFalse($isTamperedValid, "Un payload modifié doit être rejeté.");
131
+
132
+ // 3. Détection de modification sur le fingerprint
133
+ $tamperedFpPayload = $payloadWithSig;
134
+ $tamperedFpPayload['fingerprint'] = 'os:another_hash|gpu:hash2';
135
+
136
+ $isTamperedFpValid = RequestUtils::verifyChallengePayload($secret, $tamperedFpPayload, $clientIp);
137
+ $this->assertFalse($isTamperedFpValid, "Un fingerprint modifié doit être rejeté.");
138
+
139
+ // 4. Détection d'usurpation d'adresse IP (IP mismatch)
140
+ $isIpMismatchValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, '198.51.100.1');
141
+ $this->assertFalse($isIpMismatchValid, "Le payload ne doit pas être valide pour une autre adresse IP.");
142
+
143
+ // 5. Absence de signature
144
+ $isMissingSigValid = RequestUtils::verifyChallengePayload($secret, $payload, $clientIp);
145
+ $this->assertFalse($isMissingSigValid, "Un payload sans signature doit être rejeté.");
146
+ }
147
+
148
+ public function testGetBehaviorScoreReturnsProperScores(): void
149
+ {
150
+ $contextEmpty = $this->createRequestContext();
151
+ $scoreEmpty = RequestUtils::getBehaviorScore($contextEmpty);
152
+ $this->assertEquals(0.0, $scoreEmpty['behaviorScore']);
153
+
154
+ $metricsHoneypot = ['honeypotInteraction' => true];
155
+ $contextHoneypot = $this->createRequestContext([
156
+ 'headers' => ['x-behavior-metrics' => json_encode($metricsHoneypot)]
157
+ ]);
158
+ $scoreHoneypot = RequestUtils::getBehaviorScore($contextHoneypot);
159
+ $this->assertEquals(100.0, $scoreHoneypot['behaviorScore']);
160
+
161
+ $metricsNoActivity = ['honeypotInteraction' => false, 'mouseMovementsHistory' => [], 'keystrokeLatency' => 0];
162
+ $contextNoActivity = $this->createRequestContext([
163
+ 'headers' => ['x-behavior-metrics' => json_encode($metricsNoActivity)]
164
+ ]);
165
+ $scoreNoActivity = RequestUtils::getBehaviorScore($contextNoActivity);
166
+ $this->assertEquals(40.0, $scoreNoActivity['behaviorScore']);
167
+ }
168
+
169
+ public function testGetTimeInconsistencyScore(): void
170
+ {
171
+ $requestTimestamp = time() * 1000;
172
+
173
+ $metricsNormal = ['clientTimestamp' => $requestTimestamp - 100];
174
+ $contextNormal = $this->createRequestContext([
175
+ 'headers' => ['x-behavior-metrics' => json_encode($metricsNormal)]
176
+ ]);
177
+ $contextNormal->requestTimestamp = $requestTimestamp;
178
+ $scoreNormal = RequestUtils::getTimeInconsistencyScore($contextNormal);
179
+ $this->assertEquals(0.0, $scoreNormal['timeInconsistencyScore']);
180
+
181
+ $metricsReplay = ['clientTimestamp' => $requestTimestamp - 10000];
182
+ $contextReplay = $this->createRequestContext([
183
+ 'headers' => ['x-behavior-metrics' => json_encode($metricsReplay)]
184
+ ]);
185
+ $contextReplay->requestTimestamp = $requestTimestamp;
186
+ $scoreReplay = RequestUtils::getTimeInconsistencyScore($contextReplay);
187
+ $this->assertGreaterThan(0.0, $scoreReplay['timeInconsistencyScore']);
188
+ }
189
+
190
+ public function testGetCrossLayerInconsistencyOSMismatch(): void
191
+ {
192
+ $windowsHash = FingerprintBuilder::cyrb53("Windows");
193
+ $context = $this->createRequestContext([
194
+ 'headers' => [
195
+ 'x-device-fingerprint' => "os:{$windowsHash}",
196
+ 'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15'
197
+ ]
198
+ ]);
199
+
200
+ $score = RequestUtils::getCrossLayerInconsistency($context);
201
+ $this->assertEquals(50.0, $score['crossLayerInconsistencyScore']);
202
+ }
203
+
204
+ public function testGetBotScoreDetections(): void
205
+ {
206
+ $contextBot = $this->createRequestContext([
207
+ 'headers' => ['x-device-fingerprint' => 'ua:123|bot:true']
208
+ ]);
209
+ $scoreBot = RequestUtils::getBotScore($contextBot);
210
+ $this->assertEquals(100.0, $scoreBot['botScore']);
211
+
212
+ $contextCdp = $this->createRequestContext([
213
+ 'headers' => ['x-device-fingerprint' => 'ua:123|cdp:true']
214
+ ]);
215
+ $scoreCdp = RequestUtils::getBotScore($contextCdp);
216
+ $this->assertEquals(100.0, $scoreCdp['botScore']);
217
+
218
+ $contextClean = $this->createRequestContext([
219
+ 'headers' => ['x-device-fingerprint' => 'ua:123|os:456']
220
+ ]);
221
+ $scoreClean = RequestUtils::getBotScore($contextClean);
222
+ $this->assertEquals(0.0, $scoreClean['botScore']);
223
+ }
224
+
225
+ public function testGetHeaderAnomaliesFirefoxTE(): void
226
+ {
227
+ $contextFirefoxNoTe = $this->createRequestContext([
228
+ 'headers' => [
229
+ 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
230
+ 'accept-language' => 'en-US,en;q=0.9',
231
+ ]
232
+ ]);
233
+ $scoreFirefoxNoTe = RequestUtils::getHeaderAnomalies($contextFirefoxNoTe);
234
+ $this->assertEquals(30.0, $scoreFirefoxNoTe['headerAnomalyScore']);
235
+ }
236
+
237
+ public function testGetBehavioralIndicatorsRotationAndHistory(): void
238
+ {
239
+ $deviceData = [
240
+ 'ips' => ['127.0.0.1'],
241
+ 'lastFpHash' => 'cvs:original-canvas|gpu:original-gpu',
242
+ 'lastChangeTimestamp' => (time() * 1000) - 500,
243
+ 'rapidChangeCount' => 1
244
+ ];
245
+
246
+ $context = $this->createRequestContext([
247
+ 'headers' => ['x-device-fingerprint' => 'cvs:new-canvas|gpu:new-gpu']
248
+ ]);
249
+
250
+ $indicators = RequestUtils::getBehavioralIndicators($context, $deviceData);
251
+ $this->assertEquals(2, $deviceData['rapidChangeCount']);
252
+ $this->assertGreaterThan(0, $indicators['rotationScore']);
253
+ }
146
254
  }
@@ -0,0 +1,118 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\Utils\TLSClientHelloParser;
8
+ use PHPUnit\Framework\TestCase;
9
+
10
+ /**
11
+ * Tests unitaires pour le décodeur binaire TLS Client Hello.
12
+ */
13
+ class TLSClientHelloParserTest extends TestCase
14
+ {
15
+ /**
16
+ * Helper pour construire un paquet TLS Client Hello binaire valide et réaliste.
17
+ *
18
+ * @param int $sslVersion Version TLS (ex: 0x0303 pour TLS 1.2)
19
+ * @param array<int> $ciphers Liste des Cipher Suites
20
+ * @param array<array{type: int, data: string}> $extensions Liste des extensions
21
+ * @return string Paquet binaire brut
22
+ */
23
+ private function buildMockClientHello(int $sslVersion = 0x0303, array $ciphers = [4865, 2570], array $extensions = []): string
24
+ {
25
+ $recordType = pack('C', 0x16); // Handshake Record (22)
26
+ $recordVersion = pack('n', 0x0301); // TLS 1.0 record layer version
27
+ $recordLength = pack('n', 120); // Dummy length
28
+
29
+ $handshakeType = pack('C', 0x01); // Client Hello (1)
30
+ $handshakeLength = pack('C3', 0, 0, 110); // Dummy length
31
+
32
+ $clientVersion = pack('n', $sslVersion);
33
+ $random = str_repeat("\x00", 32); // Random de 32 octets
34
+ $sessionId = pack('C', 0x00); // Session ID vide (longueur 0)
35
+
36
+ // Construction des Cipher Suites
37
+ $ciphersBinary = '';
38
+ foreach ($ciphers as $cipher) {
39
+ $ciphersBinary .= pack('n', $cipher);
40
+ }
41
+ $ciphersPayload = pack('n', strlen($ciphersBinary)) . $ciphersBinary;
42
+
43
+ // Compression Methods (Standard: 1 méthode, valeur 0x00 = null)
44
+ $compression = pack('C2', 1, 0);
45
+
46
+ // Construction des Extensions
47
+ $extensionsBinary = '';
48
+ foreach ($extensions as $ext) {
49
+ $extensionsBinary .= pack('n', $ext['type']) . pack('n', strlen($ext['data'])) . $ext['data'];
50
+ }
51
+ $extensionsPayload = pack('n', strlen($extensionsBinary)) . $extensionsBinary;
52
+
53
+ return $recordType . $recordVersion . $recordLength . $handshakeType . $handshakeLength . $clientVersion . $random . $sessionId . $ciphersPayload . $compression . $extensionsPayload;
54
+ }
55
+
56
+ public function testParseValidClientHelloWithGreaseAndExtensions(): void
57
+ {
58
+ // On injecte l'extension 10 (Supported Groups / Curves)
59
+ // Data: 4 octets de longueur de liste + 29 (X25519) + 2570 (GREASE)
60
+ $extCurves = [
61
+ 'type' => 10,
62
+ 'data' => pack('n', 4) . pack('n2', 29, 2570)
63
+ ];
64
+
65
+ // On injecte l'extension 11 (EC Point Formats)
66
+ // Data: 1 octet de longueur de liste + 0 (uncompressed)
67
+ $extPoints = [
68
+ 'type' => 11,
69
+ 'data' => pack('C2', 1, 0)
70
+ ];
71
+
72
+ // On build le paquet binaire de test
73
+ $binary = $this->buildMockClientHello(
74
+ 0x0303, // SSL Version: 771 (TLS 1.2)
75
+ [4865, 2570], // Ciphers (2570 est une valeur GREASE)
76
+ [$extCurves, $extPoints]
77
+ );
78
+
79
+ $result = TLSClientHelloParser::parse($binary);
80
+
81
+ $this->assertNotNull($result);
82
+
83
+ // Vérifications des filtrages GREASE et de la construction de la chaîne JA3
84
+ // Chaîne JA3 attendue: "Version,Ciphers,Extensions,Curves,Points"
85
+ // - Version: 771
86
+ // - Ciphers: 4865 (2570 a été nettoyé car GREASE)
87
+ // - Extensions: 10-11
88
+ // - Curves: 29 (2570 a été nettoyé car GREASE)
89
+ // - Points: 0
90
+ $expectedJa3String = '771,4865,10-11,29,0';
91
+ $this->assertSame($expectedJa3String, $result['ja3_string']);
92
+ $this->assertSame(md5($expectedJa3String), $result['ja3_hash']);
93
+ }
94
+
95
+ public function testParseReturnsNullWhenPacketTooShort(): void
96
+ {
97
+ $shortBinary = str_repeat("\x16", 40);
98
+ $this->assertNull(TLSClientHelloParser::parse($shortBinary));
99
+ }
100
+
101
+ public function testParseReturnsNullOnInvalidRecordType(): void
102
+ {
103
+ // 0x17 au lieu de 0x16 (Application data au lieu de Handshake)
104
+ $invalidRecord = $this->buildMockClientHello();
105
+ $invalidRecord[0] = chr(0x17);
106
+
107
+ $this->assertNull(TLSClientHelloParser::parse($invalidRecord));
108
+ }
109
+
110
+ public function testParseReturnsNullOnInvalidHandshakeType(): void
111
+ {
112
+ // 0x02 au lieu de 0x01 (Server Hello au lieu de Client Hello)
113
+ $invalidHandshake = $this->buildMockClientHello();
114
+ $invalidHandshake[5] = chr(0x02);
115
+
116
+ $this->assertNull(TLSClientHelloParser::parse($invalidHandshake));
117
+ }
118
+ }
@@ -1,9 +1,9 @@
1
- [
2
- {
3
- "id": "problem-1",
4
- "workUnit": {
5
- "type": "simulated_annealing_iterations",
6
- "baseIterations": 10000
7
- }
8
- }
1
+ [
2
+ {
3
+ "id": "problem-1",
4
+ "workUnit": {
5
+ "type": "simulated_annealing_iterations",
6
+ "baseIterations": 10000
7
+ }
8
+ }
9
9
  ]