Skip to content

Commit befa167

Browse files
committed
Add validation for unique KeyIds in certificate chain
1 parent 4483435 commit befa167

File tree

3 files changed

+69
-13
lines changed

3 files changed

+69
-13
lines changed

SPECIFICATION.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ Reminder: This matrix validates only the end‑entity subset requirement. The is
120120
Notes
121121
- A certificate with `ROOT_CA` must be self‑signed, but it may also carry `INTERMEDIATE_CA` or `CA` (or both).
122122
- The presence of CA‑level flags does not prevent a certificate from also carrying end‑entity flags; those end‑entity bits govern what end‑entity flags it may delegate to subjects, not necessarily whether it acts as an end‑entity itself.
123+
- **Certificate uniqueness**: Each certificate in a chain must have a unique `KeyId`. Self‑signed `ROOT_CA` certificates can only appear as the final (root) certificate in a chain, never in the middle.
124+
- **End‑entity inheritance**: The subset rule (`Child.EndEntity ⊆ Issuer.EndEntity`) applies to all certificate pairs in a chain without exception, including self‑signed certificates.
123125

124126
---
125127

@@ -128,12 +130,13 @@ Notes
128130
1. Verify structure and lengths.
129131
2. Ensure each certificate has at least one signature.
130132
3. Compute `KeyId` as `SHA-256(PubKey)[0..15]` and verify it matches the embedded value.
131-
4. Build a path from leaf to a trusted root by matching `SignKeyId` to parent `KeyId`.
132-
5. For each child/parent pair (issuer = parent):
133+
4. Verify that all certificates in the chain have unique `KeyId` values. No two certificates in the same chain may share the same `KeyId`.
134+
5. Build a path from leaf to a trusted root by matching `SignKeyId` to parent `KeyId`.
135+
6. For each child/parent pair (issuer = parent):
133136
- For non-CA children: Issuer must have `CA`.
134137
- For CA-level children (has any of `ROOT_CA`, `INTERMEDIATE_CA`, `CA`): issuer must have `INTERMEDIATE_CA`.
135-
- End‑entity inheritance: For each end‑entity bit (0x0100 through 0x8000), if child has it, issuer must also have it (`Child.EndEntity ⊆ Issuer.EndEntity`).
136-
6. A certificate with `ROOT_CA` must be self‑signed and present in the trust store.
138+
- End‑entity inheritance: For each end‑entity bit (0x0100 through 0x8000), if child has it, issuer must also have it (`Child.EndEntity ⊆ Issuer.EndEntity`). This validation applies to **all** certificate pairs without exception.
139+
7. A certificate with `ROOT_CA` must be self‑signed and present in the trust store.
137140

138141
---
139142

src/Validator.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ private static function validatePath(array $path, TrustStore $store): array
114114
}
115115
}
116116

117+
// Verify that all certificates in the path have unique KeyIds
118+
// SPECIFICATION.md: Chain validation algorithm — certificates in a chain must have unique KeyIds
119+
$seenKeyIds = [];
120+
foreach ($path as $certificate) {
121+
$keyIdString = $certificate->key->id->toString();
122+
if (in_array($keyIdString, $seenKeyIds, true)) {
123+
$errors[] = new ValidationError('Certificate chain contains duplicate KeyIds', $certificate, 'unique key id validation');
124+
return ['isValid' => false, 'errors' => $errors, 'warnings' => $warnings];
125+
}
126+
$seenKeyIds[] = $keyIdString;
127+
}
128+
117129
// Verify the last certificate in path is a root CA
118130
$rootCert = end($path);
119131
if (!$rootCert->isRootCA()) {
@@ -263,11 +275,6 @@ private static function validateEndEntityFlagInheritance(Certificate $certificat
263275
{
264276
$errors = [];
265277

266-
// ROOT_CA certificates (self-signed) can have any end-entity flags
267-
if ($signer->isRootCA() && $certificate->key->id->equals($signer->key->id)) {
268-
return ['isValid' => true, 'errors' => $errors];
269-
}
270-
271278
// Get end-entity flags for both certificates
272279
// SPECIFICATION.md: "End‑Entity Inheritance Matrix" and
273280
// "End‑entity flags (non‑CA) inheritance"

tests/ValidatorTest.php

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -824,9 +824,9 @@ public function testEndEntityFlagsMustBeSubsetOfSigner()
824824
$this->assertTrue(array_any($messages, fn ($m) => str_contains($m, 'Certificate end-entity flags must be a subset of signer')));
825825
}
826826

827-
public function testRootSelfSignedAllowsAnyEndEntityFlagsForSameKey()
827+
public function testDuplicateKeyIdsInChainAreRejected()
828828
{
829-
// Hit the early return in validateEndEntityFlagInheritance when signer is self-signed ROOT_CA
829+
// Test that chains with duplicate KeyIds are rejected
830830
$rootKey = Ed25519::makeKeyPair();
831831

832832
// Root certificate: ROOT_CA + CA, self-signed
@@ -840,9 +840,9 @@ public function testRootSelfSignedAllowsAnyEndEntityFlagsForSameKey()
840840
$rootSelfSig = Signature::make($rootCertBase->toBinaryForSigning(), $rootKey);
841841
$rootCert = $rootCertBase->with(signatures: [$rootSelfSig]);
842842

843-
// Child certificate that uses the SAME key id/public key as root, with an end-entity flag
843+
// Child certificate that uses the SAME key id/public key as root (duplicate KeyId)
844844
$childBase = new Certificate(
845-
key: $rootKey->toPublicKey(),
845+
key: $rootKey->toPublicKey(), // Same key = same KeyId
846846
description: 'child_same_key',
847847
userDescriptors: [new UserDescriptor(DescriptorType::USERNAME, 'child')],
848848
flags: CertificateFlagsCollection::fromList([CertificateFlag::END_ENTITY_FLAG_1]),
@@ -858,6 +858,52 @@ public function testRootSelfSignedAllowsAnyEndEntityFlagsForSameKey()
858858

859859
$trustStore = new TrustStore([$rootCert]);
860860

861+
$result = Validator::validateChain($chain, $trustStore);
862+
$this->assertFalse($result->isValid);
863+
$messages = $result->getErrorMessages();
864+
$this->assertTrue(array_any($messages, fn ($m) => str_contains($m, 'Certificate chain contains duplicate KeyIds')));
865+
}
866+
867+
public function testEndEntityFlagInheritanceValidatedForAllCertificates()
868+
{
869+
// Test that end-entity flag inheritance is validated for all certificates, no exceptions
870+
$root_ca = $this->makeTestCert('root_ca', [CertificateFlag::ROOT_CA, CertificateFlag::INTERMEDIATE_CA, CertificateFlag::CA], ['root_ca']);
871+
$intermediate_ca = $this->makeTestCert('intermediate_ca', [CertificateFlag::INTERMEDIATE_CA, CertificateFlag::CA, CertificateFlag::END_ENTITY_FLAG_1], ['root_ca']);
872+
$end_entity = $this->makeTestCert('end_entity', [CertificateFlag::END_ENTITY_FLAG_1, CertificateFlag::END_ENTITY_FLAG_2], ['intermediate_ca']);
873+
874+
$chain = new Chain([
875+
$end_entity,
876+
$intermediate_ca,
877+
$root_ca,
878+
]);
879+
880+
$trustStore = new TrustStore([
881+
$root_ca,
882+
]);
883+
884+
$result = Validator::validateChain($chain, $trustStore);
885+
$this->assertFalse($result->isValid);
886+
$messages = $result->getErrorMessages();
887+
$this->assertTrue(array_any($messages, fn ($m) => str_contains($m, 'Certificate end-entity flags must be a subset of signer')));
888+
}
889+
890+
public function testUniqueKeyIdsAllowValidChain()
891+
{
892+
// Test that chains with unique KeyIds work correctly
893+
$root_ca = $this->makeTestCert('root_ca', [CertificateFlag::ROOT_CA, CertificateFlag::INTERMEDIATE_CA, CertificateFlag::CA, CertificateFlag::END_ENTITY_FLAG_1], ['root_ca']);
894+
$intermediate_ca = $this->makeTestCert('intermediate_ca', [CertificateFlag::INTERMEDIATE_CA, CertificateFlag::CA, CertificateFlag::END_ENTITY_FLAG_1], ['root_ca']);
895+
$end_entity = $this->makeTestCert('end_entity', [CertificateFlag::END_ENTITY_FLAG_1], ['intermediate_ca']);
896+
897+
$chain = new Chain([
898+
$end_entity,
899+
$intermediate_ca,
900+
$root_ca,
901+
]);
902+
903+
$trustStore = new TrustStore([
904+
$root_ca,
905+
]);
906+
861907
$result = Validator::validateChain($chain, $trustStore);
862908
$this->assertTrue($result->isValid);
863909
}

0 commit comments

Comments
 (0)