@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.
- package/CHANGELOG.md +331 -256
- package/README.md +62 -53
- package/composer.json +38 -38
- package/index.js +4 -4
- package/package.json +103 -103
- package/phpunit.xml +20 -20
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/fingerprint.client.js +2 -0
- package/src/js/fingerprint.js +4429 -4381
- package/src/js/mongodb-store.js +79 -79
- package/src/js/pow.solver.inline.js +31 -0
- package/src/js/pow.solver.js +31 -0
- package/src/js/tests/fingerprint.builder.test.js +79 -0
- package/src/js/tests/fingerprint.client.init.test.js +120 -0
- package/src/js/tests/fingerprint.client.test.js +105 -0
- package/src/js/tests/fingerprint.engine.test.js +371 -0
- package/src/js/tests/fingerprint.isMalicious.test.js +117 -0
- package/src/js/tests/fingerprint.test.js +2319 -0
- package/src/js/tests/ip-reputation.test.js +132 -0
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -0
- package/src/js/tests/library.test.js +96 -0
- package/src/js/tests/metrics.test.js +104 -0
- package/src/js/tests/pow.solver.test.js +198 -0
- package/src/js/tests/problem-manager.test.js +323 -0
- package/src/js/tests/stores.test.js +118 -0
- package/src/php/Challenge/ChallengeUtils.php +361 -361
- package/src/php/Config/SecurityProfiles.php +271 -266
- package/src/php/FingerprintBuilder.php +185 -185
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +1006 -1006
- package/src/php/Ja3AnomalyDetector.php +227 -227
- package/src/php/Optimization/FunctionRegistry.php +62 -62
- package/src/php/Optimization/Optimization.php +255 -255
- package/src/php/Optimization/OptimizationOperators.php +304 -304
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +104 -104
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/ChallengeUtilsTest.php +81 -81
- package/src/php/Tests/FingerprintBuilderTest.php +57 -57
- package/src/php/Tests/FingerprintClientTest.php +71 -0
- package/src/php/Tests/FingerprintEngineTest.php +299 -299
- package/src/php/Tests/IpReputationTest.php +156 -156
- package/src/php/Tests/Ja3AnomalyDetectorTest.php +179 -179
- package/src/php/Tests/MetricsTest.php +45 -45
- package/src/php/Tests/PowTest.php +39 -39
- package/src/php/Tests/ProblemManagerTest.php +296 -296
- package/src/php/Tests/RequestUtilsTest.php +253 -145
- package/src/php/Tests/TLSClientHelloParserTest.php +118 -0
- package/src/php/Tests/problems.config.json +8 -8
- package/src/php/Utils/BigInt.php +144 -144
- package/src/php/Utils/Logger.php +29 -29
- package/src/php/Utils/MaliciousPatterns.php +58 -58
- package/src/php/Utils/MetricsManager.php +166 -166
- package/src/php/Utils/RequestUtils.php +1169 -1169
- package/src/php/Utils/TLSClientHelloParser.php +117 -0
- 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\
|
|
8
|
-
use Anonympins\Fingerprint\
|
|
9
|
-
use
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
$params['
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
$
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
['x' =>
|
|
46
|
-
['x' =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
$
|
|
51
|
-
$
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
['x' => 100
|
|
60
|
-
['x' =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
$
|
|
65
|
-
$
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
['x' =>
|
|
74
|
-
['x' =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
$
|
|
79
|
-
$
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
$
|
|
106
|
-
$
|
|
107
|
-
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
$
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
$payloadWithSig
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
$
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
$tamperedPayload
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
$
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
$tamperedFpPayload
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
$
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
$
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
$
|
|
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
|
]
|