| Internet-Draft | Vaara Receipt | June 2026 |
| Sirkkavaara | Expires 23 December 2026 | [Page] |
This document specifies vaara.receipt/v1, a signed and independently recomputable record that binds a decision about an agent action to the evidence the decision was made on, and optionally to one or more external timestamp anchors. The format is canonicalized with the JSON Canonicalization Scheme (JCS) so that any third party can recompute its digests and verify its signature without access to the issuer.¶
The receipt's trust is root-agnostic: the same record is verifiable with or without a hardware trusted execution environment and is re-expressible as an IETF RATS Entity Attestation Result. Downstream specifications (a payment rail, a compliance regime, a framework integration) define profiles that pin to a version of this document and add only their own evidence schema; they do not redefine the envelope. The format described here is deployed and is recomputed today by independent implementers from public conformance vectors.¶
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 23 December 2026.¶
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.¶
A Vaara receipt is a signed, independently recomputable record that binds a decision about an agent action to the evidence it was made on, and optionally to one or more external timestamp anchors. Any system that emits or consumes Vaara receipts conforms to this document. Downstream specifications define profiles that pin to a version of this document and add only their own evidence schema; they do not redefine the envelope.¶
The receipt's trust is root-agnostic. The same record is verifiable with or without a hardware TEE and re-expressible as an IETF RATS ([RFC9334]) Entity Attestation Result (an AR4SI vector, [I-D.ietf-rats-ear]), whether rooted in a TPM 2.0 host, an AMD SEV-SNP confidential VM, or software alone. The signature and the optional external time anchor carry the evidence, not a single trust root.¶
This document packages a format that already ships and is already recomputed by independent implementers. The executable conformance fixtures live under tests/vectors/ in the source repository ([VAARA-REPO]) with a dependency-light checker (_check_independent.py) that imports only the standard library, a signature library, and a JCS implementation.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", and "MAY" in this document are to be interpreted as described in RFC 2119 ([RFC2119]).¶
All digests and all signed payloads in this specification are computed over the JSON Canonicalization Scheme (JCS, [RFC8785]). The canonicalization label for the evidenceRef.canonicalization field (Section 4) is "jcs-rfc8785". The values "JCS" and "jcs-json-v1" are accepted aliases for the same algorithm; producers SHOULD emit "jcs-rfc8785", and consumers MUST accept all three.¶
A digest is written "sha256:" followed by the lowercase hexadecimal SHA-256 ([FIPS180-4]) of the JCS-canonical bytes of the referenced object.¶
A receipt is a JSON object with these top-level members:¶
| Field | Type | Required | Meaning |
|---|---|---|---|
| version | integer | MUST | Envelope version. 1 for this document. |
| alg | string | MUST | Signature algorithm. ES256 in v1; ML-DSA-65 MAY be offered as a post-quantum scheme. |
| backLink | object | MUST | Binds this receipt to its attestation/predecessor: attestationDigest, attestationNonce. |
| decisionDerived | object | MUST | The decision and the evidence it derives from. See Section 4. |
| issuerAsserted | object | MUST | Issuer-asserted identity claims: iss, sub, iat, nonce, alg, secretVersion. |
| signature | string | MUST | Detached signature, hex. For ES256, the 64-byte r||s pair (128 hex chars). |
| timestampAnchors | array | MAY | External time attestations over this receipt. See Section 5. |
The ES256 algorithm label and its 64-byte r||s signature encoding are as defined for "ES256" in JSON Web Algorithms ([RFC7518]). The ML-DSA-65 scheme is as defined in [FIPS204].¶
The signature is computed over the JCS-canonical bytes of the object containing exactly these members, in this set, with their receipt values:¶
("version", "alg", "backLink", "decisionDerived", "issuerAsserted")
¶
"signature" and "timestampAnchors" are NOT part of the signed payload: a receipt can gain anchors after signing without invalidating the signature. A consumer MUST verify the signature by reconstructing this payload, canonicalizing it, and checking it against the public key under "alg".¶
decisionDerived carries the decision (decision, decidedAt, policyId, reason, riskScore, thresholdAllow, thresholdBlock) and one evidenceRef object that binds the decision to a recomputable evidence record:¶
| Field | Meaning |
|---|---|
| canonicalization | The label from Section 2 (jcs-rfc8785 / JCS / jcs-json-v1). |
| digest | "sha256:" of the JCS-canonical evidence record. |
| ref | An opaque locator for the evidence record (profile-defined). |
| schema | The schema id of the evidence record (profile-defined). |
The binding is recomputable: given the receipt and the evidence record, a third party confirms that sha256(JCS(evidence_record)) equals evidenceRef.digest with no access to the issuer. This is the property independent implementers verify today.¶
A timestamp anchor is an external attestation that this receipt existed no later than a stated time. Anchors are additive and optional. Each anchor binds the anchored digest, which is "sha256:" of the JCS-canonical signed payload (Section 3.1), so an anchor commits to the exact signed receipt without depending on later anchors.¶
{
"method": "rfc3161",
"anchoredDigest": "sha256:...",
"token": "<method-specific time token>",
"authority": "<optional human-readable authority id>"
}
¶
Registered methods (the registry is open; a profile MAY register more):¶
| method | What it is | Who can produce it |
|---|---|---|
| rfc3161 | An RFC 3161 ([RFC3161]) timestamp token from any Time-Stamping Authority. | Self-hostable (e.g. OpenSSL ts); needs no third party. |
| rfc3161-eidas-qualified | An RFC 3161 token from a qualified TSA under eIDAS ([eIDAS]). | A qualified trust service provider. Adds legal / court-admissible weight; this is the only thing the qualification adds over rfc3161. |
| ledger | A commitment of the anchored digest to a public ledger; the block time bounds existence. | Self-producible; trust-minimized, no TSA. |
A receipt MAY carry several anchors of different methods. The technical anchor (rfc3161, ledger) and the legal anchor (rfc3161-eidas-qualified) are independent: a producer can stand up its own time evidence and add qualified legal weight as a separate, swappable method. No single anchor method is load-bearing for the receipt's integrity, which rests on the Section 3.1 signature.¶
A profile is a downstream specification that uses this envelope unchanged and defines only its own evidence record (the schema and contents behind evidenceRef), plus any join keys it needs. A profile MUST state the vaara.receipt/vN version it pins to and SHOULD ship recomputable vectors.¶
The profiles below pin to vaara.receipt/v1. Vector paths are relative to the source repository ([VAARA-REPO]).¶
| Profile | Evidence schema | Vectors |
|---|---|---|
| x402 settlement binding | x402.settlement.*/v0 | tests/vectors/x402_settlement_v0/ |
| authorization decision | vaara.authorization/v0 | tests/vectors/authorization_v0/, tests/vectors/contiguity_v0/ |
| AP2 checkout binding | vaara.authorization/v0 (names AP2 PEF frame_id) | tests/vectors/ap2_v0/ |
This profile binds an x402 payment settlement to a Vaara receipt across an action lifecycle, on a generic rail and on the Sui exact-payment rail. It adds:¶
A third party recomputes three per-step verdicts (action-ref recomputes, settlement binding resolves, signature verifies) and one lifecycle verdict, with only the settlement and the receipt in hand. See _check_independent.py in the vectors directory.¶
This profile turns an enforcement decision into a receipt. A credential broker authorizes a tool call against a signed, attestation-bound grant with typed capability scopes; the gateway's verdict, allow or deny, is minted as a receipt instead of being discarded. The decision maps onto the envelope verdict vocabulary: an allowed call is "allow", a refused call is "block" carrying the machine reason (capability_exceeded, binding_unknown, missing_credential, ...) as decisionDerived.reason. It adds:¶
A verdict is only as meaningful as what the issuer could see. "allow" over an unbounded surface and "allow" over a stated one are identical bytes with opposite meaning, so an absent refusal reads as fact only against a declared scope: "not refused within this boundary", never "not observed". The coverage block carries that boundary in the trace itself, so it is recomputable evidence rather than a separate trust root. The verdict stays a thin read over it. The chokepoint remains an observer of what passes through it, not a claim about what does not.¶
The deny case is the point. A refused call leaves a signed, content-addressed, portable proof of the non-action: a third party recomputes the verdict from the grant and the arguments and confirms the refusal, trusting only the issuer's public key. A third party recomputes five verdicts per case (grant fingerprint, argument commitment, capability verdict, evidence binding, signature) with only the grant, the arguments, the evidence, and the receipt in hand. See _check_independent.py.¶
Coverage states the boundary; completeness makes a gap inside it provable. With the per-boundary seq contiguous by construction and the runningCount signed into each record, a dropped receipt is a missing sequence number that any holder detects from the receipts alone: the highest running count names how many exist, so a short set is self-evidently incomplete and the absent seq is named. This needs no issuer access and no external witness. The tests/vectors/contiguity_v0/ vectors and the "vaara verify-contiguity" surface carry that check. One honest limit remains: a pure tail truncation (holding 0..k with nothing after) cannot be told from a complete stream by contiguity alone, since the latest held count is then k + 1. Closing it is the job of an rfc3161 anchor over the running count (Section 5), which attests that at time T, N receipts existed under the boundary.¶
This profile binds an AP2 checkout to the post-checkout agent actions a credential broker authorizes, so the actions taken after a payment settles carry the same recomputable, gap-evident record as the authorization decisions in Section 6.3. It reuses the vaara.authorization/v0 evidence record unchanged and adds a join to the AP2 Payment Evidence Frame (PEF, AP2 PR #274):¶
The identity of the checkout is the PEF frame_id, a content address the payment side already computes; the completeness of the actions taken under it is the vaara.authorization/v0 contiguity stream. A per-action hash says an action was recorded; the running count says none inside the AP2 task boundary was dropped. A third party recomputes the frame address, confirms every receipt names that checkout, resolves each evidence binding, verifies each signature, and re-runs the gap check, with only the PEF and the held receipts in hand. See tests/vectors/ap2_v0/_check_independent.py. AP2 can pin from the point the Checkout Receipt ends rather than define a new post-settlement primitive.¶
An implementation conforms to vaara.receipt/v1 if, for every receipt it emits:¶
The committed vectors plus _check_independent.py are the reference conformance suite; running the x402 profile checker and having it exit 0 is a passing run for that profile.¶
The envelope version is the integer "version" field and the vaara.receipt/vN schema id. Additive, backward-compatible changes (new optional fields, new anchor methods, new profiles) do not bump N. A change to the signed-payload field set, the canonicalization, or the signature construction bumps N.¶
The integrity of a receipt rests on the Section 3.1 signature over the JCS-canonical signed payload, not on any timestamp anchor or trust root. A consumer MUST verify that signature against the public key named under "alg" before relying on any field. Because the signed payload excludes "signature" and "timestampAnchors", anchors added after signing cannot alter the signed content; a consumer MUST recompute each anchoredDigest from the signed payload rather than trusting the anchor's stated value.¶
Recomputability depends entirely on canonicalization. A producer and a consumer that disagree on JCS output for the same JSON value will compute different digests; implementations MUST use a conformant JCS ([RFC8785]) implementation and MUST treat any of the three accepted labels as the same algorithm.¶
The argument commitment in the authorization profile (Section 6.3) lets a receipt be published while the raw arguments stay private, but a low-entropy argument set is open to a dictionary attack against the commitment. Producers SHOULD ensure the committed object carries sufficient entropy (for example a per-call nonce) where argument confidentiality matters.¶
An absent refusal is evidence only within a declared coverage boundary (Section 6.3). A reader MUST NOT read a missing receipt as "the action did not happen"; without a coverage block it means only "not observed", and with one it means "not refused within this boundary". The completeness block makes a dropped receipt inside the boundary detectable, but a pure tail truncation is not detectable by sequence contiguity alone and requires a timestamp anchor over the running count to close.¶
This document has no IANA actions. The timestamp anchor method registry (Section 5) and the profile registry (Section 6.1) are maintained by the specification, not by IANA, in this version.¶