@anonympins/fingerprint 0.3.5 → 0.3.7
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 +41 -1
- package/README.md +1200 -1080
- package/package.json +1 -1
- package/src/js/build-client.js +4 -5
- package/src/js/fingerprint.client.js +2 -2
- package/src/js/fingerprint.js +370 -95
- package/src/js/library.js +1 -1
- package/src/js/mongodb-store.js +79 -52
- package/src/js/optimization.worker.js +2 -2
- package/src/js/problem-manager.js +2 -2
- package/src/php/DirectFingerprint.php +145 -80
- package/src/php/FingerprintEngine.php +19 -3
- package/src/php/ProblemManager.php +1 -1
- package/src/php/RequestContext.php +90 -90
- package/src/php/Store/MongoDbStore.php +105 -0
- package/src/php/Store/RedisStore.php +54 -0
- package/src/php/Tests/ChallengeUtilsTest.php +1 -1
- package/src/php/Tests/FingerprintBuilderTest.php +1 -1
- package/src/php/Tests/FingerprintEngineTest.php +4 -4
- package/src/php/Tests/IpReputationTest.php +4 -4
- package/src/php/Tests/Ja3AnomalyDetectorTest.php +1 -1
- package/src/php/Tests/MetricsTest.php +46 -0
- package/src/php/Tests/PowTest.php +2 -2
- package/src/php/Tests/ProblemManagerTest.php +5 -3
- package/src/php/Tests/RequestUtilsTest.php +66 -1
- package/src/php/Utils/MetricsManager.php +167 -0
- package/src/php/Utils/RequestUtils.php +1142 -972
- package/src/php/bin/auto-tune.php +118 -0
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Représente le contexte d'une requête HTTP, fournissant un accès unifié
|
|
9
|
-
* aux informations nécessaires pour l'analyse de l'empreinte.
|
|
10
|
-
* Cette classe est conçue pour être créée à partir d'un objet de requête
|
|
11
|
-
* standard (ex: PSR-7, Symfony, Laravel).
|
|
12
|
-
*/
|
|
13
|
-
class RequestContext
|
|
14
|
-
{
|
|
15
|
-
public string $clientIp;
|
|
16
|
-
public string $path;
|
|
17
|
-
/** @var array<string, string> */
|
|
18
|
-
public array $headers;
|
|
19
|
-
/** @var array<string, mixed> */
|
|
20
|
-
public array $query;
|
|
21
|
-
/** @var array<string, mixed>|object|null */
|
|
22
|
-
public $body;
|
|
23
|
-
/** @var array<string, string> */
|
|
24
|
-
public array $cookies;
|
|
25
|
-
public ?string $httpVersion;
|
|
26
|
-
public int $requestTimestamp;
|
|
27
|
-
|
|
28
|
-
/** @var ?array{type: string, name: string} */
|
|
29
|
-
public ?array $graphqlOperation = null;
|
|
30
|
-
|
|
31
|
-
/** @var ?array<string, mixed> */
|
|
32
|
-
public ?array $newCookieForResponse = null;
|
|
33
|
-
|
|
34
|
-
// Propriétés spécifiques qui peuvent être fournies par un proxy inverse
|
|
35
|
-
public ?string $ja3;
|
|
36
|
-
public ?string $ja4;
|
|
37
|
-
public ?string $ja4s;
|
|
38
|
-
public ?string $ja4h;
|
|
39
|
-
public ?string $http2Fingerprint;
|
|
40
|
-
public ?string $tcpFingerprint;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param string $clientIp
|
|
44
|
-
* @param string $path
|
|
45
|
-
* @param array<string, string> $headers
|
|
46
|
-
* @param array<string, mixed> $query
|
|
47
|
-
* @param array<string, mixed>|object|null $body
|
|
48
|
-
* @param array<string, string> $cookies
|
|
49
|
-
* @param string|null $httpVersion
|
|
50
|
-
* @param int|null $requestTimestamp
|
|
51
|
-
*/
|
|
52
|
-
public function __construct(
|
|
53
|
-
string $clientIp,
|
|
54
|
-
string $path,
|
|
55
|
-
array $headers,
|
|
56
|
-
array $query,
|
|
57
|
-
$body,
|
|
58
|
-
array $cookies,
|
|
59
|
-
?string $httpVersion,
|
|
60
|
-
?int $requestTimestamp = null
|
|
61
|
-
) {
|
|
62
|
-
$this->clientIp = $clientIp;
|
|
63
|
-
$this->path = $path;
|
|
64
|
-
// Normaliser les en-têtes en minuscules pour un accès cohérent
|
|
65
|
-
$this->headers = array_change_key_case($headers, CASE_LOWER);
|
|
66
|
-
$this->query = $query;
|
|
67
|
-
$this->body = $body;
|
|
68
|
-
$this->cookies = $cookies;
|
|
69
|
-
$this->httpVersion = $httpVersion;
|
|
70
|
-
$this->requestTimestamp = $requestTimestamp ?? (int)(microtime(true) * 1000);
|
|
71
|
-
|
|
72
|
-
// Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
|
|
73
|
-
$this->ja3 = $this->headers['x-ja3-hash'] ?? null;
|
|
74
|
-
$this->ja4 = $this->headers['x-ja4-hash'] ?? null;
|
|
75
|
-
$this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
|
|
76
|
-
$this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
|
|
77
|
-
$this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
|
|
78
|
-
$this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
|
|
82
|
-
*
|
|
83
|
-
* @param string $name Le nom de l'en-tête.
|
|
84
|
-
* @return string|null La valeur de l'en-tête ou null si non trouvé.
|
|
85
|
-
*/
|
|
86
|
-
|
|
87
|
-
public function getHeader(string $name): ?string
|
|
88
|
-
{
|
|
89
|
-
return $this->headers[strtolower($name)] ?? null;
|
|
90
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Représente le contexte d'une requête HTTP, fournissant un accès unifié
|
|
9
|
+
* aux informations nécessaires pour l'analyse de l'empreinte.
|
|
10
|
+
* Cette classe est conçue pour être créée à partir d'un objet de requête
|
|
11
|
+
* standard (ex: PSR-7, Symfony, Laravel).
|
|
12
|
+
*/
|
|
13
|
+
class RequestContext
|
|
14
|
+
{
|
|
15
|
+
public string $clientIp;
|
|
16
|
+
public string $path;
|
|
17
|
+
/** @var array<string, string> */
|
|
18
|
+
public array $headers;
|
|
19
|
+
/** @var array<string, mixed> */
|
|
20
|
+
public array $query;
|
|
21
|
+
/** @var array<string, mixed>|object|null */
|
|
22
|
+
public $body;
|
|
23
|
+
/** @var array<string, string> */
|
|
24
|
+
public array $cookies;
|
|
25
|
+
public ?string $httpVersion;
|
|
26
|
+
public int $requestTimestamp;
|
|
27
|
+
|
|
28
|
+
/** @var ?array{type: string, name: string} */
|
|
29
|
+
public ?array $graphqlOperation = null;
|
|
30
|
+
|
|
31
|
+
/** @var ?array<string, mixed> */
|
|
32
|
+
public ?array $newCookieForResponse = null;
|
|
33
|
+
|
|
34
|
+
// Propriétés spécifiques qui peuvent être fournies par un proxy inverse
|
|
35
|
+
public ?string $ja3;
|
|
36
|
+
public ?string $ja4;
|
|
37
|
+
public ?string $ja4s;
|
|
38
|
+
public ?string $ja4h;
|
|
39
|
+
public ?string $http2Fingerprint;
|
|
40
|
+
public ?string $tcpFingerprint;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param string $clientIp
|
|
44
|
+
* @param string $path
|
|
45
|
+
* @param array<string, string> $headers
|
|
46
|
+
* @param array<string, mixed> $query
|
|
47
|
+
* @param array<string, mixed>|object|null $body
|
|
48
|
+
* @param array<string, string> $cookies
|
|
49
|
+
* @param string|null $httpVersion
|
|
50
|
+
* @param int|null $requestTimestamp
|
|
51
|
+
*/
|
|
52
|
+
public function __construct(
|
|
53
|
+
string $clientIp,
|
|
54
|
+
string $path,
|
|
55
|
+
array $headers,
|
|
56
|
+
array $query,
|
|
57
|
+
$body,
|
|
58
|
+
array $cookies,
|
|
59
|
+
?string $httpVersion,
|
|
60
|
+
?int $requestTimestamp = null
|
|
61
|
+
) {
|
|
62
|
+
$this->clientIp = $clientIp;
|
|
63
|
+
$this->path = $path;
|
|
64
|
+
// Normaliser les en-têtes en minuscules pour un accès cohérent
|
|
65
|
+
$this->headers = array_change_key_case($headers, CASE_LOWER);
|
|
66
|
+
$this->query = $query;
|
|
67
|
+
$this->body = $body;
|
|
68
|
+
$this->cookies = $cookies;
|
|
69
|
+
$this->httpVersion = $httpVersion;
|
|
70
|
+
$this->requestTimestamp = $requestTimestamp ?? (int)(microtime(true) * 1000);
|
|
71
|
+
|
|
72
|
+
// Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
|
|
73
|
+
$this->ja3 = $this->headers['x-ja3-hash'] ?? null;
|
|
74
|
+
$this->ja4 = $this->headers['x-ja4-hash'] ?? null;
|
|
75
|
+
$this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
|
|
76
|
+
$this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
|
|
77
|
+
$this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
|
|
78
|
+
$this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
|
|
82
|
+
*
|
|
83
|
+
* @param string $name Le nom de l'en-tête.
|
|
84
|
+
* @return string|null La valeur de l'en-tête ou null si non trouvé.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
public function getHeader(string $name): ?string
|
|
88
|
+
{
|
|
89
|
+
return $this->headers[strtolower($name)] ?? null;
|
|
90
|
+
}
|
|
91
91
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Store;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adaptateur de stockage MongoDB pour le moteur Fingerprint.
|
|
9
|
+
* Utilise la bibliothèque officielle mongodb/mongodb.
|
|
10
|
+
*/
|
|
11
|
+
class MongoDbStore implements IStore
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* @var \MongoDB\Collection
|
|
15
|
+
*/
|
|
16
|
+
private $collection;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param \MongoDB\Collection $collection Collection dédiée au stockage des empreintes
|
|
20
|
+
*/
|
|
21
|
+
public function __construct($collection)
|
|
22
|
+
{
|
|
23
|
+
$this->collection = $collection;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public function get(string $key)
|
|
27
|
+
{
|
|
28
|
+
$doc = $this->collection->findOne(['_id' => $key]);
|
|
29
|
+
if (!$doc) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Vérification d'expiration active pour bypasser le délai de 60s du démon de MongoDB
|
|
34
|
+
if (isset($doc['expiresAt'])) {
|
|
35
|
+
$expiresAt = $doc['expiresAt'];
|
|
36
|
+
if ($expiresAt instanceof \MongoDB\BSON\UTCDateTime) {
|
|
37
|
+
$expiresAtMs = $expiresAt->toDateTime()->getTimestamp() * 1000;
|
|
38
|
+
$nowMs = (int)(microtime(true) * 1000);
|
|
39
|
+
if ($expiresAtMs < $nowMs) {
|
|
40
|
+
$this->delete($key);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return isset($doc['value']) ? json_decode($doc['value'], true) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function set(string $key, $value, ?int $ttl = null): void
|
|
50
|
+
{
|
|
51
|
+
$doc = [
|
|
52
|
+
'_id' => $key,
|
|
53
|
+
'value' => json_encode($value),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
if ($ttl !== null && $ttl > 0) {
|
|
57
|
+
$doc['expiresAt'] = new \MongoDB\BSON\UTCDateTime((time() + $ttl) * 1000);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$this->collection->replaceOne(
|
|
61
|
+
['_id' => $key],
|
|
62
|
+
$doc,
|
|
63
|
+
['upsert' => true]
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public function has(string $key): bool
|
|
68
|
+
{
|
|
69
|
+
$doc = $this->collection->findOne(
|
|
70
|
+
['_id' => $key],
|
|
71
|
+
['projection' => ['expiresAt' => 1]]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!$doc) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isset($doc['expiresAt']) && $doc['expiresAt'] instanceof \MongoDB\BSON\UTCDateTime) {
|
|
79
|
+
$expiresAtMs = $doc['expiresAt']->toDateTime()->getTimestamp() * 1000;
|
|
80
|
+
$nowMs = (int)(microtime(true) * 1000);
|
|
81
|
+
if ($expiresAtMs < $nowMs) {
|
|
82
|
+
$this->delete($key);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public function delete(string $key): void
|
|
91
|
+
{
|
|
92
|
+
$this->collection->deleteOne(['_id' => $key]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Automatise la configuration de l'index TTL nécessaire dans MongoDB.
|
|
97
|
+
*/
|
|
98
|
+
public function init(): void
|
|
99
|
+
{
|
|
100
|
+
$this->collection->createIndex(
|
|
101
|
+
['expiresAt' => 1],
|
|
102
|
+
['expireAfterSeconds' => 0]
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Store;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adaptateur de stockage Redis pour le moteur Fingerprint.
|
|
9
|
+
* Compatible avec phpredis et predis.
|
|
10
|
+
*/
|
|
11
|
+
class RedisStore implements IStore
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* @var mixed Une instance de \Redis ou de \Predis\Client
|
|
15
|
+
*/
|
|
16
|
+
private $redis;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param mixed $redis Client Redis déjà configuré et connecté
|
|
20
|
+
*/
|
|
21
|
+
public function __construct($redis)
|
|
22
|
+
{
|
|
23
|
+
$this->redis = $redis;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public function get(string $key)
|
|
27
|
+
{
|
|
28
|
+
$value = $this->redis->get($key);
|
|
29
|
+
if ($value === false || $value === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return json_decode($value, true);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public function set(string $key, $value, ?int $ttl = null): void
|
|
36
|
+
{
|
|
37
|
+
$stringValue = json_encode($value);
|
|
38
|
+
if ($ttl !== null && $ttl > 0) {
|
|
39
|
+
$this->redis->setex($key, $ttl, $stringValue);
|
|
40
|
+
} else {
|
|
41
|
+
$this->redis->set($key, $stringValue);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public function has(string $key): bool
|
|
46
|
+
{
|
|
47
|
+
return (bool)$this->redis->exists($key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public function delete(string $key): void
|
|
51
|
+
{
|
|
52
|
+
$this->redis->del($key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Tests;
|
|
6
6
|
|
|
7
|
-
use PHPUnit\Framework\TestCase;
|
|
8
7
|
use Anonympins\Fingerprint\Challenge\ChallengeUtils;
|
|
9
8
|
use Anonympins\Fingerprint\Utils\BigInt;
|
|
9
|
+
use PHPUnit\Framework\TestCase;
|
|
10
10
|
|
|
11
11
|
class ChallengeUtilsTest extends TestCase
|
|
12
12
|
{
|
|
@@ -4,13 +4,13 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Tests;
|
|
6
6
|
|
|
7
|
-
use
|
|
8
|
-
use Anonympins\Fingerprint\FingerprintEngine;
|
|
7
|
+
use Anonympins\Fingerprint\Config\SecurityProfiles;
|
|
9
8
|
use Anonympins\Fingerprint\FingerprintBuilder;
|
|
9
|
+
use Anonympins\Fingerprint\FingerprintEngine;
|
|
10
10
|
use Anonympins\Fingerprint\RequestContext;
|
|
11
|
-
use Anonympins\Fingerprint\Store\StoreManager;
|
|
12
11
|
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
13
|
-
use Anonympins\Fingerprint\
|
|
12
|
+
use Anonympins\Fingerprint\Store\StoreManager;
|
|
13
|
+
use PHPUnit\Framework\TestCase;
|
|
14
14
|
|
|
15
15
|
class FingerprintEngineTest extends TestCase
|
|
16
16
|
{
|
|
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Tests;
|
|
6
6
|
|
|
7
|
-
use PHPUnit\Framework\TestCase;
|
|
8
|
-
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
9
|
-
use Anonympins\Fingerprint\Store\StoreManager;
|
|
10
|
-
use Anonympins\Fingerprint\Store\IStore;
|
|
11
7
|
use Anonympins\Fingerprint\RequestContext;
|
|
8
|
+
use Anonympins\Fingerprint\Store\IStore;
|
|
9
|
+
use Anonympins\Fingerprint\Store\StoreManager;
|
|
10
|
+
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
11
|
+
use PHPUnit\Framework\TestCase;
|
|
12
12
|
|
|
13
13
|
class IpReputationTest extends TestCase
|
|
14
14
|
{
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\RequestContext;
|
|
8
|
+
use Anonympins\Fingerprint\DirectFingerprint;
|
|
9
|
+
use Anonympins\Fingerprint\Utils\MetricsManager;
|
|
10
|
+
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
11
|
+
use Anonympins\Fingerprint\Store\StoreManager;
|
|
12
|
+
use PHPUnit\Framework\TestCase;
|
|
13
|
+
|
|
14
|
+
class MetricsTest extends TestCase
|
|
15
|
+
{
|
|
16
|
+
protected function setUp(): void
|
|
17
|
+
{
|
|
18
|
+
parent::setUp();
|
|
19
|
+
StoreManager::configureStore(new InMemoryStore());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public function testIncrementCounterAndGetPrometheusMetrics(): void
|
|
23
|
+
{
|
|
24
|
+
MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
|
|
25
|
+
$metrics = MetricsManager::getPrometheusMetrics();
|
|
26
|
+
|
|
27
|
+
$this->assertStringContainsString('fingerprint_requests_total', $metrics);
|
|
28
|
+
$this->assertStringContainsString('status="passed"', $metrics);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public function testGetPrometheusMetricsViaDirectFingerprint(): void
|
|
32
|
+
{
|
|
33
|
+
$config = [
|
|
34
|
+
'metricsAuthorizationCallback' => function (RequestContext $context) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
$direct = new DirectFingerprint($config);
|
|
40
|
+
MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
|
|
41
|
+
$metrics = $direct->getPrometheusMetrics();
|
|
42
|
+
|
|
43
|
+
$this->assertNotNull($metrics);
|
|
44
|
+
$this->assertStringContainsString('fingerprint_requests_total', $metrics);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Tests;
|
|
6
6
|
|
|
7
|
-
use
|
|
7
|
+
use Anonympins\Fingerprint\Config\SecurityProfiles;
|
|
8
8
|
use Anonympins\Fingerprint\FingerprintEngine;
|
|
9
9
|
use Anonympins\Fingerprint\ProblemManager;
|
|
10
10
|
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
11
11
|
use Anonympins\Fingerprint\Store\StoreManager;
|
|
12
|
-
use
|
|
12
|
+
use PHPUnit\Framework\TestCase;
|
|
13
13
|
|
|
14
14
|
class PowTest extends TestCase
|
|
15
15
|
{
|
|
@@ -4,11 +4,13 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Tests;
|
|
6
6
|
|
|
7
|
-
use
|
|
7
|
+
use Anonympins\Fingerprint\Optimization\FunctionRegistry;
|
|
8
8
|
use Anonympins\Fingerprint\ProblemManager;
|
|
9
|
-
use Anonympins\Fingerprint\Store\IStore;
|
|
10
9
|
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
11
|
-
use Anonympins\Fingerprint\
|
|
10
|
+
use Anonympins\Fingerprint\Store\IStore;
|
|
11
|
+
use PHPUnit\Framework\TestCase;
|
|
12
|
+
|
|
13
|
+
// Assurez-vous que cette classe est autoloadable
|
|
12
14
|
|
|
13
15
|
class ProblemManagerTest extends TestCase
|
|
14
16
|
{
|
|
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Tests;
|
|
6
6
|
|
|
7
|
-
use PHPUnit\Framework\TestCase;
|
|
8
7
|
use Anonympins\Fingerprint\RequestContext;
|
|
9
8
|
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
9
|
+
use PHPUnit\Framework\TestCase;
|
|
10
10
|
|
|
11
11
|
class RequestUtilsTest extends TestCase
|
|
12
12
|
{
|
|
@@ -78,4 +78,69 @@ class RequestUtilsTest extends TestCase
|
|
|
78
78
|
$result = RequestUtils::getClickVarianceScore($context);
|
|
79
79
|
$this->assertEquals(0.0, $result['clickVarianceScore']);
|
|
80
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
|
+
}
|
|
81
146
|
}
|