Skip to content

Commit 2bb3b00

Browse files
Willyhamclaude
andcommitted
test+docs: hinted P-384 review tests and forward-compat documentation
Follow-up to #28 (hinted P-384 Nitro attestation verification). Adds focused tests covering hint-verification soundness and signature/cache/replay semantics, and documents the attestation parser's forward-compatibility limitations, which were previously undocumented. Tests (added to existing files, reusing the real fixture and helpers): - HintedNitroAttestation.t.sol: non-canonical (unreduced) inverse hint is sound, modulus-confusion hint reverts, corrupted signature never accepts under any hint stream, malleable twin (r, n-s) is accepted, and there is no on-chain anti-replay. Adds a U384TestWrapper to exercise the hinted U384 primitives (modinv / moddivAssign) directly. - IndefiniteLengthCbor.t.sol: skipped forward-compat specs for unknown-key tolerance and nested indefinite-length pcrs parsing, co-located with the regression tests that pin current behaviour. - CertManager.t.sol: skipped spec documenting the verifiedParent first-writer-wins cache-griefing edge (requires a same-key-renewed CA fixture to realise). Docs: - docs/hinted-p384-nitro-attestation.md: new "Forward-compatibility" section. - README.md: forward-compatibility note under Security considerations. - NitroValidator.sol: NatSpec on _parseAttestation recording that the strict key whitelist / lack of nested indefinite-length support is liveness-only (can only revert, never a false accept) but forward-incompatible with AWS attestation-format changes. No production logic changes; src edits are NatSpec/comments only. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 662a32c commit 2bb3b00

6 files changed

Lines changed: 329 additions & 0 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ integrator (see [docs](docs/hinted-p384-nitro-attestation.md#integrator-responsi
143143
responsibility.
144144
- **Revocation** — not supported (consistent with AWS's documented validation process, linked
145145
above).
146+
- **Forward-compatibility** — the attestation parser uses a fixed key whitelist (unknown keys
147+
revert) and does not support nested indefinite-length `pcrs`/`cabundle`. A future change to the
148+
AWS attestation format would make verification revert (never a false accept) until the contract
149+
is upgraded. AWS emits the known, definite-length format today. See
150+
[docs §10](docs/hinted-p384-nitro-attestation.md#forward-compatibility-attestation-format-changes).
146151

147152
## Build
148153

docs/hinted-p384-nitro-attestation.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,30 @@ can never silently diverge.
503503
must stay in sync with the verifier's execution order. Its DER/CBOR parsing should
504504
be reviewed for robustness.
505505

506+
### Forward-compatibility (attestation format changes)
507+
508+
The attestation parser accepts a strict, fixed shape: a known whitelist of CBOR keys, and
509+
definite-length or outer-indefinite-length maps. The two consequences below are deliberate
510+
parser behaviour and are **liveness-only** — a wrong or unrecognised shape can only cause a
511+
revert (a genuine attestation failing to verify), never a false accept. They are recorded here
512+
because the project does not otherwise document them as an accepted trade-off: each would brick
513+
verification after a future AWS attestation-format change, and resolving it needs a contract
514+
upgrade.
515+
516+
- **Unknown attestation key → revert (`"invalid attestation key"`).** If AWS adds a new field to
517+
the attestation document, every attestation carrying it becomes unverifiable. Tolerating unknown
518+
keys would be safe — all fields sit under AWS's COSE signature, so ignoring an unrecognised one
519+
cannot forge anything — but the parser has no generic CBOR skip routine. Current behaviour is
520+
pinned by `test_neg_unknownKey{Definite,Indefinite}_reverts`; the desired forward-compatible
521+
behaviour is captured (skipped) in `test_unknownKey_forwardCompat_tolerated`.
522+
- **Nested indefinite-length `pcrs` / `cabundle` → revert / empty parse.** Outer-map
523+
indefinite-length encoding is supported (via the `0xFF` break marker); nested indefinite-length
524+
containers are not. Pinned by `test_edge_innerIndefinitePcrsEmpty_outerBreakTriggered` and
525+
`test_neg_nestedIndefiniteNonEmptyArray_reverts`; desired behaviour (skipped) in
526+
`test_nestedIndefinitePcrs_forwardCompat_parsed`.
527+
528+
AWS currently emits definite-length CBOR with the known field set, so neither is triggered today.
529+
506530
### Integrator responsibilities (what the contract does NOT enforce)
507531

508532
Verification proves an attestation is genuine and well-formed. The following are

src/NitroValidator.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ contract NitroValidator {
188188
LibBytes.memcpy(dest + 13 + rawProtectedLength, payloadSrc, rawPayloadLength);
189189
}
190190

191+
/// @dev Parses the COSE payload into field pointers using a strict shape: any unrecognised
192+
/// CBOR key reverts ("invalid attestation key"), and nested indefinite-length
193+
/// `pcrs`/`cabundle` are not supported (outer-map indefinite-length IS). These are
194+
/// liveness-only constraints — they can only cause a revert, never a false accept — but
195+
/// they make verification forward-INCOMPATIBLE with attestation-format changes: a new AWS
196+
/// field would require a contract upgrade. Tolerating unknown keys would be safe (every
197+
/// field is under AWS's COSE signature). See docs §10 "Forward-compatibility".
191198
function _parseAttestation(bytes memory attestationTbs) internal pure returns (Ptrs memory) {
192199
require(attestationTbs.keccak(0, 18) == ATTESTATION_TBS_PREFIX, "invalid attestation prefix");
193200

test/CertManager.t.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ contract CertManagerTest is Test {
8080
return cm.verifyCACertWithHints(cert, parentHash, hints);
8181
}
8282

83+
// Cache-griefing liveness edge: `verifiedParent[certHash]` is written once on the cold
84+
// path (gated on cert.pubKey.length == 0) and never updated. If AWS renews an intermediate
85+
// CA with the SAME signing key but a new validity window, the renewed cert has different
86+
// DER bytes -> a different keccak256 -> a different parentCertHash in the chain. A leaf
87+
// already cached under the old parent then permanently reverts "parent cert mismatch"
88+
// against the renewed parent, with no admin override. (The wrong-parent revert mechanism
89+
// itself is covered by HintedNitroAttestationTest.test_Hinted*RejectsCachedParentMismatch.)
90+
//
91+
// SKIPPED: realising this needs a genuine same-key *renewed* CA cert, which requires an
92+
// AWS signing operation and is unavailable as a static fixture. Documented as a known
93+
// liveness edge; see security review redteam/r3-cache-replay-malleability.md (Claim 2).
94+
function test_CacheGriefingSameKeyCaRenewalBricksCachedLeaf() public {
95+
vm.skip(true, "needs a same-key-renewed AWS CA fixture (off-chain signing); documents verifiedParent first-writer-wins");
96+
}
97+
8398
function testFuzz_uint384At_LeadingZeros(uint8 numZeros, uint128 hiSeed, uint256 loSeed) public view {
8499
numZeros = uint8(bound(numZeros, 0, 16));
85100

test/IndefiniteLengthCbor.t.sol

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,4 +614,57 @@ contract NitroValidatorIndefiniteLengthTest is Test {
614614
vm.expectRevert("unexpected type");
615615
validator.parseAttestation(_buildTbs(abi.encodePacked(CBOR_MAP_INDEFINITE, entries, CBOR_BREAK)));
616616
}
617+
618+
// ══════════════════════════════════════════════════════════
619+
// FORWARD-COMPATIBILITY — known latent liveness risks (SKIPPED)
620+
//
621+
// The two reverts above are deliberate parser behaviour, but the project does not
622+
// document the forward-compatibility consequence as an accepted trade-off. They are
623+
// liveness-only (a wrong/unknown shape can only revert, never false-accept), yet a
624+
// future change to the AWS attestation format would brick verification until the
625+
// contract is upgraded. The tests below encode the DESIRED forward-compatible
626+
// behaviour and are skipped until the parser supports it (see docs §10
627+
// "Forward-compatibility").
628+
// ══════════════════════════════════════════════════════════
629+
630+
/// @dev SKIPPED. Today an unknown attestation key reverts (pinned by
631+
/// test_neg_unknownKeyDefinite_reverts). Tolerating it would be SAFE — every field is
632+
/// under AWS's COSE signature, so ignoring an unrecognised key cannot forge anything;
633+
/// the parser simply lacks a generic CBOR skip. If AWS adds any new field, every such
634+
/// attestation becomes unverifiable until a contract upgrade.
635+
function test_unknownKey_forwardCompat_tolerated() public {
636+
vm.skip(true, "latent liveness risk: unknown attestation key reverts; needs a generic CBOR skip to tolerate new AWS fields");
637+
638+
// Desired: a known field still parses and the unknown key is ignored.
639+
bytes memory entries = abi.encodePacked(
640+
hex"696d6f64756c655f6964", // key "module_id"
641+
hex"6474657374", // val "test"
642+
hex"63626164", // key "bad" (unknown)
643+
hex"6474657374" // val "test"
644+
);
645+
NitroValidator.Ptrs memory p = validator.parseAttestation(_buildTbs(abi.encodePacked(hex"a2", entries)));
646+
assertEq(p.moduleID.length(), SYNTH_MODULE_ID_LEN, "known field parsed; unknown key ignored");
647+
}
648+
649+
/// @dev SKIPPED. Outer-map indefinite-length is supported, but a NESTED indefinite-length
650+
/// pcrs map is not (pinned by test_edge_innerIndefinitePcrsEmpty_outerBreakTriggered and
651+
/// test_neg_nestedIndefiniteNonEmptyArray_reverts). If AWS emitted nested
652+
/// indefinite-length containers, verification would brick. This encodes the desired parse.
653+
function test_nestedIndefinitePcrs_forwardCompat_parsed() public {
654+
vm.skip(true, "latent liveness risk: nested indefinite-length pcrs/cabundle unsupported; would brick on an AWS encoding change");
655+
656+
// Desired: a nested indefinite-length pcrs map {0: bytes(48)} parses to one PCR.
657+
bytes memory entries = abi.encodePacked(
658+
hex"696d6f64756c655f6964", // key "module_id"
659+
hex"6474657374", // val "test"
660+
hex"6470637273", // key "pcrs"
661+
CBOR_MAP_INDEFINITE, // nested indefinite-length map
662+
hex"00", // key 0
663+
hex"5830", // val bytes(48) header
664+
new bytes(48), // 48 zero bytes
665+
CBOR_BREAK // inner break
666+
);
667+
NitroValidator.Ptrs memory p = validator.parseAttestation(_buildTbs(abi.encodePacked(hex"a2", entries)));
668+
assertEq(p.pcrs.length, 1, "nested indefinite-length pcrs should yield 1 PCR");
669+
}
617670
}

0 commit comments

Comments
 (0)