feat: forward-compatible attestation parsing (skip unknown keys, indefinite-length CBOR)#32
Merged
leopoldjoy merged 2 commits intoJun 16, 2026
Conversation
leopoldjoy
reviewed
Jun 16, 2026
This was referenced Jun 16, 2026
…finite-length CBOR) Make the Nitro attestation parser tolerant of benign AWS COSE payload format changes so they cannot brick verification until a contract upgrade. Both changes are liveness-only: the entire to-be-signed payload is still checked against AWS's COSE signature, so ignored or re-encoded content is signed by AWS and cannot change the accept decision. - L1: unknown attestation map keys are skipped (via a new generic CborDecode.skipValue) instead of reverting with "invalid attestation key". - L2: the payload map and the nested pcrs map / cabundle array are accepted in both definite- and indefinite-length CBOR encodings. Also document two known, by-design edges (no logic change): - L4: a cached cert is pinned to the parent it was first verified under (CertManager.verifiedParent + design doc). - M1-M3 integrator responsibilities and the new forward-compat behavior in the README and design doc. Tests: flip the four behavior-pinning cases in IndefiniteLengthCbor.t.sol from revert to correct parsing, and add skipValue unit tests plus nested-indefinite and unknown-key coverage. 105 tests pass; forge fmt clean. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Addresses Leopold's PR base#32 review — make malformed indefinite-length CBOR revert instead of being silently under-parsed (liveness-only; the TBS is still COSE- signature-checked, so these were never false accepts). - Outer payload map now tracks definite vs indefinite encoding: a definite map iterates exactly its declared entry count (a stray 0xFF can no longer terminate it early), and an indefinite map requires a 0xFF break marker (a missing break is no longer silently accepted at the payload end). - Indefinite pcrs map with an odd item count now reverts ("invalid pcrs map") instead of floor-dividing and dropping the trailing item. - _consumeBreak verifies the consumed byte is actually 0xFF. - CborDecode.skipValue rejects malformed indefinite values: indefinite strings must consist of same-major definite-length chunks, and indefinite maps must have an even (key/value) item count. - Extracted cabundle/pcrs parsing into _parseCabundle / _parsePcrs helpers (also resolves stack-too-deep from the added locals). Tests: add skipValue malformed-indefinite cases (non-string chunk, wrong-major chunk, odd map) and _parseAttestation reverts (odd indefinite pcrs, missing outer break). 111 passed; forge fmt clean. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
24acf88 to
122ded3
Compare
leopoldjoy
approved these changes
Jun 16, 2026
leanthebean
added a commit
to leanthebean/nitro-validator
that referenced
this pull request
Jun 17, 2026
Focused tests from the security review of the hinted P-384 change (tests only). Revocation landed in base#29 and the forward-compatibility implementation + docs in base#32, so this carries only the adversarial soundness tests that part of the review surfaced. test/hinted/HintedNitroAttestation.t.sol (via a small U384TestWrapper): - test_UnreducedInverseHintIsSound — a non-canonical inverse hint (inv + p) passes the on-chain b*inv == 1 check and yields the identical reduced result, so it cannot change the accept decision. - test_ModulusConfusionHintReverts — a hint valid mod n is rejected where mod p is expected. - test_CorruptedSignatureNeverAccepts — no hint stream makes a corrupted signature verify: non-self-consistent hints (empty/garbage/original/padded) revert at the inverse-hint gate, and self-consistent hints collected for the corrupted sig pass the gate but fail the final ECDSA curve check (returns false). - test_MalleableTwinSignatureAccepted — (r, n-s) also verifies (intentional, CURVE_LOW_S_MAX = n-1) and decodes to the identical attestation. - test_AttestationReplayHasNoOnChainProtection — the same (tbs, sig) verifies repeatedly; freshness/replay is the integrator's responsibility. test/CertManager.t.sol: - test_CacheGriefingSameKeyCaRenewalBricksCachedLeaf — skipped spec documenting the verifiedParent first-writer-wins liveness edge. Adds P384HintCollector.collectVerifyHintsAllowInvalid so a self-consistent hint stream can be collected for a signature that fails the final check. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
leopoldjoy
pushed a commit
that referenced
this pull request
Jun 17, 2026
…33) Focused tests from the security review of the hinted P-384 change (tests only). Revocation landed in #29 and the forward-compatibility implementation + docs in #32, so this carries only the adversarial soundness tests that part of the review surfaced. test/hinted/HintedNitroAttestation.t.sol (via a small U384TestWrapper): - test_UnreducedInverseHintIsSound — a non-canonical inverse hint (inv + p) passes the on-chain b*inv == 1 check and yields the identical reduced result, so it cannot change the accept decision. - test_ModulusConfusionHintReverts — a hint valid mod n is rejected where mod p is expected. - test_CorruptedSignatureNeverAccepts — no hint stream makes a corrupted signature verify: non-self-consistent hints (empty/garbage/original/padded) revert at the inverse-hint gate, and self-consistent hints collected for the corrupted sig pass the gate but fail the final ECDSA curve check (returns false). - test_MalleableTwinSignatureAccepted — (r, n-s) also verifies (intentional, CURVE_LOW_S_MAX = n-1) and decodes to the identical attestation. - test_AttestationReplayHasNoOnChainProtection — the same (tbs, sig) verifies repeatedly; freshness/replay is the integrator's responsibility. test/CertManager.t.sol: - test_CacheGriefingSameKeyCaRenewalBricksCachedLeaf — skipped spec documenting the verifiedParent first-writer-wins liveness edge. Adds P384HintCollector.collectVerifyHintsAllowInvalid so a self-consistent hint stream can be collected for a signature that fails the final check. Generated with Claude Code Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the Nitro attestation parser forward-compatible with benign changes to AWS's COSE payload format, so such a change cannot brick verification until a contract upgrade. Addresses two liveness findings from a security review (L1, L2) and documents two by-design edges (L4, M1–M3).
Both code changes are liveness-only and cannot cause a false accept: the entire to-be-signed payload is still hashed and checked against AWS's COSE signature in
validateAttestationWithHints, so any skipped or re-encoded content is signed by AWS, and a field the parser ignores can never change the accept decision. Fields the contract reads (pcrs,cabundle,certificate,moduleID, …) are parsed exactly as before; malformed/truncated input still reverts.What changed
L1 — unknown attestation keys are skipped, not rejected.
Previously an unrecognised map key reverted with
"invalid attestation key", meaning any new field AWS added would permanently reject every attestation. Now the value is skipped via a new generic CBOR walker and parsing continues.L2 — definite and indefinite-length CBOR are both accepted.
The outer payload map and the nested
pcrsmap andcabundlearray each parse in either definite-length form or indefinite-length form (0xBF/0x9F…0xFF). Previously the nested containers assumed a definite count, so an encoder switch to indefinite length would have silently produced an emptypcrs/cabundleand (via a leaked inner break marker) truncated the rest of the map.src/CborDecode.sol: addskipValue, a generic, bounds-checked CBOR data-item skipper (all major types, definite + indefinite).src/NitroValidator.sol: rework_parseAttestationfor both behaviors; add_isIndefinite,_countIndefiniteItems,_consumeBreakhelpers and forward-compat NatSpec.Docs only (no logic change):
CertManager.verifiedParentcomment + "First-verified parent pinning" section in the design doc). This is intentional: warm reuse skips signature verification, so it must reflect the exact chain that was cryptographically checked. Harmless for Nitro because short-lived (~3h) leaves self-heal.Out of scope (intentionally)
block.timestamp, so a mid-flow expiry reverts rather than silently passing. Historical/retrospective verification remains an off-chain concern; no grace window orasOfparameter is introduced.Testing
forge test— 105 passed, 0 failed.forge fmt --check src test— clean.NITRO_RUN_FFI=true forge test --ffi --match-test test_OffchainWitness— passes (off-chain ↔ on-chain witness parity).NitroValidatorruntime size well under the EIP-170 limit.The four behavior-pinning tests in
test/IndefiniteLengthCbor.t.solthat previously asserted the old revert-on-unknown-key / break-on-indefinite behavior are flipped to assert correct parsing, andskipValueunit tests plus nested-indefinite and unknown-key coverage are added.🤖 Generated with Claude Code