Skip to content

test: pin the CertManager verified-cert packed layout - #82

Open
Dusk1e wants to merge 1 commit into
base:mainfrom
Dusk1e:test/pin-certmanager-packed-layout
Open

test: pin the CertManager verified-cert packed layout#82
Dusk1e wants to merge 1 commit into
base:mainfrom
Dusk1e:test/pin-certmanager-packed-layout

Conversation

@Dusk1e

@Dusk1e Dusk1e commented Aug 11, 2026

Copy link
Copy Markdown

Adds direct coverage for the verified[certHash] packed encoding and the assembly offsets _loadVerified uses to read it back. Test-only — no production code is touched.

Why

_saveVerified stores abi.encodePacked(ca, notAfter, maxPathLen, subjectHash, pubKey), so the payload is:

ca(1) || notAfter(8) || maxPathLen(8) || subjectHash(32) || pubKey(48)   // 97 bytes

_loadVerified reads the four fixed-width fields with mload(add(packed, D + N)). Reading the 32-byte word that ends at the end of a field puts that field in the low bytes, so the constant is payload offset + field width:

field payload offset (D) width (N) mload constant (D + N)
ca 0 1 0x1
notAfter 1 8 0x9
maxPathLen 9 8 0x11
subjectHash 17 32 0x31

The constants therefore look shifted by one field against the payload offsets, which is exactly what you would expect an off-by-one to look like. It has been reported as one twice — #37 and #50 — and both proposed changing the constants to 0x0 / 0x1 / 0x9 / 0x11, which would read the length slot instead of the payload.

The offsets on main are correct. What was missing is a test that says so: the warm-cache path was only exercised through full attestation fixtures, which fail as a whole and do not isolate a decoding fault.

What this adds

test/CertManagerPackedLayout.t.sol, six tests over a CertManager harness that exposes _saveVerified / _loadVerified:

  • test_saveVerified_writesDocumentedByteLayout — asserts total length and each field at its documented payload offset.
  • test_loadVerified_roundTripsEveryField — save/load round trip on concrete values.
  • test_loadVerified_roundTripsNonCaEntryca == false specifically. ca is read out of a word whose upper bytes hold the tail of the length slot, so this is the field most likely to silently pick them up.
  • testFuzz_loadVerified_roundTripsEveryField — same round trip, fuzzed over all five fields.
  • test_loadVerified_returnsRootCaConstants — the constructor-written root entry against the pinned ROOT_CA_* constants, so the assertion is not only against test-authored values.
  • test_loadVerified_returnsEmptyCertForUnknownHash — the packed.length == 0 branch.

These have teeth

I rewrote the offsets in _loadVerified to the values #50 proposes and re-ran the file:

[FAIL: ca: true != false]              test_loadVerified_roundTripsNonCaEntry
[FAIL: notAfter: 24833 != 1893456000]  test_loadVerified_roundTripsEveryField
[FAIL: notAfter: 37121 != 2519044085]  test_loadVerified_returnsRootCaConstants
[FAIL: notAfter: 24833 != ...]         testFuzz_loadVerified_roundTripsEveryField
Suite result: FAILED. 2 passed; 4 failed

24833 is 0x6101 — the 0x61 length byte shifted in over the ca byte. main was then restored, and the suite passes.

If this lands it gives #50 a definitive answer and stops the same report coming back a third time.

Checks

  • forge test — 204 tests, 203 passed, 1 skipped (197 before this PR, +6 here), 0 failed.
  • forge fmt --check test/CertManagerPackedLayout.t.sol — clean. (Repo-wide forge fmt --check already reports diffs on every source file with stock Foundry 1.7.1; this file is not part of that.)

`_saveVerified` stores each verified cert as
`abi.encodePacked(ca, notAfter, maxPathLen, subjectHash, pubKey)` and
`_loadVerified` reads the four fixed-width fields back with
`mload(add(packed, D + N))`, which lands an N-byte field at payload
offset D in the low bytes of the loaded word. That gives 0x1, 0x9, 0x11
and 0x31 for fields whose payload offsets are 0, 1, 9 and 17.

The two sets of numbers do not match by design, and the layout had no
direct test: the warm-cache path was only covered indirectly through
full attestation fixtures, which do not isolate a decoding fault. The
offsets have now twice been reported as off-by-one (base#37, base#50).

Add tests that assert the stored encoding field by field, round-trip
every field through save/load including a `ca == false` entry and a
fuzzed case, check the constructor-written root entry against the
pinned ROOT_CA_* constants, and cover the empty-cache branch.

Verified these have teeth: rewriting the offsets to the values proposed
in base#50 fails four of the six, including `ca` flipping to true for a
non-CA entry.
@osr21

osr21 commented Aug 22, 2026

Copy link
Copy Markdown

Non-blocking integration data point from a delayed-settlement consumer: pinning this packed layout is valuable because warm validation is part of the authorization boundary, and the explicit ca == false / full-width cases make the assembly idiom much harder to accidentally “fix” incorrectly.

The additional property we had to enforce downstream is that a successful _loadVerified round-trip or a previously accepted attestation is not current trust. Our relay accepts an attestation in one transaction and may settle later, so it retains the chain’s certificate identity keys and calls isRevoked for each one on every accepted-intent read. Otherwise a revocation after validation but before settlement leaves a stale authorization usable until its local expiry.

That is outside this PR’s intentionally narrow layout scope, and main already tests revocation during validator reuse. I’m mentioning it because this test correctly protects the storage primitive while consumers still need to keep loadVerified and downstream decision caches out of their trust model.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants