@anonympins/fingerprint 0.7.4 → 0.8.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 +59 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +1784 -1738
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +489 -99
- package/src/js/fingerprint.utils.js +252 -2
- package/src/js/pow.worker.js +2 -2
- package/src/js/tests/cross_parity.test.js +3 -3
- package/src/js/tests/dns-circuit-breaker.test.js +6 -6
- package/src/js/tests/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.init.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +37 -12
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
- package/src/js/tests/library.test.js +94 -95
- package/src/js/tests/obfuscation.test.js +8 -8
- package/src/js/tests/quicFingerprint.test.js +47 -0
- package/src/js/tests/tcpFingerprint.test.js +4 -4
- package/src/js/tests/threatIntel.test.js +40 -1
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +392 -86
- package/src/php/Config/SecurityProfiles.php +79 -61
- package/src/php/DirectFingerprint.php +55 -8
- package/src/php/FingerprintBuilder.php +13 -13
- package/src/php/FingerprintClient.php +6 -6
- package/src/php/FingerprintEngine.php +2122 -1690
- package/src/php/Ja3AnomalyDetector.php +22 -22
- package/src/php/Optimization/FunctionRegistry.php +4 -4
- package/src/php/Optimization/Optimization.php +6 -6
- package/src/php/RequestContext.php +11 -11
- package/src/php/Store/InMemoryStore.php +96 -66
- package/src/php/Store/MongoDbStore.php +2 -2
- package/src/php/Store/RedisStore.php +1 -1
- package/src/php/Store/StoreManager.php +48 -35
- package/src/php/Tests/ChallengeUtilsTest.php +31 -0
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintClientTest.php +70 -70
- package/src/php/Tests/PowTest.php +37 -39
- package/src/php/Tests/QuicFingerprintTest.php +115 -53
- package/src/php/Tests/ThreatIntelTest.php +34 -0
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/BlockList.php +13 -13
- package/src/php/Utils/Logger.php +19 -1
- package/src/php/Utils/MaliciousPatterns.php +6 -6
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +362 -1
- package/src/php/Utils/TLSClientHelloParser.php +385 -369
- package/src/php/WordPress/WpDbStore.php +7 -7
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
- package/src/php/WordPress/package.php +80 -101
- package/src/php/WordPress/readme.txt +21 -5
- package/src/php/bin/auto-tune.php +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
|
@@ -8,22 +8,36 @@ use Anonympins\Fingerprint\Store\StoreManager;
|
|
|
8
8
|
use Anonympins\Fingerprint\FingerprintBuilder;
|
|
9
9
|
use Anonympins\Fingerprint\Utils\BigInt;
|
|
10
10
|
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
11
|
-
|
|
11
|
+
use Anonympins\Fingerprint\Utils\Env;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Utility class for generating and verifying Proof-of-Work challenges.
|
|
15
15
|
*/
|
|
16
16
|
class ChallengeUtils
|
|
17
17
|
{
|
|
18
|
-
/** @var array<string, float>
|
|
18
|
+
/** @var array<string, float> Local cache of converted floats to avoid repeated pack/unpack system calls */
|
|
19
19
|
private static array $froundCache = [];
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Emulates JavaScript's Math.fround: rounds a float to the nearest 32-bit
|
|
23
|
+
* single-precision float. Uses a local cache to avoid repeated pack/unpack calls.
|
|
24
|
+
*
|
|
25
|
+
* @param float $value The float value to round.
|
|
26
|
+
* @return float The value rounded to single precision.
|
|
27
|
+
*/
|
|
21
28
|
private static function fround(float $value): float
|
|
22
29
|
{
|
|
23
30
|
$key = (string)$value;
|
|
24
31
|
return self::$froundCache[$key] ?? (self::$froundCache[$key] = unpack('f', pack('f', $value))[1]);
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Hashes a string seed into a normalized float between 0 and 1
|
|
36
|
+
* using a simple 32-bit rolling hash (similar to Java's String.hashCode).
|
|
37
|
+
*
|
|
38
|
+
* @param string $seed The seed string to hash.
|
|
39
|
+
* @return float A pseudo-random float in the range [0, 1).
|
|
40
|
+
*/
|
|
27
41
|
public static function hashSeedToFloat(string $seed): float
|
|
28
42
|
{
|
|
29
43
|
$hash = 0;
|
|
@@ -36,6 +50,15 @@ class ChallengeUtils
|
|
|
36
50
|
return abs($hash % 1000000) / 1000000;
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Derives a set of 4 unique sample indices from the client IP and a secret,
|
|
55
|
+
* using an HMAC-SHA256 over the client IP and a 5-minute time window.
|
|
56
|
+
* This makes the indices deterministic but rotating over time.
|
|
57
|
+
*
|
|
58
|
+
* @param string $clientIp The client IP address.
|
|
59
|
+
* @param string $secret The shared secret used for the HMAC.
|
|
60
|
+
* @return array<int> An array of 4 unique indices in the range [0, 63].
|
|
61
|
+
*/
|
|
39
62
|
public static function deriveSampleIndices(string $clientIp, string $secret): array
|
|
40
63
|
{
|
|
41
64
|
$timeWindow = (int)floor(time() / (60 * 5));
|
|
@@ -56,6 +79,19 @@ class ChallengeUtils
|
|
|
56
79
|
return $indices;
|
|
57
80
|
}
|
|
58
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Verifies a GPU Proof-of-Work solution based on the logistic map
|
|
84
|
+
* (chaotic iteration x_{n+1} = r * x_n * (1 - x_n) with r = 3.9999).
|
|
85
|
+
* Only a subset of the 64 values is recomputed, using indices derived
|
|
86
|
+
* from the client IP and a shared secret.
|
|
87
|
+
*
|
|
88
|
+
* @param string $seed The challenge seed.
|
|
89
|
+
* @param int $iterations The number of logistic map iterations per value.
|
|
90
|
+
* @param string $solution A comma-separated string of 64 float values.
|
|
91
|
+
* @param string $clientIp The client IP used to derive the sample indices.
|
|
92
|
+
* @param string $secret The secret used to derive the sample indices.
|
|
93
|
+
* @return bool True if the sampled values match, false otherwise.
|
|
94
|
+
*/
|
|
59
95
|
public static function verifyGpuPow(string $seed, int $iterations, string $solution, string $clientIp = '127.0.0.1', string $secret = 'gpu-pow-salt'): bool
|
|
60
96
|
{
|
|
61
97
|
$values = explode(',', $solution);
|
|
@@ -82,6 +118,7 @@ class ChallengeUtils
|
|
|
82
118
|
return true;
|
|
83
119
|
}
|
|
84
120
|
|
|
121
|
+
/** Templates used to generate signed trap URLs that lure malicious crawlers. */
|
|
85
122
|
private const TRAP_URL_TEMPLATES = [
|
|
86
123
|
'/includes/config-{RANDOM}.php',
|
|
87
124
|
'/.env.{RANDOM}',
|
|
@@ -92,6 +129,14 @@ class ChallengeUtils
|
|
|
92
129
|
'/.git/config_{RANDOM}'
|
|
93
130
|
];
|
|
94
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Emulates a 32-bit signed integer multiplication (like Math.imul in JS),
|
|
134
|
+
* handling overflow correctly.
|
|
135
|
+
*
|
|
136
|
+
* @param int $a The first operand.
|
|
137
|
+
* @param int $b The second operand.
|
|
138
|
+
* @return int The 32-bit signed result of a * b.
|
|
139
|
+
*/
|
|
95
140
|
private static function imul(int $a, int $b): int
|
|
96
141
|
{
|
|
97
142
|
$ah = ($a >> 16) & 0xffff;
|
|
@@ -103,11 +148,21 @@ class ChallengeUtils
|
|
|
103
148
|
return (($hi << 16) | ($lo & 0xffff)) | 0;
|
|
104
149
|
}
|
|
105
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Deterministically generates a 1024-byte block of pseudo-random data
|
|
153
|
+
* from a seed and a block index, using a rolling hash (cyrb53-based).
|
|
154
|
+
* This is the building block for the proof-of-space challenge.
|
|
155
|
+
*
|
|
156
|
+
* @param string $seed The seed string.
|
|
157
|
+
* @param int $blockIndex The index of the block to generate.
|
|
158
|
+
* @param int $blockSize The size of the block in bytes (default 1024).
|
|
159
|
+
* @return string The raw binary block content.
|
|
160
|
+
*/
|
|
106
161
|
private static function generateBlock(string $seed, int $blockIndex, int $blockSize = 1024): string
|
|
107
162
|
{
|
|
108
163
|
$block = str_repeat("\x00", $blockSize);
|
|
109
164
|
$h = FingerprintBuilder::cyrb53($seed . ":" . $blockIndex);
|
|
110
|
-
|
|
165
|
+
|
|
111
166
|
$h_int = (int)bcmod($h, '4294967296');
|
|
112
167
|
for ($i = 0; $i < $blockSize; $i++) {
|
|
113
168
|
$h_int = self::imul($h_int ^ $i, 1597334677);
|
|
@@ -116,6 +171,15 @@ class ChallengeUtils
|
|
|
116
171
|
return $block;
|
|
117
172
|
}
|
|
118
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Registers a cooperative node in the store, keyed by its IP subnet.
|
|
176
|
+
* Entries older than 2 minutes are pruned before adding the new node.
|
|
177
|
+
*
|
|
178
|
+
* @param string $clientIp The client IP (used to compute the subnet).
|
|
179
|
+
* @param string $nodeId The unique node identifier.
|
|
180
|
+
* @param string $seed The node's seed.
|
|
181
|
+
* @return void
|
|
182
|
+
*/
|
|
119
183
|
public static function registerCooperativeNode(string $clientIp, string $nodeId, string $seed): void
|
|
120
184
|
{
|
|
121
185
|
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
@@ -125,20 +189,28 @@ class ChallengeUtils
|
|
|
125
189
|
$store = StoreManager::getStore();
|
|
126
190
|
$key = "coop-pospace:subnet:{$subnet}";
|
|
127
191
|
$nodes = $store->get($key) ?? [];
|
|
128
|
-
|
|
192
|
+
|
|
129
193
|
$now = time();
|
|
130
|
-
//
|
|
194
|
+
// Clean up expired nodes (older than 2 minutes)
|
|
131
195
|
$nodes = array_filter($nodes, fn($n) => ($now - $n['timestamp']) < 120);
|
|
132
|
-
|
|
196
|
+
|
|
133
197
|
$nodes[$nodeId] = [
|
|
134
198
|
'nodeId' => $nodeId,
|
|
135
199
|
'seed' => $seed,
|
|
136
200
|
'timestamp' => $now
|
|
137
201
|
];
|
|
138
|
-
|
|
202
|
+
|
|
139
203
|
$store->set($key, $nodes, 120);
|
|
140
204
|
}
|
|
141
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Finds a random active peer node in the same subnet as the client,
|
|
208
|
+
* excluding a given node ID.
|
|
209
|
+
*
|
|
210
|
+
* @param string $clientIp The client IP (used to compute the subnet).
|
|
211
|
+
* @param string $excludeNodeId The node ID to exclude from the results.
|
|
212
|
+
* @return array|null The peer node data, or null if none is found.
|
|
213
|
+
*/
|
|
142
214
|
public static function findPeerInSubnet(string $clientIp, string $excludeNodeId): ?array
|
|
143
215
|
{
|
|
144
216
|
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
@@ -148,7 +220,7 @@ class ChallengeUtils
|
|
|
148
220
|
$store = StoreManager::getStore();
|
|
149
221
|
$key = "coop-pospace:subnet:{$subnet}";
|
|
150
222
|
$nodes = $store->get($key) ?? [];
|
|
151
|
-
|
|
223
|
+
|
|
152
224
|
$now = time();
|
|
153
225
|
$activePeers = [];
|
|
154
226
|
foreach ($nodes as $id => $node) {
|
|
@@ -156,14 +228,25 @@ class ChallengeUtils
|
|
|
156
228
|
$activePeers[] = $node;
|
|
157
229
|
}
|
|
158
230
|
}
|
|
159
|
-
|
|
231
|
+
|
|
160
232
|
if (empty($activePeers)) {
|
|
161
233
|
return null;
|
|
162
234
|
}
|
|
163
|
-
|
|
235
|
+
|
|
164
236
|
return $activePeers[array_rand($activePeers)];
|
|
165
237
|
}
|
|
166
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Handles cooperative peer-to-peer operations (federation threat intel sharing,
|
|
241
|
+
* node registration, peer discovery, WebRTC signaling, block requests/responses).
|
|
242
|
+
* Enforces cooperative signature verification for every operation except
|
|
243
|
+
* the federation threat intel sharing path.
|
|
244
|
+
*
|
|
245
|
+
* @param array $params The request parameters (includes `coop_op`).
|
|
246
|
+
* @param string $clientIp The client IP address.
|
|
247
|
+
* @param array $config Additional configuration (federated peers, thresholds, keys).
|
|
248
|
+
* @return array|null The response payload, or null if `coop_op` is missing.
|
|
249
|
+
*/
|
|
167
250
|
public static function handleCooperativeRequest(array $params, string $clientIp = '127.0.0.1', array $config = []): ?array
|
|
168
251
|
{
|
|
169
252
|
$op = $params['coop_op'] ?? null;
|
|
@@ -252,7 +335,7 @@ class ChallengeUtils
|
|
|
252
335
|
return ['error' => 'Missing node_id'];
|
|
253
336
|
}
|
|
254
337
|
|
|
255
|
-
// ---
|
|
338
|
+
// --- COOPERATIVE SIGNATURE VERIFICATION ---
|
|
256
339
|
$challengeContext = $store->get("secret:{$nodeId}");
|
|
257
340
|
if (!$challengeContext || empty($challengeContext['clientSecret'])) {
|
|
258
341
|
return ['error' => 'Invalid or expired node_id'];
|
|
@@ -295,7 +378,7 @@ class ChallengeUtils
|
|
|
295
378
|
if (!hash_equals($expectedSig, $coopSig)) {
|
|
296
379
|
return ['error' => 'Invalid cooperative signature'];
|
|
297
380
|
}
|
|
298
|
-
// ---
|
|
381
|
+
// --- END OF VERIFICATION ---
|
|
299
382
|
|
|
300
383
|
switch ($op) {
|
|
301
384
|
case 'register':
|
|
@@ -343,7 +426,7 @@ class ChallengeUtils
|
|
|
343
426
|
if (empty($peerId) || empty($requestId)) {
|
|
344
427
|
return ['error' => 'Invalid parameters'];
|
|
345
428
|
}
|
|
346
|
-
|
|
429
|
+
|
|
347
430
|
$queueKey = "coop-mailbox:queue:{$peerId}";
|
|
348
431
|
$requests = $store->get($queueKey) ?? [];
|
|
349
432
|
$requests[] = [
|
|
@@ -367,7 +450,7 @@ class ChallengeUtils
|
|
|
367
450
|
if (empty($requesterId) || empty($requestId)) {
|
|
368
451
|
return ['error' => 'Invalid parameters'];
|
|
369
452
|
}
|
|
370
|
-
|
|
453
|
+
|
|
371
454
|
$responseKey = "coop-mailbox:res:{$requesterId}:{$requestId}";
|
|
372
455
|
$store->set($responseKey, ['block_data' => $blockData], 30);
|
|
373
456
|
return ['status' => 'delivered'];
|
|
@@ -385,6 +468,18 @@ class ChallengeUtils
|
|
|
385
468
|
return null;
|
|
386
469
|
}
|
|
387
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Generates a proof-of-space challenge for a client, including random block
|
|
473
|
+
* queries. If a peer is found in the same subnet, a cooperative component is
|
|
474
|
+
* added to the challenge (peer ID and a random block index).
|
|
475
|
+
*
|
|
476
|
+
* @param string $clientIp The client IP address.
|
|
477
|
+
* @param string $nonce The challenge nonce.
|
|
478
|
+
* @param float $suspicionFactor The suspicion factor for the request.
|
|
479
|
+
* @param string $originalUrl The original requested URL (used as redirect path).
|
|
480
|
+
* @param array $securityConfig The security configuration array.
|
|
481
|
+
* @return array The challenge details.
|
|
482
|
+
*/
|
|
388
483
|
public static function generateSpaceChallenge(string $clientIp, string $nonce, float $suspicionFactor, string $originalUrl, array $securityConfig): array
|
|
389
484
|
{
|
|
390
485
|
$pospaceConfig = $securityConfig['pospace'] ?? [];
|
|
@@ -408,12 +503,12 @@ class ChallengeUtils
|
|
|
408
503
|
'path' => $originalUrl
|
|
409
504
|
];
|
|
410
505
|
|
|
411
|
-
//
|
|
506
|
+
// Attempt cooperative coupling with a peer node in the same subnet
|
|
412
507
|
$peer = self::findPeerInSubnet($clientIp, $nonce);
|
|
413
508
|
if ($peer !== null) {
|
|
414
509
|
$challenge['peerId'] = $peer['nodeId'];
|
|
415
510
|
$challenge['peerBlockIdx'] = random_int(0, $maxBlocks - 1);
|
|
416
|
-
|
|
511
|
+
|
|
417
512
|
$store = StoreManager::getStore();
|
|
418
513
|
$store->set("coop-assoc:{$nonce}", [
|
|
419
514
|
'peerNodeId' => $peer['nodeId'],
|
|
@@ -425,14 +520,26 @@ class ChallengeUtils
|
|
|
425
520
|
return $challenge;
|
|
426
521
|
}
|
|
427
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Verifies a proof-of-space solution by reconstructing the expected hash
|
|
525
|
+
* from the queried blocks. If a cooperative association exists for the nonce,
|
|
526
|
+
* the peer block is also appended to the combined data.
|
|
527
|
+
*
|
|
528
|
+
* @param string $nonce The challenge nonce.
|
|
529
|
+
* @param string $solution The client-provided solution hash.
|
|
530
|
+
* @param array $queries The list of queried block indices.
|
|
531
|
+
* @param string $seed The seed used to generate the blocks.
|
|
532
|
+
* @param string $clientSecret The client secret.
|
|
533
|
+
* @return bool True if the solution matches, false otherwise.
|
|
534
|
+
*/
|
|
428
535
|
public static function verifySpacePoW(string $nonce, string $solution, array $queries, string $seed, string $clientSecret): bool
|
|
429
536
|
{
|
|
430
537
|
$combined = '';
|
|
431
538
|
foreach ($queries as $idx) {
|
|
432
539
|
$combined .= self::generateBlock($seed, (int)$idx);
|
|
433
540
|
}
|
|
434
|
-
|
|
435
|
-
//
|
|
541
|
+
|
|
542
|
+
// Cooperative proof verification
|
|
436
543
|
$store = StoreManager::getStore();
|
|
437
544
|
$assoc = $store->get("coop-assoc:{$nonce}");
|
|
438
545
|
if ($assoc !== null) {
|
|
@@ -449,6 +556,15 @@ class ChallengeUtils
|
|
|
449
556
|
return hash_equals($hash, $solution);
|
|
450
557
|
}
|
|
451
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Verifies a Schnorr-style Zero-Knowledge Proof using the secp256k1 prime.
|
|
561
|
+
* Checks that g^s ≡ t * y^c (mod p), where c = SHA-256(g, y, t) mod p.
|
|
562
|
+
*
|
|
563
|
+
* @param string $yStr The public key y (hex).
|
|
564
|
+
* @param string $tStr The commitment t (hex).
|
|
565
|
+
* @param string $sStr The response s (hex).
|
|
566
|
+
* @return bool True if the proof is valid, false otherwise.
|
|
567
|
+
*/
|
|
452
568
|
public static function verifyZkpProof(string $yStr, string $tStr, string $sStr): bool
|
|
453
569
|
{
|
|
454
570
|
try {
|
|
@@ -474,7 +590,11 @@ class ChallengeUtils
|
|
|
474
590
|
}
|
|
475
591
|
|
|
476
592
|
/**
|
|
477
|
-
*
|
|
593
|
+
* Retrieves the secret key for PoW tasks from environment variables.
|
|
594
|
+
* Throws an exception in production if the secret is missing.
|
|
595
|
+
*
|
|
596
|
+
* @return string The PoW secret.
|
|
597
|
+
* @throws \RuntimeException If missing in production.
|
|
478
598
|
*/
|
|
479
599
|
public static function getPowSecret(): string
|
|
480
600
|
{
|
|
@@ -487,9 +607,71 @@ class ChallengeUtils
|
|
|
487
607
|
}
|
|
488
608
|
|
|
489
609
|
/**
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
610
|
+
* Programmatically generates an Ed25519 key pair in PEM format
|
|
611
|
+
* matching OpenSSL CLI:
|
|
612
|
+
* `openssl genpkey -algorithm ed25519 -out issuer-private.pem`
|
|
613
|
+
* `openssl pkey -in issuer-private.pem -pubout -out issuer-public.pem`
|
|
614
|
+
*
|
|
615
|
+
* @param string $outDir Target directory to store the PEM files.
|
|
616
|
+
* @param array $options Options including custom file names.
|
|
617
|
+
* @return array{privateKeyPath: string, publicKeyPath: string, privateKey: string, publicKey: string}
|
|
618
|
+
* @throws \RuntimeException If OpenSSL or Ed25519 is not supported or key generation fails.
|
|
619
|
+
*/
|
|
620
|
+
public static function generateIssuerPemKeys(string $outDir = '', array $options = []): array
|
|
621
|
+
{
|
|
622
|
+
if (!defined('OPENSSL_KEYTYPE_ED25519')) {
|
|
623
|
+
throw new \RuntimeException("Ed25519 is not supported in this OpenSSL environment.");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
$pkey = @openssl_pkey_new(["private_key_type" => constant('OPENSSL_KEYTYPE_ED25519')]);
|
|
627
|
+
if (!$pkey || !@openssl_pkey_export($pkey, $privateKeyPem)) {
|
|
628
|
+
throw new \RuntimeException("Failed to generate Ed25519 private key.");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
$details = openssl_pkey_get_details($pkey);
|
|
632
|
+
$publicKeyPem = $details['key'] ?? '';
|
|
633
|
+
if (empty($publicKeyPem)) {
|
|
634
|
+
throw new \RuntimeException("Failed to extract Ed25519 public key.");
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if ($outDir === '') {
|
|
638
|
+
$outDir = dirname(__DIR__, 2) . '/config';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!is_dir($outDir)) {
|
|
642
|
+
if (function_exists('wp_mkdir_p')) {
|
|
643
|
+
wp_mkdir_p($outDir);
|
|
644
|
+
} else {
|
|
645
|
+
mkdir($outDir, 0755, true);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
$privName = $options['privateKeyName'] ?? 'issuer-private.pem';
|
|
650
|
+
$pubName = $options['publicKeyName'] ?? 'issuer-public.pem';
|
|
651
|
+
|
|
652
|
+
$privateKeyPath = rtrim($outDir, '/\\') . '/' . $privName;
|
|
653
|
+
$publicKeyPath = rtrim($outDir, '/\\') . '/' . $pubName;
|
|
654
|
+
|
|
655
|
+
file_put_contents($privateKeyPath, $privateKeyPem);
|
|
656
|
+
@chmod($privateKeyPath, 0600);
|
|
657
|
+
file_put_contents($publicKeyPath, $publicKeyPem);
|
|
658
|
+
@chmod($publicKeyPath, 0644);
|
|
659
|
+
|
|
660
|
+
return [
|
|
661
|
+
'privateKeyPath' => $privateKeyPath,
|
|
662
|
+
'publicKeyPath' => $publicKeyPath,
|
|
663
|
+
'privateKey' => $privateKeyPem,
|
|
664
|
+
'publicKey' => $publicKeyPem,
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Generates an encrypted and signed stateless ticket containing the
|
|
670
|
+
* authorization context. Uses Ed25519 if a private key is available,
|
|
671
|
+
* otherwise falls back to AES-256-CBC + HMAC-SHA256.
|
|
672
|
+
*
|
|
673
|
+
* @param array $payload The authorization context to embed.
|
|
674
|
+
* @return string The encoded ticket.
|
|
493
675
|
*/
|
|
494
676
|
public static function generateStatelessTicket(array $payload): string
|
|
495
677
|
{
|
|
@@ -513,17 +695,19 @@ class ChallengeUtils
|
|
|
513
695
|
$iv = random_bytes(16);
|
|
514
696
|
$encrypted = openssl_encrypt(json_encode($payload), 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
|
515
697
|
$signature = hash_hmac('sha256', $iv . $encrypted, $key, true);
|
|
516
|
-
|
|
698
|
+
|
|
517
699
|
return rtrim(strtr(base64_encode($iv), '+/', '-_'), '=') . '.' .
|
|
518
|
-
|
|
519
|
-
|
|
700
|
+
rtrim(strtr(base64_encode($encrypted), '+/', '-_'), '=') . '.' .
|
|
701
|
+
rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
|
|
520
702
|
}
|
|
521
703
|
|
|
522
704
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* @
|
|
705
|
+
* Decodes and validates an encrypted and signed stateless ticket.
|
|
706
|
+
* Supports both Ed25519 and AES-256-CBC + HMAC-SHA256 formats.
|
|
707
|
+
*
|
|
708
|
+
* @param string $ticket The ticket to decode.
|
|
709
|
+
* @param string $secret Optional secret for HMAC verification.
|
|
710
|
+
* @return array|null The decoded payload, or null if invalid.
|
|
527
711
|
*/
|
|
528
712
|
public static function parseStatelessTicket(string $ticket, string $secret = ''): ?array
|
|
529
713
|
{
|
|
@@ -538,13 +722,13 @@ class ChallengeUtils
|
|
|
538
722
|
};
|
|
539
723
|
$payloadJson = $base64UrlDecode($parts[1]);
|
|
540
724
|
$signature = $base64UrlDecode($parts[2]);
|
|
541
|
-
|
|
725
|
+
|
|
542
726
|
$ed25519PubKey = Env::get('ED25519_PUBLIC_KEY');
|
|
543
727
|
if (!$ed25519PubKey) {
|
|
544
728
|
self::logError("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
|
|
545
729
|
return null;
|
|
546
730
|
}
|
|
547
|
-
|
|
731
|
+
|
|
548
732
|
$publicKey = openssl_pkey_get_public($ed25519PubKey);
|
|
549
733
|
if ($publicKey && openssl_verify($payloadJson, $signature, $publicKey, null) === 1) {
|
|
550
734
|
return json_decode($payloadJson, true);
|
|
@@ -579,8 +763,18 @@ class ChallengeUtils
|
|
|
579
763
|
}
|
|
580
764
|
|
|
581
765
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
766
|
+
* Checks whether a pass ticket is valid. Supports opaque tickets (via the store)
|
|
767
|
+
* and the legacy stateless fallback. Handles greenlist, ZKP, same-IP, same-subnet,
|
|
768
|
+
* and cross-network roaming validations.
|
|
769
|
+
*
|
|
770
|
+
* @param string|null $ip The client IP.
|
|
771
|
+
* @param string|null $ticket The ticket to validate.
|
|
772
|
+
* @param string $deviceId The client device ID.
|
|
773
|
+
* @param string $deviceHash The client device hash.
|
|
774
|
+
* @param bool $allowCrossNetworkRoaming Whether to allow roaming across networks.
|
|
775
|
+
* @param string $secret Optional secret for legacy ticket validation.
|
|
776
|
+
* @param string $zkpProof Optional ZKP proof in the format "y:t:s".
|
|
777
|
+
* @return bool True if the ticket is valid, false otherwise.
|
|
584
778
|
*/
|
|
585
779
|
public static function isTicketValid(
|
|
586
780
|
?string $ip,
|
|
@@ -595,7 +789,7 @@ class ChallengeUtils
|
|
|
595
789
|
return false;
|
|
596
790
|
}
|
|
597
791
|
|
|
598
|
-
//
|
|
792
|
+
// Try stateless validation first
|
|
599
793
|
$ticketData = self::parseStatelessTicket($ticket, $secret);
|
|
600
794
|
if ($ticketData !== null) {
|
|
601
795
|
$expiry = $ticketData['expiry'] ?? null;
|
|
@@ -606,18 +800,23 @@ class ChallengeUtils
|
|
|
606
800
|
if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
|
|
607
801
|
return false;
|
|
608
802
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
803
|
+
if (!empty($ticketData['greenlist']) || str_starts_with((string)$storedDeviceHash, 'webauthn:greenlist:')) {
|
|
804
|
+
if (empty($deviceId) || $deviceId === $storedDeviceId) {
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
809
|
+
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
810
|
+
if (!empty($zkpProof)) {
|
|
811
|
+
$zkpParts = explode(':', $zkpProof);
|
|
812
|
+
if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
|
|
813
|
+
if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
|
|
814
|
+
return true;
|
|
617
815
|
}
|
|
618
816
|
}
|
|
619
|
-
return false;
|
|
620
817
|
}
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
621
820
|
if ($ip === $originalIp) {
|
|
622
821
|
return true;
|
|
623
822
|
}
|
|
@@ -645,18 +844,23 @@ class ChallengeUtils
|
|
|
645
844
|
$store->delete("ticket:{$ticket}");
|
|
646
845
|
return false;
|
|
647
846
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
847
|
+
if (!empty($ticketData['greenlist']) || str_starts_with((string)$storedDeviceHash, 'webauthn:greenlist:')) {
|
|
848
|
+
if (empty($deviceId) || $deviceId === $storedDeviceId) {
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
853
|
+
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
854
|
+
if (!empty($zkpProof)) {
|
|
855
|
+
$zkpParts = explode(':', $zkpProof);
|
|
856
|
+
if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
|
|
857
|
+
if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
|
|
858
|
+
return true;
|
|
656
859
|
}
|
|
657
860
|
}
|
|
658
|
-
return false;
|
|
659
861
|
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
660
864
|
|
|
661
865
|
if ($ip === $originalIp) {
|
|
662
866
|
return true;
|
|
@@ -675,7 +879,7 @@ class ChallengeUtils
|
|
|
675
879
|
return !empty($deviceId) && $deviceId === $storedDeviceId && !empty($deviceHash) && $deviceHash === $storedDeviceHash;
|
|
676
880
|
}
|
|
677
881
|
|
|
678
|
-
//
|
|
882
|
+
// Backward-compatible fallback for legacy signed tickets (stateless)
|
|
679
883
|
if (!str_contains($ticket, ':')) {
|
|
680
884
|
return false;
|
|
681
885
|
}
|
|
@@ -691,7 +895,12 @@ class ChallengeUtils
|
|
|
691
895
|
}
|
|
692
896
|
|
|
693
897
|
/**
|
|
694
|
-
*
|
|
898
|
+
* Computes the difficulty target for a CPU challenge based on the
|
|
899
|
+
* suspicion factor. Higher suspicion yields a harder (smaller) target.
|
|
900
|
+
*
|
|
901
|
+
* @param float $suspicionFactor The suspicion factor (0.0 to 1.0).
|
|
902
|
+
* @param array $securityConfig The security configuration.
|
|
903
|
+
* @return string The target as a hex string.
|
|
695
904
|
*/
|
|
696
905
|
public static function calculateCpuTarget(float $suspicionFactor, array $securityConfig): string
|
|
697
906
|
{
|
|
@@ -702,7 +911,7 @@ class ChallengeUtils
|
|
|
702
911
|
$totalDifficultyBits = $minDifficultyBits + $suspicionFactor * ($maxDifficultyBits - $minDifficultyBits);
|
|
703
912
|
|
|
704
913
|
if ($totalDifficultyBits <= 0) {
|
|
705
|
-
//
|
|
914
|
+
// Maximum target (trivial challenge)
|
|
706
915
|
return (BigInt::pow(2, 256)->sub(new BigInt(1)))->toHex();
|
|
707
916
|
}
|
|
708
917
|
|
|
@@ -711,7 +920,15 @@ class ChallengeUtils
|
|
|
711
920
|
}
|
|
712
921
|
|
|
713
922
|
/**
|
|
714
|
-
*
|
|
923
|
+
* Creates the base data block used by the CPU challenge. The fingerprint
|
|
924
|
+
* parts are sorted to ensure deterministic ordering.
|
|
925
|
+
*
|
|
926
|
+
* @param string $nonce The challenge nonce.
|
|
927
|
+
* @param string $clientSecret The client secret.
|
|
928
|
+
* @param string $fingerprint The client fingerprint (pipe-separated).
|
|
929
|
+
* @param string $clientIp The client IP.
|
|
930
|
+
* @param string $tlsSessionId The TLS session ID.
|
|
931
|
+
* @return string The concatenated base block.
|
|
715
932
|
*/
|
|
716
933
|
public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint, string $clientIp = '', string $tlsSessionId = ''): string
|
|
717
934
|
{
|
|
@@ -719,13 +936,23 @@ class ChallengeUtils
|
|
|
719
936
|
$filteredParts = array_filter($parts);
|
|
720
937
|
sort($filteredParts);
|
|
721
938
|
$sortedFingerprint = implode('|', $filteredParts);
|
|
722
|
-
|
|
939
|
+
|
|
723
940
|
return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:{$clientIp}:{$tlsSessionId}:";
|
|
724
941
|
}
|
|
725
942
|
|
|
726
943
|
/**
|
|
727
|
-
*
|
|
728
|
-
*
|
|
944
|
+
* Verifies a CPU Proof-of-Work solution and, on success, generates a
|
|
945
|
+
* stateless ticket. In HTTP (insecure) mode, no SHA-256 computation is
|
|
946
|
+
* required and a ticket is issued directly.
|
|
947
|
+
*
|
|
948
|
+
* @param string $clientIp The client IP.
|
|
949
|
+
* @param int $ticketTtl The ticket time-to-live in milliseconds.
|
|
950
|
+
* @param string $nonce The challenge nonce.
|
|
951
|
+
* @param string $solution The client-provided solution.
|
|
952
|
+
* @param array $challengeContext The challenge context (cpuTarget, baseBlock, isHttp).
|
|
953
|
+
* @param string $deviceId The client device ID.
|
|
954
|
+
* @param string $deviceHash The client device hash.
|
|
955
|
+
* @return string|null The opaque ticket on success, or null on failure.
|
|
729
956
|
*/
|
|
730
957
|
public static function verifyCpuTargetPoWAndGenerateTicket(
|
|
731
958
|
string $clientIp,
|
|
@@ -736,9 +963,9 @@ class ChallengeUtils
|
|
|
736
963
|
string $deviceId = '',
|
|
737
964
|
string $deviceHash = ''
|
|
738
965
|
): ?string {
|
|
739
|
-
//
|
|
966
|
+
// In HTTP (insecure) mode, no SHA-256 computation is required
|
|
740
967
|
if (!empty($challengeContext['isHttp'])) {
|
|
741
|
-
self::logError('[FP Server Verify]
|
|
968
|
+
self::logError('[FP Server Verify] HTTP mode detected (insecure policy): validation without SHA-256 accepted.');
|
|
742
969
|
$expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
|
|
743
970
|
return self::generateStatelessTicket([
|
|
744
971
|
'expiry' => $expiry,
|
|
@@ -765,7 +992,7 @@ class ChallengeUtils
|
|
|
765
992
|
|
|
766
993
|
if ($isValid) {
|
|
767
994
|
self::logError('[FP Server Verify] CPU PoW verification PASSED.');
|
|
768
|
-
|
|
995
|
+
|
|
769
996
|
$expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
|
|
770
997
|
$payload = [
|
|
771
998
|
'expiry' => $expiry,
|
|
@@ -787,7 +1014,13 @@ class ChallengeUtils
|
|
|
787
1014
|
}
|
|
788
1015
|
|
|
789
1016
|
/**
|
|
790
|
-
*
|
|
1017
|
+
* Checks the Token Bucket rate limiter for challenge requests from a subnet.
|
|
1018
|
+
* Tokens are refilled at a constant rate up to a maximum capacity.
|
|
1019
|
+
*
|
|
1020
|
+
* @param string $clientIp The client IP (subnet is used as the rate-limit key).
|
|
1021
|
+
* @param float $capacity The bucket capacity (max tokens).
|
|
1022
|
+
* @param float $refillRate The token refill rate (tokens per second).
|
|
1023
|
+
* @return bool True if the request is allowed, false if rate-limited.
|
|
791
1024
|
*/
|
|
792
1025
|
public static function checkChallengeRateLimit(string $clientIp, float $capacity = 5.0, float $refillRate = 0.1): bool
|
|
793
1026
|
{
|
|
@@ -826,7 +1059,14 @@ class ChallengeUtils
|
|
|
826
1059
|
}
|
|
827
1060
|
|
|
828
1061
|
/**
|
|
829
|
-
*
|
|
1062
|
+
* Derives the set of challenged block indices for a memory PoW solution,
|
|
1063
|
+
* based on the seed, the solution, and the number of blocks.
|
|
1064
|
+
*
|
|
1065
|
+
* @param string $seed The challenge seed.
|
|
1066
|
+
* @param int $solution The client solution (used as a salt in the hash).
|
|
1067
|
+
* @param int $numBlocks The total number of blocks.
|
|
1068
|
+
* @param int $k The number of challenged indices to derive (default 4).
|
|
1069
|
+
* @return array<int> The list of challenged block indices.
|
|
830
1070
|
*/
|
|
831
1071
|
private static function getChallengedIndices(string $seed, int $solution, int $numBlocks, int $k = 4): array
|
|
832
1072
|
{
|
|
@@ -839,6 +1079,16 @@ class ChallengeUtils
|
|
|
839
1079
|
return $indices;
|
|
840
1080
|
}
|
|
841
1081
|
|
|
1082
|
+
/**
|
|
1083
|
+
* Verifies a Merkle proof for a given leaf hash, index, and expected root.
|
|
1084
|
+
* Recomputes the root by hashing pairs of siblings along the path.
|
|
1085
|
+
*
|
|
1086
|
+
* @param string $leafHash The hex hash of the leaf.
|
|
1087
|
+
* @param int $index The leaf index in the tree.
|
|
1088
|
+
* @param array $proof The list of sibling hashes (hex).
|
|
1089
|
+
* @param string $root The expected Merkle root (hex).
|
|
1090
|
+
* @return bool True if the proof is valid, false otherwise.
|
|
1091
|
+
*/
|
|
842
1092
|
private static function verifyMerkleProof(string $leafHash, int $index, array $proof, string $root): bool
|
|
843
1093
|
{
|
|
844
1094
|
$currentHash = $leafHash;
|
|
@@ -851,6 +1101,17 @@ class ChallengeUtils
|
|
|
851
1101
|
return $currentHash === $root;
|
|
852
1102
|
}
|
|
853
1103
|
|
|
1104
|
+
/**
|
|
1105
|
+
* Legacy memory PoW verification, used for low-difficulty challenges with
|
|
1106
|
+
* a simple numeric solution. Rebuilds the whole buffer and replays the
|
|
1107
|
+
* random walk to compare with the provided solution.
|
|
1108
|
+
*
|
|
1109
|
+
* @param string $nonce The challenge nonce.
|
|
1110
|
+
* @param int $solution The client-provided solution.
|
|
1111
|
+
* @param int $difficulty The difficulty (in MB).
|
|
1112
|
+
* @param string $clientSecret The client secret.
|
|
1113
|
+
* @return bool True if the solution matches, false otherwise.
|
|
1114
|
+
*/
|
|
854
1115
|
private static function verifyMemoryPoWLegacy(string $nonce, int $solution, int $difficulty, string $clientSecret): bool
|
|
855
1116
|
{
|
|
856
1117
|
$size = $difficulty * 1024 * 1024;
|
|
@@ -877,6 +1138,17 @@ class ChallengeUtils
|
|
|
877
1138
|
return $finalHash === $solution;
|
|
878
1139
|
}
|
|
879
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* Verifies a memory PoW solution. Supports both the modern JSON format
|
|
1143
|
+
* (with Merkle proofs) and the legacy numeric format for low difficulties.
|
|
1144
|
+
* Enforces a maximum allowed difficulty to prevent DoS.
|
|
1145
|
+
*
|
|
1146
|
+
* @param string $nonce The challenge nonce.
|
|
1147
|
+
* @param string $solution The client-provided solution (JSON or numeric string).
|
|
1148
|
+
* @param int $difficulty The difficulty (in MB). 0 means the challenge is skipped.
|
|
1149
|
+
* @param string $clientSecret The client secret.
|
|
1150
|
+
* @return bool True if the solution is valid, false otherwise.
|
|
1151
|
+
*/
|
|
880
1152
|
public static function verifyMemoryPoW(
|
|
881
1153
|
string $nonce,
|
|
882
1154
|
string $solution,
|
|
@@ -962,7 +1234,12 @@ class ChallengeUtils
|
|
|
962
1234
|
}
|
|
963
1235
|
|
|
964
1236
|
/**
|
|
965
|
-
*
|
|
1237
|
+
* Emulates JavaScript's 32-bit signed integer multiplication (Math.imul),
|
|
1238
|
+
* handling overflow correctly.
|
|
1239
|
+
*
|
|
1240
|
+
* @param int $a The first operand.
|
|
1241
|
+
* @param int $b The second operand.
|
|
1242
|
+
* @return int The 32-bit signed result of a * b.
|
|
966
1243
|
*/
|
|
967
1244
|
private static function gmp_imul(int $a, int $b): int
|
|
968
1245
|
{
|
|
@@ -974,9 +1251,11 @@ class ChallengeUtils
|
|
|
974
1251
|
}
|
|
975
1252
|
|
|
976
1253
|
/**
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
1254
|
+
* Generates a signed trap URL. The URL is picked from a template and a
|
|
1255
|
+
* signature is appended as a query parameter.
|
|
1256
|
+
*
|
|
1257
|
+
* @param string $nonce The nonce used to sign the URL.
|
|
1258
|
+
* @return string The trap URL.
|
|
980
1259
|
*/
|
|
981
1260
|
public static function generateTrapUrl(string $nonce): string
|
|
982
1261
|
{
|
|
@@ -989,11 +1268,13 @@ class ChallengeUtils
|
|
|
989
1268
|
}
|
|
990
1269
|
|
|
991
1270
|
/**
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
* @param string $
|
|
996
|
-
* @
|
|
1271
|
+
* Verifies whether a given path and signature form a valid trap URL for
|
|
1272
|
+
* the given nonce. Uses hash_equals for timing-safe comparison.
|
|
1273
|
+
*
|
|
1274
|
+
* @param string $path The request path.
|
|
1275
|
+
* @param string $signature The signature from the query string.
|
|
1276
|
+
* @param string $nonce The nonce to verify against.
|
|
1277
|
+
* @return bool True if the trap URL is valid, false otherwise.
|
|
997
1278
|
*/
|
|
998
1279
|
public static function verifyTrapUrl(string $path, string $signature, string $nonce): bool
|
|
999
1280
|
{
|
|
@@ -1001,13 +1282,15 @@ class ChallengeUtils
|
|
|
1001
1282
|
return false;
|
|
1002
1283
|
}
|
|
1003
1284
|
$expectedSignature = substr(hash_hmac('sha256', $nonce . $path, self::getPowSecret()), 0, 16);
|
|
1004
|
-
//
|
|
1285
|
+
// Use hash_equals for timing-safe comparison.
|
|
1005
1286
|
return hash_equals($expectedSignature, $signature);
|
|
1006
1287
|
}
|
|
1007
1288
|
|
|
1008
1289
|
/**
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1290
|
+
* Loads the JavaScript solver code for inline injection into challenge pages.
|
|
1291
|
+
* Searches several candidate paths, including WordPress plugin directories.
|
|
1292
|
+
*
|
|
1293
|
+
* @return string The JavaScript solver code, or an empty string if not found.
|
|
1011
1294
|
*/
|
|
1012
1295
|
private static function getPowSolverCode(): string
|
|
1013
1296
|
{
|
|
@@ -1030,10 +1313,20 @@ class ChallengeUtils
|
|
|
1030
1313
|
return file_get_contents($solverPath) ?: '';
|
|
1031
1314
|
}
|
|
1032
1315
|
}
|
|
1033
|
-
self::logError("[ChallengeUtils]
|
|
1316
|
+
self::logError("[ChallengeUtils] Error: The pow.solver.inline.js file was not found at the expected location.");
|
|
1034
1317
|
return '';
|
|
1035
1318
|
}
|
|
1036
1319
|
|
|
1320
|
+
/**
|
|
1321
|
+
* Generates the HTML page for a proof-of-space challenge, embedding the
|
|
1322
|
+
* solver code and the challenge script. The page initializes local storage,
|
|
1323
|
+
* runs the proof-of-space solver, and redirects with the solution.
|
|
1324
|
+
*
|
|
1325
|
+
* @param array $challengeDetails The challenge details (nonce, sizeMb, queries, path, peerId, peerBlockIdx).
|
|
1326
|
+
* @param string $clientSecret The client secret.
|
|
1327
|
+
* @param array $securityConfig The security configuration.
|
|
1328
|
+
* @return string The generated HTML page.
|
|
1329
|
+
*/
|
|
1037
1330
|
public static function generateSpaceChallengePage(array $challengeDetails, string $clientSecret, array $securityConfig): string
|
|
1038
1331
|
{
|
|
1039
1332
|
$nonce = $challengeDetails['nonce'];
|
|
@@ -1079,14 +1372,21 @@ class ChallengeUtils
|
|
|
1079
1372
|
}
|
|
1080
1373
|
|
|
1081
1374
|
/**
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
* @param array $
|
|
1087
|
-
* @param
|
|
1088
|
-
* @param string $
|
|
1089
|
-
* @
|
|
1375
|
+
* Generates the HTML page for a combined CPU + Memory proof-of-work challenge.
|
|
1376
|
+
* The page embeds the solver code, the challenge script, and hidden trap links
|
|
1377
|
+
* (rendered in random tags/positions) to lure malicious crawlers.
|
|
1378
|
+
*
|
|
1379
|
+
* @param array $cpuChallengeDetails The CPU challenge details (nonce, target, path).
|
|
1380
|
+
* @param int $memoryDifficulty The memory difficulty in MB (0 to skip).
|
|
1381
|
+
* @param string $clientSecret The client secret.
|
|
1382
|
+
* @param array $securityConfig The security configuration.
|
|
1383
|
+
* @param array $trapUrls The list of trap URLs to embed.
|
|
1384
|
+
* @param string $originalFingerprint The original client fingerprint.
|
|
1385
|
+
* @param string $clientIp The client IP.
|
|
1386
|
+
* @param string $tlsSessionId The TLS session ID.
|
|
1387
|
+
* @param string|null $baseBlock Optional pre-computed base block.
|
|
1388
|
+
* @param bool $isHttps Whether the request is over HTTPS.
|
|
1389
|
+
* @return string The generated HTML page.
|
|
1090
1390
|
*/
|
|
1091
1391
|
public static function generateCombinedPoWChallengePage(
|
|
1092
1392
|
array $cpuChallengeDetails,
|
|
@@ -1181,6 +1481,12 @@ class ChallengeUtils
|
|
|
1181
1481
|
);
|
|
1182
1482
|
}
|
|
1183
1483
|
|
|
1484
|
+
/**
|
|
1485
|
+
* Logs an error message using the PHP error log.
|
|
1486
|
+
*
|
|
1487
|
+
* @param string $message The message to log.
|
|
1488
|
+
* @return void
|
|
1489
|
+
*/
|
|
1184
1490
|
private static function logError(string $message): void
|
|
1185
1491
|
{
|
|
1186
1492
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Challenge diagnostic logging
|