Receipt envelope profile
September 16, 2026 · View on GitHub
Status: DRAFT, house proposal. Not standardized, not agreed with any second party, not published anywhere but this repository. Nothing here changes the native receipt or what it proves — see NON_CLAIMS.md.
Proposed identifier: proofbundle/receipt-envelope-profile/v0.1.
The identifier is an Owner decision and is deliberately marked as such: it is proposed here, not
adopted. A name settles because citing it is easier than renaming it, not because it exists.
This profile is a profile to a standard, not a format beside one. IETF RFC 9943, An Architecture for Trustworthy and Transparent Digital Supply Chains, is the published SCITT architecture — Standards Track, IETF stream, SCITT working group, June 2026, verified at the RFC Editor on 2026-08-30 — and
One reference this page does not make, and says so rather than making it silently. RFC 9942 (COSE Receipts) was published alongside RFC 9943 and is the document that specifies receipts. This page is a receipt envelope profile and cites only the architecture. Whether 9942 is the closer reference for the rules below is NOT MEASURED: the RFC text has not been read here, and a citation added from its title alone would be the kind of claim R6 exists against. Named so a reader does not mistake the absence for a judgement.
draft-mih-sokolov-scitt-payload-binding sits on top of it — an individual submission, no stream
and no intended RFC status, verified at the IETF datatracker on 2026-08-30. R1 to R4 are stated
against that draft, by reference. R5 and R6 are our addition.
The revision this page was first written against is not the current one, and the draft has been
retitled. R1 to R4 were stated against revision -02 of 24 Aug 2026, then titled "Canonical
Payload Binding: A Signed Statement Construction Profile". The draft is now at -05 of 11 Sep
2026 and titled "Canonicalization Declaration for SCITT Signed Statements"; the draft name is
unchanged. The clause-by-clause re-measurement against -05 is in
SCITT_CPB_MAPPING.md,
including which cited clauses kept their number and which two moved.
Precisely why, because the weaker wording is the true one. An earlier draft of this page said the draft "says in its own section 1.1 that it does not cover them". Read against the draft itself, that is not what section 1.1 says: it excludes payload content formats, artifact types, application meaning, registration policy and transports, and it does not name evaluation coverage or key trust at all. So those two are absent from the draft, not deliberately excluded by it — a different and smaller claim, and the one the text supports.
The one sentence everything follows from
A reported field is the report of a check that was carried out. If a verifier reports
binding_checked, it checked that binding. If it reports valid, it did so against the schema the
receipt names. What was not checked is not reported, not even in a weakened form.
R1 — one normative canonicalization
RFC 8785, one serializer, pinned in the schema. Both sides compute the same content_id before any
signature is checked. The serializer is reached through a public interface, not a private module.
Counter-proof. The three divergence vectors from the issue thread: UTF-16 key ordering over a
pair where the code-unit order and the code-point order disagree, 1e-7, and the integer 2. Each
must produce a differing content_id under a non-conformant canonicalization and must turn the check
red. The shipped vector uses U+1F600 with U+FF3A; the thread's U+FFFF was measured 2026-08-30 and
diverges identically, so either pair carries the axis — a surrogate pair sorts BEFORE any BMP
character by code units and AFTER most of them by code points.
Two of those three cannot arise in this format, and saying so is part of the rule. Measured
2026-08-30: the claim profile refuses Python floats outright (_reject_non_jcs) and requires decimal
STRINGS, so 1e-7 and 2.0 never reach a serializer here. A byte-comparison vector for them would
be theatre — it would compare two renderings of a value this format does not accept. The honest
counter-proof for those two axes is the refusal itself, and it ships as one: both objects must be
refused, and a planted defect that accepts a float turns the case red with the serialized value
printed. So R1 is carried by three vectors, not one: the ordering-and-escaping divergence (a single
object that gets both wrong under a non-conformant serializer), the refusal, and a positive
control. The two axes are removed rather than resolved, which is a weaker claim than resolving
them and the true one.
Provenance. Measured 2026-08-26 and corrected 2026-08-28 against inspect-receipts@397ae3ad;
basis office/handoff_journal/20260828T104501Z. The first pass reported "no finding" and was wrong
because it measured jcs.canonicalize and rfc8785.dumps rather than the alias that signing and
verification actually run. That alias builds JSONEncoder(sort_keys=True, ensure_ascii=True, …), so
non-ASCII is escaped to \uXXXX while RFC 8785 emits raw UTF-8 outside the mandatory escapes. The
divergence is string escaping alone; ordering and numbers are RFC-8785-correct and the UTF-16 key
sort holds. ASCII-only receipts stay identical, {"café":1} does not. The three vectors in the
thread do not isolate escaping, which is how the first pass missed it.
R2 — the schema id is read
The verifier reads schema and decides before it checks anything else. If it does not know the
schema id, it refuses to answer. It does not fall back to best effort and it does not ignore the
field.
The refusal is its own outcome, not invalid. A consumer must be able to distinguish this
receipt is invalid from I cannot judge this receipt.
Counter-proof. An otherwise valid receipt carrying a foreign schema id. Refusal is expected.
Provenance. Measured 2026-08-26 against inspect-receipts@397ae3ad: the same probe returns
valid=True.
Carried since 2026-08-30, and it was the third one. Measured on our own path first:
decode_eval_claim returned None for a foreign schema id AND for a broken receipt — the same
collapse of the two outcomes that this rule exists against. That contract is released and callers
depend on it, so it is unchanged; classify_eval_claim adds the distinction as a new function.
The ordering is part of the rule: authenticity is decided FIRST, because a broken signature is
judgeable and answering "I cannot judge this" would let a forger buy silence by renaming the schema
field. That ordering was documented and unproven until a planted defect removing it left the whole
corpus green; the vector that discriminates it exists because the meta-test found the hole, not
because writing the vectors found it.
R3 — a reported binding is a binding that was performed
binding_checked is set only when the binding was complete. A binding of type content-hash without
the digest it is supposed to bind is not a binding but an empty declaration, and it fails closed.
A verifier that ran a weaker check never reports the stronger one. It may reject a stronger binding; that is permitted and honest.
Counter-proof. {"type": "content-hash", "digest_alg": "sha256"} without eval_log_sha256
while the receipt carries that field. Fail-closed is expected.
Provenance. Measured 2026-08-26 against inspect-receipts@397ae3ad: the same probe returns
valid=True, binding_checked=content-hash.
R4 — key resolution fails closed
An unresolvable kid is invalid, not valid-with-reservation. A key shipped inside the receipt alone
never carries a valid=True.
Counter-proof. Three cases: no anchor, unknown kid, embedded key differs from the published one. All three must come out invalid.
Provenance. Measured 2026-08-26 against inspect-receipts@397ae3ad: holds in all three cases.
R5 — coverage does not follow from integrity
The requirement, unchanged. A consumer must be able to tell we looked and found nothing from we never looked. A receipt that verifies cleanly and whose scope never contained the operation in question is otherwise indistinguishable from one that contained it and found nothing. That is a real gap and it is why this rule is in the list.
The field form is NOT ours to state, and this profile no longer states one. An earlier draft of
this page proposed three numbers and presented them as our contribution. Measured 2026-08-30 in the
scitt@ietf.org list archive: draft-hillier-coverage-attestation-00, The Coverage Attestation
Profile (CAP-1), Joel David Hillier, Certisyn Inc., 20 August 2026 — ten days older than that
draft of this page — specifies exactly this question, and its normative line rules out the shape we
had proposed: for each stratum the number of eligible units must equal the checked units plus the
individually justified unchecked ones, and a remainder that only balances by subtraction must
be rejected. Three numbers whose third follows from the other two are such a remainder.
So R5 states the requirement and points at the work that specifies it. It does not propose a field form, and it is not our contribution. The form proofbundle will carry is not yet decided.
No counter-proof ships for R5 in this revision, and that is a decision rather than an omission: a counter-proof against a shape we have just withdrawn would test nothing. It follows once CAP-1 has been read.
Honest limit on this very paragraph. What has been read so far is CAP-1's summary and two of the thread's 129 messages. The draft itself is unread. Rebuilding a CAP-1-shaped field set from a summary would be guesswork, and guesswork is not a state this profile is allowed to ship in — which is the same standard R5 exists to hold others to.
R6 — every rule brings its own counter-proof
Whoever claims this profile ships the executable counter-proofs for R1 to R5, plus one positive control each. Detection rate 100 percent, otherwise the profile counts as unmet.
A profile without shipped counter-proofs is a statement of intent.
Shipped since 2026-08-30. conformance/envelope_profile/ — ten vectors (R1 three, R2 three, R3 two, R4 two), at least one
counter-proof and one positive control per rule R1 to R4, all running through our own emit and
verify path rather than a purpose-built mock. R5 carries none, deliberately: see R5 above.
Detection rate is MEASURED, not asserted. Final state: nine effective planted defects, nine caught — seven in the full round over the corpus, two more for the R1 vectors added afterwards. The path there matters more than the number, so it is written out rather than summarised. (This count was itself wrong once: an earlier draft said eight, and the error was found by recounting the vectors against the manifest rather than trusting the sentence.)
- Two escaped on the first attempt, and each escape bought a vector that was missing: the authenticity ordering under R2 (no vector was both unverifiable and foreign-schema, so removing the ordering changed no verdict), and a hand-signed coverage block under R5 (the unit tests caught it, the corpus did not — and the corpus is the outward authority).
- Two further attempts were ineffective rather than escaped — they are NOT in the nine, because a mutation that changes nothing is not a defect the corpus failed to catch. It looks identical in the output and is not the same thing at all. One replaced a branch whose neighbour carried the same effect; one removed the float branch, after which the value was still refused by the next clause — the mutation changed the message, not the property. Both were replaced with mutations that change the property. A mutation that changes nothing proves nothing about the test.
- One gap was NOT found by a planted defect at all, and the distinction is worth keeping: R1's second and third divergence axes were missing from the shipped counter-proof although this document claimed all three. That came from reading the document against the corpus, not from the meta-test. A meta-test measures whether a shipped check can fail; it cannot notice a check that was never shipped. An escaped defect is worth more than a caught one here — the caught ones confirm what was already believed, the escaped ones name what was not. R6 has no vector of its own on purpose: a case asserting "the cases exist" would be the tautology this rule warns about.
Provenance. House governance rule, first written in the 2026-08-26 draft. It has no external
source, and we do not claim one. Its standing against us is recorded: at the time of writing this
page our own mutation gate is not green, which is precisely the state R6 declares unmet.
What this profile deliberately does NOT say
no mandated signature algorithm
no mandated trust anchor; did:web and trust-anchor stay the issuer's choice
no mandated time anchor
no field for honesty or for the quality of an evaluation
no shared file extension and no shared media type
The envelope governs what a verifier may say. It does not govern whom to believe.
Anchor neutrality is an Owner decision and a red line, not a bargaining position. A shared format carrying a foreign did:web root as its default is not supported. The anchor is a field of the issuer, never a property of the format. This applies to our own anchor as well.
The profile creates no certifying authority. Conformance is demonstrated, not conferred. No party certifies it to another, and no party is appointed to judge another's conformance. Whoever claims the profile runs the counter-proofs and publishes the result. (Owner decision 2026-08-30; wording submitted for approval.)
Relationship to RFC 9943 and the SCITT payload-binding draft
Read clause by clause in SCITT_CPB_MAPPING.md. Summary, nothing asserted here that is not measured there:
| Rule | Relation to the draft |
|---|---|
| R1 | congruent — the draft's canonicalization registry entry jcs is plain RFC 8785 JCS, SHA-256, lowercase hex. Our behaviour matches; our token is jcs-sha256-v1. A mapping question, not a contradiction. |
| R2 | congruent — the draft requires a verifier to distinguish "type in no registry" from "not in my copy"; that is R2's separate outcome. |
| R3 | congruent since 2026-08-30, additively — the draft requires type, digest_alg, digest as mandatory and purpose conditionally. The optional typedDigest on evidenceRefs[] carries that shape (digestAlgorithm for digest_alg, see the mapping). It adds to the existing digest, which stays required and unchanged. Correction to this table's first pass: the conformant shape already existed in the same schema as relationDigest; the gap was internal inconsistency, not absence. |
| R4 | addition — the draft requires only that the protected header carry kid or x5chain. Resolution, trust anchors and fail-closed do not appear in it. |
| R5 | stated, not specified here — the CPB draft does not cover coverage, but draft-hillier-coverage-attestation-00 (20 Aug 2026) does, and it rules out the shape this page once proposed. R5 names the requirement and points there; proofbundle's field form is undecided. |
| R6 | addition — a governance rule; it appears in neither document. |
Our leaf construction is declared here, and the reason is narrower than this page used to say.
Our bundle passes the payload as Merkle leaf input (src/proofbundle/bundle.py,
merkle.leaf_hash(payload)).
An earlier version of this paragraph called that a declared divergence from section 7.1 of the
CPB draft, on the grounds that 7.1 requires leaf_input = bytes.fromhex(D). That reading did
not survive the source, and the correction is recorded in the mapping this page binds itself to.
Read at source on 2026-08-30 against -02 and again on 2026-09-13 against -05, the section opens
in both revisions with the same sentence: "This profile imposes no leaf construction on a
Verifiable Data Structure." The MUST that follows is conditional — it applies where a
Transparency Service's VDS keys its log on the derived identifier — and we do not key on it, we
bind the payload. So there is neither conformance nor violation here, and no divergence to declare.
What does reach us is section 5.1, and it is a duty to declare rather than to change. A payload class must specify which representation it uses for each field carrying a derived identifier, and a verifier must not silently coerce between the listed forms. This paragraph discharges that duty for us: our log leaf is the payload, not the derived identifier in any representation. A rebuild is not proposed, and the count behind that is in the mapping under G2.
Implementations measured against this profile
- CSOAI-ORG/inspect-receipts at
cc8d6219(tree identical to PR headef2ec277), measured 2026-09-05 in issue #147 with the same probes as at397ae3ad: R1 is met (0 of 6 vectors diverge againstjcs.canonicalizeandrfc8785), R4 is met, the trust anchor is the emitter's field; R2 is met for a present foreign schema id (the verifier answers UNCHECKABLE, its own outcome) and not met for an absent one (the verifier answers VALID); R3 is not met (binding_checkedis reported without a digest to compare). Measured, not certified.
How to cite this profile
Receipt envelope profile
Identifier: proofbundle/receipt-envelope-profile/v0.1 (proposed; Owner decision pending)
Version: 0.1
Repository: https://github.com/b7n0de/proofbundle
File: docs/RECEIPT_ENVELOPE_PROFILE.md
Retrieved: <date you read it>
No DOI field appears until a deposit exists. There is no placeholder that looks like an identifier and no invented number.
Version rule
A change to a normative rule raises the version; older versions stay readable and are not overwritten. A version without a rule for how it changes is half a version.