Skip to content

Commit 5265549

Browse files
authored
Add test that covers the new mutants (#26)
* Add test that covers the new mutants * php-cs-fixer * Test it more directly * Test outside the trait test * Nits * Add more exclusions
1 parent 4ac343b commit 5265549

5 files changed

Lines changed: 166 additions & 10 deletions

File tree

infection.json.dist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"FediE2EE\\PKD\\Features\\FetchTrait::fetchUnverifiedAuxData",
2121
"FediE2EE\\PKD\\Features\\FetchTrait::fetchUnverifiedAuxDataByID",
2222
"FediE2EE\\PKD\\Features\\FetchTrait::fetchRecentMerkleRoot",
23+
"FediE2EE\\PKD\\Features\\PublishTrait::preamble",
2324
"FediE2EE\\PKD\\Features\\VerifyTrait::fetchPublicKeys",
25+
"FediE2EE\\PKD\\Features\\VerifyTrait::fetchAuxDataByID",
2426
"FediE2EE\\PKD\\Features\\VerifyTrait::fetchAuxData"
2527
]
2628
},
@@ -72,17 +74,25 @@
7274
},
7375
"IncrementInteger": {
7476
"ignore": [
77+
"FediE2EE\\PKD\\Features\\VerifyTrait",
7578
"FediE2EE\\PKD\\Features\\APTrait::ensureHttpClientConfigured"
7679
]
7780
},
7881
"DecrementInteger": {
7982
"ignore": [
83+
"FediE2EE\\PKD\\Features\\VerifyTrait",
8084
"FediE2EE\\PKD\\Features\\APTrait::ensureHttpClientConfigured",
8185
"FediE2EE\\PKD\\Features\\VerifyTrait::verifyInclusionProofInternal"
8286
]
8387
},
88+
"Continue_": {
89+
"ignore": [
90+
"FediE2EE\\PKD\\Features\\VerifyTrait"
91+
],
92+
},
8493
"MatchArmRemoval": {
8594
"ignore": [
95+
"FediE2EE\\PKD\\Features\\VerifyTrait",
8696
"FediE2EE\\PKD\\Features\\PublishTrait::getInternalHpke"
8797
]
8898
},

src/Features/FetchTrait.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
use FediE2EE\PKD\Extensions\ExtensionException;
88
use FediE2EE\PKD\Values\AuxData;
99
use FediE2EE\PKD\Crypto\Exceptions\{
10+
CryptoException,
1011
HttpSignatureException,
1112
JsonException,
1213
NetworkException,
13-
NotImplementedException
14-
};
14+
NotImplementedException};
1515
use GuzzleHttp\Exception\GuzzleException;
16+
use ParagonIE\Certainty\Exception\CertaintyException;
1617
use SodiumException;
1718
use function is_array, is_null, is_string, urlencode;
1819

@@ -32,7 +33,9 @@ trait FetchTrait
3233
*
3334
* @return PublicKey[]
3435
*
36+
* @throws CertaintyException
3537
* @throws ClientException
38+
* @throws CryptoException
3639
* @throws GuzzleException
3740
* @throws HttpSignatureException
3841
* @throws JsonException

src/Features/VerifyTrait.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ protected function parseInclusionProof(array $row): InclusionProof
392392
* @throws ClientException If the format is invalid
393393
* Supported $hashFunction 'sha256', 'sha384', 'sha512', 'blake2b'
394394
*/
395-
protected function decodeMerkleRoot(string $merkleRoot, string $hashFunction): string
395+
public function decodeMerkleRoot(string $merkleRoot, string $hashFunction): string
396396
{
397397
$prefix = 'pkd-mr-v1:';
398398
if (!str_starts_with($merkleRoot, $prefix)) {
@@ -403,11 +403,10 @@ protected function decodeMerkleRoot(string $merkleRoot, string $hashFunction): s
403403
$decoded = Base64UrlSafe::decodeNoPadding($encoded);
404404

405405
$expectedByteLen = match ($hashFunction) {
406-
'sha256' => 32,
406+
'blake2b', 'sha256' => 32,
407407
'sha384' => 48,
408408
'sha512' => 64,
409-
'blake2b' => 32, // variable-length 8 to 512 bits (1 to 64 bytes) but 32 minimum for safety.
410-
default => 32, // Fallback to 32 bytes as a minimum but $hashFunc should never be null.
409+
default => throw new ClientException("Hash function not in allow list"),
411410
};
412411

413412
if (strlen($decoded) < $expectedByteLen) {

tests/unit/Features/VerifyTraitTest.php

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use GuzzleHttp\HandlerStack;
2222
use ParagonIE\ConstantTime\Base64UrlSafe;
2323
use PHPUnit\Framework\Attributes\CoversClass;
24+
use PHPUnit\Framework\Attributes\DataProvider;
2425
use PHPUnit\Framework\Attributes\Group;
2526
use PHPUnit\Framework\TestCase;
2627
use Random\RandomException;
@@ -677,10 +678,12 @@ public function getAuxDataType(): string
677678
{
678679
return 'test-type';
679680
}
681+
680682
public function getRejectionReason(): string
681683
{
682684
return 'Invalid';
683685
}
686+
684687
public function isValid(string $auxData): bool
685688
{
686689
return true;
@@ -749,10 +752,12 @@ public function getAuxDataType(): string
749752
{
750753
return 'test-type';
751754
}
755+
752756
public function getRejectionReason(): string
753757
{
754758
return 'Invalid';
755759
}
760+
756761
public function isValid(string $auxData): bool
757762
{
758763
return true;
@@ -812,10 +817,12 @@ public function getAuxDataType(): string
812817
{
813818
return 'test-type';
814819
}
820+
815821
public function getRejectionReason(): string
816822
{
817823
return 'Invalid';
818824
}
825+
819826
public function isValid(string $auxData): bool
820827
{
821828
return true;
@@ -884,10 +891,12 @@ public function getAuxDataType(): string
884891
{
885892
return 'wanted-type';
886893
}
894+
887895
public function getRejectionReason(): string
888896
{
889897
return 'Invalid';
890898
}
899+
891900
public function isValid(string $auxData): bool
892901
{
893902
return true;
@@ -972,10 +981,12 @@ public function getAuxDataType(): string
972981
{
973982
return 'test-type';
974983
}
984+
975985
public function getRejectionReason(): string
976986
{
977987
return 'Invalid';
978988
}
989+
979990
public function isValid(string $auxData): bool
980991
{
981992
return true;
@@ -1182,7 +1193,7 @@ public function testFetchPublicKeysTreeSizeCoercion(): void
11821193
'leaf-index' => $proof->index,
11831194
]],
11841195
'merkle-root' => $merkleRoot,
1185-
'tree-size' => (string) $tree->getSize(),
1196+
'tree-size' => (string)$tree->getSize(),
11861197
],
11871198
'fedi-e2ee:v1/api/actor/get-keys'
11881199
);
@@ -1343,10 +1354,12 @@ public function getAuxDataType(): string
13431354
{
13441355
return 'test-type';
13451356
}
1357+
13461358
public function getRejectionReason(): string
13471359
{
13481360
return 'Invalid';
13491361
}
1362+
13501363
public function isValid(string $auxData): bool
13511364
{
13521365
return true;
@@ -1398,10 +1411,12 @@ public function getAuxDataType(): string
13981411
{
13991412
return 'test-type';
14001413
}
1414+
14011415
public function getRejectionReason(): string
14021416
{
14031417
return 'Invalid';
14041418
}
1419+
14051420
public function isValid(string $auxData): bool
14061421
{
14071422
return true;
@@ -1454,10 +1469,12 @@ public function getAuxDataType(): string
14541469
{
14551470
return 'test-type';
14561471
}
1472+
14571473
public function getRejectionReason(): string
14581474
{
14591475
return 'Invalid';
14601476
}
1477+
14611478
public function isValid(string $auxData): bool
14621479
{
14631480
return true;
@@ -1499,7 +1516,7 @@ public function isValid(string $auxData): bool
14991516
'leaf-index' => $proof->index
15001517
]],
15011518
'merkle-root' => $merkleRoot,
1502-
'tree-size' => (string) $tree->getSize()
1519+
'tree-size' => (string)$tree->getSize()
15031520
],
15041521
'fedi-e2ee:v1/api/actor/aux-info'
15051522
);
@@ -1563,10 +1580,12 @@ public function getAuxDataType(): string
15631580
{
15641581
return 'test-type';
15651582
}
1583+
15661584
public function getRejectionReason(): string
15671585
{
15681586
return 'Invalid';
15691587
}
1588+
15701589
public function isValid(string $auxData): bool
15711590
{
15721591
return true;
@@ -1602,4 +1621,74 @@ public function isValid(string $auxData): bool
16021621

16031622
$client->fetchAuxData('alice@example.com', 'test-type');
16041623
}
1624+
1625+
public static function hashFunctionProvider(): array
1626+
{
1627+
return [
1628+
['blake2b'],
1629+
['sha256'],
1630+
['sha384'],
1631+
['sha512'],
1632+
];
1633+
}
1634+
1635+
/**
1636+
* @throws ClientException
1637+
* @throws CryptoException
1638+
* @throws NotImplementedException
1639+
* @throws SodiumException
1640+
*/
1641+
#[DataProvider("hashFunctionProvider")]
1642+
public function testHardCodedHashLengths(string $hashFunc): void
1643+
{
1644+
if ($hashFunc === 'blake2b') {
1645+
$expectedHashLength = 32;
1646+
} else {
1647+
$expectedHashLength = strlen(hash($hashFunc, '', true));
1648+
}
1649+
// strlen("pkd-mr-v1:") == 10
1650+
$expectedEncodedLength = 10 + (int) ceil($expectedHashLength * 4 / 3);
1651+
$serverPk = $this->serverKey->getPublicKey();
1652+
$client = new ReadOnlyClient('http://pkd.test', $serverPk);
1653+
1654+
// Build a simple tree with known leaves
1655+
$leaves = ['leaf1', 'leaf2', 'leaf3', 'leaf4'];
1656+
$tree = new Tree($leaves, $hashFunc);
1657+
$rawRoot = $tree->getRoot();
1658+
$this->assertNotNull($rawRoot);
1659+
$this->assertSame($expectedHashLength, strlen($rawRoot));
1660+
$merkleRoot = $tree->getEncodedRoot();
1661+
$this->assertSame($expectedEncodedLength, strlen($merkleRoot));
1662+
1663+
// We need to actually fail if the code is mutated:
1664+
$proof = $tree->getInclusionProof('leaf1');
1665+
$result = $client->verifyInclusionProof(
1666+
$hashFunc,
1667+
$merkleRoot,
1668+
'leaf1',
1669+
$proof,
1670+
$tree->getSize()
1671+
);
1672+
$this->assertTrue($result, 'Inclusion proof verification failed');
1673+
}
1674+
1675+
public static function merkleRootProvider(): array
1676+
{
1677+
return [
1678+
['blake2b', 32],
1679+
['sha256', 32],
1680+
['sha384', 48],
1681+
['sha512', 64],
1682+
];
1683+
}
1684+
1685+
#[DataProvider("merkleRootProvider")]
1686+
public function testDecodeMerkleRoot(string $hashFunc, int $zeroes): void
1687+
{
1688+
$serverPk = $this->serverKey->getPublicKey();
1689+
$client = new ReadOnlyClient('http://pkd.test', $serverPk);
1690+
$encoded = (new Tree([], $hashFunc))->getEncodedRoot();
1691+
$out = $client->decodeMerkleRoot($encoded, $hashFunc);
1692+
$this->assertSame($zeroes, strlen($out));
1693+
}
16051694
}

0 commit comments

Comments
 (0)