From 7865203fee68dcdfa39ab037c31db9583b3a66bc Mon Sep 17 00:00:00 2001 From: Leopold Joy Date: Fri, 24 Jul 2026 21:33:19 +0100 Subject: [PATCH] fix: validate cached ancestor expiry Co-authored-by: OpenCode --- src/CertManager.sol | 4 +++- test/CertManager.t.sol | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/CertManager.sol b/src/CertManager.sol index 29ecfd0..6a38a80 100644 --- a/src/CertManager.sol +++ b/src/CertManager.sol @@ -244,6 +244,9 @@ contract CertManager is ICertManager { function _requireCachedChainNotRevoked(bytes32 certHash) internal view { while (certHash != bytes32(0)) { _requireNotRevoked(_revocationKey(certHash)); + VerifiedCert memory cert = _loadVerified(certHash); + if (cert.pubKey.length == 0) revert IncompleteCertChain(); + require(!_certificateExpired(cert.notAfter), "cert expired"); if (certHash == ROOT_CA_CERT_HASH) { return; } @@ -267,7 +270,6 @@ contract CertManager is ICertManager { parent = _loadVerified(parentCertHash); require(parent.pubKey.length > 0, "parent cert unverified"); _requireCachedChainNotRevoked(parentCertHash); - require(!_certificateExpired(parent.notAfter), "parent cert expired"); require(parent.ca, "parent cert is not a CA"); require(!ca || parent.maxPathLen != 0, "maxPathLen exceeded"); } diff --git a/test/CertManager.t.sol b/test/CertManager.t.sol index 6db1293..33426e7 100644 --- a/test/CertManager.t.sol +++ b/test/CertManager.t.sol @@ -635,6 +635,13 @@ contract RevocationChainHarness is CertManager { verifiedParent[child] = parent; } + function setCachedCert(bytes32 certHash, uint64 notAfter) external { + _saveVerified( + certHash, + VerifiedCert({ca: true, notAfter: notAfter, maxPathLen: -1, subjectHash: bytes32(0), pubKey: new bytes(96)}) + ); + } + function requireCachedChainNotRevoked(bytes32 certHash) external view { _requireCachedChainNotRevoked(certHash); } @@ -655,15 +662,34 @@ contract RequireCachedChainNotRevokedTest is Test { function test_PassesWhenChainReachesPinnedRoot() public { bytes32 root = cm.ROOT_CA_CERT_HASH(); + cm.setCachedCert(CHILD, type(uint64).max); + cm.setCachedCert(PARENT, type(uint64).max); cm.setParent(CHILD, PARENT); cm.setParent(PARENT, root); // Walks CHILD -> PARENT -> ROOT and returns without reverting. cm.requireCachedChainNotRevoked(CHILD); } + function test_RevertsWhenCachedGrandparentIsExpired() public { + vm.warp(1000); + bytes32 grandparent = bytes32(uint256(3)); + + cm.setCachedCert(CHILD, 2000); + cm.setCachedCert(PARENT, 2000); + cm.setCachedCert(grandparent, 999); + cm.setParent(CHILD, PARENT); + cm.setParent(PARENT, grandparent); + cm.setParent(grandparent, cm.ROOT_CA_CERT_HASH()); + + vm.expectRevert("cert expired"); + cm.requireCachedChainNotRevoked(CHILD); + } + function test_RevertsWhenChainDoesNotReachRoot() public { // verifiedParent[PARENT] is unset (bytes32(0)), so the chain is broken: it can never // reach ROOT_CA_CERT_HASH. The fixed function must fail closed instead of returning. + cm.setCachedCert(CHILD, type(uint64).max); + cm.setCachedCert(PARENT, type(uint64).max); cm.setParent(CHILD, PARENT); vm.expectRevert(CertManager.IncompleteCertChain.selector); cm.requireCachedChainNotRevoked(CHILD);