Skip to content

Commit 607d4a6

Browse files
leanthebeanclaude
andauthored
fix: fail closed on broken cached cert chain (L-01) (#46)
Address BLOCKSEC-5249 finding L-01. Generated with Claude Code Co-authored-by: Claude <noreply@anthropic.com>
1 parent e41a74a commit 607d4a6

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

src/CertManager.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ contract CertManager is ICertManager {
232232
}
233233
certHash = verifiedParent[certHash];
234234
}
235+
// Fail closed: a chain that terminates at bytes32(0) without reaching the pinned root is
236+
// broken and must not be treated as a verified, non-revoked chain. Reverting here instead
237+
// of returning silently means revocation safety never depends on upstream guards.
238+
revert("incomplete cert chain");
235239
}
236240

237241
function _verifyCert(

test/CertManager.t.sol

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,52 @@ contract CertManagerTest is Test {
346346
return der;
347347
}
348348
}
349+
350+
/// @dev Exposes the internal revocation-chain walk and lets tests seed the `verifiedParent`
351+
/// cache directly so the broken-chain (fail-closed) behaviour can be exercised in isolation.
352+
contract RevocationChainHarness is CertManager {
353+
constructor() CertManager(new P384Verifier()) {}
354+
355+
function setParent(bytes32 child, bytes32 parent) external {
356+
verifiedParent[child] = parent;
357+
}
358+
359+
function requireCachedChainNotRevoked(bytes32 certHash) external view {
360+
_requireCachedChainNotRevoked(certHash);
361+
}
362+
}
363+
364+
/// @dev Regression coverage for BLOCKSEC-5249 finding L-01: `_requireCachedChainNotRevoked`
365+
/// previously fell through and returned silently when a cached chain terminated at
366+
/// bytes32(0) without reaching the pinned root, i.e. it failed open on a broken chain.
367+
contract RequireCachedChainNotRevokedTest is Test {
368+
RevocationChainHarness internal cm;
369+
370+
bytes32 internal constant CHILD = bytes32(uint256(1));
371+
bytes32 internal constant PARENT = bytes32(uint256(2));
372+
373+
function setUp() public {
374+
cm = new RevocationChainHarness();
375+
}
376+
377+
function test_PassesWhenChainReachesPinnedRoot() public {
378+
bytes32 root = cm.ROOT_CA_CERT_HASH();
379+
cm.setParent(CHILD, PARENT);
380+
cm.setParent(PARENT, root);
381+
// Walks CHILD -> PARENT -> ROOT and returns without reverting.
382+
cm.requireCachedChainNotRevoked(CHILD);
383+
}
384+
385+
function test_RevertsWhenChainDoesNotReachRoot() public {
386+
// verifiedParent[PARENT] is unset (bytes32(0)), so the chain is broken: it can never
387+
// reach ROOT_CA_CERT_HASH. The fixed function must fail closed instead of returning.
388+
cm.setParent(CHILD, PARENT);
389+
vm.expectRevert("incomplete cert chain");
390+
cm.requireCachedChainNotRevoked(CHILD);
391+
}
392+
393+
function test_RevertsOnZeroCertHash() public {
394+
vm.expectRevert("incomplete cert chain");
395+
cm.requireCachedChainNotRevoked(bytes32(0));
396+
}
397+
}

0 commit comments

Comments
 (0)