From 3242d80bf9a735cf989208d61a6e228954e57516 Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Thu, 18 Jun 2026 13:18:31 -0700 Subject: [PATCH] fix: canonicalize certificate cache keys Key non-root certificate verification cache entries by the signed tbsCertificate instead of the full DER bytes so ECDSA signature malleability cannot create shadow cache identities. Keep the pinned root under ROOT_CA_CERT_HASH and reject malleable aliases of that trust anchor. Add regression coverage for malleated certificate signatures and update docs/scripts to use the returned cache key. --- README.md | 11 +- docs/hinted-p384-nitro-attestation.md | 48 ++++---- script/BaseSepoliaDemo.s.sol | 11 +- src/CertManager.sol | 34 +++++- src/ICertManager.sol | 4 +- test/CertManager.t.sol | 145 ++++++++++++++++++++++- test/hinted/HintedNitroAttestation.t.sol | 13 +- 7 files changed, 230 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 1bf8312..3f455c9 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,10 @@ own document signature. `ROOT_CA_CERT_HASH` / `keccak256(rootCert)` for the first non-root CA; `0` is only for the pinned root itself) - `certManager.verifyClientCertWithHints(leafCert, parentCertHash, hints)` + Use each CA verification's returned hash as the `parentCertHash` for the next child. For non-root + certs this cache key is `keccak256(tbsCertificate)`, not `keccak256(certBytes)`, so malleable + signature encodings share one cache entry. Leaf verification returns metadata, not a child + `parentCertHash`. 2. **Validation — verify the document signature.** Once the chain is cached: - `validator.validateAttestationWithHints(attestationTbs, signature, attestationHints)` - **Precondition:** the whole `cabundle` + leaf must already be cached from phase 1. This call @@ -93,8 +97,8 @@ contains a revoked certificate. `ROOT_CA_CERT_HASH` as an emergency global halt (the root is identified by its pinned hash, since it is never parsed on-chain). - The revoker can call `revokeCert` or `revokeCerts` for non-root certificate identity keys. -- `loadVerified` is a raw cache read; returned metadata does not imply the certificate is - currently trusted. +- `loadVerified` is a raw cache read by verification cache key; returned metadata does not imply + the certificate is currently trusted. ### Example consumer @@ -164,7 +168,8 @@ integrator (see [docs](docs/hinted-p384-nitro-attestation.md#integrator-responsi to `block.timestamp` (seconds) or match the `nonce` to a challenge. Enforce freshness yourself if you need it. - **Signature malleability** — low-S is not enforced (AWS does not emit low-S), so the malleable - twin `(r, n-s)` also verifies. Never use the signature as a uniqueness key; dedupe on canonical + twin `(r, n-s)` also verifies. Certificate caching excludes the outer signature bytes, but + integrators must still never use attestation signatures as uniqueness keys; dedupe on canonical attestation fields instead. - **Enclave policy** — checking `pcrs` / `moduleID` against the enclave image(s) you trust is your responsibility. diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md index f71b3cb..d2424f4 100644 --- a/docs/hinted-p384-nitro-attestation.md +++ b/docs/hinted-p384-nitro-attestation.md @@ -361,8 +361,9 @@ document signature. Once the leaf and its CA chain are cached and unexpired, a later attestation signed by the same leaf is a **single transaction** (`validateAttestationWithHints`) carrying only the COSE signature hints. The cabundle certs are not re-verified — they are -reloaded by `keccak256(cert)` identity, checked against their original cached parent, -and their cached metadata is re-checked. +reloaded by verification cache key (`ROOT_CA_CERT_HASH` for the pinned root, +`keccak256(tbsCertificate)` for non-root certs), checked against their original cached +parent, and their cached metadata is re-checked. Practical reuse cases: @@ -371,12 +372,12 @@ Practical reuse cases: document → 2 transactions; - **CA chain and leaf cached:** validate the document only → 1 transaction. -**Cache reuse** is allowed when: the submitted DER hashes to a cached cert; the cert -is unexpired (`notAfter ≥ block.timestamp`); the cached CA/client role matches; and -`parentCertHash` matches the parent used during cold verification; and neither the cert -nor its cached parent chain is revoked. The cache is global on-chain state — once any -caller verifies a cert, others reuse it until expiry or revocation, but only under the -same parent binding. +**Cache reuse** is allowed when: the submitted DER contains a TBS that hashes to a +cached cert; the cert is unexpired (`notAfter ≥ block.timestamp`); the cached CA/client +role matches; `parentCertHash` matches the parent used during cold verification; and +neither the cert nor its cached parent chain is revoked. The cache is global on-chain +state — once any caller verifies a cert, others reuse it until expiry or revocation, +but only under the same parent binding. ### Revocation model AWS's Nitro attestation documentation disables CRL checking in its sample validation @@ -410,23 +411,24 @@ cached descendant cannot keep verifying through an ancestor that was later revok Revocation is checked independently of `notAfter`, so a revoked cert is untrusted even if its X.509 validity period has not expired. -`loadVerified` is intentionally a raw cache read. A non-empty return value means the cert -metadata was cached previously; it does not imply the cert is currently trusted, unexpired, -or unrevoked. +`loadVerified` is intentionally a raw cache read by verification cache key. A non-empty +return value means the cert metadata was cached previously; it does not imply the cert is +currently trusted, unexpired, or unrevoked. **First-verified parent pinning.** A cached cert is pinned to the parent it was first -verified under: cold verification records `verifiedParent[certHash]` once, and every -later warm reuse requires the caller to present that exact `parentCertHash` (a mismatch -reverts with `parent cert mismatch`). This is a deliberate, conservative binding — warm -reuse skips signature verification, so it must reflect the precise chain that was -cryptographically checked, not merely *a* valid same-subject issuer. The liveness -consequence is that if the same certificate is genuinely issued under two different CA -objects (for example a same-key CA renewal that produces new DER bytes, hence a new -parent hash), the cached leaf keeps verifying only through its first parent; a second -caller chaining it through the renewed parent must wait for the cached entry to expire. -For AWS Nitro this is effectively a non-issue because leaf certificates are short-lived -(~3h) and expire long before their issuing CA is rotated, so the binding self-heals; it -is documented here as a known edge rather than a fixed bug. +verified under: cold verification records `verifiedParent[certHash]` once, where +`certHash` is the canonical cache key, and every later warm reuse requires the caller to +present that exact `parentCertHash` (a mismatch reverts with `parent cert mismatch`). +This is a deliberate, conservative binding — warm reuse skips signature verification, +so it must reflect the precise chain that was cryptographically checked, not merely *a* +valid same-subject issuer. The liveness consequence is that if the same certificate is +genuinely issued under two different CA objects (for example a same-key CA renewal that +produces a new TBS, hence a new parent hash), the cached leaf keeps verifying only +through its first parent; a second caller chaining it through the renewed parent must +wait for the cached entry to expire. For AWS Nitro this is effectively a non-issue +because leaf certificates are short-lived (~3h) and expire long before their issuing CA +is rotated, so the binding self-heals; it is documented here as a known edge rather than +a fixed bug. **Warm-only guard.** `validateAttestationWithHints` re-runs the cabundle checks with an *empty* hint stream. Cached certs return before signature verification; a missing cert diff --git a/script/BaseSepoliaDemo.s.sol b/script/BaseSepoliaDemo.s.sol index 067aa8a..c18241c 100644 --- a/script/BaseSepoliaDemo.s.sol +++ b/script/BaseSepoliaDemo.s.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.26; import {Script, console2} from "forge-std/Script.sol"; +import {Asn1Decode, LibAsn1Ptr, Asn1Ptr} from "../src/Asn1Decode.sol"; import {CborDecode, CborElement, LibCborElement} from "../src/CborDecode.sol"; import {CertManagerDemo} from "../test/helpers/CertManagerDemo.sol"; import {ICertManager} from "../src/ICertManager.sol"; @@ -20,7 +21,9 @@ contract NitroValidatorScriptParser is NitroValidator { /// @dev Uses vm.ffi to run the off-chain hint tools; invoke the script with Foundry's `--ffi` flag. contract BaseSepoliaDemo is Script { + using Asn1Decode for bytes; using CborDecode for bytes; + using LibAsn1Ptr for Asn1Ptr; using LibBytes for bytes; using LibCborElement for CborElement; @@ -97,7 +100,13 @@ contract BaseSepoliaDemo is Script { leaf = certManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); require(leaf.pubKey.length > 0, "leaf not cached"); console2.log("cached client cert"); - console2.logBytes32(keccak256(clientCert)); + console2.logBytes32(_certCacheKey(clientCert)); + } + + function _certCacheKey(bytes memory certificate) internal pure returns (bytes32) { + Asn1Ptr root = certificate.root(); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + return certificate.keccak(tbsCertPtr.header(), tbsCertPtr.totalLength()); } function _loadAttestation() internal returns (bytes memory) { diff --git a/src/CertManager.sol b/src/CertManager.sol index 1a5c0e2..f1f997b 100644 --- a/src/CertManager.sol +++ b/src/CertManager.sol @@ -48,7 +48,8 @@ contract CertManager is ICertManager { bytes32 public constant BASIC_CONSTRAINTS_OID = 0x6351d72a43cb42fb9a2531a28608c278c89629f8f025b5f5dc705f3fe45e950a; // keccak256(hex"551d13") bytes32 public constant KEY_USAGE_OID = 0x45529d8772b07ebd6d507a1680da791f4a2192882bf89d518801579f7a5167d2; // keccak256(hex"551d0f") - // certHash -> VerifiedCert + // certHash -> VerifiedCert. The root is keyed by ROOT_CA_CERT_HASH; every non-root cert is keyed + // by keccak256(tbsCertificate), excluding the outer malleable ECDSA signature bytes. mapping(bytes32 => bytes) public verified; // certHash -> parent cert hash used during cold verification. // A cached cert is pinned to the parent it was FIRST verified under: warm reuse requires the @@ -120,12 +121,13 @@ contract CertManager is ICertManager { /// match the parent used during cold verification. On a cold cert, `signatureHints` must /// contain the real off-chain inverse hints for the cert signature; they are re-verified /// on-chain, so a wrong hint only reverts. Pass 0 only when submitting the pinned root; - /// otherwise pass the cached parent cert hash. + /// otherwise pass the cached parent cert hash. The returned hash is ROOT_CA_CERT_HASH for + /// the pinned root and keccak256(tbsCertificate) for every non-root cert. function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) external returns (bytes32) { - bytes32 certHash = keccak256(cert); + bytes32 certHash = _certCacheKey(cert); _verifyCert(cert, certHash, true, parentCertHash, signatureHints); return certHash; } @@ -138,12 +140,13 @@ contract CertManager is ICertManager { external returns (VerifiedCert memory) { - return _verifyCert(cert, keccak256(cert), false, parentCertHash, signatureHints); + return _verifyCert(cert, _certCacheKey(cert), false, parentCertHash, signatureHints); } /// @notice Return raw cached certificate metadata without current trust checks. /// @dev A non-empty return value only means the cert was cached previously. It may now be - /// expired or revoked; use the verification entrypoints for trust-aware reuse. + /// expired or revoked; use the verification entrypoints for trust-aware reuse. Pass the + /// cache key returned by the verification entrypoints. function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory) { return _loadVerified(certHash); } @@ -262,6 +265,9 @@ contract CertManager is ICertManager { bytes32 identity; (cert, identity) = _verifyUncachedCert(certificate, ca, parent, signatureHints); + // The pinned root is already present under ROOT_CA_CERT_HASH. Do not allow a signature + // malleability twin of that same trust anchor to become a second cached parent key. + require(!_isPinnedRootAlias(certHash, cert), "root cert alias"); // Reject by (issuer, serial) identity so a re-encoded twin of a revoked cert cannot pass. _requireNotRevoked(identity); _saveVerified(certHash, cert); @@ -273,6 +279,24 @@ contract CertManager is ICertManager { return cert; } + function _certCacheKey(bytes memory certificate) internal pure returns (bytes32) { + bytes32 rawCertHash = keccak256(certificate); + if (rawCertHash == ROOT_CA_CERT_HASH) { + return ROOT_CA_CERT_HASH; + } + + Asn1Ptr root = certificate.root(); + require(root.totalLength() == certificate.length, "invalid cert length"); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + return certificate.keccak(tbsCertPtr.header(), tbsCertPtr.totalLength()); + } + + function _isPinnedRootAlias(bytes32 certHash, VerifiedCert memory cert) internal pure returns (bool) { + return certHash != ROOT_CA_CERT_HASH && cert.ca && cert.subjectHash == ROOT_CA_CERT_SUBJECT_HASH + && cert.pubKey.length == ROOT_CA_CERT_PUB_KEY.length + && keccak256(cert.pubKey) == keccak256(ROOT_CA_CERT_PUB_KEY); + } + function _verifyUncachedCert( bytes memory certificate, bool ca, diff --git a/src/ICertManager.sol b/src/ICertManager.sol index 69a4a5c..0c22351 100644 --- a/src/ICertManager.sol +++ b/src/ICertManager.sol @@ -18,6 +18,8 @@ interface ICertManager { function revoked(bytes32 certHash) external view returns (bool); + /// @return The cache key to use as the parent for child certs: ROOT_CA_CERT_HASH for the pinned + /// root, otherwise keccak256(tbsCertificate). function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) external returns (bytes32); @@ -26,7 +28,7 @@ interface ICertManager { external returns (VerifiedCert memory); - /// @notice Raw cache read. A returned cert may be expired or revoked. + /// @notice Raw cache read by verification cache key. A returned cert may be expired or revoked. function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory); function isRevoked(bytes32 certHash) external view returns (bool); diff --git a/test/CertManager.t.sol b/test/CertManager.t.sol index 687e222..6df470c 100644 --- a/test/CertManager.t.sol +++ b/test/CertManager.t.sol @@ -5,6 +5,7 @@ import {Test, console} from "forge-std/Test.sol"; import {CertManager} from "../src/CertManager.sol"; import {ICertManager} from "../src/ICertManager.sol"; import {Asn1Decode, LibAsn1Ptr, Asn1Ptr} from "../src/Asn1Decode.sol"; +import {LibBytes} from "../src/LibBytes.sol"; import {P384Verifier} from "../src/P384Verifier.sol"; import {P384HintCollector} from "./helpers/HintedNitroTestHelpers.sol"; @@ -22,6 +23,10 @@ contract Asn1DecodeHarness { } contract CertManagerTest is Test { + using Asn1Decode for bytes; + using LibAsn1Ptr for Asn1Ptr; + using LibBytes for bytes; + Asn1DecodeHarness public harness; function setUp() public { @@ -71,6 +76,47 @@ contract CertManagerTest is Test { _verifyCA(cm, collector, CB3, parentHash); // reverts with "invalid sig" on unpatched code } + function test_VerifyCACertWithHints_MalleableSignatureUsesSameTbsCacheKey() public { + vm.warp(1775145600); + CertManager cm = new CertManager(new P384Verifier()); + P384HintCollector collector = new P384HintCollector(); + + bytes32 rootHash = keccak256(CB0); + assertEq(rootHash, cm.ROOT_CA_CERT_HASH(), "CB0 must be the pinned root"); + + bytes memory twin = _malleateCertSignature(CB1); + assertNotEq(keccak256(twin), keccak256(CB1), "malleated cert must have different raw bytes"); + assertEq(_tbsHash(twin), _tbsHash(CB1), "malleated cert must keep the signed TBS"); + + bytes memory parentPubKey = cm.loadVerified(rootHash).pubKey; + bytes memory twinHints = collector.collectCertSignatureHints(twin, parentPubKey); + + bytes32 twinKey = cm.verifyCACertWithHints(twin, rootHash, twinHints); + assertEq(twinKey, _tbsHash(CB1), "non-root certs are cached by TBS hash"); + assertNotEq(twinKey, keccak256(CB1), "cache key must exclude the malleable signature"); + + bytes32 canonicalKey = cm.verifyCACertWithHints(CB1, rootHash, ""); + assertEq(canonicalKey, twinKey, "canonical cert should hit the same warm cache entry"); + } + + function test_VerifyCACertWithHints_RejectsMalleableRootAlias() public { + vm.warp(1775145600); + CertManager cm = new CertManager(new P384Verifier()); + P384HintCollector collector = new P384HintCollector(); + + bytes32 rootHash = keccak256(CB0); + assertEq(rootHash, cm.ROOT_CA_CERT_HASH(), "CB0 must be the pinned root"); + + bytes memory rootTwin = _malleateCertSignature(CB0); + assertNotEq(keccak256(rootTwin), rootHash, "malleated root must have a different raw hash"); + + bytes memory rootPubKey = cm.loadVerified(rootHash).pubKey; + bytes memory hints = collector.collectCertSignatureHints(rootTwin, rootPubKey); + + vm.expectRevert("root cert alias"); + cm.verifyCACertWithHints(rootTwin, rootHash, hints); + } + function _verifyCA(CertManager cm, P384HintCollector collector, bytes memory cert, bytes32 parentHash) internal returns (bytes32) @@ -82,8 +128,8 @@ contract CertManagerTest is Test { // Cache-griefing liveness edge: `verifiedParent[certHash]` is written once on the cold // path (gated on cert.pubKey.length == 0) and never updated. If AWS renews an intermediate - // CA with the SAME signing key but a new validity window, the renewed cert has different - // DER bytes -> a different keccak256 -> a different parentCertHash in the chain. A leaf + // CA with the SAME signing key but a new validity window, the renewed cert has a different + // TBS -> a different cache key -> a different parentCertHash in the chain. A leaf // already cached under the old parent then permanently reverts "parent cert mismatch" // against the renewed parent, with no admin override. (The wrong-parent revert mechanism // itself is covered by HintedNitroAttestationTest.test_Hinted{CA,Client}CertRejectsCachedParentMismatch.) @@ -99,6 +145,101 @@ contract CertManagerTest is Test { ); } + function _tbsHash(bytes memory certificate) internal pure returns (bytes32) { + Asn1Ptr root = certificate.root(); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + return certificate.keccak(tbsCertPtr.header(), tbsCertPtr.totalLength()); + } + + function _malleateCertSignature(bytes memory certificate) internal pure returns (bytes memory result) { + (Asn1Ptr root, Asn1Ptr sigPtr, Asn1Ptr sigRoot, Asn1Ptr sigSPtr) = _certSignaturePtrs(certificate); + bytes memory twinS = _malleatedS(certificate, sigSPtr); + + int256 delta = int256(twinS.length) - int256(sigSPtr.totalLength()); + result = _replaceNode(certificate, sigSPtr, twinS, delta); + + _writeDerLength(result, root, _addDelta(root.length(), delta)); + _writeDerLength(result, sigPtr, _addDelta(sigPtr.length(), delta)); + _writeDerLength(result, sigRoot, _addDelta(sigRoot.length(), delta)); + } + + function _certSignaturePtrs(bytes memory certificate) + internal + pure + returns (Asn1Ptr root, Asn1Ptr sigPtr, Asn1Ptr sigRoot, Asn1Ptr sigSPtr) + { + root = certificate.root(); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(tbsCertPtr); + sigPtr = certificate.nextSiblingOf(sigAlgoPtr); + Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); + sigRoot = certificate.rootOf(sigBPtr); + Asn1Ptr sigRPtr = certificate.firstChildOf(sigRoot); + sigSPtr = certificate.nextSiblingOf(sigRPtr); + } + + function _malleatedS(bytes memory certificate, Asn1Ptr sigSPtr) internal pure returns (bytes memory) { + (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); + (uint128 twinHi, uint256 twinLo) = _p384OrderMinus(shi, slo); + return _derEncodeP384Integer(abi.encodePacked(twinHi, twinLo)); + } + + function _replaceNode(bytes memory certificate, Asn1Ptr ptr, bytes memory replacement, int256 delta) + internal + pure + returns (bytes memory result) + { + result = new bytes(_addDelta(certificate.length, delta)); + + uint256 prefixLen = ptr.header(); + for (uint256 i = 0; i < prefixLen; ++i) { + result[i] = certificate[i]; + } + for (uint256 i = 0; i < replacement.length; ++i) { + result[prefixLen + i] = replacement[i]; + } + + uint256 suffixStart = ptr.header() + ptr.totalLength(); + uint256 suffixLen = certificate.length - suffixStart; + uint256 resultSuffixStart = prefixLen + replacement.length; + for (uint256 i = 0; i < suffixLen; ++i) { + result[resultSuffixStart + i] = certificate[suffixStart + i]; + } + } + + function _p384OrderMinus(uint128 hi, uint256 lo) internal pure returns (uint128 twinHi, uint256 twinLo) { + uint128 nHi = type(uint128).max; + uint256 nLo = 0xffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973; + uint128 borrow = lo > nLo ? 1 : 0; + unchecked { + twinHi = nHi - hi - borrow; + twinLo = nLo - lo; + } + } + + function _addDelta(uint256 value, int256 delta) internal pure returns (uint256) { + return delta < 0 ? value - uint256(-delta) : value + uint256(delta); + } + + function _writeDerLength(bytes memory der, Asn1Ptr ptr, uint256 length) internal pure { + uint256 headerLen = ptr.content() - ptr.header(); + if (headerLen == 2) { + require(length < 128, "short length overflow"); + der[ptr.header() + 1] = bytes1(uint8(length)); + } else if (headerLen == 3) { + require(der[ptr.header() + 1] == 0x81, "expected 0x81 length"); + require(length < 256, "0x81 length overflow"); + der[ptr.header() + 2] = bytes1(uint8(length)); + } else if (headerLen == 4) { + require(der[ptr.header() + 1] == 0x82, "expected 0x82 length"); + require(length < 65536, "0x82 length overflow"); + der[ptr.header() + 2] = bytes1(uint8(length >> 8)); + der[ptr.header() + 3] = bytes1(uint8(length)); + } else { + revert("unsupported length header"); + } + } + function testFuzz_uint384At_LeadingZeros(uint8 numZeros, uint128 hiSeed, uint256 loSeed) public view { numZeros = uint8(bound(numZeros, 0, 16)); diff --git a/test/hinted/HintedNitroAttestation.t.sol b/test/hinted/HintedNitroAttestation.t.sol index b3c63a1..3db936c 100644 --- a/test/hinted/HintedNitroAttestation.t.sol +++ b/test/hinted/HintedNitroAttestation.t.sol @@ -6,7 +6,9 @@ import {NitroValidator} from "../../src/NitroValidator.sol"; import {CertManager} from "../../src/CertManager.sol"; import {CertManagerDemo} from "../helpers/CertManagerDemo.sol"; import {ICertManager} from "../../src/ICertManager.sol"; +import {Asn1Decode, LibAsn1Ptr, Asn1Ptr} from "../../src/Asn1Decode.sol"; import {CborDecode} from "../../src/CborDecode.sol"; +import {LibBytes} from "../../src/LibBytes.sol"; import {P384Verifier} from "../../src/P384Verifier.sol"; import {Sha2Ext} from "../../src/Sha2Ext.sol"; import {P384HintCollector} from "../helpers/HintedNitroTestHelpers.sol"; @@ -73,7 +75,10 @@ contract NitroValidatorParseHarness is NitroValidator { } contract HintedNitroAttestationTest is Test { + using Asn1Decode for bytes; using CborDecode for bytes; + using LibAsn1Ptr for Asn1Ptr; + using LibBytes for bytes; uint256 constant HINTED_MODEXP_FLOOR_DELTA = 300; // EIP-7883 floor 500 - EIP-2565 floor 200 uint256 constant TX_CAP = 16_777_216; @@ -1036,7 +1041,7 @@ contract HintedNitroAttestationTest is Test { summary.leaf = certManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); uint256 clientCurrentGas = g0 - gasleft(); - bytes32 leafHash = keccak256(clientCert); + bytes32 leafHash = _certCacheKey(clientCert); ICertManager.VerifiedCert memory cachedLeaf = certManager.loadVerified(leafHash); assertTrue(cachedLeaf.pubKey.length > 0, "client cert must be cached"); assertFalse(cachedLeaf.ca, "client cert must be cached as client"); @@ -1100,6 +1105,12 @@ contract HintedNitroAttestationTest is Test { return a >= b ? a : b; } + function _certCacheKey(bytes memory certificate) internal pure returns (bytes32) { + Asn1Ptr root = certificate.root(); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + return certificate.keccak(tbsCertPtr.header(), tbsCertPtr.totalLength()); + } + function _cacheCertBundleWithHints(bytes memory attestationTbs) internal returns (ICertManager.VerifiedCert memory leaf)