Skip to content

Read a witness's signed iat and grade, and refuse anything else - #70

Open
imran-siddique wants to merge 1 commit into
mainfrom
witness-signed-metadata
Open

Read a witness's signed iat and grade, and refuse anything else#70
imran-siddique wants to merge 1 commit into
mainfrom
witness-signed-metadata

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

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 -65537 carrying its grade. Receipts built with neither are byte-for-byte the shape we verify today, so docs/evidence/witness-2026-09-07 needs no regeneration.

The bug this fixes before it bites. verify_witness_receipt compared 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 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 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 iat is surfaced and sets its limit; a signed grade binds only when it matches the response; an unreviewed label is refused; the CWT map must carry iat and nothing else; and a receipt with neither field is still the pre-fix shape. test_untrusted_metadata_does_not_change_signed_scope is untouched, because a signed iat is 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

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 StevenMih left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. 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.

  1. 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.

  2. -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.

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