Read a witness's signed iat and grade, and refuse anything else - #70
Read a witness's signed iat and grade, and refuse anything else#70imran-siddique wants to merge 1 commit into
Conversation
The witness is adding two optional protected headers: a CWT Claims map (label 15, RFC 9597) carrying iat (label 6, RFC 8392), which is the witness clock at registration, and a private-use label -65537 carrying its grade. Receipts built with neither are byte-for-byte the shape we already verify. Our verifier compared the protected header against one fixed encoding, so the first receipt carrying either field would have been refused as an unsupported profile. It now parses the header, still requires alg -8 and vds 1, accepts those two labels, and refuses any other: unreviewed signed metadata is not neutral just because it is signed. Two limits stop being permanently false. witness_time_established follows a signed iat. grade_cryptographically_bound requires the signed grade to be the grade the response reports, since a private-use label carries no registered meaning on its own; a signed value that disagrees with the untrusted one binds nothing. Six tests mint receipts over the captured inclusion proof with our own key, so nothing here asserts the witness has deployed. The existing negative test for submitter-supplied metadata is untouched, because a signed iat is not that. README and LIMITATIONS said the grade is unsigned and there is no witness time. That is true of the captured receipt and becomes false the day a receipt carries either, so both now point at the two verifier fields. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMuXK1es7dsRsPtvC5Tkpd
StevenMih
left a comment
There was a problem hiding this comment.
Approving. Two things I want on the record before the comments.
First, you found a bug in your own verifier that our change would have triggered, and you found it before we deployed. verify_witness_receipt compared the protected header against cbor2.dumps({1: -8, 395: 1}) by byte equality, so the first receipt carrying either new field would have been refused as an unsupported profile — from our side that would have looked like a witness outage, and we would have debugged it on the wrong side of the wire. This is the second time in two weeks that reading the other party's code caught something neither of us would have found in our own.
Second, grade_cryptographically_bound requiring the signed grade to equal the reported one is better than what I would have specified. A private-use label carries no registered meaning, so a signed value that disagrees with the untrusted one binds nothing — that is exactly right, and it is a distinction our own draft does not currently make. I would like to borrow it.
Minting the test receipts with your own key so that no test asserts we have deployed anything is the correct discipline, and I note test_untrusted_metadata_does_not_change_signed_scope is untouched as agreed.
Three comments, in order of how much they matter.
- set(claims) != {CWT_IAT} makes any future CWT claim a breaking change — including ours. Today we emit exactly {6: iat}, so this passes. But RFC 9597's CWT Claims header is a general map, and the obvious next additions are iss (1) and sub (2) to name the witness and the log inside the signature. Under this rule, the first receipt carrying iss alongside iat is refused — the same class of failure this PR fixes, one layer in.
Your stated principle argues for refusing, and I do not want to talk you out of it. But if exactly-{6} is the profile, then it is a profile and it should be written down somewhere we can both cite, so that adding a claim is a coordinated change rather than an outage. Options, in the order I would take them: require iat, ignore other registered CWT claims, and refuse only unregistered ones; or keep the strict rule and put a line in LIMITATIONS saying that the accepted CWT claim set is exactly {iat} by agreement, and that a witness adding a claim must coordinate. Either is fine. Silence is what I would avoid.
-
iat is accepted unbounded, and it flips a limit to true. The only check is positive integer. A witness clock in 1970 or 2099 sets witness_time_established just as readily as a plausible one, and that field is exactly the thing a reader will lean on. You already hold the data for a cheap sanity check: a witness cannot have registered a checkpoint before the checkpoint existed, so iat >= checkpoint.timestamp is a real cross-check rather than a heuristic, and an upper bound of "not far beyond the capture time" costs nothing. Refuse, or surface it and leave the limit false — I would refuse, on the same reasoning as the unreviewed header.
-
-65537 is private use and unregistered, and your verifier now depends on the exact value. That was my choice and I should carry the consequence: I said in my last mail that if a registered label ever lands, the value moves and I will send a migration note before it does. It is worth one line in the code comment or LIMITATIONS saying the label is provisional and by bilateral agreement, so that a third implementer reading this file does not take -65537 for a standard.
Nothing blocking. (1) I would resolve before merge, since it is the one that can bite us both again.
Action State is adding two optional COSE protected headers to stage-1 receipts: a CWT Claims map (label 15, RFC 9597) carrying
iat(label 6, RFC 8392), the witness clock at registration, and a private-use label-65537carrying its grade. Receipts built with neither are byte-for-byte the shape we verify today, sodocs/evidence/witness-2026-09-07needs no regeneration.The bug this fixes before it bites.
verify_witness_receiptcompared the protected header against one fixed encoding,cbor2.dumps({1: -8, 395: 1}). The first receipt carrying either new field would have been refused as an unsupported profile. It now parses the header, still requiresalg -8andvds 1, accepts those two labels, and refuses any other. Unreviewed signed metadata is not neutral just because it is signed.Two limits stop being permanently false.
witness_time_establishedfollows a signediat.grade_cryptographically_boundrequires the signed grade to equal the grade the response reports. A private-use label carries no registered meaning on its own, so a signed value that disagrees with the untrusted one binds nothing.Tests. Six new cases mint receipts over the captured inclusion proof with our own key, so none of them asserts that the witness has deployed anything. They cover: the capture carries neither field; a signed
iatis surfaced and sets its limit; a signed grade binds only when it matches the response; an unreviewed label is refused; the CWT map must carryiatand nothing else; and a receipt with neither field is still the pre-fix shape.test_untrusted_metadata_does_not_change_signed_scopeis untouched, because a signediatis not submitter-supplied metadata. 19 tests pass.Docs. README and LIMITATIONS asserted that the grade is unsigned and that there is no witness time. True of the captured receipt, false the day a receipt carries either, so both now point at the two verifier fields instead of restating a fact with a shelf life.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RMuXK1es7dsRsPtvC5Tkpd