| Internet-Draft | SCITT AI-Agent Action Receipts | August 2026 |
| Toraman | Expires 16 February 2027 | [Page] |
This document profiles the IETF SCITT (Supply Chain Integrity, Transparency, and Trust) architecture for AI-agent action receipts: tamper-evident, signed, offline-verifiable records of what an autonomous agent was recorded as doing at the governed boundary, under which recorded principal class, with what recorded verdict, and -- where the issuer records one -- under which policy identity. Each receipt is a signed record over a canonical JSON payload, hash-chained so that each record commits to its predecessor, and presented either bare -- the payload with its own native signature -- or enveloped in a COSE_Sign1. This revision specifies how such a receipt is carried as a SCITT Signed Statement, with the protected claims a Transparency Service requires, so that a receipt can be registered. Registration obtains a Transparency Service's signed proof that the statement was registered in its log -- a property a self-signed chain cannot provide alone. It does not, by itself, give an offline holder non-equivocation: that requires consistency proofs and monitoring of the log, which this profile does not specify. The profile makes a deliberately narrow, checkable claim: this is an issuer-authenticated, signature-verifiable, tamper-evident record of the action, the recorded principal class, the recorded verdict, and any policy identity the receipt carries. It explicitly does not claim that the agent was correct, safe, or wise, that the recorded inputs were true or complete, that a named approver authorized this exact action before it ran, that a downstream controller succeeded, or that any physical effect occurred. This revision separates those last three as distinct claims with independent failure behaviour, states the boundary of a shared action digest, and keeps a deterministic offline policy-replay capability out of scope.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 16 February 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
An AI agent books the flight, moves the money, or tells a machine on a factory floor to move an object. Afterwards someone asks a plain question: what actually happened, and who can prove it?¶
Today the answer usually comes from the party that ran the agent: its own logs, its own summary, its own word. This creates a first-party evidence problem when an action is disputed.¶
This document defines a receipt: one small, signed record per governed agent action, describing what was done, under which recorded principal class, with what verdict, and -- when the issuer records it -- under which policy. A bare receipt can be checked by anyone holding the issuing key; an enveloped one is checked against that key and the key that signed the envelope, which need not be the same (Section 4.2). Either way the check needs no access to the agent, the vendor, or any running service. It is deliberately narrow. A receipt proves that this record was issued by that key and has not been altered since. It does not prove that the agent was right, that its inputs were true, or that the world changed the way the record suggests.¶
Being narrow is the point. A record that promises more than it can support is worse than no record, because a relying party cannot tell where the support ends. This profile therefore states, in normative language, the things a receipt does not establish -- and, where the industry needs those things established, points at the separate documents that do it.¶
Three documents, one system. This revision is written as one part of a shipped, measured architecture, and a reader should be able to see the whole shape from here:¶
The division of labour is the argument: the record says what the governed boundary decided or dispatched, the digest says which action a later document is about, and the witness says what an outside system observed. Each fails on its own (Section 8.1), and no one of them is asked to carry the others' weight.¶
The gap this fills is a specification gap, not only a product one. Protocol specifications for agent connectivity and orchestration, and the agent runtimes built on them, leave the accountability layer -- a standardized, tamper-evident, issuer-authenticated record of an agent's actions -- open. OWASP's MCP Top 10 lists "Lack of Audit and Telemetry" as MCP08:2025 [OWASP-MCP]. Several recent efforts emit signed PERMIT/DENY decision receipts for agent actions; what is missing is a single, SCITT-native, COSE-based profile for per-action receipts that is (a) carriable as a Signed Statement for registration in a Transparency Service -- a log a third party can check for consistency -- and (b) honest about exactly what a receipt does and does not prove.¶
This profile does not define a new signature container. It defines an application payload profile carried in a COSE_Sign1 [RFC9052], so the outer signature can be verified by conforming COSE implementations that support the fully-specified Ed25519 algorithm identifier used by this profile. Section 4.3 specifies the additional protected claims that make one an RFC 9943 Signed Statement, and states plainly which of those the reference implementation does not yet emit.¶
What this revision adds. Review on the SCITT mailing list in July 2026 found that the single phrase "real-world outcome" in -00 hides three claims that a consumer must be able to fail separately: whether a named person authorized this exact action before it ran, what a downstream machine reported afterwards, and whether the intended physical effect actually occurred. Section 8 and Section 9 pull them apart and require each to be reported as a result a verifier renders, not a caveat a reader has to remember. Section 10 draws the boundary around the value that lets records about one action be joined at all. Appendix A tells the whole story of this revision -- what was asked, what was built and measured, and what the document now guarantees -- and every change is listed there.¶
The base record noa.receipt/0.1 is not modified: no member is added, removed, or re-typed. What
that guarantees is byte-level and nothing wider: every receipt, hash chain and checkpoint produced
under -00 still hashes to the same value, its signatures are still signatures over the same bytes,
and no linkage is invalidated. It does not follow that a -00 receipt is accepted by a -01
verifier -- acceptance is a separate question, answered in the next paragraph.¶
It does not guarantee that every -00 receipt is valid under -01, and the difference is worth
enumerating rather than glossing. This revision states validity conditions -00 did not: the verdict,
risk-class and governance-mode vocabularies are closed with a MUST-reject (Section 2); the
object is closed against unknown members (Section 3); a timestamp must be a valid RFC 3339
date-time and not merely shape-correct (Section 3.3); id is bounded at 128 code points,
seven members must be non-empty strings and a present governance.compliance.verdict must be
ALLOW or DENY (Section 3.1); sig.alg must be the exact literal ed25519
(Section 4.2); and the cross-field rules of
Section 3.1 refuse combinations -00 permitted. A receipt carrying an out-of-vocabulary
verdict, an unknown member, an impossible instant, an empty or over-long identifier, or
contradictory simulation markers would have
passed a permissive -00 verifier and is malformed here. A verifier's NFC configuration moves the
verdict on non-NFC strings in both directions (Section 3.3).¶
For a document adopting this profile's chain convention by reference (Section 1.2), the practical statement is therefore: the construction is unchanged; the acceptance rules are stricter.¶
Two related SCITT-AI drafts occupy different slots: an EU AI Act Article 50 disclosure profile (what content was generated and disclosed) and a session-archive format (a portable bundle of a whole agent session). This profile is orthogonal to both: it defines the per-action signed receipt -- one signed record per agent action, carried bare or in a COSE_Sign1 (Section 4), and holding the action, the recorded principal class, the recorded verdict, and any policy identity the issuer chose to record.¶
Three slots adjacent to this one are named here because this revision defines its boundaries against them rather than against prose: a reconciliation layer that admits heterogeneous attestations about one action and reports per-source verdicts [I-D.hillier-scitt-arp]; an executor-side admission boundary that decides whether an action may be consumed [I-D.schrock-action-evidence-boundary]; and a correlation value that lets records produced by parties with no shared schema commit to the same action projection [I-D.toraman-noa-action-digest]. None of the three is a dependency of this profile, and this profile is not a dependency of them.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
Requirements in this document are written in one of two forms. Most name their actor directly,
and most sections address a single actor and say so in their first sentence; where a section carries
requirements for more than one, each requirement names its own and the section preamble lists them.
The rest are written about an artifact -- "a receipt MUST contain no member other than those
shown", "sig.value MUST be canonical base64", "the kid SHOULD be carried in the protected
header" -- a shorthand used deliberately for schema and encoding constraints, because writing each
of them twice would bury them.¶
An artifact-form sentence binds the Producer that builds the artifact, at the strength written.
It does not, on its own, bind the verifier that receives it, and the two strengths are not
symmetric: a Producer MAY is a permission, and the corresponding verifier duty is to ACCEPT what
that permission allows, not to enjoy the same latitude. Section 4 shows the shape — kid MAY be
carried in the unprotected header, and a verifier MUST resolve it from either bucket.¶
A verifier's disposition is therefore stated explicitly wherever the profile requires one: an artifact-form MUST is paired with the rejection it implies, and an artifact-form SHOULD or MAY is paired with the acceptance a verifier owes a conforming artifact that exercises the permission. An artifact-form SHOULD is never, by itself, a ground for a verifier to reject. Where this document states a Producer SHOULD and assigns no verifier disposition, none is required: the receiver's behaviour is unconstrained and conformance is not tested on it.¶
A sentence that fits neither form is an editorial error rather than a hidden obligation. Two kinds of sentence deliberately carry no keyword and no obligation: the blocks marked non-normative, and statements about what a future revision might do. Seven actors appear: three verifiers of increasing scope, and four parties that make, operate, register and consume. A section states its actor in its first sentence where the whole section is addressed to one; where a section mixes actors, each requirement names its own.¶
| Actor | Holds / is given | Is required to |
|---|---|---|
| Producer (Issuer) | the signing key, and the facts it records | build receipts to Section 3, Section 3.1 and Section 3.2, and sign them per Section 4.2 |
| Receipt Verifier | one receipt; a keyring; optionally an identity manifest | check that one object: shape, canonical form, the recomputed chain.hash, the signatures it is given, and -- when a manifest is supplied -- attribution |
| Chain Verifier | a presented set of receipts; a keyring; optionally a checkpoint and an identity manifest | everything above for each member, plus the set-level conditions of Section 5.1 |
| Evidence Verifier (not a conformance role in this revision) | all of the above, plus the separately signed artifacts of Section 8 and Section 9, a trust configuration, and the consumer intent it was asked to answer | everything above, plus authorization, outcome, signer relationship and sufficiency for that intent -- described here, but NOT testable against this document, because those artifacts, that configuration and the result representation are all left undefined (Section 1.5). Nothing in this revision can be conformant or nonconformant as an Evidence Verifier; the passages addressed to it are analysis, and a future revision or companion must define its inputs and outputs before the role can carry conformance |
| Transparency Service | a submitted Signed Statement, and a means of resolving the registrant's verification key | apply the Registration Policy of Section 7, which states which of its checks are conditional on what it can resolve |
| Relying Party | a verifier's result, and any receipts it chose to keep | read that result within the limits Section 8 states; where it compares, indexes or alerts on receipt fields itself, apply the byte-comparison and parsing rules of Section 3.1 and Section 13 |
| Deployment (Operator) | the running system: producers, verifiers, keys, and what is presented or registered | choose the configurations this profile leaves open -- identifier syntax, NFC mode, resource limits, checkpoint issuance and freshness, what is registered -- and present complete chains for verification |
Three rules govern the table, and each closes a way of misreading it.¶
Several requirements in Section 6.1, Section 8, Section 9 and Section 10 are addressed to the Evidence Verifier over objects whose wire formats are not specified here. (The attribution rule of Section 6 is not among them: it is a Receipt-Verifier requirement, conditional on a manifest being supplied.) This section lists them once, so that no reader mistakes a boundary for an omission and no implementer mistakes an unimplementable MUST for an interoperability requirement:¶
agent.id to authorized key identifiers): its encoding,
distribution, signing, versioning and revocation rules;¶
UNTRUSTED, and the per-receipt outcomes VALID,
MALFORMED and TAMPERED -- so that implementations describing the same finding use the same
word. Two results are deliberately not tokenised in this revision: the conflict result of
Section 8.2 and the unproven-authorization result of Section 9. A third case that might look
like one is not: the indeterminate comparison of Section 10 is reported with
Section 8.3's INDETERMINATE, which is a token this document defines. An implementation MAY report
the two untokenised results under a name of its own; what it MUST NOT do is present that name as
this profile's vocabulary, since a second implementation reading this document will not have
chosen the same one. Whether they should be tokenised here is left to the working group
(Section 15). This revision also does not define how any
of them are carried, so two implementations will not be wire-compatible in their outputs until
something does.¶
Where such an object is absent, the corresponding requirement is not a requirement a conforming implementation can fail: it has nothing to evaluate, and Section 8 states what a bare receipt cannot establish. Deployments that need these claims need those artifact definitions, which live in the companion documents and in the reference implementation cited in Section 12 -- not here.¶
Definitions bind every actor. Where an entry carries a requirement it names its own actor: the entries below address the Producer (what it MUST emit), the Receipt Verifier (what it MUST reject), the Relying Party (what it MUST NOT read into a value) and the Deployment (what it selects).¶
agent.principal carries one of exactly four enumerated values -- HUMAN, SERVICE,
POLICY, SANDBOX_SIM -- and nothing else. It is a class marker, not an identity: it names no
person, service or key, and a verifier MUST NOT read an identity out of it. SANDBOX_SIM marks a
simulated action.¶
governance.verdict, drawn from a closed set of exactly seven
uppercase tokens. (-00 called this a terminal verdict. It is not: DEFERRED records that no
decision has been made, and ALLOWED records a permission a later attempt may still consume. The
name is corrected here rather than carried forward.) The tokens are: ALLOWED, BLOCKED, DEFERRED, EXECUTED, FAILED, ROLLED_BACK,
SIMULATED. The comparison is exact and case-sensitive; no other value is valid, and a verifier
MUST reject a receipt whose verdict is outside this set as malformed rather than treating it as
an unrecognized extension. Two of the seven carry the normative weight used elsewhere in this
document: ALLOWED records that the action was permitted at the governed boundary, and is the
verdict from which an authorization descends -- an Evidence Verifier MUST NOT accept a receipt
carrying any other verdict as the authorization for an action (Section 9); EXECUTED records that
the action was dispatched or attempted there, and nothing about what happened downstream
(Section 8.1). EXECUTED covers both "dispatched" and "attempted": those are different
states, this field does not separate them, and neither means the action took effect. The
remaining five record a refusal (BLOCKED), a decision not yet made (DEFERRED), a failure at
the governed boundary (FAILED), an action the issuer records as compensated (ROLLED_BACK --
the profile can check neither the original effect nor the compensation), and a simulation
(SIMULATED).¶
agent.principal == SANDBOX_SIM, governance.verdict == SIMULATED, and
governance.sandboxed. A producer MUST make them agree: if any one of them marks the action as
simulated, governance.sandboxed MUST be true, and a verifier MUST reject a receipt in which
governance.sandboxed is false while either of the other two marks a simulation. The converse
is deliberately permitted -- a sandboxed run may carry an ordinary lifecycle verdict -- because a
deployment may exercise the real decision path inside a sandbox. What the rule buys is exactly
one thing: a receipt cannot say both at once. An issuer that consistently marks a real action as
simulated, or a simulation as real, produces a receipt no verifier can contradict -- the record is
the issuer's account of its own run, and this profile has no external truth source for it
(Section 8). Without the rule even the internal contradiction would pass, and a reader could not
tell which of two markers to believe.¶
action.riskClass, drawn from a closed set of exactly five uppercase
tokens: LOW, MEDIUM, HIGH, CRITICAL, IRREVERSIBLE. The comparison is exact and
case-sensitive, and a verifier MUST reject a receipt carrying any other value as malformed rather
than treating it as an unrecognized extension. The value is issuer-assigned: it records the class
the issuing boundary applied to this action, not an independent assessment of danger, and a
relying party MUST NOT read it as one. A Deployment that gates approval authority on risk -- for
example requiring a higher approver tier for CRITICAL and IRREVERSIBLE than for HIGH --
selects that tier from this field, which is why an issuer that understates it weakens its own
approval requirement rather than the verifier's checks (Section 9).¶
governance.mode, drawn from a closed set of exactly four
lowercase tokens: off, shadow, approvals_on, on. The same exactness applies: the tokens
are lowercase, the comparison is case-sensitive, and any other value MUST be rejected as
malformed. The field records which governance posture the issuing boundary was operating under
when the receipt was produced. This profile assigns it no authorization semantics: a verifier
MUST NOT infer from any value that an approval was obtained, that a policy was enforced, or that
an action was permitted -- those are governance.verdict and the artifacts of Section 9.¶
policyHash) used as its published
identity. A deployment will normally want it to be deterministic and side-effect-free -- that is
what makes a commitment to it worth anything -- but this profile specifies no way to evaluate a
policy and therefore no way for a verifier to check either property; the words are a description
of intended practice, not a requirement it can test. A receipt carries that hash only when the optional
commitment of Section 3.3 is present; governance.ruleId alone is a label, not a
commitment.¶
agent.id to its
authorized key identifier(s) (kid), used to authenticate which agent acted, not merely
that a trusted key signed.¶
agent.id
of the receipt at sequence 0 (Section 5.2), which is a set rather than one key -- so that a
re-heading attacker cannot substitute a foreign checkpoint. Without a manifest there is no such
binding and the property does not hold.¶
Every requirement below names its actor: construction rules bind the Producer, validation rules the Receipt Verifier, the tenant rule of Section 3.3 the Chain Verifier, and the comparison rules for identifiers and timestamps the Relying Party (Section 1.4).¶
The receipt payload is the JSON Canonicalization Scheme (JCS) [RFC8785] serialization of a JSON object. A Producer MUST emit integers only (no floating-point), and a Receipt Verifier MUST reject a payload carrying a non-integer number. String normalization is specified in Section 3.2: producers MUST emit NFC, and verifiers MUST NOT normalize. A verifier MAY be configured to refuse a non-NFC receipt, but a verifier that accepts one MUST NOT repair it, and in either mode a Relying Party MUST NOT infer that a verified receipt's strings are NFC (Section 3.3). The fields:¶
{
"spec": "noa.receipt/0.1",
"id": "<receipt id>",
"ts": "<RFC 3339 date-time>",
"scope": { "tenant": "<id>"?, "chain": "<chain id>" },
"agent": { "id": "<agent id>",
"model": "<vendor/model|null>"?,
"principal": "HUMAN|SERVICE|POLICY|SANDBOX_SIM" },
"action": { "id": "<tool/action id>",
"canonical": "<risk-table key>",
"riskClass": "LOW|MEDIUM|HIGH|CRITICAL|IRREVERSIBLE",
"paramsHash": "sha256:<hex>|hmac-sha256:<hex>",
"reversible": <bool>,
"rollbackRef": "<id>|null"? },
"governance": { "mode": "<governance mode>",
"verdict": "<lifecycle verdict>",
"sandboxed": <bool>,
"ruleId": "<id>|null"?,
"approval": { "by": "<approver>",
"at": "<RFC 3339 date-time>" }|null?,
"compliance": { "policyHash": "sha256:<hex>",
"readSetHash": "sha256:<hex>",
"inputsHash": "sha256:<hex>",
"verdict": "ALLOW|DENY"? }|null? },
"chain": { "seq": <int>, ; >= 0
"prevHash": "sha256:<hex>|null",
"hash": "sha256:<hex>" },
"sig": { "alg": "ed25519",
"kid": "<key id>",
"value": "<base64 Ed25519 signature>" }
}
¶
The object is closed. A receipt MUST contain no member other than those shown here, at any
level; a verifier MUST reject an unknown member rather than ignore it. Extension by adding members
is therefore not possible in noa.receipt/0.1 -- that is what makes the hash construction of
Section 4.2 reproducible across implementations, and it is why Section 8.4 carries
new claims in separate artifacts instead.¶
A ? marks a member that MAY be absent; every other member is REQUIRED, including sig. No member
of this record is added, removed, or re-typed by this revision; -00's figure omitted sig and did
not mark the optional members, and both omissions are corrected here to match the shipped schema
(Section 3.3).¶
A Producer MUST NOT place raw prompts, tool arguments or secrets in any member, and the
commitments this record carries are hashes rather than values. That is a rule about what a producer
may write, not a property the format enforces, and its limit is worth stating precisely -- several
members (id, agent.id, agent.model,
action.id, action.canonical, governance.ruleId, governance.approval.by,
action.rollbackRef) are constrained in length and emptiness at most (Section 3.1) and in
content not at all, so a producer can put anything in them and no verifier can detect it. The prohibition binds producers; it is not a property a verifier can check,
and Section 14 says what follows. paramsHash MAY be hmac-sha256:<hex> with a
tenant-scoped key where a plain SHA-256 over a low-entropy value (an amount, an id, a boolean)
would be guessable.¶
These commitments are per-producer, and this profile does not make them comparable. Neither
action.paramsHash nor the three hashes of governance.compliance has a preimage construction
defined here: what is hashed, in what serialization, under what domain separation, is the
producer's own contract with whoever will later open the commitment. Two conforming producers given
identical parameters can therefore compute different values -- by design rather than by omission.
Section 10 explains why a cross-producer join key needs a construction identifier before
any two parties may compare digests, and names the companion that defines one. A verifier MUST NOT
compare these values across producers, and MUST NOT treat equality of them as evidence of anything.
For the keyed form the same reasoning cuts harder: the key is a tenant secret, so an offline third
party cannot check an hmac-sha256 commitment at all, and this profile defines no key identifier,
rotation rule or distribution mechanism for it. A producer that rotates that key can still open
earlier commitments for as long as it retains the retired key -- and cannot once it destroys it.
Which of those happens is a deployment's key-management decision, not something this profile
requires or can check; what it means is that the openability of a keyed commitment depends on a
practice outside the record.¶
chain.hash is "sha256:" followed by the lowercase hex of SHA-256 over the JCS serialization of
the receipt with chain.hash and sig.value removed; Section 4.2 pins that construction
byte-exactly, together with the signature computed over the same digest. Receipts in one scope.chain are linked by prevHash; prevHash is null only at the
genesis receipt (seq == 0).¶
The cross-field rules address the Receipt Verifier; the emission obligations they imply address the Producer. The tenant rule of Section 3.3 addresses the Chain Verifier.¶
The figure above shows shapes; this subsection fixes the meanings a verifier may rely on, and the combinations it must refuse. Where a member's meaning is the issuer's to choose, that is said plainly rather than left to be inferred.¶
Notation. In the figure, null denotes the JSON null literal, never the four-character string
"null"; a quoted alternative such as "sha256:<hex>|null" means "either that string form or JSON
null". Angle brackets mark a value the issuer supplies.¶
The figure's types are normative. A Receipt Verifier MUST reject a receipt in which a member's
JSON type differs from the one shown, rather than coerce it: <bool> means the JSON literals true
or false and never the strings "true"/"false" or the numbers 1/0; <int> means a JSON number
that is an integer within the range Section 3.2 fixes; every other member is a string except
where null is shown as an alternative. Three shapes are fixed exactly, because a later comparison
depends on them:¶
chain.seq MUST be an integer and MUST NOT be negative;¶
chain.hash, chain.prevHash when not null, and each of governance.compliance's
policyHash, readSetHash and inputsHash MUST be "sha256:" followed by exactly 64 lowercase
hexadecimal digits;¶
action.paramsHash MUST be that same form or "hmac-sha256:" followed by exactly 64 lowercase
hexadecimal digits.¶
Uppercase hexadecimal is not an accepted spelling of any of them. These are byte-level rules, not presentation preferences: the digest is inside the signed payload, so two spellings of one digest are two different receipts.¶
spec. The exact string noa.receipt/0.1. A verifier MUST reject any other value rather than
attempt a best-effort parse: this profile defines no version negotiation, no downgrade rule, and no
forward-compatibility contract, and a future wire version will carry a different spec and its own
signing domain (Section 8.4).¶
id, and identifiers generally. id identifies this receipt within its issuer's own naming.
This profile defines no namespace, no uniqueness requirement across issuers, and no collision
procedure -- and the same is true of scope.chain, agent.id, action.id, action.canonical,
governance.ruleId, action.rollbackRef and governance.approval.by. Two issuers may use the same
string for different things. A relying party that treats any of them as globally meaningful MUST
supply the namespace itself, typically by scoping to the issuing key; equality of these strings
across receipts from different issuers establishes nothing (Section 13).¶
Two syntactic bounds are pinned, because every implementation already enforces them and no document said so. Both bind the Producer on construction and the Receipt Verifier on validation:¶
id MUST be at most 128 Unicode code points, and a verifier MUST reject a longer one. The unit
is code points, not UTF-16 code units: an identifier of 65 astral characters is 65 code points
and 130 code units, so a verifier counting units rejects a receipt a verifier counting points
accepts, on identical signed bytes.¶
id, scope.chain, agent.id, action.id, action.canonical,
sig.kid and sig.value. governance.approval.by is deliberately not among them -- it is an
opaque label this profile never authenticates (Section 6.1), and forbidding one of its values
would suggest the others mean something. The checkpoint object of Section 5.2 carries the same
rule for three further members, which is ten string boundaries across the two objects.¶
ts. The time the issuer recorded the receipt. It is not the time a decision was taken, nor
the time an action was attempted, nor an observation time, and it is signer-chosen: nothing in this
profile makes it checkable (Section 3.3).¶
action.canonical. A key into the issuer's own risk table -- the stable name under which the
issuer classifies this kind of action. The table is not defined here, has no registry, and is not
comparable across issuers; action.id names the specific tool or operation invoked.¶
Cross-field rules a verifier MUST enforce. These are the combinations that would let one record say two things:¶
governance.sandboxed MUST be true whenever agent.principal is SANDBOX_SIM or
governance.verdict is SIMULATED (Section 2);¶
action.rollbackRef MUST be absent or null unless action.reversible is true, and a
governance.verdict of ROLLED_BACK MUST be accompanied by action.reversible: true;¶
governance.compliance.verdict is present it records the policy evaluation's own result,
which is a different question from governance.verdict; the two MUST NOT be read as one, and a
verifier MUST NOT resolve a disagreement between them by preferring either -- it reports both. The
member is a closed two-value set: where present it MUST be exactly ALLOW or DENY, and a
verifier MUST reject any other value rather than carry an unrecognized string through into a
report a relying party will read.¶
What no rule constrains, stated so nobody assumes it. governance.approval may accompany any
lifecycle verdict, and its at may be later than ts: this profile places no temporal invariant on
them, because a signer-chosen timestamp cannot establish that an approval preceded an action
(Section 6.1). A verifier MUST NOT infer ordering from these fields.¶
A digest is only interoperable if the parameters that produced it are pinned. Naming a
canonicalization ("canonical JSON") without pinning its parameters is the failure mode that does
not announce itself: two implementations each behave correctly under their own reading and
produce different bytes, or -- worse -- the same bytes for inputs that should differ. This section
therefore pins every parameter of the construction above, normatively and exhaustively for the
receipt object as a whole. One carve-out belongs next to the claim rather than three sections
away: the encoding of the action sub-object is not fixed for a party computing its own digest
over that sub-object, because an optional member may be absent or present-as-null and the two
produce different bytes (Section 10). Inside a receipt that choice is signed and therefore
settled; across producers computing an independent digest it is not.¶
| Parameter | Value for noa.receipt/0.1
|
|---|---|
| Serialization | JCS [RFC8785] |
| Member sort | UTF-16 code unit (RFC 8785); NOT Unicode code point -- the two diverge for astral characters, which encode as surrogates D800-DBFF and sort BEFORE E000-FFFF
|
| Number rendering | RFC 8785; integers only, abs(n) <= 2^53 - 1; non-integer and non-finite values MUST be rejected |
| Unicode normalization | NONE applied by the canonicalizer, ever. Producers MUST emit NFC ([UAX15]); verifiers MUST NOT normalize before hashing. A verifier MAY refuse a non-NFC receipt outright (see Section 3.3); what it MUST NOT do is silently repair one |
| String escaping | RFC 8785: " (U+0022) and \ (U+005C) escaped; the short forms \b \t \n \f \r used where defined; every other control character below U+0020 escaped as \u00xx; every other code point emitted literally as UTF-8 |
| Unpaired surrogates | MUST be rejected (they would collapse to U+FFFD at the UTF-8 hashing step, mapping distinct inputs onto one digest) |
| Duplicate member names | MUST be rejected at the parse boundary, not resolved last-wins |
| Forbidden member names |
__proto__, prototype and constructor MUST be rejected at the parse boundary, in every JSON document a verifier parses -- receipt, keyring, identity manifest, checkpoint -- and not filtered after parsing |
| Digest | SHA-256 ([FIPS180-4]), lowercase hex, sha256: prefixed. action.paramsHash MAY instead be HMAC-SHA-256 ([RFC2104]) under a tenant-scoped key, written hmac-sha256:<hex>
|
| Timestamps | RFC 3339 [RFC3339] date-time, with Z or a numeric offset -- the accepted spellings are pinned in Section 3.3, which permits both. A verifier MUST reject a value that is not a valid RFC 3339 date-time, and valid syntax still does not make a timestamp truthful |
| Domain separation | the receipt signature is over the UTF-8 bytes of "NOA-Receipt-v0.1-sig:" followed by the raw SHA-256 of the hash input, pinned byte-exactly in Section 4.2; the checkpoint signature uses a distinct tag (Section 5.2), so neither can be replayed as the other |
The forbidden-name rule is a parse rule rather than a schema rule, and the difference is the whole point of it: in several host languages, materializing a member with one of those names mutates the object being built, so a document carrying one can change how the verifier's own checks behave before any schema runs. A receipt could not carry them in any case, since the object is closed (Section 3) -- but the keyring, identity manifest and checkpoint a verifier also parses are not closed by this profile, and the parse boundary is where all four documents meet the same rule.¶
On the outer COSE path, verifiers MUST NOT normalize, re-order, or re-render a received payload before verifying it: the bytes as received are the bytes that were signed. On the derived path of Section 4.2 the verifier does re-serialize, by construction -- it must, since two members are omitted -- and this table is exactly the rule that makes that re-serialization reproduce the producer's bytes.¶
action.paramsHash is a per-producer commitment to that producer's own parameter set. It is NOT
a cross-producer correlation key and MUST NOT be treated as a shared action digest: it does not
commit to the tenant, chain, authorization, execution attempt, or nonce, and it may legitimately
repeat across retries of the same action. Section 10 states what a construction must carry
before any two parties may compare digests at all.¶
Seven members of Section 3 MAY be absent: scope.tenant, agent.model, action.rollbackRef,
governance.ruleId, governance.approval, governance.compliance, and -- inside a present
compliance -- its verdict. Absence is a producer's version or configuration choice; it is never,
by itself, evidence of tampering, and a verifier MUST NOT convert it into one. Five of them --
agent.model, action.rollbackRef, governance.ruleId, governance.approval and
governance.compliance -- may also be present-as-null, which a verifier MUST treat as the value
null and not as absence: the two encodings produce different canonical bytes and therefore
different hashes and signatures.¶
The two whose absence changes what a verifier may conclude are singled out below; the rest are ordinary optional fields carrying no security consequence beyond that rule.¶
scope.tenant MAY be absent, and the security-bearing rule is stated first (this one is
Chain-Verifier-class, since it compares receipts to one another): a conforming Chain
Verifier MUST reject a transition between two present, unequal scope.tenant values within
one scope.chain. One chain belongs to one tenant, so a change of tenant inside it is a splice
of two histories rather than a version change. Absence then carries forward rather than
resetting: a verifier MUST NOT treat an absent scope.tenant as a fresh start, so
A -> absent -> B is the same cross-tenant splice as A -> B and MUST receive the same verdict,
while A -> absent -> A (the same tenant resuming) and absent -> A (enrichment) remain valid.
Without the carry-forward rule, an optional field becomes a splice channel.¶
governance.compliance MAY be present. When present it carries a commitment to the policy's
published identity (policyHash), to the policy's closed read-set (readSetHash), and to the
recorded decision inputs (inputsHash) -- hashes only, never the inputs themselves. Its presence
does NOT make the receipt policy-replayable under this profile: this document does not specify
the evaluation grammar, operator set, or evaluation order that a re-derivation would need, and
Section 11 still governs. Its presence also does not establish that the referenced policy was
adequate or in force.¶
On rejecting non-NFC input. A verifier MAY offer a mode that refuses any receipt carrying a non-NFC string, reporting it as malformed. That mode is conformant and is a reasonable default for a relying party that indexes on these fields; what is NOT conformant is normalizing a received payload before hashing it, in any mode. A verifier's default SHOULD be stated in its documentation, because the two modes give different answers on the same bytes and a caller cannot tell which it got from a boolean.¶
On timestamps. ts and governance.approval.at are RFC 3339 [RFC3339] date-time values,
and a conforming verifier MUST reject a value that is not a valid RFC 3339 date-time. A
digit-and-separator shape check is not sufficient: 2026-13-45T99:99:99Z matches the shape while
lying outside the grammar's own numeric ranges, and a receipt carrying it is malformed.¶
Because these values are inside the hashed payload, the spelling is part of the signed bytes, and two spellings of one instant are two different receipts with two different hashes. The profile therefore pins what is accepted rather than leaving it to each implementation:¶
T and Z separators MAY be upper or lower case, as RFC 3339 permits;¶
+HH:MM / -HH:MM) is permitted as well as Z; a receipt is not required to
be written in UTC, and -00:00 carries the meaning RFC 3339 gives it;¶
date-time whose seconds field reads 60, and a verifier MUST reject
one. This profile forbids the leap-second representation rather than requiring anyone to validate
it. The reason is that no other rule here can be enforced offline without it: [RFC3339],
Section 5.7 admits 60 only at an actual leap second, adjusted by the numeric offset, and says
future leap seconds cannot be predicted -- so a verifier holding no leap-second table cannot
decide whether 2030-06-30T23:59:60Z is a valid date-time at all, and the requirement to reject
what is not valid RFC 3339 would have no determinate answer. Forbidding the representation costs a
Producer nothing it needs: an agent action stamped during a leap second is recorded at :59 or at
the following second, and the receipt still denotes an instant. This is a deliberate narrowing of
RFC 3339 by this profile, not a claim about RFC 3339, and Section 15 records it as a
decision a working group may reverse.¶
A verifier MUST NOT normalize a timestamp before hashing, for the same reason it MUST NOT normalize anything else (Section 3.2). Relying parties comparing timestamps across receipts MUST parse them into instants first: byte equality and instant equality are different questions here.¶
Valid RFC 3339 syntax does not establish that the timestamp is truthful; it remains signer-chosen. A verifier that accepts a well-formed timestamp has learned that the issuer wrote a well-formed timestamp, and nothing about when anything happened.¶
governance.ruleId is a signer-asserted label naming which rule fired. It is not a content
commitment to a policy, and a relying party MUST NOT treat it as one. Where a receipt carries no
governance.compliance, the receipt's "policy identity" is exactly that label and nothing
stronger. (-00's terminology defined a policyHash that -00's payload never carried; this
subsection resolves that inconsistency in favour of what the wire format actually does.)¶
This profile adds no member to noa.receipt/0.1. A claim that this record does not carry travels
as a separate signed artifact, linked by hash -- see Section 8.4 for why that choice is
load-bearing rather than stylistic.¶
A receipt may be presented in two forms, and this profile defines both. A receipt's payload
is the JSON object of Section 3, and its native signature (Section 4.2) is computed over
that object; a receipt presented as payload plus native signature, with no COSE envelope, is a
bare receipt. A receipt carried inside a COSE_Sign1 is an enveloped receipt, and only an
enveloped receipt can become a Signed Statement (Section 4.3). Both forms are legitimate
presentations: the reference implementation's chain path verifies bare receipts and never touches
COSE (Section 12), while its COSE path produces the enveloped form. Every requirement
below that names the envelope applies to the enveloped form only; a Receipt Verifier given a bare
receipt checks the payload, the recomputed chain.hash and the native signature, and reports that
no envelope was present. Whether a SCITT-native profile should require the envelope is left open
(Section 15).¶
Every requirement below names its actor: what a Producer MUST emit, and what a Receipt Verifier -- given one receipt and a keyring (Section 1.4) -- MUST check. No set and no evidence artifacts are needed.¶
An enveloped receipt is a COSE_Sign1 carried as the tagged form, CBOR tag 18: a Producer MUST
emit the tag, and a verifier MUST reject an untagged COSE structure rather than guess at its type.
Its protected header MUST contain alg;
the receipt MUST additionally carry a key identifier, which SHOULD be in the protected header and MAY
be in the unprotected header (see below -- this avoids the RFC 2119 contradiction of requiring kid in
the protected header while also permitting it in the unprotected one). In detail:¶
alg (label 1) = -19 (Ed25519) as registered in the IANA COSE Algorithms registry by
[RFC9864], Section 2.2 (the fully-specified Ed25519 identifier; [RFC8032] Section 5.1
parameter set). Issuers MUST use -19. Verifiers MUST reject every other alg value
(algorithm-confusion defense). That includes the polymorphic EdDSA identifier
-8, which [RFC9864], Section 4.2.2 marks Deprecated (it was registered in [RFC9053]). It is
not a special case, and for this profile there is no
legacy-compatibility exception. The signing key MUST be an OKP key with crv = 6 (Ed25519), the
curve registered for Ed25519 in the COSE Elliptic Curves registry ([RFC9053], Section 7.1); a
verifier that can inspect the resolved key's type or curve MUST reject any key that is not
Ed25519 rather than attempt the verification, since a check against a key of another type answers
a different question than the alg value claims.
[RFC9864] makes alg = -19 self-disambiguating, so a verifier MUST NOT rely on the key's crv
alone to determine the algorithm.¶
kid (label 4) is the
baseline: a Producer MUST emit one, and it MUST be sufficient on its own for a verifier holding
the right keyring to resolve the verification key. The kid SHOULD be carried in the protected header (where it is covered by
the signature and cannot be stripped or swapped); it MAY instead be carried in the unprotected
header -- a verifier MUST resolve it from either bucket, preferring the protected copy when both
are present, and MUST report which bucket it used. Resolving a key and naming an identity are two
different acts, and this profile separates them: an unprotected kid MAY be used to resolve
the verification key, and MUST NOT be reported as the emitter's identity, because it is not
covered by the signature and can be swapped without breaking it (Section 6). [RFC9052] treats kid as a hint that need
not be unique; this profile narrows that for a verifier's own key store. Where one kid resolves
to more than one key, a verifier MUST try each candidate and accept only if exactly one verifies;
where more than one key was tried it MUST report the identifier as ambiguous rather than silently
choose -- attribution built on a collision
is attribution to the wrong party. kid is an opaque byte string in [RFC9052] and this profile
does not narrow that; what it does require is that no verifier resolve it through a lossy
decode. A verifier whose key store is keyed by text MUST decode the kid bytes, re-encode the
result, and require the original bytes back before using the identifier, and MUST refuse to
resolve one that fails that round trip -- a lossy decode maps every invalid byte sequence onto
the same replacement character, so distinct signed identifiers collapse onto one entry and
attribution goes to whoever holds it.¶
x5t (label 34) or x5chain (label 33) as defined by [RFC9360].
A receipt MUST NOT rely on them as its only key identifier, because a verifier conforming to this
profile is required to resolve kid and is not required to process certificates. The reference
implementation ignores both headers and refuses a receipt carrying neither a protected nor an
unprotected kid (Section 12).¶
kid, content type, CWT_Claims, or crit itself -- is never on
its own a reason to reject a COSE_Sign1. What decides the outcome is crit (label 2).
This profile requires a verifier to ignore any header parameter it does not understand that
crit does not list, rather than reject the object -- that rule is this document's, not
[RFC9052]'s, which defines crit without imposing it. What [RFC9052], Section 3.1 does state
is the fatal direction: a crit entry naming a header parameter that is not in the protected
bucket is a fatal processing error. A verifier MUST likewise reject an object whose crit lists a
header parameter it cannot process.
Section 4.3 adds one further rejection rule, for CWT_Claims appearing in both buckets.¶
The protected header MUST satisfy the Core Deterministic Encoding Requirements of [RFC8949], Section 4.2.1: shortest-form argument encodings and map keys sorted in bytewise lexicographic order of their deterministic encodings, each key occurring once. Verifiers MUST reject any other encoding.¶
Construction rules address the Producer; verification rules address the Receipt Verifier, which is given the receipt and a keyring (Section 1.4).¶
The payload is the JCS serialization from Section 3, carried attached as the COSE_Sign1
payload. This profile does not define a detached-payload mode: in COSE, verifying a detached
payload requires the verifier to be supplied the payload bytes themselves for the Sig_structure,
so a detached receipt is not self-contained, and chain.hash -- a digest over the receipt with
chain.hash and sig.value removed -- is a chain-linkage value, not a substitute for those bytes.
A verifier conforming to this profile MUST reject a COSE_Sign1 whose payload element is absent or
null. (An earlier revision said a detached payload's digest was bound through chain.hash; that
was wrong in both directions, and is corrected rather than carried forward. A detached mode, if
one is ever wanted, is future work -- see Section 12.)¶
A receipt in this profile carries two independent signatures, and conflating them is the failure this subsection exists to prevent.¶
sig.value, is computed over a domain-separated preimage,
fixed byte-exactly by the construction below.¶
Sig_structure over the protected
header and the payload -- and the payload is the whole receipt including its native sig
member. The external_aad field of that structure is the empty byte string in this profile;
a verifier MUST use a zero-length external_aad and MUST NOT accept an envelope that only
verifies under some application-supplied value, since nothing in the envelope would record what
that value was.¶
The native construction, in full:¶
HASH-INPUT = JCS( the receipt object with exactly two members
removed: chain.hash and sig.value; every other
member, including sig.alg and sig.kid, is present )
MESSAGE = UTF8("NOA-Receipt-v0.1-sig:") ; 21 octets: tag then ":"
|| SHA-256(UTF8(HASH-INPUT)) ; RAW 32-octet digest,
; NOT its hex form
; 21 + 32 = 53 octets
sig.value = base64(Ed25519(MESSAGE)) ; standard alphabet,
; with padding
sig.alg = "ed25519" ; exact lowercase literal
chain.hash = "sha256:" || lowercase-hex(SHA-256(UTF8(HASH-INPUT)))
¶
|| denotes octet-string concatenation, and base64 is the standard alphabet with padding
([RFC4648], Section 4). Two encoding rules close variants that would otherwise let one signature
be written two ways: sig.value MUST be canonical base64 -- exactly one padded form, no whitespace,
no URL-safe alphabet, and the unused bits of the final quantum set to zero, as [RFC4648],
Section 3.5 requires of a canonical encoding -- and MUST decode to exactly 64 octets. Re-encoding
the decoded value and requiring the original string back enforces every one of those rules at once,
and is the check a verifier SHOULD implement rather than testing each separately. The signature
itself MUST be a
canonical Ed25519 encoding, with the scalar S reduced modulo the group order. A verifier MUST
reject a non-canonical encoding of either kind rather than accept it as an equivalent form. This
matters more here than in a bare COSE profile because sig.value is excluded from the hash input
(Section 4.2): a permissive decoder would let two byte-different receipts share one chain
hash. The digest that is signed and the digest that appears in
chain.hash are the same digest of the same input, in two encodings. The domain tag means a receipt
signature cannot be replayed as a signature for any other object class in the same system, and the
checkpoint tag (Section 5.2) is distinct from it.¶
The native algorithm label is checked before any cryptography. sig.alg MUST be the exact
lowercase string ed25519. A Receipt Verifier MUST reject any other value -- including a
differently cased spelling, and including a value naming an algorithm it happens to support --
before attempting a signature check. A verifier that selects its algorithm from the resolved key or
from its own configuration answers a different question than the receipt asks.¶
The verification equation is pinned. A verifier MUST check the equation
[S]B = R + [k]A' -- the cofactorless check that [RFC8032], Section 5.1.7 names as "sufficient,
but not required" beside the cofactored [8][S]B = [8]R + [8][k]A'. Both are permitted by RFC 8032
and they do not accept the same set of signatures: every signature the cofactorless equation accepts
is accepted by the cofactored one, and the reverse does not hold. Leaving the choice to the
implementer would leave validity itself implementation-defined, and in a hash chain a
disagreement about one signature is a disagreement about accepted history -- which is the hazard
this profile exists to remove. This revision therefore pins the narrower equation, so that two
conforming verifiers accept the same set. Section 15 records that the working group may
revisit the choice; what it may not do is leave it unmade.¶
Determinism is a product of this equation together with the encoding rules around it -- the
canonical S above, the canonical base64 that carries the signature, and the public-key rules
below -- and not of the equation alone. Section 12 reports which shipped verifiers
satisfy the pinned equation and which do not.¶
The verification key itself. Three rules bind whichever actor resolves a key for a native signature check, and they exist for the same reason -- the same signed chain must not be accepted by one verifier and rejected by another:¶
sig.alg is the same
algorithm-confusion defect Section 4.1 closes on the envelope path.¶
2^255 - 19. A public key that is one of the eight canonical small-order Ed25519 point encodings
MUST also be rejected. Both rules are about the key, not about the verification equation:
they remove the two classes on which widely deployed libraries were measured to disagree, so that
two verifiers handed one signed chain do not accept two different histories. The verification
equation itself is pinned above, not left to the implementer.¶
The relationship between the two signatures is normative:¶
kid identifies
the party that emitted this envelope and sig.kid identifies the party that signed this
receipt into its chain. A relying party MUST NOT collapse the two identities, and neither may a
verifier: they are two claims about two different parties, and Section 6 says what each
is checked against rather than letting one stand for both. A verifier MUST report every identifier
it reports as an identity, saying which claim each belongs to -- always the native sig.kid,
and the outer kid when it came from the protected bucket. An outer kid taken from the
unprotected bucket resolved a key and named nobody (Section 4.1), and MUST NOT be reported
as the emitter's identity.¶
chain.hash MUST be recomputed, and a mismatch MUST be rejected. A Receipt Verifier MUST
compute "sha256:" followed by the lowercase hex of SHA-256 over HASH-INPUT -- the same input
the native signature covers -- and compare it to the receipt's chain.hash. If they differ the
receipt MUST be rejected as tampered. This check is not optional and is not implied by either
signature: HASH-INPUT removes chain.hash, so sig.value does not cover it, and the outer
COSE signature that does cover it may have been applied by a different party (Section 4.2).
Without this recomputation a receipt whose chain.hash is an arbitrary value passes every other
Receipt-Verifier check, and every artifact that references a receipt by hash
(Section 8.4) inherits the forgery.¶
chain.hash are
verified over a derived input: the payload is parsed, the two named members are omitted, the
result is re-serialized under Section 3.2, and that string is hashed. A verifier MUST run
both and MUST NOT substitute one input for the other.¶
JCS(parse(bytes)) == bytes. If they differ,
the receipt MUST be rejected rather than repaired, because the field values the verifier would go
on to report are then not the values the outer signature covers. The derived input of the native
path omits two members by construction and therefore can never equal the received bytes; nothing
in this profile asks it to.¶
The mapping binds the Producer. The evaluation rule at the end binds whichever actor evaluates a receipt as a Signed Statement, and names it there.¶
A COSE_Sign1 built to Section 4.1 alone is a valid signed object, but it is not by
itself a Signed Statement in the sense of [RFC9943]. That architecture states two protected-header
requirements in the body of its Section 6: the protected header of a Signed Statement MUST include
the CWT Claims header parameter, whose value MUST include the Issuer Claim (label 1) and the
Subject Claim (label 2); and kid MUST be present in the protected header whenever neither x5t
nor x5chain is present there. A receipt that omits them will be refused at registration by a
conforming Transparency Service. This subsection specifies the mapping, so that the difference
between "a signed receipt" and "a registrable Signed Statement" is a checkable property rather than
a hopeful sentence.¶
To be carried as a Signed Statement under this profile, a receipt MUST additionally have:¶
| Requirement | Value for this profile |
|---|---|
CWT_Claims header parameter (label 15) in the protected header, encoded as specified in [RFC9597], Section 2 |
REQUIRED ([RFC9943], Section 6) |
iss (CWT claim 1) |
the Issuer of the Signed Statement: the authority on whose behalf the receipt is emitted. It is a signer-asserted string until something binds it to the verification key -- Section 7 requires a Transparency Service to make that binding and states that this profile defines no mechanism for it. This profile does not require iss to equal the identity that submits the statement for registration; the two are separable, and a Transparency Service applies its own Registration Policy to the submission |
iss when x5t or x5chain is in the protected header |
additionally MUST be a string conforming to the URI syntax, of length 1 to 8192 ([RFC9943], Section 6) |
sub (CWT claim 2) |
the scope.chain identifier, so one chain is one subject. scope.chain carries no namespace of its own (Section 3.1), so two issuers can choose the same string: a Transparency Service MUST scope subjects by issuer rather than treat sub as globally unique, and a chain carrying several agents remains one subject |
kid in the protected header |
REQUIRED. [RFC9943], Section 6 makes this conditional on the absence of x5t/x5chain; this profile requires a resolvable kid unconditionally (Section 4.1) and requires it protected for registration (Section 7), so the condition never relaxes it here |
The CWT_Claims header parameter MUST occur exactly once across the two header buckets, as
[RFC9597], Section 2 requires: a receipt carrying label 15 in both the protected and unprotected
headers is malformed, and a verifier MUST reject it rather than prefer one copy.¶
These claims are necessary, and this profile does not claim they are sufficient. [RFC9943]
describes a Statement as carrying a media type that tells a Transparency Service what it is holding,
and this revision registers none (Section 16) while leaving the protected content type
a SHOULD. So a receipt that carries iss and sub has met the requirement this profile can state
and may still be refused by a service that dispatches on a media type it does not have. Closing that
is the media-type registration named in Section 15, not a further claim here.¶
A verifier or Transparency Service evaluating a receipt as a Signed Statement MUST reject one
that lacks protected CWT_Claims carrying both iss and sub, per [RFC9943], Section 6. A verifier evaluating a receipt
merely as a signed record MUST NOT infer registrability from a successful signature check: the two
are different questions and this profile keeps them apart deliberately.¶
A receipt carried as a Signed Statement SHOULD also carry a protected content type (label 3)
describing the payload. This revision registers no media type (Section 16), which has a
consequence worth stating rather than leaving to discovery: until one exists, nothing makes two
conforming producers choose the same value, and where they choose differently a Transparency Service
that dispatches policy on content type will treat them as different formats. A Deployment SHOULD therefore agree the value with the
Transparency Service it registers against, and MUST NOT rely on it for cross-deployment
interoperability. Registering a media type is the first IANA action a future revision should
request.¶
Producers that do not emit these claims produce receipts that verify offline exactly as specified everywhere else in this document, and that a conforming TS is entitled to refuse. The reference implementation is currently in that position; Section 12 states it as a named conformance gap rather than leaving a reader to discover it at registration time.¶
This section's number, title and anchor are unchanged from -00, and the construction it defines is unchanged. Two sentences of -00's paragraph were factually wrong and are corrected here rather than reproduced and contradicted twenty lines later. Appendix A states the before and after, for the benefit of a document that adopts this section by reference (Section 1.2).¶
Each receipt carries a monotonic chain.seq (genesis = 0) and prevHash. A Chain Verifier checking
a presented chain MUST verify: each prevHash equals the prior receipt's chain.hash; the genesis
receipt has prevHash == null; and, when a signed checkpoint is present and an identity manifest
is supplied, that the checkpoint was signed by one of the keys that manifest authorizes for the
genesis receipt's agent.id, so that a foreign checkpoint produced by a re-heading attacker is
rejected -- without a manifest that binding does not hold (Section 5.2). This profile detects
in-band tampering within a presented chain, and detects tail truncation only against an
authenticated checkpoint (Section 5.1), never from the presented set alone; it does NOT, by
itself, detect equivocation (an issuer signing two divergent chains) -- that requires
registration in a SCITT Transparency Service or an equivalent external witness.¶
Section 5 and this subsection address the Chain Verifier (Section 1.4): a Receipt Verifier is given no set, cannot evaluate these conditions, and does not fail them.¶
This subsection is NEW in -01. Section 5 keeps its number, title and anchor because another profile adopts it by reference (Section 1.2); two of its sentences are corrected in this revision and Appendix A states which; everything below adds Chain-Verifier-class requirements that -00 did not state, and Appendix A lists each of them.¶
The checks above are properties of a receipt SET, not of any receipt. Six conditions are named below, and every one of them is a property of the presented receipts, decided by comparing them to one another: a verifier presented with any single receipt from the set will find that receipt individually well-formed, correctly hashed, and correctly signed. They are named so that a reconciling verifier can report WHICH set-level property failed, rather than collapsing every one of them onto a single "invalid".¶
A checkpoint is deliberately not among them. It is a separate object, supplied separately, and what it answers is a separate question -- so it has its own axis, defined after this table. Nothing on that axis is a chain condition, and nothing on it can fail a chain. That separation is Section 8.3's rule applied to this section: a finding about one object is never a finding about another.¶
| Axis | Failure |
|---|---|
chain-link-broken
|
a prevHash does not equal the prior receipt's chain.hash; a non-genesis receipt carries prevHash == null; or the genesis receipt carries a prevHash that is not null (Section 5 requires it to be) |
chain-sequence-duplicate
|
two receipts in one scope.chain carry the same chain.seq
|
chain-sequence-gap
|
the presented chain.seq values are not contiguous, or do not begin at 0. A conforming Chain Verifier requires the complete chain from genesis; see below |
chain-scope-mismatch
|
the presented set does not all carry one scope.chain. A Chain Verifier is handed a set as presented and checks that it is one chain; it does not assume it. A supplied checkpoint that names a different chain is not this condition -- it is a finding about the checkpoint (Section 5.2) |
chain-tenant-split
|
two present, unequal scope.tenant values appear within one scope.chain (Section 3.3) |
chain-key-discontinuity
|
two receipts in the presented chain carry the same agent.id and different sig.kid
|
A Chain Verifier MUST detect all six conditions -- each is decided from the presented set alone, so none of them depends on an input the verifier may not have been given -- and MUST report them distinctly enough that a consumer can tell which held.¶
The checkpoint axis, which is not a chain condition. A checkpoint answers one question: has the tail of this chain been cut? A Chain Verifier MUST report that question in exactly one of three states, and MUST NOT let any of them alter the verdict the six conditions produced:¶
checkpoint-head-conflict: the presented head and an
authenticated checkpoint disagree. Which of them is wrong -- a deleted tail, a fork, or a signer
error -- is not established by the comparison, and a verifier MUST NOT name a cause it did not
observe.¶
checkpoint-unverified is this document's identifier for the authentication case.¶
Two consequences follow, and both are the reason the axis is separate. A presented chain that satisfies all six conditions is a valid chain whatever arrives beside it: a defective checkpoint subtracts an answer, it does not create a failure. And whether the verifier could authenticate the checkpoint is what decides between answered and unusable -- never whether a keyring happened to be supplied. Section 12 measures what the other reading costs: every shipped verifier branches on whether a keyring was passed, and both branches produce a finding against the chain.¶
The distinction between the conditions is operational, not cosmetic: chain-sequence-duplicate and chain-scope-mismatch
can indicate a splice of two genuine histories -- every receipt involved verifies in isolation. A
duplicate sequence has innocent causes too, a retransmitted copy being the common one: where the two
receipts at that sequence are byte-identical a verifier SHOULD say so, and where they differ it has
found an issuer that signed two histories at one position, which is the serious case -- while
chain-link-broken indicates in-band alteration. Reporting them as one verdict tells a relying
party that something is wrong but not what an operator must do next.¶
The six labels above are this document's identifiers for the six conditions, and
checkpoint-head-conflict and checkpoint-unverified are its identifiers for the two named
checkpoint-axis outcomes. A Chain Verifier MAY report any of them under its own strings; what it
MUST do is make them distinguishable in its output, and keep the chain conditions distinguishable
from the checkpoint axis. No particular spelling is required on the wire. Implementers
aligning with a reconciliation layer should note that these conditions correspond to divergence
axes carried by [I-D.hillier-scitt-arp]; that alignment was measured against this profile's own
adversarial corpus (see Section 12).¶
Completeness is required, not optional. A Chain Verifier MUST reject a presentation that does
not begin at seq == 0, and a Deployment presenting a chain for verification MUST present it
from genesis. This is
what every implementation of this profile already does -- five verifiers in five languages each
require 0..n-1, though only one of the five is an independent oracle for the others
(Section 12) -- and a presentation of, say, seq 1 and seq 2 is refused as a
missing genesis rather than accepted as a partial view. Verification of a range that does not
start at genesis is therefore not a capability of this profile: it is neither specified here nor
implemented anywhere, and Section 12 lists it as such. -00 said "a verifier checking
a range of receipts" and left the boundary of a "range" unstated; this revision states it. No
implementation behaviour changes: the stricter reading is the one every implementation has always
enforced, and only the documentation has caught up.¶
What a checkpoint comparison can and cannot show. A mismatch between the presented head and a
checkpoint's headHash can reveal truncation relative to that checkpoint -- that is, relative to
one signed assertion made at one moment by one key. It is not a general detector of tail
truncation: a prover who withholds both the tail and the checkpoint that would expose it presents a
set with no mismatch in it, and no property of the presented set reveals the omission
(Section 13). Registration in a Transparency Service replaces the prover's own
checkpoint with a third party's signed proof of registration, which is stronger and still not a
general answer; Section 7 states exactly what it is worth to an offline holder.¶
An older checkpoint over a longer chain is not truncation. A checkpoint asserts a head at the
moment it was minted; a chain that has grown since then legitimately presents a later head. A
verifier MUST therefore distinguish two mismatches. Where the checkpoint's headHash appears in the
presented chain at exactly its highestSeq, and the presented head's chain.seq is greater, the
presented chain extends the checkpoint consistently: this is not checkpoint-head-conflict, and a
verifier MUST NOT report it as a conflict. Every other mismatch -- a head at a lower sequence, or a
checkpoint whose asserted head does not appear in the presented chain at its own sequence at all --
is checkpoint-head-conflict: the presented set and an authenticated checkpoint disagree about the head.
A verifier MUST report the disagreement and MUST NOT report a cause for it. Deletion of a tail is
one explanation; an issuer that signed two divergent histories is another; a checkpoint minted over
a different lineage, or a signer error, is a third. The comparison separates none of them, and
naming one is the same manufacture of a specific accusation Section 8.3 forbids -- which is why this
revision renamed the condition rather than keep -00's reading of it as truncation. The reference implementation does not draw this distinction and reports
one verdict for both cases; that is recorded as a conformance gap in Section 12.¶
Why the unauthenticated case turns on authentication, and not on inputs. The rule above -- that
a checkpoint the verifier did not authenticate yields checkpoint-unverified and never a truncation
finding -- is the one this whole document turns on. A checkpoint is supplied to the verifier, often
by the same party whose chain is in question. If an unauthenticated one could establish truncation, anyone able to hand a verifier a
file could mint a signed-looking forensic conclusion about someone else's chain -- an accusation
manufactured from attacker-controlled bytes. Only an authenticated checkpoint carries the authority
to say a tail is missing, and checkpoint-unverified is the indeterminate result that names the
reason instead. Section 8.3 states the general form of the rule, over every input rather than this
one; Section 12 measures two places where a shipped verifier breaks it.¶
The prohibitions in the other direction are unchanged: an unauthenticated checkpoint MUST NOT be reported as a satisfied tail check, and MUST NOT upgrade any result. A verifier MUST state, in its output, whether the checkpoint that produced any tail conclusion was authenticated, and Section 13 states what an authenticated one is and is not worth.¶
One agent, one key, within one presented chain. A Chain Verifier MUST reject a presented chain
in which two receipts share an agent.id and differ in sig.kid, reporting
chain-key-discontinuity. The condition needs no keyring: it compares the receipts to one another.¶
This is the condition that catches the ordinary form of chain extension, and what it catches is
narrower than "extension is closed". Consider a party that holds any key the verifier trusts and
appends receipts to another party's chain under the victim's own agent.id: the links are intact,
there is no sequence gap, the scope.chain is constant and the tenant is constant, so none of the
other conditions fires. What gives that attacker away is that the appended receipts carry a
different sig.kid while claiming the same agent.¶
Two escapes remain, and they belong with the claim rather than after it, because a reader who takes the claim without them will believe the chain is closed when it is not:¶
agent.id, and chains may carry several. An attacker holding another
trusted key can append receipts under a new agent.id and no condition in the table fires: the
set is still one chain, one tenant, contiguous and correctly linked. Closing that needs a rule
about which agents may appear in a chain -- an append-authority binding this profile does not
define (Section 1.5). The identity manifest binds an agent.id to keys; it does not bind
a chain to a set of agents, and a Chain Verifier given one still has nothing to test the new
agent against.¶
sig.kid strings, because a Chain Verifier is defined over a presented set and needs no keyring
to evaluate it. One consequence is unavoidable and is stated rather than hidden: two different
keys presented under one kid are indistinguishable to it, so an attacker who can get a second
key registered under the victim's identifier is not caught by this condition -- Section 7
and the keyring, not this comparison, are where that is addressed. A verifier MAY additionally
compare resolved key material where it can, and if it does it MUST report that as a separate
result, because it is a different check with a different failure set; what it MUST NOT do is
substitute one for the other under this condition's name.¶
Within one presented chain, then, one agent speaks under one key identifier or the set fails -- and the two escapes above are the boundary of that sentence, not exceptions to be discovered later. The cost is the corresponding limit stated in Section 5.2: key rotation is not expressible inside a chain, and a deployment that rotates a signing key starts a new one.¶
Sequence orders a chain; timestamps do not. chain.seq and prevHash are the only ordering
this profile defines. ts is signer-chosen (Section 3.3) and this profile places no
requirement that it be non-decreasing along a chain: a set whose timestamps run backwards can be
fully conformant on every axis above, and a relying party that sorts or reasons by ts is using a
field no check constrains. Where a deployment needs temporal ordering it MUST obtain it from
something other than these records.¶
Where more than one condition holds, a verifier MUST report all of them rather than the first it found: an operator's next step depends on the set, and a single-value result field cannot carry it. This profile defines no wire encoding for that report -- how a verifier's output is structured is outside it (Section 1.5) -- only that the conditions be individually recoverable from it.¶
A condition that holds is not a note; it is a failure. A Chain Verifier MUST NOT return a
positive result for a set on which any of the six conditions holds. That rule has no exception in
this revision, and one case is worth spelling out because the reasoning above cuts the other way:
two receipts at the same chain.seq that are byte-identical are a retransmitted copy rather than a
divergent history, and a verifier SHOULD say so in its report -- but the condition still holds, so
the result is still a failure. De-duplicating such a set and returning a positive result is not
conformant here. Whether a future revision should permit it is an open question (Section 15);
this one answers it in the strict direction so that two verifiers cannot disagree about the same
bytes today.¶
A verifier MUST NOT report a chain-level axis as a per-receipt failure, and MUST NOT infer from the absence of these axes that the presented set is complete: completeness against records the prover never presented is not a property of the presented set (see Section 13).¶
The structure and signing rules address the Producer of a checkpoint; the authentication and scoping rules address the Chain Verifier, and apply only when a checkpoint is supplied.¶
A checkpoint is a separately signed assertion of a chain's current head. It is not a receipt and is not part of any chain; it is the object the tail check of Section 5.1 compares against.¶
{
"spec": "noa.checkpoint/0.1",
"chain": "<chain id>", ; the scope.chain it speaks for
"highestSeq": <int>, ; chain.seq of the head receipt
"headHash": "sha256:<hex>", ; chain.hash of that receipt
"ts": "<RFC 3339 date-time>",
"sig": { "alg": "ed25519", "kid": "<key id>",
"value": "<base64>" }
}
¶
Its signature mirrors the receipt construction of Section 4.2, and is pinned here to the same byte-exactness:¶
CHECKPOINT-HASH-INPUT =
JCS( the checkpoint object with exactly one member removed:
sig.value; sig.alg and sig.kid remain present, and there
is no chain.hash member to omit )
CHECKPOINT-MESSAGE =
UTF8("NOA-Checkpoint-v0.1-sig:") ; 24 octets: tag then ":"
|| SHA-256(UTF8(CHECKPOINT-HASH-INPUT))
; RAW 32-octet digest,
; NOT its hex form
sig.value = base64(Ed25519(CHECKPOINT-MESSAGE))
sig.alg = "ed25519"
¶
The domain tag is distinct from the receipt tag, so neither signature can be replayed as the other, and the colon is part of the signed prefix in both constructions.¶
The object is closed, and a Chain Verifier MUST validate its structure before authenticating
it. A checkpoint MUST carry exactly the members shown above and no others, at either level, and
sig MUST carry exactly alg, kid and value. A Chain Verifier MUST reject a checkpoint
carrying any other member. Closure is load-bearing here in a way it is not for an ordinary schema:
CHECKPOINT-HASH-INPUT copies every member of the object except sig.value, so a smuggled member
sits inside the signature. Its signer signs it honestly, the signature verifies, and the
closed-object check is the only control that rejects it. The value rules, stated as requirements
rather than left to be read off a figure:¶
spec MUST be the exact string noa.checkpoint/0.1;¶
chain MUST be a non-empty string, and a Chain Verifier MUST compare it to the scope.chain of
the presented set. A checkpoint naming another chain is not evidence about this one, and the
finding is about the checkpoint: the verifier MUST report that the checkpoint does not
describe the presented chain, MUST NOT use it for any tail conclusion, and MUST NOT let it fail an
otherwise valid presentation. It is not a chain-level condition (Section 5.1), for the reason
Section 8.3 gives: nothing about the presented receipts failed;¶
highestSeq MUST be an integer, at least 0, and within the range Section 3.2 fixes for
numbers;¶
headHash MUST be "sha256:" followed by 64 lowercase hexadecimal digits;¶
ts MUST be a valid RFC 3339 date-time, under the same rule and for the same reason as a
receipt's (Section 3.3);¶
sig.alg MUST be the exact string ed25519; sig.kid and sig.value MUST be non-empty
strings; and sig.value MUST satisfy the encoding rules of Section 4.2.¶
A checkpoint failing any of these value rules is malformed, and so is one whose chain does not
match. Every such failure is a finding about the checkpoint: it MUST NOT establish
checkpoint-head-conflict, MUST NOT be reported as a finding about the chain, and MUST NOT by itself
turn an otherwise valid presented chain into a failure. The object the verifier was handed is
defective, which is a different statement from the records being defective; a verifier that
conflates the two lets anyone who can hand over a file manufacture an accusation about someone
else's history. What the verifier MUST do is report that it was given a checkpoint it could not use,
and answer the tail question as unanswered (Section 5.1) -- the same outcome as being given no
checkpoint at all, because that is what it effectively has.¶
Which key may sign it. A checkpoint is authenticated against the same keyring as the receipts.
Authentication either succeeds or it does not, and a verifier that cannot perform it -- because it
was given no keyring, or none holding that signer's key -- holds an unauthenticated checkpoint
rather than an absent one: it MUST report every checkpoint-derived conclusion as unauthenticated
(Section 5.1), and MUST NOT fall back to comparing heads.
Key-level authentication alone, however, only establishes that some trusted key asserted this
head. To bind the assertion to the chain, a Chain Verifier that was supplied an identity manifest
(Section 6) MUST require the checkpoint's sig.kid to be one of the keys that manifest
authorizes for the
agent.id of the genesis receipt -- the receipt at seq == 0, the chain opener. A checkpoint
signed by any other trusted key MUST be rejected as UNTRUSTED -- the result defined in
Section 6 for a valid signature by a key this identity is not authorized to use -- rather
than accepted as a tail check;
this is what stops a co-trusted key from certifying its own truncated head.¶
Two scoping rules follow, and both are limits rather than features. The comparison is between two
native key identifiers -- the checkpoint's own sig.kid, and the identifiers the manifest
authorizes for the genesis receipt's agent.id -- because a checkpoint is a native artifact and is
not carried in a COSE envelope; a deployment that wraps receipts for registration therefore has two identifier
spaces, and MUST NOT assume the outer envelope's kid is the one the manifest authorizes here. And
the guarantee is opener-scoped: on a chain carrying more than one agent.id, the opener's
checkpoint certifies the opener's view of the head and does not certify a co-agent's tail. A Chain
Verifier MUST NOT report an unqualified tail check on such a chain, and MUST state which agent's
view the check covers.¶
What a checkpoint does not commit to, and what follows. The object above binds a chain identifier, a sequence, a head hash and a timestamp. It does not bind the tenant, the issuer, the agent set, the genesis hash, or any manifest version. Three consequences are normative for a Chain Verifier:¶
ts is signer-chosen, so an authentic checkpoint stays
authentic forever. This profile defines no expiry, and a verifier MUST NOT treat a checkpoint's
age as bounded by anything in the object. A deployment that needs freshness MUST impose a maximum
age from its own clock and say so in its output; an attacker who can replay an obsolete but
authentic checkpoint otherwise faces no barrier.¶
agent.id, a checkpoint authorized for the opener says nothing about receipts
signed by a co-agent -- the requirement for that case is stated once, above.¶
agent.id to the first sig.kid it presents and rejects any later change as a key swap
(Section 12). In-chain key rotation is therefore not expressible in this revision:
a deployment that rotates a signing key starts a new chain, and this profile defines no rotation
or revocation mechanism.¶
Checkpoint issuance policy -- how often a checkpoint is minted, how it is distributed, and how a relying party obtains one it did not receive from the prover -- is deployment-specific and is not specified here. A verifier given no checkpoint performs no tail check at all. It MUST report the tail question as unanswered, and MUST NOT report it as an absence of truncation. The absence vocabulary that names such a result is defined once, in Section 8.3; a Chain Verifier that uses it is reading a definition, not inheriting an Evidence-Verifier obligation. Those are different answers, which is what Section 8.3 exists to keep apart.¶
Attribution addresses the Receipt Verifier, conditional on an identity manifest being supplied. The relationship rules of Section 6.1 address the Evidence Verifier and depend on artifacts listed in Section 1.5.¶
agent.id is a signer-asserted label. To authenticate WHICH agent acted (not merely that a
trusted key signed), a verifier MAY be supplied an identity manifest binding agent.id to its
authorized key identifiers.¶
Each signature attributes a different party, and the manifest answers only one of the two questions. An enveloped receipt carries two identifiers, and treating either as a substitute for the other is the failure this subsection exists to prevent:¶
sig.kid attributes the agent: it is the key that signed this receipt into its
chain, and it is the identifier agent.id makes a claim about. When a manifest is supplied, a
verifier MUST check the (agent.id, native sig.kid) pairing against it, and MUST reject an
unauthorized pairing as UNTRUSTED -- a result distinct from both "valid" and "tampered": the
bytes are intact and the signature verifies, and the key that produced it is not one this
agent.id is authorized to use. A verifier MUST report that as its own outcome rather than
folding it into a signature failure, for the same reason Section 5.1 separates its conditions.¶
kid attributes the party that emitted this envelope -- an issuer
submitting its own receipt, or a relay presenting someone else's. A verifier that reports it as an
identity at all MUST take it from the protected bucket, since an unprotected identifier is not
covered by any signature. This profile defines no authorization list for that party: the manifest
binds agents to keys, not envelopes to emitters.¶
kid to satisfy the agent check: an envelope signed by a key that happens to
be authorized for agent.id says nothing about who signed the receipt inside it. And a verifier
MUST NOT reject a receipt solely because its outer signer is not one of the agent's authorized
keys: a relay is a legitimate presentation, and the agent claim is carried by the native signature
it did not touch. A verifier MUST report the two results separately.¶
A checkpoint has one signature and one claim: the manifest check is against the checkpoint's own
sig.kid, under the opener-binding rule of Section 5.2.¶
Without a manifest, attribution is key-level only -- "some key the verifier trusts signed this" and nothing about which agent -- and a verifier MUST surface that limitation to the relying party.¶
governance.approval.by names an approver. The receipt's signature establishes that the issuer
emitted this record and has not since altered it; it does not establish that the named party held
any key, saw this action, or approved it before it ran. A relying party MUST NOT treat
governance.approval as pre-execution authorization by the named party. Section 9 states
what would be required instead.¶
Two levels, and the difference between them is the whole answer. The strength of an approval claim depends on what the relying party was given, and a specification that states only one level misleads in one direction or the other.¶
governance.approval.by is an opaque string.
The verification rules of this profile neither resolve it against a key nor compare it to any
signer. It is covered by the issuer's signature -- so the issuer cannot alter the name after the
fact -- but the name and any key are unrelated at this layer: a receipt naming a person who has
never held a key verifies exactly like one naming the true approver. A relying party holding only
a receipt therefore holds a recorded name, not a cryptographic party.¶
governance.approval.by to name the key
that signed the approval, and refusing to issue otherwise. That is a producer-side control, so it
binds nobody downstream and a verifier MUST NOT assume it ran. What it changes is what the
issuer knows, not what a later reader can check: the receipt still carries an opaque string, and
no member records that the gate ran (Section 8.1). A holder of that receipt alone is in the
first case above, whatever the issuer's process was; the name becomes checkable only when the
approval artifacts of Section 9 travel with it. The reference implementation's gate and
grant signer both enforce the control (Section 12).¶
A Receipt Verifier or Chain Verifier that reports anything about an approval MUST state that its
result rests on the opaque name alone. That requirement is testable: its input is a receipt and
its subject is what a receipt carries. The second level -- a name backed by the approval artifacts
of Section 9 -- is described here but carries no requirement in this revision, because those
artifacts are not defined by this document. A bare receipt is precisely where the
confusion arises -- and MUST NOT let a bare receipt's approval field be read as the second. Neither level establishes that a named human
physically held the signing device: that binding is an out-of-band enrolment ceremony, outside
every check either level performs, and it MUST be stated as a non-claim wherever an approval is
presented. Nor does either level establish that anyone read, understood, or consented to what was
displayed.¶
Section 12 reports a shipped instance of the second level and of the producer-side control, and states exactly which component leaves the name opaque and which refuses it.¶
The rest of this subsection is NON-NORMATIVE in this revision. It runs over the signed outcome artifacts and the trust configuration Section 1.5 lists as undefined here, and over a "request signer" this document can only describe by example. Nothing below can be tested against this specification alone, so none of it is stated as an RFC 2119 requirement: it is what a companion that defines those artifacts has to require, written now so that the companion inherits the analysis instead of rediscovering it. An implementation cannot fail this subsection, and Section 15 records the work of turning it into requirements somewhere it can be tested.¶
Where an outcome about the action is itself signed, independence is decided by comparing the outcome signer against two other parties, because a deployment that separates them has two distinct keys and collapsing them hides a real dependency:¶
A verifier has to derive and report each relationship separately rather than a single merged verdict: an outcome independent of the approver but signed by the requesting gate is not independent evidence, and neither is its mirror image. Comparing against whichever key merely emitted the receipt answers a third question and is not a substitute for either. Both relationships have to be derived by the verifier from its own trust configuration. A self-declared relationship -- a producer writing "independent" about itself -- is not evidence of independence, and an undetermined relationship cannot be upgraded to an independent one.¶
A verifier that reports such a relationship draws the value from the vocabulary below -- closed, though a verifier need not be able to produce every member of it (see the rule after the table) -- names the two parties it compared, marks the result as verifier-derived, and carries the identity of the trust configuration it used -- for example a trust-policy digest over the thresholds applied and a registry-snapshot digest over the key material consulted -- so that the determination can be recomputed by anyone holding the same configuration. This profile does not define those digests' constructions (Section 1.5), so reproducibility here is scoped to parties who share a construction rather than promised in general. What it requires is that a verifier reporting a relationship publish enough for someone else to recompute it, and state the construction where consumers of the result can find it; values computed under different constructions are not comparable:¶
| Relationship | Meaning |
|---|---|
SAME_SIGNING_KEY
|
one key signed both |
SAME_ADMINISTRATIVE_PARTY
|
distinct keys under one administrative registry |
SEPARATE_ROLE_SAME_ORGANIZATION
|
distinct authorized purposes, one organization |
INDEPENDENT_ORGANIZATION
|
distinct organizations, established from the configuration |
UNKNOWN
|
the configuration does not establish a relationship |
Two rules make this testable rather than decorative.¶
INDEPENDENT_ORGANIZATION from what it can return, rather than returning it on weak evidence.¶
SAME_SIGNING_KEY is decided on key material, not on a key identifier. One key
presented under two identifiers, in two roles, is the same signer; a determination that compares
identifier strings alone reports independence to an attacker who registers a second name for one
key.¶
UNKNOWN is the default and is not a failure of the artifact; it is a statement about the
verifier's configuration. Where a consumer asks for independent evidence (Section 8.2), no result
other than INDEPENDENT_ORGANIZATION satisfies that intent. Separate keys prove only
distinct keys; separate processes improve isolation; separate roles prove purpose authorization.
None of the three, alone, establishes independent administrative control. Section 12 reports a
shipped instance of this determination, including the value it deliberately does not offer.¶
This section addresses the Transparency Service, except the first sentence, which addresses the submitting Deployment.¶
A receipt carrying the protected claims of Section 4.3 is a Signed Statement and MAY be submitted to a Transparency Service. A receipt without them is not, and MUST NOT be described as registrable.¶
The Registration Policy for this profile is stated as a list because each item closes a specific substitution. A Transparency Service registering a statement under this profile:¶
iss -- the resolved key or certificate belongs to that Issuer, or an explicit
delegation from that Issuer to the signing key is presented and checked. [RFC9943],
Section 6.3 states the same requirement in its own terms: the signature is verified with the
verification key of the Issuer. A service that cannot make that binding MUST NOT record the
statement as that Issuer's;¶
alg of -19 and
deterministic CBOR -- and MUST require a protected kid for registration specifically. A
receipt whose kid is only in the unprotected header conforms to Section 4.1 and is not
registrable under this policy: registration binds a subject to an issuer, and an identifier the
signature does not cover cannot do that. Producers intending to register MUST protect the kid;¶
CWT_Claims are present and carry both iss and sub;¶
sub equals the payload's scope.chain, so that the subject a log indexes is
the chain the statement is actually about; and¶
iss and the party submitting the
statement, and records which policy it applied. This profile does not require those two to be the same party, does not define the
policy, and -- decisively -- defines neither the minimum contents of such a record nor anywhere
it is carried, so a service that refused every submission under an unnamed policy would satisfy
any wording used here. What the profile can do is say plainly that the question exists and that a
service which never asks it is registering statements whose issuer relationship nobody checked.¶
What iss is worth, and where it stops. This profile defines no issuer key discovery: no
registry, no well-known location, no delegation format. Item 1 therefore states a requirement whose
mechanism lives outside this document, and a deployment must supply one. Until it does -- and for
any holder who has only the receipt, with no registration behind it -- iss is an issuer-asserted
string: it names the authority the signer claims to speak for, and nothing in the envelope proves
the relationship. A Transparency Service, and any Receipt Verifier that reads iss at all,
MUST NOT report it as an authenticated identity on the strength of a valid outer signature alone,
and MUST say which it had: an identity bound to the verification key, or a string the signer
asserted.¶
A Transparency Service knows this policy applies by agreement with the registrant, not by inspection: no media type is registered for the payload (Section 16), so a service cannot dispatch on content type alone. Until one exists, a deployment states the profile out of band.¶
Items 4 and 5 are the ones a reader should not skim. Item 4 is what stops a trusted registrant
obtaining a log entry -- and a Transparency Receipt -- for a receipt nobody in the chain ever
signed. Its cost is stated rather than hidden: a Transparency Service that cannot resolve native
signer keys registers nothing under this profile, which is a real operational constraint and the
honest consequence of having no way to publish "not checked". Without item 5
an issuer can register a chain-A receipt under sub = chain-B, leaving the log holding a subject
history that no receipt supports. On success the TS returns a Transparency Receipt: a third party's
signed proof that this statement was registered in its log. In [RFC9942]'s terms that is a proof
about the log's verifiable data structure, not the COSE countersignature header parameter of
[RFC9052]; the two are different constructs, and this profile means the first everywhere it says
Transparency Receipt.¶
What registration is worth, stated exactly. An inclusion proof establishes membership in the view that log served. Non-equivocation -- that the log did not serve one history to one party and a different history to another -- is not established by any single receipt. It requires consistency proofs between log states and a party that monitors them; this profile specifies neither the monitoring role nor any requirement on anyone to perform it, and a relying party that wants non-equivocation MUST obtain it from the Transparency Service's own ecosystem.¶
The offline case gets the same scepticism this document applies to checkpoints. A Transparency Receipt handed to an offline relying party by the prover is prover-supplied evidence, exactly like the checkpoint of Section 5.2: a better signer, and still no way for a party that never queries the log to detect a log that served two views.¶
Two further honesty notes. Nothing in Section 5 supplies any of this, so a deployment that never registers has strictly less. And no reproducible demonstration of registration against an external Transparency Service has been published for this profile (Section 12): the policy above is a specification, not a report of an executed interop.¶
The non-claims in this section constrain what any actor may assert, and each is written against the actor it binds. Two blocks are marked NON-NORMATIVE where they appear -- the claim-separation duty of Section 8.1 and the intent machinery of Section 8.2 -- because their inputs are artifacts Section 1.5 lists as undefined; the absence vocabulary of Section 8.3 is normative, because a verifier's own results are something this document can constrain.¶
A receipt in this profile is emitted at the governed boundary: the point where an action was admitted, refused, deferred, or dispatched. Everything beyond that boundary -- what a downstream controller did with the request, and what happened physically -- is outside the record.¶
The following are distinct claims. They may share one action digest as a join key (Section 10), but digest equality proves none of them. Each has to be evaluated and reported separately, so that one can fail while the others stand -- a requirement for whichever document defines claims 2 and 3, since this one does not (Section 1.5):¶
A receipt under this profile carries NONE of claims 2 and 3, and carries claim 1 only as the
signer-asserted label described in Section 6.1. A deployment MAY apply the producer-side control Section 6.1 recommends -- refusing to issue
unless governance.approval.by names the key that signed the approval -- but no receipt carries
evidence that the control ran, so a verifier MUST NOT report anything about it. Whether the control
was applied is a fact about the issuer's process, established outside this profile or not at all. These are NORMATIVE non-claims, scoped to what a receipt establishes. A Producer MUST NOT
present a receipt as evidence of any of the three. A verifier evaluating a receipt -- alone or as
part of a chain -- MUST NOT return a result whose named fields assert any of them on that basis,
and where a consumer asked for one it MUST report that the receipt does not establish it. For claims
2 and 3 Section 8.2 names the intent the consumer asked for and the evidence it would require. For
claim 1 it does not: this profile defines no intent token for pre-execution authorization, so the
report carries the non-claim above rather than a name from Section 8.2.
The prohibition is about the basis of an assertion rather than about the claims themselves: a
verifier that also holds an authenticated outcome or observation artifact may report what that
artifact establishes, which is the case Section 8.2 describes.¶
In particular, governance.verdict == EXECUTED records that the action was dispatched, or
attempted, at the governed boundary. It does not attest that a controller accepted the request,
that the request succeeded, or that any physical effect followed. A controller reporting
"succeeded" attests success under its own semantics; that is a report, and a report is not an
effect.¶
A receipt may be cryptographically impeccable and still be the wrong evidence for the question being asked. Two questions therefore have to be kept apart:¶
One of the four intents is normative here and three are not, and the line is drawn by what this
document defines. A verifier that supports intents at all MUST answer BASE_ACTION_RECEIPT from
receipt validity alone, and MUST NOT report it as satisfied for an object that fails Section 3 or
Section 4: its predicate is defined here in full, so two implementations can be tested against the
same answer. The other three are satisfied only by artifacts Section 1.5 lists as undefined,
and the machinery around them -- how an intent is supplied, how sufficiency is compared, how the
answer is carried -- is undefined with them. Those three rows and the paragraphs that follow the
table are NON-NORMATIVE in this revision, for that reason -- with one exception, labelled where it
appears at the end of this subsection, which is normative because its subject is a receipt. It names four intents so that the common questions have shared spellings, and defines
each by the evidence that answers it:¶
| Intent | Satisfied only by |
|---|---|
BASE_ACTION_RECEIPT
|
a receipt valid under Section 3 and Section 4 |
CONTROLLER_OUTCOME
|
an authenticated controller-outcome artifact bound to this action. Section 8.4 describes what such an artifact must say; this revision defines no wire format for it (Section 1.5) |
PHYSICAL_COMPLETION
|
an authenticated physical-observation artifact bound to this action, from a witness the verifier trusts for that purpose. Same status: described in Section 8.4, not defined here |
INDEPENDENT_OUTCOME
|
one of the two artifact-based intents above -- CONTROLLER_OUTCOME or PHYSICAL_COMPLETION -- whose signer the verifier derives as INDEPENDENT_ORGANIZATION (Section 6.1). A receipt cannot satisfy it: the receipt's issuer is the party whose account is in question, so it is never independent of itself |
Only the first is satisfiable from a receipt alone. The other three need artifacts
Section 1.5 lists as undefined in this revision, and INDEPENDENT_OUTCOME is additionally
unsatisfiable by any verifier that cannot derive organizational independence -- which, per
Section 12, is every implementation this document reports on. A verifier that cannot
satisfy an intent reports that in those terms rather than as a failure of the artifact.¶
A verifier that supports intents should support at least these four and report a named insufficiency rather than a generic failure: a relying party that asked for physical proof and received a valid receipt has learned something specific, and collapsing that onto "invalid" destroys it. Where two authenticated claims conflict -- a controller reporting success and a witness reporting that the effect did not occur -- the conflict is its own result, carrying both claims, with neither erased in favour of the other; this document defines no token for it (Section 1.5).¶
One rule here is normative, because it constrains what may be said about artifacts this document does define. A verifier MUST NOT report an unqualified "physical completion proven". The strongest defensible result is proven to the stated policy, naming the trust configuration that made it so, and a receipt never supplies it (Section 8.1).¶
A verifier MUST keep the following distinct, and MUST NOT collapse them:¶
| Result | Meaning |
|---|---|
NOT_CLAIMED
|
the producer made no claim on this point |
NOT_PROVIDED
|
a claim may exist, but the verifier was not given acceptable evidence of it |
NOT_APPLICABLE
|
the point does not arise for this action |
INDETERMINATE
|
evidence was presented but does not settle the question |
"The verifier did not receive acceptable evidence" is not "no evidence exists", and rendering the first as the second turns a gap in a presentation into a negative finding about the world. This is the same discipline Section 5.1 applies to set-level failures: name the condition, do not merge it. A verifier MUST NOT define a result code that asserts a negative fact about the world it did not observe; Section 12 records a shipped verifier that carries no such code by design.¶
A finding names the object that failed, and no other. The rule the paragraph above is one instance of, stated once so that an implementer can apply it to the case this document did not foresee: a failure to authenticate or to understand one object MUST NOT produce a finding about a different object. The subject of the finding is the object that failed.¶
The rule is about whose failure is being reported, not about whether failures may be reported at
all. A receipt whose own signature does not verify is TAMPERED -- a finding about that receipt,
and the correct one. A receipt this profile cannot parse is malformed -- again about that receipt.
What is forbidden is transferring the failure: an unauthenticated checkpoint yields
checkpoint-unverified, a finding about the checkpoint, and never a finding about the chain
(Section 5.1); a checkpoint whose structure or scope is wrong is a finding about the checkpoint
(Section 5.2); a trust document a verifier does not recognise is a finding about that document,
never about the receipts it was supposed to authenticate. In each case the honest object -- the
chain, the receipts -- is not accused of anything, because nothing about it failed.¶
This matters because the accusation is the whole thing these artifacts exist to make credible. A verifier that turns its own inability to read one file into a finding against someone else's records has produced the one output a verification tool must never produce, and Section 12 records two measured places where a shipped implementation does exactly that.¶
A machine-checkable non-claim carried as a field of the base receipt -- for example a
physical_completion member defaulting to "none" -- was proposed on the SCITT list and is
not adopted here, for two reasons that are worth stating rather than asserting.¶
First, mechanics: adding a member to noa.receipt/0.1 changes the JCS bytes of every receipt that
carries it, and therefore the receipt hash, the native signature, the prevHash linkage, every
checkpoint over that chain, and every archived conformance vector. That cost is payable only at a
new wire version with a new signing domain, never by mutating a version other implementations have
already frozen.¶
Second, semantics: a default value on a frozen record cannot distinguish "the producer considered physical completion and claims nothing" from "the producer has never heard of the question". A default is not a statement.¶
The claims of Section 8.1 therefore belong in separate signed artifacts, each linked to the authorization by hash -- at minimum the hash of the receipt that carried the authorizing verdict and the hash of the execution grant, and OPTIONALLY a shared action digest (Section 10) -- and each carrying its own signer, its own vocabulary, and its own failure. A controller-outcome artifact says what a controller reported; a physical-observation artifact says what a witness observed, under a named method, in a named window, against a criterion digest fixed before execution. Motor or controller telemetry alone is not an observation of the object.¶
This revision does not define those artifacts' wire formats. It fixes the vocabulary and the failure model they must satisfy, so that the work can land in one place instead of fragmenting across profiles, and so that a receipt consumer today can be told exactly which question a receipt does not answer.¶
The first rule below binds the Producer that emits a digest -- it must travel with a construction identifier -- and every actor that compares one, in practice an Evidence Verifier. The second rule binds whoever reads the comparison.¶
Records about one action are only comparable if they carry a value that means the same thing to both producers. Two rules make that possible, and this profile states both while defining neither construction.¶
Determinability, not assumption. This rule -- the one paragraph below, and nothing else in this section -- is NON-NORMATIVE here: the digest it governs is not a member of this record and is defined by a companion (Section 1.5), and it is written because a companion defining one has to carry it. The two requirements at the end of this section are normative and are labelled there. Any digest offered as a cross-producer join key travels with a construction identifier, and that identifier commits to the construction's canonicalization parameters -- serialization, member sort, number rendering, normalization, escaping, digest algorithm, and domain separation. A verifier presented with a digest under an unrecognised or non-matching construction identifier does not compare the values at all. The comparison then yields nothing -- not a match, and not a mismatch -- and is reported as indeterminate: "these records were not shown to be about the same action", never "these records are about different actions". That is the same distinction Section 8.3 draws; a question the verifier could not reach is not a negative answer to it. Two constructions that differ only in whether they apply Unicode normalization will agree on almost every input and disagree on the ones that matter; a shared vector, not a shared name, is what establishes that two parties compute the same value.¶
Correlation is not attestation. Digest equality establishes that two records commit to the same action projection -- the same field values under the same construction. It establishes nothing about whether that action was authorized, whether a controller reported anything, or whether any effect occurred; each of those fails independently (Section 8.1). A digest is a join key, and a join key that is mistaken for an authorization claim is the substitution Section 9 forbids.¶
Two rules do stay normative here, because their subject is this record. action.paramsHash
does not satisfy the first rule above and MUST NOT be used as a join key (Section 3.2); and a
verifier MUST NOT treat equality of any digest as evidence that an action was authorized, which is
the substitution Section 9 is about. Nor does the base record carry a member for a shared digest: noa.receipt/0.1 is
closed (Section 3), so a correlation value travels beside a receipt in whatever artifact
references it, never inside one. A deployment that wants correlation does not modify the receipt.¶
A construction meeting both rules is published: [I-D.toraman-noa-action-digest] fixes the projection members, the domain tag, the preimage, the digest, and a published conformance corpus (counted in Section 12). One consumer of it is [I-D.toraman-noa-settlement-evidence]. This profile does not restate that construction, does not require it, and would apply the same two rules to any other.¶
On indeterminate positive controls. In an independent reconciliation run reported on the SCITT list on 27 July 2026 (https://mailarchive.ietf.org/arch/msg/scitt/zWZBQXXSGV-vgbTeOLTjlb9DGLg/), a positive control was held INDETERMINATE because the digest it carried came from a construction the reconciler had no vector for. That is the rule above working, not a harness defect: a positive control that upgrades itself on an unverified binding makes every negative case unreliable too.¶
Three things follow, and they are different things.¶
action object cannot do so
reproducibly today, because this profile has never fixed that object's encoding: rollbackRef is
optional, and present-as-null and omitted produce different digests. Every receipt in the shipped
corpus carries it, which is a convention, not a rule an implementer can cite. Any party wanting
to compute a digest over action therefore needs two things this revision does not provide: a
normative rule fixing the encoding, and an expected value published per conformance vector so a
positive control has something to equal. Both are named here as work rather than left for a
harness to discover -- which is the same rule this section applies to everyone else.¶
This section addresses the Producer and every verifier: none of them may present a receipt as carrying a re-derivable verdict.¶
A deterministic, offline policy-replay capability -- a verifier re-running the in-force policy over the recorded inputs to re-derive the recorded verdict, with no access to the agent, model, or any service -- is a distinct capability that this revision deliberately does NOT specify. The optional commitment of Section 3.3 carries hashes of a policy, its read-set, and the recorded inputs; it does not carry the replay construction (the deterministic evaluation grammar, operator set, evaluation order, and conformance vectors), which is left to a separate companion profile and is not normative here. A Producer and every verifier MUST NOT represent a receipt under this profile as carrying a re-derivable verdict, and MUST NOT infer from a present commitment that the referenced policy was adequate or in force.¶
This section is to be removed by the RFC Editor before publication, per [RFC7942]. Removing it will renumber what follows, so a citation of this revision by section number should be read against this revision. References to this section from the body are pointers to measurement, never to normative content: no requirement depends on it.¶
A zero-runtime-dependency reference implementation (signer, offline verifier, deterministic
CBOR, COSE_Sign1 profile, JCS canonicalizer, conformance vectors, and separately implemented
verifiers in four further languages) is available under the Apache-2.0 license at
https://github.com/NordenSoft/noa. Unless explicitly stated otherwise, the measurements below are
from main commit 4764cab. Later repository changes are identified by their own commit hashes and
are not included in the pinned snapshot's counts. No number in this section is an estimate: each
was produced by running a command or counting files in that tree. Three classes of measurement are
reproducible from what is written here -- the corpus counts (three are file counts in the named
directories; the action-digest and settlement-evidence corpora are each a single vectors.json
whose entry array is counted, which is why counting files there gives the wrong answer), the
conformance run (node impl-py/conformance.mjs), and the repository facts cited to a path. The
rest -- the 25-verdict cross-field measurement below, the divergence runs of D1, and the decoded
header bytes -- were produced by ad-hoc scripts that are not committed, so they are reported as
measurements with their inputs described rather than as anything a reader can re-run today.
Committing them is open work.¶
Algorithm identifier. The implementation emits the fully-specified Ed25519 identifier -19
([RFC9864]) and rejects every other alg, including the polymorphic EdDSA identifier -8,
which [RFC9864], Section 4.2.2 marks Deprecated (it was registered in [RFC9053]). There is one emission site and one verification gate; no code path
emits -8 and no legacy-compatibility flag exists.¶
The protected header it builds is the deterministic CBOR map {1: -19, 4: kid} -- two members,
because the kid is in the signed bucket, so attribution cannot be swapped without breaking the
signature. Its bytes therefore begin a2 01 32 04 ..., the trailing part being the kid byte
string; a measured envelope's header reads a2 01 32 04 49 ... for a nine-octet kid. A forged
envelope carrying -8 under a genuine Ed25519 signature -- the only construction that tests the
alg pin rather than the signature -- is rejected at the product API with "protected header alg is
not Ed25519 (-19, RFC 9864)"; so are -7, alg as a text string, a non-canonical two-byte
encoding of -19, a duplicated alg label, and an absent alg.¶
One implementation, stated as such. The -19/-8 behaviour above is a property of a single
COSE implementation. The separately implemented Python verifier does not implement COSE at all, and
the Go, Rust and C# verifiers contain none, so they provide no confirmation in either direction.
A third-party check does exist and did run -- a foreign stack (cbor2 + PyCA cryptography) reads
alg = -19 from a shipped vector, verifies the Ed25519 signature over the RFC 9052
Sig_structure, and rejects a tampered payload -- but that corpus holds one vector and it is a
positive one. At the pinned snapshot that vector's own metadata still carried the stale a10132
header string beside an envelope whose protected header holds both alg and kid, so the file
contradicted itself; df777a6 corrected it. There is no cross-implementation NEGATIVE vector for -8; that refusal is proven
only inside this implementation's own tests, and closing it is one of the open vector commitments
recorded below.¶
The two signatures. receiptToCose wraps the JCS-canonical receipt as the COSE_Sign1 payload;
receiptFromCose verifies the COSE signature, re-canonicalizes the payload and requires byte
equality with the signed bytes, and validates the receipt shape -- it does not verify the
native sig.value or any chain property, which is the separate verifyChain path. The two checks
are genuinely independent, which is what Section 4.2 says they are; what no entry point does
is run both, recorded as a gap below.¶
Chain-level axes. All six chain conditions of Section 5.1 are detected and reported
distinctly, under the implementation's own reason strings rather than under this document's labels:
a broken link, a duplicate sequence number, a sequence gap, more than one chain partition in one
input, the cross-tenant split, and a mid-chain key change for one agent -- the last reported as a
"key swap", in both the TypeScript and the separately implemented Python verifier. What it gets
wrong is the checkpoint axis, on both of its named outcomes: checkpoint-unverified is not
distinguished at all, and checkpoint-head-conflict is reported over-broadly by treating a
legitimate extension as one. Both are in the gap list below. scope.tenant drift across one chain is
fail-closed by default, and absence of the member does not reset the comparison.¶
On the word "separately". What is demonstrated is five implementations in five languages that agree on a shared corpus, not five independent derivations of this specification: the Go, Rust and C# sources describe themselves as ports of the Python reference, and the Python one was written against the TypeScript one. Agreement between a source and its port is weaker evidence than agreement between two independent readings of a specification, and this document does not claim the stronger thing.¶
Cross-implementation conformance, run rather than cited. node impl-py/conformance.mjs,
executed at the pinned 4764cab snapshot on 2026-08-14, ran 99 checks and exited 0, with its own summary line reporting that
the separately implemented Python verifier agrees with the TypeScript reference on every vector, including the
impersonation, truncation and duplicate-key security verdicts. The Go, Rust and C# verifiers are
each compared against the Python one over a shared file-based corpus, and the TypeScript reference
is compared against it too, so the topology is a star with the Python reference at its centre rather
than five implementations tested against one another; what that can and cannot settle is stated at
the end of the register below. No claim of demonstrated external interoperability across all five
is made here.¶
Conformance gaps against this revision's own rules, at the pinned snapshot. What follows is the output of two independent reverse audits -- code read first, then the requirement it should have mapped to -- and it is deliberately long. An earlier draft of this section listed six gaps and called that the whole count; the claim was false, and the correction is to publish the register rather than to shorten it. "All five" below means the TypeScript reference and the separately implemented Python, Go, Rust and C# verifiers. A fix for the first three is in flight in the reference repository; this register describes the code as measured at the pinned snapshot, not the code as it will be.¶
One measurement covers the first three rows, and it was run rather than reasoned. Five receipts
were built, each carrying action.riskClass: CRITICAL on a wire.transfer action, and each was
presented as identical bytes to all five verifiers: (A) agent.principal: SANDBOX_SIM with
governance.sandboxed: false and governance.verdict: EXECUTED; (B) governance.verdict:
SIMULATED with governance.sandboxed: false; (C) action.reversible: false with
action.rollbackRef: "snap_1"; (D) governance.verdict: ROLLED_BACK with action.reversible:
false; (E) ts: "2026-13-45T99:99:99.000Z". All 25 combinations returned VALID.¶
A and B are the worst of the five, for the reason this profile exists at all: a reader has to be
able to tell a real action from a rehearsal. A and B are signed, chain-valid, fully verifying
receipts for an action the issuer itself classified CRITICAL, and they can be argued in both
directions afterwards -- the issuer can say the record shows a simulation, the counterparty can say
it shows an execution, and the receipt supports both readings. The receipt carries no amount and
this profile defines no way to open paramsHash (Section 3), so nothing here establishes what the
action was worth; the risk class the issuer assigned is the whole of what the record says about its
seriousness.¶
governance.sandboxed: true whenever agent.principal is SANDBOX_SIM or governance.verdict
is SIMULATED. The requirement is satisfied by no shipped verifier: each validates the primitive
type of the three members and stops, so cases A and B verify in all five.¶
action.rollbackRef to be absent or null unless action.reversible is true, and a
ROLLED_BACK verdict to be accompanied by action.reversible: true. Neither relationship is
checked by any shipped verifier; cases C and D verify in all five.¶
date-time. Every implementation
validates the shape of the string and none of them asks whether it denotes an instant, measured
rather than inferred: case E -- month 13, day 45, hour 99 -- verifies in all five. The reference's
own source records why it stays there, and the reason is narrower than an earlier draft of this
register claimed: tightening the predicate changes no bytes, no digest and no signing domain, so
it is not a wire-format change. What it changes is the set of already-signed receipts an
existing verifier accepts, which the pinned source calls "an API change, never a format change" --
five implementations have to move together or they disagree on receipts nobody can re-sign. That
is a release-coordination problem, not a version-of-the-record problem.¶
receiptFromCose verifies the outer COSE signature, re-canonicalizes the payload and
validates its shape but never touches sig.value; the native signature is checked in the chain
path. A caller can obtain both properties by calling both, and nothing prevents it; what the
repository does not contain at the pinned snapshot is a caller that does. The Receipt Verifier
this profile defines is not an entry point the library exposes, and not one its own callers
assemble.¶
JCS(parse(bytes)) == bytes over the bytes as received. receiptFromCose performs exactly that
comparison; the bare chain path in all five is handed parsed values and cannot prove the original
bytes were canonical.¶
checkpoint-unverified and forbids a truncation or tampering finding. With a keyring, all five
answer TAMPERED; the four that carry a reason string give "checkpoint not authenticated against
keyring" or their own wording of it, and the Go verifier returns the status token alone. Without a
keyring the head comparison runs anyway: an honest, complete chain presented together with an
attacker-signed checkpoint returns TAMPERED in all five -- measured, on identical bytes -- under
the reference's message "tail truncated/extended". Neither branch produces the indeterminate result this revision requires, and both are the
violation of Section 8.3 in its clearest form: a file anyone can hand the verifier becomes a
finding against someone else's records. It is also the shape Section 5.1 now forbids in as many
words: the obligation keys on whether the checkpoint authenticated, and every shipped verifier
branches on whether a keyring was passed.¶
{1: -19, 4: kid} and no CWT_Claims, so what it mints
is not registrable (Section 4.3); the verifier ignores label 15 as a non-critical header,
and a committed test shows it accepting a protected header carrying a malformed 15: bstr as a
perfectly good generic COSE_Sign1. No code path anywhere evaluates a receipt as a Signed
Statement. This profile requires no Producer to implement Signed Statement production -- bare
receipts are a defined presentation (Section 4) -- so the honest statement is that the reference
cannot be registered, not that it violates a requirement. It is listed here because a reader who
assumes registrability would be wrong, and because item 1 of the list further below says the same
thing about the verifier half.¶
MALFORMED, TAMPERED and UNTRUSTED is recoverable from its output. In all five, the
remaining two conditions are conflated by G6 and G7, so six is also the ceiling.¶
kid cannot be handled at all. Section 4.1 requires a verifier to
try every key a kid resolves to, to accept only if exactly one verifies, and to report the
identifier as ambiguous when more than one was tried. Every shipped keyring is a map from a kid
string to one key, so the requirement is not expressible without changing that type: no
implementation can satisfy it today and none reports the case.¶
scope.tenant values. The shipped verifier
exposes requireTenantConsistency, which defaults to refusing such a chain but can be set to
false -- and with it set, the repository's own vector for that case returns VALID on a chain
whose tenant drifts. It is documented and opt-in, which makes it a migration path rather than an
accident, and it is still a configuration in which the implementation does not conform.¶
kid alongside the parsed receipt, whose sig.kid is the native one, so a
caller does not have to re-read its input. What is missing is the labelling and the second check
-- the result does not distinguish "this key emitted the envelope" from "this key signed the
receipt into its chain", and the native signature is not verified on that path at all. G21 is the
sharper form of the same defect.¶
tailChecked field; the other four return ordinary
chain validity with no distinct absence result.¶
governance.approval at all, so it reports nothing about
approval and the conditional never fires. Nothing here is broken. What is true, and why the row
stays: a consumer that wants the distinction cannot obtain it from these results, and an
implementation that later begins reporting approval takes on the obligation at that moment.¶
kid, the CWT claims, a well-formed payload, the native signature where the key is
resolvable, and sub == scope.chain. No implementation performs them, because none claims that
role -- and Section 1.4 is explicit that an implementation conforms to the role whose obligations
it performs, so declining a role is not a failure of it. The register carries the row so that a
reader counting shipped coverage against this document's roles sees the hole, not because
something is broken.¶
crit does not list. The reference's CBOR decoder
refuses the whole envelope when such a header's value uses a CBOR major type it does not
implement, which is a refusal reached before header policy runs.¶
[S]B = R + [k]A'. Read from source: the Python verifier computes exactly that
equation; the Rust verifier calls verify_strict, which its own header names as cofactorless; and
the Go verifier calls the standard library, whose implementation carries the comment
[S]B = R + [k]A and byte-compares the recomputed point against the signature's first half. The
TypeScript reference verifies through node:crypto, so the equation it executes is the one its
linked OpenSSL implements, not one this repository chooses: measured on Node v23.7.0 linked to
OpenSSL 3.6.3, that library's Ed25519 verifier recomputes the strict equation and names it as
such, so the reference satisfies the pin in that pairing. It is stated per version because
that is what was measured; a different Node/OpenSSL pairing is a different measurement, and an
in-repository comment describing the path as "cofactored" is a comment about key validation and is
wrong about the equation. The C# verifier delegates to a BouncyCastle assembly whose source is not
present in the environment these measurements were taken in, so its equation is [UNVERIFIED] --
recorded that way rather than assumed in either direction. An earlier draft of this register
asserted categorically that the TypeScript reference failed the pin, on the strength of that
source comment. That was false, and the correction is recorded here rather than quietly dropped:
a register that over-accuses is as unreliable as one that under-reports.¶
sig.kid, and forbids an authorized
outer kid from standing in for it. The shipped COSE path does the opposite: it pairs
receipt.agent.id with the outer envelope kid and never checks the native sig.kid against
the manifest at all (src/cose/receipt-cose.ts, the identity-binding block). Two consequences
follow, and both were run against the built package rather than reasoned about. Legitimate
relay -- native signature by the key the manifest authorizes for alice, envelope signed by an
unrelated relay: ok:false, "agent \"alice\" is not authorized for signing key \"k-relay\"",
refusing a presentation Section 6 says MUST NOT be refused on that ground. Laundering --
native signature by a key authorized for nobody, claiming alice, wrapped by the key the manifest
does authorize for alice: ok:true, with the returned payload's native sig.kid naming the
unauthorized key. Both verdicts are the reverse of the rule. The chain path is not affected -- it
checks the native sig.kid -- so this is a defect of the envelope entry point only. The probe is
an ad-hoc script and is not committed, which is the reproducibility class stated at the top of this
section.¶
Behaviour the reference enforces that this revision does not require. These are not gaps against
a rule; they are places where a receipt this profile calls conformant is refused by the reference
implementation. That is an interop hazard in the opposite direction and it is stated here rather
than discovered in a bake-off. The classes that earlier reviews found in this position have left
this list, because this revision states them as requirements: checkpoint structure and scope; the
native sig.alg literal; the key type, encoding and public-point rules; the id bound; the
non-empty-string members; the forbidden member names; compliance.verdict; and the UTF-8 round trip
before a kid is resolved. What remains:¶
noa.signing-key-lifecycle/0.1
document in which each key carries a retiredAt, and a receipt, checkpoint or envelope signed by
a retired key is refused -- deliberately, since a signer-chosen timestamp
cannot prove an artifact predates a compromise. Whether that is a divergence from the profile
depends on something this document does not fix: a keyring holds the keys a verifier trusts, and
nothing here requires a retired key to remain in one. A deployment that simply removes it reaches
the same refusal without the lifecycle format, and S2 below says why both are conformant --
keyring encoding and lifecycle are undefined here. What the format actually buys is a stated
reason travelling with the refusal instead of a silent absence. Whether revocation belongs in the
profile is a question for the next revision. A related detail of the same format: it carries the public key as an
encoded key with no separate curve field, so the crv = 6 requirement of Section 4.1 is
checked by inspecting the encoded key, which the reference does.¶
kid refuses the object. Section 4.1 now requires a verifier
not to resolve an identifier through a lossy decode. The reference goes further and refuses
the whole envelope, so a deployment using opaque non-UTF-8 identifiers cannot use it at all.¶
impl-go/parse.go), the C# verifier inherits
JsonDocument's default maximum depth of 64, and the Rust verifier relies on serde_json's
nesting limit, which its own source names as the control. Python has no explicit gate and stops at
the interpreter's recursion limit. So five verifiers enforce four different ceilings, and a
document between the tightest and the loosest is refused by some and accepted by others.
Section 13 assigns limits to the Deployment and fixes no number here, so every
one of them conforms -- which is precisely why the disagreement is a fact about interop rather
than a defect in any of them. An earlier draft of this register said the other four had "no
equivalent fixed limits"; that was false, and this row is the correction.¶
Divergences among the five that the conformance harness cannot see. The harness compares verdicts over a shared corpus. A behaviour only one implementation has, or that no vector exercises, is invisible to it -- and these were found by reading the five sources against one another rather than by running them.¶
{kid: publicKey} keyring, once with this package's own noa.signing-key-lifecycle/0.1 trust
document carrying the same key with retiredAt: null. Plain keyring: VALID in all five.
Lifecycle document: VALID in TypeScript, TAMPERED in Python, Go, Rust and C#, the Python
reason reading "unknown kid k-life at seq 0". The four do not know the format, read it as a flat
map, fail to find the kid, and conclude the receipt was signed outside the trust root -- so a
legitimate trust document holding the correct key makes four of five verifiers accuse an honest
chain of tampering. It is G6's defect reached through a different input, and the same violation of
Section 8.3. No file under conformance/ mentions the format, so no runner ever feeds it to them.¶
y >= q rejection in their own source. The Go verifier rejects the eight
small-order encodings and has no y-coordinate gate at all, inheriting whatever its standard
library does. Section 4.2 makes that rejection a MUST as of this revision, so the Go
verifier does not state a check it is now required to make.¶
alg pin, crit handling, deterministic encoding, kid resolution -- has
cross-implementation evidence in either direction.¶
What the harness can and cannot settle, stated once. conformance/MATRIX.md is generated from
four runs: the TypeScript reference against the Python one, and then the Go, Rust and C# verifiers
each against the Python one. The topology is a star with the Python reference at its centre, not
five implementations tested against one another -- so a behaviour the Python verifier does not
implement cannot be seen by that harness at all, and five-way agreement and five-way nonconformance
coexist without contradiction. G1 to G3 are exactly that case: all five agree with each other, and
none of them agrees with this document.¶
Two claims in the reference implementation's own comments are retracted by this revision and not
yet corrected there. src/cose/receipt-cose.ts calls its output a "COSE_Sign1 Signed Statement"
and promises "the external non-equivocation anchor NOA's self-signed chain lacks". Neither survives
this revision: Section 4.3 shows what a Signed Statement additionally requires and the
register above records that the implementation emits none of it, while Section 7 narrows what
registration supplies. A reader who opens the code will find the older wording; it is a comment,
not a control, and correcting it is filed.¶
Conformance corpora, counted at the pinned snapshot. conformance/vectors/attack/ 16 files and
conformance/vectors/malformed/ 9 (25 adversarial in total); conformance/action-digest/ 33
vectors, 34 in the repository since df777a6; packages/rail-x402/conformance/settlement-evidence/
72 (1 accept, 57 reject, 14 control); packages/evidence/conformance/ 77 bundles. Alongside them,
conformance/golden/ answers a different question: whether a real past release's own signed output
still verifies today.¶
What a third party measured. On 27 July 2026 an independent reconciliation implementation ran
against this corpus at a pinned commit and reported on the SCITT list
(https://mailarchive.ietf.org/arch/msg/scitt/zWZBQXXSGV-vgbTeOLTjlb9DGLg/): 23 of 23
adversarial files then published produced at least one integrity-class divergence, with
cross-chain-splice the clean case -- both receipts verify in isolation and the forgery exists only
in the set. The same run measured a shared subject digest agreeing with another party's
canonicalization corpus on 22 of 22 cases, while a separate ledger-indexing construction that
applies Unicode normalization agreed on 20 of 22 under a UTF-16 member sort and 19 of 22 under a
code-point sort -- the collisions being invisible without a shared vector, which is the argument
Section 10 makes normative. The corpus has since grown from 23 adversarial files to 25.¶
What is still missing. The purpose-specific negative vectors that Section 8, Section 8.2 and
Section 9 call for are not in the public corpus, with one exception: the
attribution-substitution vector landed at df777a6. The rest -- a controller success with no
physical evidence, a controller success against a witness reporting non-completion, same-party
evidence presented as independent, a missing signer relationship, a stale or replayed observation,
and the positive baselines mirroring each -- remain committed and unwritten, as does a
cross-implementation negative for the alg pin.¶
Not implemented, stated plainly. Ten things, so that no reader has to infer them. Item 1 is gap G8 of the register above. Item 2 is permitted but unimplemented; items 3 and 4 are capabilities this profile deliberately excludes (Section 4.2, Section 5.1); the rest are absences with nothing owed against a rule. The distinction is the one Section 8.3 insists on, applied to this section.¶
{1: -19, 4: kid} and no CWT_Claims (label 15), so what it mints does not
meet Section 4.3 and a conforming Transparency Service is entitled to refuse it at
registration. The verifier is the other half, and the half a reader is most likely to assume: it
ignores label 15 as a non-critical header, and a committed test shows it accepting a
protected header carrying a malformed 15: bstr as a perfectly good generic COSE_Sign1. No code
path anywhere evaluates a receipt as a Signed Statement.¶
x5t or x5chain in addition to kid; the verifier ignores both headers and
resolves kid alone. Nothing is excluded -- it is simply not built.¶
seq == 0; a presentation starting at seq 1 is refused as a missing genesis, not verified as
a partial view. Section 5.1 states that as the rule. A verifier that could authenticate a
mid-chain range against a trusted anchor is a real capability and this profile does not have one.¶
4764cab snapshot -- the registry
an Evidence Verifier would consult to decide which action classes require settlement evidence.
Device enrolment through pairing is a different mechanism and did exist at that snapshot, with
its own conformance vectors; this item is not about it. Without the action-class registry,
settlement evidence
cannot be required, so at this snapshot its absence costs nothing and its presence buys no
upgrade. Subsequent implementation work is outside the measurements reported here.¶
alg pin. The attribution-substitution vector that was
open in earlier drafts of this list is no longer among them: it landed at df777a6.¶
The boundary item 1 leaves is worth stating in the sharpest available words: "the signature verifies" and "an RFC 9943 Signed Statement validates" are different properties, and this implementation currently establishes only the first. Closing it is work on both sides -- the producer must emit the protected claims, and the verifier must gain a mode that checks them and refuses a receipt that lacks or malforms them, per Section 4.3 and [RFC9597], Section 2, which defines label 15 as a CWT Claims map that occurs exactly once across the two header buckets.¶
Every one of these is a boundary this document states rather than a gap it hides. Where a reader finds a normative requirement here whose enforcement is on this list, that requirement is a specification for implementers -- including this one -- and not a report of shipped behaviour.¶
Analysis binds every actor. Where a sentence carries a requirement it names its actor; the requirements in this section fall on the Producer (what MUST NOT appear on a receipt), on verifiers (what they MUST NOT report), and on the Relying Party (how results may be read).¶
This profile attests exactly two things: (1) the record is tamper-evident and
signature-verifiable under the issuer's key; and (2) the recorded fields -- action, principal
class, verdict, and whichever optional members are present -- are bound into that signature.
"Policy identity" is one of the optional ones: both carriers may be absent (Section 3.3),
and a receipt that omits them records no policy identity at all while remaining conformant. The principal field is a class marker and
not an identity: a receipt recording HUMAN names no human, and a relying party MUST NOT read one
out of it. It does NOT attest that the recorded
inputs are true, complete, or timely (no capture-completeness); that the policy was adequate; that
no action occurred outside the instrumented boundary; that the agent was correct, safe, or wise;
that a named approver authorized this exact action before it ran; that a controller succeeded; or
that any physical effect occurred. These non-goals are NORMATIVE: implementations and relying
parties MUST NOT imply the stronger claims from a receipt.¶
Equivocation -- one issuer signing two divergent histories and showing each to a different party --
is not detectable by any check in this profile. Section 5.1 catches a fork only when both branches
are presented together, as a duplicate sequence; nothing reaches a branch the prover withholds. Registration in a Transparency Service is a necessary step toward detecting them and is
not a sufficient one: detection additionally requires consistency proofs between log states and a
party that monitors them (Section 7), neither of which this profile specifies or requires. A
deployment that registers and never monitors has an auditable record and no equivocation defence. Identity attribution above key level requires the out-of-band
identity manifest. Algorithm-confusion and key-confusion attacks are mitigated by the strict
alg = -19 and pinned-curve requirements of Section 4. Raw prompts, tool arguments, and secrets
MUST NOT appear on a receipt; only hashes (optionally HMAC for low-entropy values) are carried.¶
The scope of a checkpoint check is the scope of the key that signed it. A checkpoint can reveal
truncation relative to itself, and only to the strength of the binding between checkpoint authority
and chain. With an identity manifest, that authority is bound to the keys the manifest authorizes
for the genesis receipt's agent.id -- a set, not one key (Section 5.2) -- so a re-heading
attacker's checkpoint over its own appended head is rejected. WITHOUT a
manifest, checkpoint authentication is key-level only: any key the verifier trusts can mint a
checkpoint over any head, so a co-trusted key holder can delete the most recent receipts, sign a
checkpoint over the truncated head, and obtain a verified tail check over an incomplete chain.
A Chain Verifier MUST surface this limitation to the relying party whenever a checkpoint
authenticates without a manifest, and MUST NOT report such a result as an unqualified
tail-truncation check. Even with a manifest the guarantee is opener-scoped: on a chain carrying
more than one agent.id, the opener's checkpoint does not certify a co-agent's tail.¶
A verifier never normalizes a non-NFC string, and this profile sets no default for whether it
rejects one (Section 3.3); the analysis below is written for the accepting configuration,
and a verifier in the refusing configuration simply never sees the ambiguity. In the accepting
configuration, because a non-NFC string is neither normalized nor rejected
(Section 3.2, Section 3.3), two receipts may carry agent.id or action.id values
that render identically but differ in bytes, and both verify. A verifier configured in the optional
refusal mode rejects such a receipt as malformed instead -- which removes the ambiguity for that
verifier and for nobody else, since the same bytes still verify elsewhere. Relying parties that match, index, or alert on these fields MUST compare them as bytes. Normalizing
at their own layer before comparing is not an equivalent option: it is precisely what collapses
two distinct, validly signed identifiers onto one index entry, match or alert subject, which is the
collision this paragraph exists to warn about. A relying party MUST NOT assume that visual equality
implies byte equality.¶
A trusted key can try to extend someone else's chain, and one condition stops part of it. Every
receipt in a chain is individually valid; nothing in a receipt says who is entitled to add the
next one. A party holding any key the verifier trusts can therefore append well-formed receipts
to another party's chain -- correct prevHash, contiguous sequence, same scope.chain, same
tenant -- and, where the appended receipts claim the victim's own agent.id, every condition
but one stays silent. The one that fires is chain-key-discontinuity (Section 5.1): those
receipts carry a different sig.kid under that same agent.id. A Chain Verifier that omits the
check accepts a forged tail as history. Two escapes remain, stated here rather than left for an
attacker to find: an attacker who appends under a new agent.id trips no condition at all,
because this profile defines no append authority for a chain (Section 5.1, Section 1.5);
and an attacker whose second key is registered under the victim's kid defeats a comparison
made on identifiers rather than on resolved key material. Where the attacker instead reuses the
victim's own agent.id and sig.kid, it is signing with the victim's key, which is a
key-compromise problem this profile cannot address.¶
A receipt can be re-wrapped by anyone who holds a trusted key. The payload is self-contained
and self-signed, and Section 4.2 permits the outer COSE signer to differ from the native
signer. It follows that any party whose key a verifier trusts can take another party's receipt,
strip its envelope, and re-emit the same payload under its own outer signature: both signatures
verify, because both are genuine. Nothing in the bytes distinguishes the original emitter from a
relay. The consequences are bounded but real: the relay cannot alter a field without breaking the
native signature, so the content is safe; what it can do is present someone else's record as
something it emitted, and -- if the verifier attributes from the outer kid -- have that
attribution land on itself. What stops that is the attribution model of Section 6, and it
is worth being exact about which half does the work. The manifest binds agent.id to authorized
keys and is checked against the native sig.kid, so a relay cannot make itself the agent: the
agent claim is carried by a signature the relay did not produce and cannot alter. The relay is not
refused for relaying -- Section 6 forbids rejecting a receipt solely because its outer
signer is not one of the agent's keys, because relaying is a legitimate presentation. What a
verifier MUST NOT do is report the outer signer as the party that made the record. Without a
manifest, attribution is key-level only and even the agent claim is unbound. Registration does not help here: a Transparency Service that follows
Section 7 will happily register a relayed statement, because every signature in it is valid.¶
Same-party evidence is the forensic trap this profile names rather than solves. A signed
outcome produced under the same administrative control as the authorization is not independent
evidence, however sound its cryptography; independence is a property of the trust configuration a
verifier applies, never of a field a producer writes about itself (Section 6.1). An
implementation that renders UNKNOWN as "independent" has defeated the control.¶
Substitution across claim classes is a signature-valid attack. Both the post-execution record presented as authorization and the machine decision presented as human approval (Section 9) carry genuine signatures. An Evidence Verifier that returns one boolean cannot express "signature good, claim wrong". That is a defect in what such a result can tell its reader, not a conformance failure: Section 8.2's claim-class machinery is non-normative in this revision, so nothing here makes a one-boolean result nonconformant. A reader deciding whether to rely on such a verifier should treat the missing distinction as the gap it is.¶
Parsing attacker-supplied input is a cost this profile does not bound. A verifier ingests JSON
and CBOR from whoever presents them, and the checks required here -- duplicate-member detection,
surrogate validation, canonical re-serialization -- are all performed on that input before anything
about it is trusted. This profile bounds exactly one string -- id, at 128 code points
(Section 3.1) -- and sets no maximum payload size, nesting depth,
member count, or chain length, and a Chain Verifier is additionally required to hold a complete
chain from genesis (Section 5.1), which grows without bound. A Deployment MUST impose its
own limits and MUST refuse input that exceeds them rather than attempting a partial verification;
a limit that is exceeded is not a tampering finding and MUST NOT be reported as one. A deployment
that omits limits has a denial-of-service surface, not a correctness problem.¶
An unpinned digest construction is a silent join. Comparing digests produced under different constructions can both miss a real correlation and, where one construction normalizes and another does not, map two different actions onto one value. Section 10 requires the comparison to fail closed and be reported as indeterminate.¶
Absence of scope.tenant is not a tenancy reset (Section 3.3); a verifier that treats
it as one accepts a cross-tenant splice that carries no tampering evidence of its own.¶
This section addresses the Producer and the Deployment (Section 1.4); verifiers inherit no obligation from it.¶
A receipt is designed to be shown to parties who were not present when the action ran, so what it carries is a privacy decision as much as a security one.¶
Raw parameters are forbidden, not prevented. This profile prohibits a Producer from placing
raw prompts, tool arguments, secrets or other sensitive parameters anywhere on a receipt
(Section 3), and action.paramsHash carries a digest in place of the values. What the format
cannot do is enforce it: several members are unconstrained strings (Section 3.1), so a
producer that writes a secret into one produces a receipt every verifier accepts. A deployment
handling sensitive parameters therefore needs a producer-side control, and a relying party that
needs the parameters must obtain them out of band. Whether it can then open the commitment is a matter for the
producer's own contract, not for this profile: Section 3 states that the preimage construction is
undefined here, and for the keyed form the key is a tenant secret. So the privacy property is
one-directional -- the receipt does not disclose the parameters themselves -- and the verification
property it might seem to imply does not follow from this document alone. What the receipt does
disclose is set out below: a guessable commitment is an oracle, and the identifiers and timestamps
are linkable by design.¶
A hash of a low-entropy value is not a privacy control. Where the committed parameters are
guessable -- an amount, a boolean, a small identifier -- SHA-256 is a confirm-by-guess oracle
rather than a shield, since anyone can hash a candidate and compare. For those cases the profile
provides the keyed form hmac-sha256:<hex> under a tenant-scoped key, which removes the oracle
from anyone who does not hold the key. Producers SHOULD use it whenever the parameter space is
enumerable, and MUST NOT treat a plain digest of a low-entropy value as a disclosure control.¶
What remains linkable, by design. A receipt still carries scope.chain, agent.id,
action.id, action.canonical, timestamps and, where present, governance.approval.by. These are
opaque strings to this format, but they are stable across receipts, which is what makes a chain a
chain: any party holding two receipts can tell they concern the same chain or the same agent, and
timestamps support timing correlation. A Deployment that treats those identifiers as sensitive SHOULD
choose opaque, non-descriptive values -- an internal identifier rather than a person's address --
because the format cannot enforce that and a verifier cannot undo a descriptive choice.¶
Registration publishes, to the extent the log retains. Submitting a Signed Statement places it
and the CWT Claims in its protected header into an append-only log whose purpose is that third
parties can inspect it. How much of the payload they can see is the log's choice, not this
profile's. [RFC9943], Section 6.2 contemplates statements whose payload is carried as a hash
rather than in full -- and that construction is not conformant with this profile as specified:
Section 4.2 requires the receipt itself to be the attached payload, and Section 7
requires a Transparency Service to validate that payload as a receipt and to check sub against its
scope.chain. A hash-only statement fails both. A deployment that needs to disclose less therefore
cannot get it from this revision; defining a mapping -- what the hash covers, and what a service can
still check -- is work for a future one. CWT Claims in COSE headers are not encrypted
([RFC9597], Section 3), so iss and sub are visible to every party that can read the log. A
deployment SHOULD decide what may be published before it registers: transparency rests on an
append-only sequence, which [RFC9943], Section 5.1.3 requires the log's verifiable data structure
to guarantee -- it cannot be modified, deleted or reordered -- so an entry cannot be un-registered,
and this document defines no redaction mechanism. What a service retains and serves beyond that history is its own policy, which
this profile does not constrain.¶
Signer relationship reporting is a disclosure too. The trust-configuration digests that Section 6.1 requires alongside a derived relationship commit to a policy and a registry snapshot; they do not disclose their contents, and a Producer SHOULD keep it that way rather than inlining registry material into a result a relying party may forward.¶
This section is to be removed by the RFC Editor before publication. It records what this revision deliberately did not settle, so that a reader does not mistake an open question for an oversight.¶
iss for the authority the outer Signed
Statement is emitted on behalf of; Section 6.1 calls the native signer the issuer; and a relay
that wraps someone else's receipt is a fourth party using the same envelope position
(Section 13). Nothing here is contradictory, and an implementer can still build
from it -- but a reader has to carry the disambiguation in their head, and the ambiguity invites
exactly the native/outer collapse Section 6 forbids. Whether these should be distinct
named roles, and which of them iss denotes when they differ, is a naming decision with security
consequences and it belongs to the working group.¶
date-time whose seconds field reads 60,
and a verifier to reject one. The alternative this revision rejected was to admit 60 and require
nobody to validate it, which is not implementable offline: [RFC3339], Section 5.7 admits 60
only at an actual leap-second position adjusted by the value's offset, and says future leap
seconds cannot be predicted, so a verifier holding no table cannot decide whether such a value is
a valid date-time -- and the requirement to reject what is not valid would have no determinate
answer. Pinning a table was rejected too: a table is state that ages, and offline verifiers
acquire it at different times, so two conforming verifiers would still disagree about a historical
instant. Forbidding the representation is the only branch that leaves every verifier the same
answer, and it costs a Producer nothing it needs. The working group may prefer one of the branches
this revision declined; the cost of each is stated here so the choice can be made on evidence.¶
agent.id
within a presented chain, which is what closes the chain-extension attack; the price is stated in
Section 5.2: rotation is not expressible inside a chain, so a deployment that rotates a signing
key starts a new one. The reference implementation has gone further and refuses a receipt signed
by a key its trust document marks retired, which this profile does not require
(Section 12). Whether a rotation or revocation mechanism belongs in the profile --
and if so, whether it may relax the one-key rule or must sit beside it -- is a genuine trade
between an attack this revision closes and an operational need every deployment eventually has.¶
content type cannot be
interoperable across deployments. Two answers are available and the document does not choose
between them: register a media type and stay on the Standards Track, or publish as Experimental
while the profile's open questions settle and register later. What would be wrong is to ship a
wire format on the Standards Track with neither.¶
This section carries no normative requirement on any actor.¶
This document has no IANA actions in this revision, and one of those absences is load-bearing:
without a registered media type for the action-receipt payload, the content type of
Section 4.3 cannot be interoperable across deployments. A future revision ought to
register one, and a Standards Track document that carries a wire format arguably should not ship without it
-- this revision states the gap rather than papering over it. It uses the existing IANA COSE Algorithms
registry value -19 (Ed25519) and the existing COSE/CWT header parameters. The vocabularies named
in Section 6.1, Section 8.2, and Section 8.3 are shared spellings for reporting, not an
interoperable output format -- this revision defines no result schema (Section 1.5), so two
implementations using the same word still do not produce comparable output -- and they are not
proposed for registration in this revision. A future revision could request registration of
a media type for the action-receipt payload and/or of CWT or COSE claims for the payload fields.¶
This section is to be removed by the RFC Editor before publication.¶
This revision exists because of a public review thread. What follows is that story in order: what was asked, what was built and measured in answer, and what this document now guarantees. Every difference between the posted -00 and this text is accounted for here -- a reader diffing the two should find no change this section does not explain.¶
Section 5 keeps its number, its title and its XML anchor (hash-chaining-and-completeness), so
both numeric and fragment references from [PSER] continue to resolve, and the construction it
defines -- chain.seq, prevHash, the linkage rule -- is unchanged.¶
Two sentences of its paragraph were wrong in -00 and are corrected in this revision. A document that adopted Section 5 by reference inherited both, so the change is stated here in the terms an implementer needs:¶
seq == 0), so a foreign checkpoint produced by a re-heading attacker is rejected". That binding
does not exist on its own: it requires an identity manifest, and it binds to the set of keys the
manifest authorizes for the genesis receipt's agent.id. -01 says so, and says plainly that
without a manifest the property does not hold. An implementation that relied on -00's sentence
believes it rejects a re-heading attacker and does not.¶
Nothing else in the paragraph changed, and no receipt changes validity because of these two corrections: they describe what verification establishes, not how a receipt is built. Subsections 5.1 and 5.2 are NEW, and the principal requirements they add are:¶
seq == 0, refusing a presentation that begins elsewhere (MUST);¶
checkpoint-unverified instead;¶
ts (MUST), since no check constrains it;¶
ts is
signer-chosen and this profile defines no expiry;¶
spec, the four value shapes and the sig sub-object -- and never turn a malformed checkpoint
into a finding about the chain (MUST NOT);¶
chain to the presented scope.chain (MUST), report a mismatch
as a finding about the checkpoint, and never let it fail an otherwise valid presented chain
(MUST NOT);¶
sig.kid to be authorized for the
genesis receipt's agent.id and reject any other trusted key as UNTRUSTED (MUST);¶
agent.id and differ in sig.kid (MUST),
reporting chain-key-discontinuity;¶
kid is the identifier an identity manifest
authorizes for a checkpoint (MUST NOT) -- a checkpoint is a native artifact and the two identifier
spaces are separate;¶
This list is not exhaustive, and this revision no longer claims that it is. Three earlier drafts made that claim and each was wrong: the list is written by hand, 5.1 and 5.2 are long, and a hand-written inventory of a long section rots the moment either changes. It records the principal additions -- most binding a Chain Verifier, four binding a Deployment as marked, one conditional on an option a verifier need not take. None constrains a Producer or changes how a chain is built, so a profile that adopts the convention to build receipts is unaffected. A reader who needs the complete set has to read 5.1 and 5.2, which is the honest instruction.¶
One documentation narrowing, disclosed rather than buried. -00 said "a verifier checking a
range of receipts MUST verify ..." and never said what a range may be. 5.1 now states that a
conforming verifier requires the complete chain from seq == 0 and refuses a presentation that
begins elsewhere. That is a narrowing of the text, not of any behaviour: all five verifiers -- the reference
implementation, the Python one compared directly to it, and the Go, Rust and C# ones compared to
Python -- already hardcode 0..n-1, and a presentation of seq 1,2 has always been refused as a
missing genesis. So no receipt that verified under a -00 implementation fails under a -01
implementation, and no implementation changes; what changed is that the requirement is now written
down where an implementer can read it, and the capability nobody has -- verifying a mid-chain range
against a trusted anchor -- is named as absent in Section 12 instead of being implied
by silence.¶
The same care applies to the gap condition itself: a correctly linked chain presenting 0 then 2
was underspecified in -00 and is a chain-sequence-gap failure under 5.1. Any implementation of
-00 already rejected it, so this is again documentation catching up to behaviour rather than a
change of outcome -- but an implementer who read -00 literally and built a permissive verifier would
find their verifier non-conforming under -01, and that is worth saying plainly.¶
What happened to every -00 section, checked against the posted text rather than remembered. Sections 1 through 7 keep their -00 numbers. The rest moved, and one was removed:¶
| In -00 | In -01 |
|---|---|
| 1-7 | unchanged numbers |
| 8 -- Relationship to ACTA signed receipts (equivalence), with its mapping table | removed. It changed no requirement, and a field-by-field mapping to one other individual draft did not earn a section here |
| 9 -- Policy-replay out of scope | 11 |
| 10 -- Security Considerations | 13 |
| 11 -- IANA Considerations | 16 |
| Appendix A -- Implementation status | 12, promoted into the body where RFC 7942 places it |
| Appendices B and C -- the two reference lists | 17.1 and 17.2 |
| -- | 8, 9 and 10 are new (external-world non-claims, authorization vs evidence, the digest boundary) |
| -- | 14, Privacy Considerations, is new, and -00 had no privacy section at all |
| -- | 15, Open questions for the working group, is new |
| -- | Appendix A (this change log) and Appendix B (acknowledgments) are new |
The record format itself is untouched: no member of noa.receipt/0.1 is added, removed, or
re-typed.¶
kid (label 4), or a certificate reference (x5t label 34 / x5chain
label 33) where a PKI is used" -- a disjunction, not a contradiction. -01 makes a resolvable kid
mandatory and demotes the certificate references to additional-only, because a verifier conforming
to this profile is required to resolve kid and is not required to process certificates. A
producer that satisfied -00 by carrying x5t alone is not conformant to -01. Section 4 now says
kid SHOULD be protected,
MAY be unprotected, MUST be resolvable from either with the protected copy preferred, and that a
verifier MUST report which bucket it used -- because attribution from an unprotected identifier is
not covered by the signature, so an unprotected kid may resolve a key but never names an
identity (Section 6). It also states that additional registered headers MUST NOT cause
rejection unless listed in crit.¶
policyHash the payload never carried. Section 3.3 resolves it
in favour of what the wire format does: governance.ruleId is a label, the optional
governance.compliance member is where a policy commitment lives, and neither makes a receipt
replayable.¶
scope.tenant was shown as always present. It is optional, and Section 3.3 states
the rule that matters: absence MUST NOT reset a tenancy comparison, or an optional field becomes
a splice channel.¶
a10132 as the
protected header this implementation emits. It is not: that encodes {1: -19} alone, and the
emitted header is the two-member map {1: -19, 4: kid} -- the kid having been moved into the
signed bucket, which the same document's own Section 4 says. The identifier claim was right and the
byte-level claim was not; Section 12 now states the map, and the paired a10127 claim about
the deprecated identifier is dropped rather than repeated. It was found by an adversarial pass
that decoded a shipped vector -- one command, which is exactly how a reviewer would have found it.¶
sig. -00's figure ended at chain, which made its own
"JCS(receipt WITHOUT chain.hash AND WITHOUT sig.value)" construction impossible to implement
from the figure. sig (with alg, kid, value) is now shown as the mandatory member the
schema has always required.¶
agent.model,
action.rollbackRef, governance.ruleId, governance.approval and compliance.verdict join
scope.tenant and governance.compliance, and the figure marks each. The rule that
present-as-null is not absence -- different bytes, different hash -- is stated with them.¶
chain.hash. COSE requires the detached bytes themselves for the Sig_structure, and
chain.hash is a chain-linkage value computed over the receipt with chain.hash and sig.value
removed. The shipped parser refuses a non-byte-string payload. Section 4.2 now defines the
attached form only, and a detached mode is future work.¶
alg and
then that they SHOULD reject -8 unless a legacy mode was negotiated. -8 is not a special
case, no such mode exists in any implementation, and the exception is deleted.¶
kid required, matching a verifier that ignores x5t/x5chain.¶
"
and \; and a verifier MAY refuse a non-NFC receipt outright, which the shipped verifier offers
as a mode. What no verifier may do -- normalize before hashing -- is unchanged and is the rule that
matters.¶
introduction,
relationship-to-other-work, identity-binding,
policy-replay-is-out-of-scope-for-this-revision, security-considerations,
implementation-status, hash-chaining-and-completeness, and the two reference sections. Every
one of the -00 anchors is restored in the published XML, so both numeric and fragment references
from -00 continue to resolve.¶
id and the seven non-empty
members, and the closed ALLOW/DENY set for governance.compliance.verdict
(Section 3.1); the three JSON member names refused at the parse boundary (Section 3.2);
the exact sig.alg literal, the Ed25519 key-type rule, the canonical key encoding and the two
public-point rejections (Section 4.2); the UTF-8 round trip before a kid is resolved
(Section 4.1); and the checkpoint's closed object, value shapes and chain equality
(Section 5.2). One of them is not stated by one implementation, which Section 12
records. The general rule of Section 8.3 -- that a verifier's own failure to authenticate or
understand an input MUST NOT become a finding against the subject of the verification -- is new
as a rule; the two requirements it generalizes were already in Section 5.1 and Section 5.2.¶
sub against
scope.chain. Without the first, a trusted registrant can obtain a log entry for a receipt nobody
in the chain signed; without the second, a chain-A receipt can be registered under chain-B.¶
chain-tenant-split. And every shipped verifier already rejects a mid-chain key change for one
agent while no revision had ever named it: that is chain-key-discontinuity, the condition that
stops a trusted-but-unrelated key from extending another party's chain. The set of chain
conditions is now six, and the two checkpoint-derived results that earlier drafts of this revision
counted among them are on a separate axis: a checkpoint is a separate object answering a separate
question, and nothing on that axis can fail a chain (Section 5.1).¶
JCS(parse(bytes)) == bytes over the whole
payload.¶
spec exactness, the
absence of identifier namespaces, what ts means, and the cross-field rules that stop one record
saying two things -- including the simulation-marker rule, which -00 left open.¶
kid and CWT Claims requirements. RFC 9943 has no such
subsections -- its Section 6 runs 6, 6.1, 6.2, 6.3 -- and both requirements live in the body of
Section 6. The citations now say Section 6. The error entered through a summarized reading of the
RFC rather than its text; every external section pinpoint in this revision has since been checked
against the RFC's own published text.¶
This revision is the product of a public review thread on the SCITT mailing list in July 2026, archived at https://mailarchive.ietf.org/arch/browse/scitt/?q=noa-scitt-ai-agent-receipt. The reviewers named here disagreed usefully, contributed concrete text, and in one case ran their own implementation against this profile's adversarial corpus rather than arguing about it. Errors that remain are the author's.¶