@anonympins/fingerprint 0.4.2 → 0.4.3
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 +17 -0
- package/README.md +66 -60
- package/package.json +1 -1
- package/src/js/fingerprint.js +10 -2
- package/src/js/pow.solver.js +531 -497
- package/src/js/problem-manager.js +24 -0
- package/src/js/tests/fingerprint.test.js +2351 -2319
- package/src/js/tests/pow.solver.test.js +233 -197
- package/src/js/tests/problem-manager.test.js +358 -322
- package/src/php/FingerprintEngine.php +10 -2
- package/src/php/Optimization/FunctionRegistry.php +63 -62
- package/src/php/Optimization/OptimizationOperators.php +401 -304
- package/src/php/ProblemManager.php +29 -0
- package/src/php/Tests/FingerprintEngineTest.php +330 -299
- package/src/php/Tests/ProblemManagerTest.php +376 -296
- package/src/php/Tests/RequestUtilsTest.php +256 -253
- package/src/php/Tests/problems.config.json +3 -3
- package/src/php/Utils/RequestUtils.php +1 -1
|
@@ -1,254 +1,257 @@
|
|
|
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' => [
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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' => [
|
|
248
|
+
'x-device-fingerprint' => 'cvs:new-canvas|gpu:new-gpu',
|
|
249
|
+
'user-agent' => 'Test UA'
|
|
250
|
+
]
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
$indicators = RequestUtils::getBehavioralIndicators($context, $deviceData);
|
|
254
|
+
$this->assertEquals(2, $deviceData['rapidChangeCount']);
|
|
255
|
+
$this->assertGreaterThan(0, $indicators['rotationScore']);
|
|
256
|
+
}
|
|
254
257
|
}
|
|
@@ -555,7 +555,7 @@ class RequestUtils
|
|
|
555
555
|
*/
|
|
556
556
|
private static function extractStablePart(string $fpString): string
|
|
557
557
|
{
|
|
558
|
-
$stableKeys = ['
|
|
558
|
+
$stableKeys = ['ua', 'ja3', 'ja4', 'h2', 'tcp'];
|
|
559
559
|
$parts = explode('|', $fpString);
|
|
560
560
|
$stableParts = [];
|
|
561
561
|
foreach ($parts as $part) {
|