Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d4b1580
Tighten permissions on HPKE keys
soatok Feb 21, 2026
5622c8c
Add "burndown-enabled" to /api/info
soatok Feb 21, 2026
5316822
Fix burndown/revoke nits
soatok Feb 21, 2026
b8050c2
Loop on decremented variable
soatok Feb 21, 2026
d87c3bc
Boyscouting
soatok Feb 21, 2026
a1cf9d9
Fix actor/hostname handling
soatok Feb 21, 2026
7ab1aed
TOTP is required for BurnDown
soatok Feb 21, 2026
2e3fbe2
Don't interpolate SQL strings
soatok Feb 21, 2026
3c3f4c2
Enforce BurnDown
soatok Feb 21, 2026
f86498a
Cap penalty to 2^60 times the base penalty
soatok Feb 21, 2026
b92ef97
Prevent locking race conditions
soatok Feb 21, 2026
7b46587
Throw on error
soatok Feb 21, 2026
014d99a
Fix BurnDown tests
soatok Feb 21, 2026
ffecbff
Specification compliance nit
soatok Feb 21, 2026
2431db1
Use hash_equals() here
soatok Feb 21, 2026
fa0b869
Remove dead constant
soatok Feb 21, 2026
3395618
Rate-limit filesystem backend: use exclusive locks
soatok Feb 21, 2026
5cf21c4
Throw on NULL hostnames
soatok Feb 21, 2026
c8ba2cc
Update test
soatok Feb 21, 2026
8c32d87
Regenerate Reference Docs
soatok Feb 21, 2026
da806c8
Fix sqlite
soatok Feb 21, 2026
9fbf888
Add missing annotation
soatok Feb 21, 2026
c019d52
Do we need to clear the transactions?
soatok Feb 21, 2026
ffcffef
Proposed fix
soatok Feb 21, 2026
189c715
More rollbacks
soatok Feb 21, 2026
32c028b
More retries
soatok Feb 21, 2026
fdc5314
Force rollback
soatok Feb 21, 2026
11612ec
Fix timeouts
soatok Feb 21, 2026
6c40a66
Regenerate docs
soatok Feb 21, 2026
b4cb201
Use different test domain
soatok Feb 21, 2026
5c1f512
Lock for shorter intervals
soatok Feb 21, 2026
049aa06
END TRANSACTION in finally block
soatok Feb 21, 2026
694ba26
Nits
soatok Feb 22, 2026
231190a
Attempted fix: correct PRAGMA usage
soatok Feb 22, 2026
e5da38e
Remove kludge
soatok Feb 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions autoload-phpunit.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ function tableExists(EasyDB $db, string $tableName): bool
mkdir(__DIR__ . '/tmp/db/', 0777, true);
}
$temp = __DIR__ . '/tmp/db/' . sodium_bin2hex(random_bytes(16)) . '-test.db';
$pkdConfig->withDatabase(new EasyDBCache(new PDO('sqlite:' . $temp)));
$mainPDO = new PDO('sqlite:' . $temp);
$mainPDO->exec('PRAGMA jounral_mode=WAL');
$mainPDO->exec('PRAGMA busy_timeout=5000');
$pkdConfig->withDatabase(new EasyDBCache($mainPDO));
chmod($temp, 0777);

// Create second DB connection for testing concurrency
$GLOBALS['PKD_PHPUNIT_DB'] = new EasyDBCache(new PDO('sqlite:' . $temp));
$secondPDO = new PDO('sqlite:' . $temp);
$secondPDO->exec('PRAGMA jounral_mode=WAL');
$secondPDO->exec('PRAGMA busy_timeout=5000');
$GLOBALS['PKD_PHPUNIT_DB'] = new EasyDBCache($secondPDO);

// Call cleanup-test-db.php to cleanup test file after phpunit is finished.
if (getenv('AUTO_CLEANUP_TEST_DB')) {
Expand Down
1 change: 1 addition & 0 deletions config/hpke.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
'encaps-key' =>
Base64UrlSafe::encodeUnpadded($encapsKey->bytes),
], JSON_PRETTY_PRINT));
chmod(__DIR__ . '/hpke.json', 0600);
}
return new HPKE($hpke, $decapsKey, $encapsKey);
22 changes: 14 additions & 8 deletions docs/reference/classes/meta.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ Server configuration parameters
| `$hostname` | `string` | (readonly) |
| `$cacheKey` | `string` | (readonly) |
| `$httpCacheTtl` | `int` | (readonly) |
| `$serverAllowsBurnDown` | `bool` | (readonly) |

### Methods

#### [`__construct`](../../../src/Meta/Params.php#L19-L45)
#### [`__construct`](../../../src/Meta/Params.php#L19-L46)

Returns `void`

Expand All @@ -45,34 +46,39 @@ These parameters MUST be public and MUST have a default value
- `$hostname`: `string` = 'localhost'
- `$cacheKey`: `string` = ''
- `$httpCacheTtl`: `int` = 60
- `$serverAllowsBurnDown`: `bool` = true

**Throws:** `DependencyException`

#### [`getActorUsername`](../../../src/Meta/Params.php#L47-L50)
#### [`getActorUsername`](../../../src/Meta/Params.php#L48-L51)

Returns `string`

#### [`getCacheKey`](../../../src/Meta/Params.php#L52-L55)
#### [`getBurnDownEnabled`](../../../src/Meta/Params.php#L53-L56)

Returns `bool`

#### [`getCacheKey`](../../../src/Meta/Params.php#L58-L61)

Returns `string`

#### [`getHashFunction`](../../../src/Meta/Params.php#L57-L60)
#### [`getHashFunction`](../../../src/Meta/Params.php#L63-L66)

Returns `string`

#### [`getHostname`](../../../src/Meta/Params.php#L62-L65)
#### [`getHostname`](../../../src/Meta/Params.php#L68-L71)

Returns `string`

#### [`getHttpCacheTtl`](../../../src/Meta/Params.php#L67-L70)
#### [`getHttpCacheTtl`](../../../src/Meta/Params.php#L73-L76)

Returns `int`

#### [`getOtpMaxLife`](../../../src/Meta/Params.php#L72-L75)
#### [`getOtpMaxLife`](../../../src/Meta/Params.php#L78-L81)

Returns `int`

#### [`getEmptyTreeRoot`](../../../src/Meta/Params.php#L77-L80)
#### [`getEmptyTreeRoot`](../../../src/Meta/Params.php#L83-L86)

Returns `string`

Expand Down
10 changes: 5 additions & 5 deletions docs/reference/classes/ratelimit.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Returns `?DateTimeImmutable`

**Throws:** `DateMalformedIntervalStringException`

#### [`getIntervalFromFailureCount`](../../../src/RateLimit/DefaultRateLimiting.php#L249-L262)
#### [`getIntervalFromFailureCount`](../../../src/RateLimit/DefaultRateLimiting.php#L249-L263)

Returns `DateInterval`

Expand All @@ -152,7 +152,7 @@ Returns `DateInterval`

**Throws:** `DateMalformedIntervalStringException`

#### [`recordPenalty`](../../../src/RateLimit/DefaultRateLimiting.php#L268-L277)
#### [`recordPenalty`](../../../src/RateLimit/DefaultRateLimiting.php#L269-L278)

Returns `void`

Expand All @@ -165,7 +165,7 @@ Returns `void`

**Throws:** `DateMalformedIntervalStringException`

#### [`increaseFailures`](../../../src/RateLimit/DefaultRateLimiting.php#L282-L296)
#### [`increaseFailures`](../../../src/RateLimit/DefaultRateLimiting.php#L283-L297)

Returns `FediE2EE\PKDServer\RateLimit\RateLimitData`

Expand Down Expand Up @@ -235,15 +235,15 @@ Returns `string`

- `$array`: `array`

#### [`getRequestActor`](../../../src/RateLimit/DefaultRateLimiting.php#L155-L175)
#### [`getRequestActor`](../../../src/RateLimit/DefaultRateLimiting.php#L155-L173)

Returns `?string`

**Parameters:**

- `$request`: `Psr\Http\Message\ServerRequestInterface`

#### [`getRequestDomain`](../../../src/RateLimit/DefaultRateLimiting.php#L177-L185)
#### [`getRequestDomain`](../../../src/RateLimit/DefaultRateLimiting.php#L175-L183)

Returns `?string`

Expand Down
8 changes: 4 additions & 4 deletions docs/reference/classes/requesthandlers-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,13 @@ static · Returns `string`

### Methods

#### [`__construct`](../../../src/RequestHandlers/Api/BurnDown.php#L49-L52)
#### [`__construct`](../../../src/RequestHandlers/Api/BurnDown.php#L54-L57)

Returns `void`

**Throws:** `DependencyException`

#### [`handle`](../../../src/RequestHandlers/Api/BurnDown.php#L69-L91)
#### [`handle`](../../../src/RequestHandlers/Api/BurnDown.php#L79-L106)

Returns `Psr\Http\Message\ResponseInterface`

Expand All @@ -336,7 +336,7 @@ Returns `Psr\Http\Message\ResponseInterface`

- `$request`: `Psr\Http\Message\ServerRequestInterface`

**Throws:** `CacheException`, `CertaintyException`, `CryptoException`, `DependencyException`, `HPKEException`, `JsonException`, `NotImplementedException`, `ParserException`, `SodiumException`, `TableException`, `InvalidArgumentException`
**Throws:** `BaseJsonException`, `BundleException`, `CacheException`, `CertaintyException`, `ConcurrentException`, `CryptoException`, `DateMalformedStringException`, `DependencyException`, `HPKEException`, `InvalidArgumentException`, `JsonException`, `NotImplementedException`, `ParserException`, `RandomException`, `SodiumException`, `TableException`

#### [`getVerifiedStream`](../../../src/RequestHandlers/Api/BurnDown.php#L39-L62)

Expand Down Expand Up @@ -2707,7 +2707,7 @@ static · Returns `string`

### Methods

#### [`handle`](../../../src/RequestHandlers/Api/Info.php#L34-L45)
#### [`handle`](../../../src/RequestHandlers/Api/Info.php#L34-L46)

Returns `Psr\Http\Message\ResponseInterface`

Expand Down
22 changes: 11 additions & 11 deletions docs/reference/classes/tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ Return the witness data (including public key) for a given origin

**Throws:** `TableException`

#### [`addWitnessCosignature`](../../../src/Tables/MerkleState.php#L112-L157)
#### [`addWitnessCosignature`](../../../src/Tables/MerkleState.php#L112-L160)

**API** · Returns `bool`

Expand All @@ -354,29 +354,29 @@ Return the witness data (including public key) for a given origin

**Throws:** `CryptoException`, `DependencyException`, `JsonException`, `NotImplementedException`, `ProtocolException`, `SodiumException`, `TableException`

#### [`getCosignatures`](../../../src/Tables/MerkleState.php#L162-L180)
#### [`getCosignatures`](../../../src/Tables/MerkleState.php#L165-L183)

Returns `array`

**Parameters:**

- `$leafId`: `int`

#### [`countCosignatures`](../../../src/Tables/MerkleState.php#L182-L192)
#### [`countCosignatures`](../../../src/Tables/MerkleState.php#L185-L195)

Returns `int`

**Parameters:**

- `$leafId`: `int`

#### [`getLatestRoot`](../../../src/Tables/MerkleState.php#L200-L209)
#### [`getLatestRoot`](../../../src/Tables/MerkleState.php#L203-L212)

**API** · Returns `string`

**Throws:** `DependencyException`, `SodiumException`

#### [`insertLeaf`](../../../src/Tables/MerkleState.php#L228-L292)
#### [`insertLeaf`](../../../src/Tables/MerkleState.php#L231-L295)

**API** · Returns `bool`

Expand All @@ -386,27 +386,27 @@ Insert leaf with retry logic for deadlocks

- `$leaf`: `FediE2EE\PKDServer\Tables\Records\MerkleLeaf`
- `$inTransaction`: `callable`
- `$maxRetries`: `int` = 5
- `$maxRetries`: `int` = 20

**Throws:** `ConcurrentException`, `CryptoException`, `DependencyException`, `NotImplementedException`, `RandomException`, `SodiumException`

#### [`getLeafByRoot`](../../../src/Tables/MerkleState.php#L311-L327)
#### [`getLeafByRoot`](../../../src/Tables/MerkleState.php#L314-L330)

**API** · Returns `?FediE2EE\PKDServer\Tables\Records\MerkleLeaf`

**Parameters:**

- `$root`: `string`

#### [`getLeafByID`](../../../src/Tables/MerkleState.php#L332-L348)
#### [`getLeafByID`](../../../src/Tables/MerkleState.php#L335-L351)

**API** · Returns `?FediE2EE\PKDServer\Tables\Records\MerkleLeaf`

**Parameters:**

- `$primaryKey`: `int`

#### [`getHashesSince`](../../../src/Tables/MerkleState.php#L388-L430)
#### [`getHashesSince`](../../../src/Tables/MerkleState.php#L391-L435)

**API** · Returns `array`

Expand Down Expand Up @@ -994,7 +994,7 @@ Returns `void`

**Throws:** `JsonException`, `TableException`

#### [`getHistory`](../../../src/Tables/ReplicaHistory.php#L71-L82)
#### [`getHistory`](../../../src/Tables/ReplicaHistory.php#L71-L84)

Returns `array`

Expand All @@ -1006,7 +1006,7 @@ Returns `array`

**Throws:** `JsonException`

#### [`getHistorySince`](../../../src/Tables/ReplicaHistory.php#L88-L108)
#### [`getHistorySince`](../../../src/Tables/ReplicaHistory.php#L90-L110)

Returns `array`

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/classes/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -873,15 +873,15 @@ Returns `string`

- `$array`: `array`

#### [`getRequestActor`](../../../src/Traits/NetworkTrait.php#L155-L175)
#### [`getRequestActor`](../../../src/Traits/NetworkTrait.php#L155-L173)

Returns `?string`

**Parameters:**

- `$request`: `Psr\Http\Message\ServerRequestInterface`

#### [`getRequestDomain`](../../../src/Traits/NetworkTrait.php#L177-L185)
#### [`getRequestDomain`](../../../src/Traits/NetworkTrait.php#L175-L183)

Returns `?string`

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This document lists all API routes defined via `#[Route]` attributes.
|---------------|---------------|--------|
| `/` | [`RequestHandlers\IndexPage`](../../src/RequestHandlers/IndexPage.php) | `handle` |
| `/.well-known/webfinger` | [`RequestHandlers\ActivityPub\Finger`](../../src/RequestHandlers/ActivityPub/Finger.php) | `handle` |
| `/api/burndown` | [`RequestHandlers\Api\Revoke`](../../src/RequestHandlers/Api/Revoke.php) | `handle` |
| `/api/burndown` | [`RequestHandlers\Api\BurnDown`](../../src/RequestHandlers/Api/BurnDown.php) | `handle` |
| `/api/checkpoint` | [`RequestHandlers\Api\Checkpoint`](../../src/RequestHandlers/Api/Checkpoint.php) | `handle` |
| `/api/extensions` | [`RequestHandlers\Api\Extensions`](../../src/RequestHandlers/Api/Extensions.php) | `handle` |
| `/api/history` | [`RequestHandlers\Api\History`](../../src/RequestHandlers/Api/History.php) | `handle` |
Expand All @@ -24,7 +24,7 @@ This document lists all API routes defined via `#[Route]` attributes.
| `/api/replicas/{replica_id}/actor/{actor_id}/keys/key/{key_id}` | [`RequestHandlers\Api\ReplicaInfo`](../../src/RequestHandlers/Api/ReplicaInfo.php) | `actorKey` |
| `/api/replicas/{replica_id}/history` | [`RequestHandlers\Api\ReplicaInfo`](../../src/RequestHandlers/Api/ReplicaInfo.php) | `history` |
| `/api/replicas/{replica_id}/history/since/{hash}` | [`RequestHandlers\Api\ReplicaInfo`](../../src/RequestHandlers/Api/ReplicaInfo.php) | `historySince` |
| `/api/revoke` | [`RequestHandlers\Api\BurnDown`](../../src/RequestHandlers/Api/BurnDown.php) | `handle` |
| `/api/revoke` | [`RequestHandlers\Api\Revoke`](../../src/RequestHandlers/Api/Revoke.php) | `handle` |
| `/api/server-public-key` | [`RequestHandlers\Api\ServerPublicKey`](../../src/RequestHandlers/Api/ServerPublicKey.php) | `handle` |
| `/api/totp/disenroll` | [`RequestHandlers\Api\TotpDisenroll`](../../src/RequestHandlers/Api/TotpDisenroll.php) | `handle` |
| `/api/totp/enroll` | [`RequestHandlers\Api\TotpEnroll`](../../src/RequestHandlers/Api/TotpEnroll.php) | `handle` |
Expand Down
3 changes: 3 additions & 0 deletions public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
if ($ex instanceof RateLimitException) {
// Rate-limited by the Middleware
http_response_code(420);
if (!is_null($ex->rateLimitedUntil)) {
header('Retry-After: ' . $ex->rateLimitedUntil->format(DateTimeInterface::ATOM));
}
echo $ex->getMessage(), PHP_EOL;
if (!is_null($ex->rateLimitedUntil)) {
echo 'Try again after: ',
Expand Down
6 changes: 6 additions & 0 deletions src/Meta/Params.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
public string $hostname = 'localhost',
public string $cacheKey = '',
public int $httpCacheTtl = 60,
public bool $serverAllowsBurnDown = true,

Check warning on line 26 in src/Meta/Params.php

View workflow job for this annotation

GitHub Actions / Infection on PHP 8.4

Escaped Mutant for Mutator "TrueValue": @@ @@ public string $hostname = 'localhost', public string $cacheKey = '', public int $httpCacheTtl = 60, - public bool $serverAllowsBurnDown = true, + public bool $serverAllowsBurnDown = false, ) { if (!Tree::isHashFunctionAllowed($this->hashAlgo)) { throw new DependencyException('Disallowed hash algorithm');
) {
if (!Tree::isHashFunctionAllowed($this->hashAlgo)) {
throw new DependencyException('Disallowed hash algorithm');
Expand All @@ -49,6 +50,11 @@
return $this->actorUsername;
}

public function getBurnDownEnabled(): bool
{
return $this->serverAllowsBurnDown;
}

public function getCacheKey(): string
{
return $this->cacheKey;
Expand Down
4 changes: 2 additions & 2 deletions src/Protocol/Payload.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
ProtocolMessageInterface
};
use FediE2EE\PKDServer\Traits\JsonTrait;
use function array_key_exists, json_decode;
use function array_key_exists;

readonly class Payload
{
Expand Down Expand Up @@ -42,7 +42,7 @@ public function decode(): array
*/
public function getMerkleTreePayload(): string
{
$decoded = json_decode($this->rawJson, true);
$decoded = self::jsonDecode($this->rawJson);
if (array_key_exists('key-id', $decoded)) {
unset($decoded['key-id']);
}
Expand Down
5 changes: 3 additions & 2 deletions src/RateLimit/DefaultRateLimiting.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
break;
}

} while ($data->failures > 0);
} while ($failures > 0);
// Either way, return the updated rate-limit info:
return $data->withFailures($failures)->withCooldownStart($start);
}
Expand Down Expand Up @@ -251,11 +251,12 @@
if ($failures < 1) {
return new DateInterval('PT0S');
}
$milliseconds = $this->baseDelay << ($failures - 1);
$shift = min($failures - 1, 60);
$milliseconds = $this->baseDelay << $shift;
$seconds = (int) floor($milliseconds / 1000);
$us = ($milliseconds % 1000) * 1000;
$interval = DateInterval::createFromDateString($seconds . ' seconds + ' . $us . ' microseconds');
if (!($interval instanceof DateInterval)) {

Check warning on line 259 in src/RateLimit/DefaultRateLimiting.php

View workflow job for this annotation

GitHub Actions / Infection on PHP 8.4

Escaped Mutant for Mutator "LogicalNot": @@ @@ $seconds = (int) floor($milliseconds / 1000); $us = ($milliseconds % 1000) * 1000; $interval = DateInterval::createFromDateString($seconds . ' seconds + ' . $us . ' microseconds'); - if (!($interval instanceof DateInterval)) { + if ($interval instanceof DateInterval) { throw new DateMalformedIntervalStringException('Invalid interval string'); } return $interval;

Check warning on line 259 in src/RateLimit/DefaultRateLimiting.php

View workflow job for this annotation

GitHub Actions / Infection on PHP 8.4

Escaped Mutant for Mutator "InstanceOf_": @@ @@ $seconds = (int) floor($milliseconds / 1000); $us = ($milliseconds % 1000) * 1000; $interval = DateInterval::createFromDateString($seconds . ' seconds + ' . $us . ' microseconds'); - if (!($interval instanceof DateInterval)) { + if ($interval instanceof DateInterval) { throw new DateMalformedIntervalStringException('Invalid interval string'); } return $interval;
throw new DateMalformedIntervalStringException('Invalid interval string');
}
return $interval;
Expand Down
2 changes: 1 addition & 1 deletion src/RateLimit/Storage/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function set(string $type, string $identifier, RateLimitData $data): bool
'expires' => time() + $this->ttl,
'data' => self::jsonEncode($data),
]);
return file_put_contents($file, $bundled) !== false;
return file_put_contents($file, $bundled, LOCK_EX) !== false;
}

/**
Expand Down
Loading
Loading