Skip to main content

attest: Portable, Offline-Verifiable Digital Purchase Receipts
draft-martinalli-open-purchase-receipts-00

Document Type Active Internet-Draft (individual)
Author Samuele Martinalli
Last updated 2026-08-06
RFC stream (None)
Intended RFC status (None)
Formats
Stream Stream state (No stream defined)
Consensus boilerplate Unknown
RFC Editor Note (None)
IESG IESG state I-D Exists
Telechat date (None)
Responsible AD (None)
Send notices to (None)
draft-martinalli-open-purchase-receipts-00
Individual Submission                                      S. Martinalli
Internet-Draft                                             6 August 2026
Intended status: Informational                                          
Expires: 7 February 2027

     attest: Portable, Offline-Verifiable Digital Purchase Receipts
               draft-martinalli-open-purchase-receipts-00

Abstract

   This document specifies attest, a signed digital purchase-receipt
   envelope that a buyer holds and that any party can verify offline,
   without contacting the issuer or any third-party service.  It defines
   the receipt envelope and payload format, a restricted JSON
   canonicalization profile ("attest-JCS", built on RFC 8785), a pinned
   Ed25519 signature ruleset, an optional hybrid Ed25519+ML-DSA-65 post-
   quantum-resistant signature profile, issuer key and artifact
   manifests with rotation and compromise handling, a layered
   verification algorithm, and revocation-record semantics.  This
   document is a snapshot profile: it distills, and never supersedes,
   the living attest specification maintained in the attest source
   repository.  It normatively specifies exactly the core receipt format
   and the hybrid signature profile; the living specification's
   transparency-log, anchoring, and issuer-mediated transfer material is
   summarized only as non-normative pointers in Section 12 of this
   document.

Status of This Memo

   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 7 February 2027.

Martinalli               Expires 7 February 2027                [Page 1]
Internet-Draft          attest Purchase Receipts             August 2026

Copyright Notice

   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.

Table of Contents

   1.  Introduction  . . . . . . . . . . . . . . . . . . . . . . . .   2
     1.1.  Relationship to the living specification  . . . . . . . .   3
   2.  Conventions and Terminology . . . . . . . . . . . . . . . . .   4
   3.  Receipt Envelope  . . . . . . . . . . . . . . . . . . . . . .   5
     3.1.  Payload field registry  . . . . . . . . . . . . . . . . .   6
     3.2.  Buyer commitment and binding  . . . . . . . . . . . . . .  11
   4.  Canonicalization  . . . . . . . . . . . . . . . . . . . . . .  12
   5.  Signature Profiles  . . . . . . . . . . . . . . . . . . . . .  13
     5.1.  The ed25519 profile (attest_version "0.1")  . . . . . . .  13
     5.2.  The hybrid ed25519+ml-dsa-65 profile (attest_version
           "0.2")  . . . . . . . . . . . . . . . . . . . . . . . . .  14
   6.  Trust Manifests, Rotation, and Continuity . . . . . . . . . .  16
   7.  Verification Algorithm  . . . . . . . . . . . . . . . . . . .  20
   8.  Revocation  . . . . . . . . . . . . . . . . . . . . . . . . .  22
   9.  Security Considerations . . . . . . . . . . . . . . . . . . .  24
   10. Privacy Considerations  . . . . . . . . . . . . . . . . . . .  25
   11. IANA Considerations . . . . . . . . . . . . . . . . . . . . .  26
   12. Extensions  . . . . . . . . . . . . . . . . . . . . . . . . .  26
     12.1.  Transparency logging and anchoring (pointer only)  . . .  27
     12.2.  Issuer-mediated transfer (pointer only)  . . . . . . . .  27
   13. Normative References  . . . . . . . . . . . . . . . . . . . .  27
   14. Informative References  . . . . . . . . . . . . . . . . . . .  28
   Appendix A.  Relationship to Existing Standards . . . . . . . . .  29
   Author's Address  . . . . . . . . . . . . . . . . . . . . . . . .  31

1.  Introduction

   A digital purchase today is ordinarily a revocable license living
   inside one company's platform, not a thing the buyer holds.  When the
   platform shuts down, delists a title, or changes terms, the only
   record that a purchase ever happened typically lived in the seller's
   own database and disappears with it.  Existing consumer-protection
   efforts — disclosure requirements that a digital "purchase" is
   actually a license, and remedies for non-conforming digital content —
   address access and disclosure, but a legal remedy without evidence

Martinalli               Expires 7 February 2027                [Page 2]
Internet-Draft          attest Purchase Receipts             August 2026

   that a purchase occurred is difficult to exercise once the seller's
   records are gone.

   attest defines an evidence layer for exactly that gap: a signed
   receipt envelope the store issues once, at the point of purchase,
   that the buyer keeps like a paper receipt and that anyone — the
   buyer, a marketplace, a friend, a preservation project — can verify
   offline, forever, against the issuer's own published key material.
   attest deliberately does not keep content alive, strip DRM, host or
   index content, process payment, or define a resale/transfer protocol
   on its own account (transfer is addressed only by the non-normative
   pointer in Section 12); it is only the evidence layer that lets
   whatever rights a buyer has survive the seller's disappearance.

   Every implementation's conformance to the material this document
   specifies is exercised by the cross-language conformance vector
   corpus at docs/spec/vectors/ in the attest source repository —
   specifically the vector groups covering the v0.1 baseline and the
   v0.2 hybrid profile (the living specification's own Section 15 and
   Section 6, respectively). docs/conformance.md in that same repository
   documents the public, adapter-based process by which any independent
   implementation, in any language, can run that corpus against its own
   code and self-certify conformance; that process, not this document,
   is the operative conformance procedure.

1.1.  Relationship to the living specification

   attest's normative source of truth is the living specification
   maintained at docs/spec/attest-v0.1.md and docs/spec/attest-v0.2.md
   in the attest source repository ([ATTEST-REPO]), together with the
   JSON Schema and the cross-language conformance vector corpus that
   repository also carries.  This document is a snapshot profile of that
   living specification's core receipt format and hybrid signature
   profile: it distills normative text from the living specification
   into Internet-Draft form, but the living specification remains
   authoritative, and a conflict between this document and the living
   specification is resolved in the living specification's favor.
   Within this document specifically, a payload MUST satisfy the
   structural constraints of the core receipt schema: those constraints
   are precisely the ones expressed by the revision-pinned attest
   receipt JSON Schema ([ATTEST-SCHEMA], attest-receipt.schema.json
   pinned at tag v0.4.0, the mirrored v0.1 revision 5 / v0.2 revision 6
   state), EXCEPT its Stage 3 (v0.2 §17.8) holder-binding conditional on
   license.transferable and buyer.pubkey, which belongs to the Stage 3
   extension (Section 12) and is outside this profile's normative
   surface.  This draft's own payload field registry (Section 3.1) is a
   descriptive, prose summary of that same structure, kept in step with
   it, never a second and independent normative source: the schema

Martinalli               Expires 7 February 2027                [Page 3]
Internet-Draft          attest Purchase Receipts             August 2026

   itself, minus that one named Stage 3 conditional, is the precise
   structural authority a conforming implementation validates against
   (Section 7 Step 5).

   This document mirrors attest-v0.1.md at revision 5.

   This document mirrors attest-v0.2.md at revision 6, for the complete
   core/hybrid surface it normatively incorporates: Section 2 (the
   hybrid Ed25519+ML-DSA-65 signature profile itself), Section 3 and
   Section 3.1 (the hybrid verification algorithm substituted for
   verification steps 1 and 4, and its pinned error-literal table),
   Section 4 (manifest continuity and trust for a hybrid signer),
   Section 6.2 (the structural ceilings that bind hybrid envelopes and
   manifests exactly as they bind v0.1's), and Section 13 and
   Section 13.1 (the hybrid AND-rule extended to revocation records and
   artifact manifests, and the mixed-keyset prohibition and migration
   ceremony); the same document's transparency, anchoring, and issuer-
   mediated transfer material is summarized only as a non-normative
   pointer in Section 12 of this document and remains normative solely
   in the living specification, never here.

   A reader checking this snapshot for drift compares the two revision
   integers declared above against the "Revision log" section at the end
   of each named file in the living repository; because those logs only
   grow, a later revision existing there when this document is read is
   expected, and is not by itself a defect in this document — it means
   the living specification has moved on and this document has not yet
   been updated to match.

2.  Conventions and Terminology

   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.

   This document uses three actor names, taken unchanged from the living
   specification:

   Issuer:  the entity that signs receipts, identified by a DNS domain
      it controls.  A marketplace or merchant-of-record may act as
      issuer on behalf of a named publisher.

   Buyer:  the holder of an exported receipt.

   Verifier:  any software that executes the verification algorithm of
      Section 7 against a receipt envelope.

Martinalli               Expires 7 February 2027                [Page 4]
Internet-Draft          attest Purchase Receipts             August 2026

   Two terminology defusals are stated here explicitly, because each
   collides with a term a reader may already associate with a different
   IETF architecture:

   A "receipt" in this document is a signed purchase-evidence statement
   — the payload-plus-signatures envelope of Section 3 — held and
   presented by the buyer who purchased it.  It is _not_ a SCITT
   transparency-service receipt in the sense of [RFC9943], which is a
   proof that a signed statement was registered in an append-only log.
   This document defines no transparency-log registration mechanism at
   all; the living specification's optional, non-normative-here
   transparency layer is pointed to, but not specified, in Section 12.

   "attest" is this project's name, chosen with no relationship intended
   to the IETF Remote Attestation Procedures (RATS) architecture
   [RFC9334].  This document makes no RATS claim of any kind: it defines
   no Attester, no RATS-sense Verifier, no Relying Party role mapping,
   no Evidence or Attestation Results semantics, and no claim about the
   integrity or trustworthiness of any execution environment.  A reader
   arriving from a RATS background should treat the project name as a
   false cognate.

3.  Receipt Envelope

   A receipt is transmitted as a JSON object with exactly three top-
   level members: payload (the only signed content, see Section 3.1),
   signatures (an array; see Section 5 for its required shape under each
   signature profile), and an OPTIONAL delivery member.

   delivery is _unsigned_: it is outside payload and is not covered by
   any signature.  When present, it MAY carry delivery.salt (the
   base64url, unpadded, encoding of the 16 raw bytes used as the buyer-
   commitment salt, see Section 3.2) and delivery.issuer_manifest (a
   key-manifest object, see Section 6, usable directly as a trust-store
   entry).  An envelope carrying delivery.salt is a private artifact:
   implementations MUST strip it before treating an envelope as
   generally shareable.  Tampering with delivery can neither forge nor
   invalidate a receipt, because it is not itself signed; the salt it
   may carry only has meaning insofar as it reproduces the signed
   buyer.commitment, and any embedded manifest snapshot is independently
   signature-checked on its own terms.

Martinalli               Expires 7 February 2027                [Page 5]
Internet-Draft          attest Purchase Receipts             August 2026

3.1.  Payload field registry

   payload is the sole signed object.  Every object below permits
   additional, unlisted properties; an unrecognized top-level payload
   field is signed and valid, reported only as a warning (Section 7),
   never as a schema error — this is the format's forward-compatibility
   mechanism.

    +================+=================+=============================+
    | Field          | Required        | Semantics                   |
    +================+=================+=============================+
    | attest_version | REQUIRED        | Const "0.1" or "0.2"; fixes |
    |                |                 | the payload shape and       |
    |                |                 | signature profile for this  |
    |                |                 | receipt (see Section 5).    |
    +----------------+-----------------+-----------------------------+
    | receipt_id     | REQUIRED        | A ULID (Crockford base32,   |
    |                |                 | 26 characters, leading      |
    |                |                 | character 0-7).             |
    +----------------+-----------------+-----------------------------+
    | issued_at      | REQUIRED        | UTC timestamp; anchors key- |
    |                |                 | validity and refund_window  |
    |                |                 | revocation checks.          |
    +----------------+-----------------+-----------------------------+
    | supersedes     | schema-optional | ULID or null; informational |
    |                |                 | lineage pointer to a prior  |
    |                |                 | receipt_id.  Never an       |
    |                |                 | implicit revocation of the  |
    |                |                 | superseded receipt.         |
    +----------------+-----------------+-----------------------------+
    | issuer         | REQUIRED        | Object; see below.          |
    +----------------+-----------------+-----------------------------+
    | buyer          | REQUIRED        | Object; see below.          |
    +----------------+-----------------+-----------------------------+
    | work           | REQUIRED        | Object; see below.          |
    +----------------+-----------------+-----------------------------+
    | license        | REQUIRED        | Object; see below.          |
    +----------------+-----------------+-----------------------------+
    | survivability  | REQUIRED        | Object; see below.          |
    +----------------+-----------------+-----------------------------+

                    Table 1: Top-level payload fields

Martinalli               Expires 7 February 2027                [Page 6]
Internet-Draft          attest Purchase Receipts             August 2026

      +=====================+==========+===========================+
      | Field               | Required | Semantics                 |
      +=====================+==========+===========================+
      | issuer.id           | REQUIRED | Lowercase DNS domain (two |
      |                     |          | or more labels); roots    |
      |                     |          | key discovery and issuer- |
      |                     |          | binding (Section 7).      |
      +---------------------+----------+---------------------------+
      | issuer.display_name | REQUIRED | Human-readable name;      |
      |                     |          | carries no cryptographic  |
      |                     |          | weight.                   |
      +---------------------+----------+---------------------------+

                          Table 2: issuer object

    +=======================+==========+==============================+
    | Field                 | Required | Semantics                    |
    +=======================+==========+==============================+
    | buyer.commitment      | REQUIRED | base64url, 32 decoded bytes; |
    |                       |          | a scrypt commitment over a   |
    |                       |          | normalized identifier        |
    |                       |          | (Section 3.2).               |
    +-----------------------+----------+------------------------------+
    | buyer.identifier_type | REQUIRED | Enum issuer-account | email. |
    +-----------------------+----------+------------------------------+
    | buyer.pubkey          | OPTIONAL | base64url, 32 decoded bytes, |
    |                       |          | or null; Ed25519 public key  |
    |                       |          | for the challenge-response   |
    |                       |          | binding path.                |
    +-----------------------+----------+------------------------------+

                           Table 3: buyer object

Martinalli               Expires 7 February 2027                [Page 7]
Internet-Draft          attest Purchase Receipts             August 2026

   +======================+===============+===========================+
   | Field                | Required      | Semantics                 |
   +======================+===============+===========================+
   | work.title           | REQUIRED      |                           |
   +----------------------+---------------+---------------------------+
   | work.publisher       | REQUIRED      | Names the publisher of    |
   |                      |               | record.                   |
   +----------------------+---------------+---------------------------+
   | work.edition         | OPTIONAL      |                           |
   +----------------------+---------------+---------------------------+
   | work.identifiers     | REQUIRED      | Object, one or more       |
   |                      |               | string-valued properties  |
   |                      |               | (issuer-scoped            |
   |                      |               | identifiers).             |
   +----------------------+---------------+---------------------------+
   | work.artifact_series | conditionally | Issuer-scoped series      |
   |                      | required      | identifier; the current   |
   |                      |               | artifact set for a series |
   |                      |               | is resolved from issuer-  |
   |                      |               | signed artifact manifests |
   |                      |               | (Section 6), never from   |
   |                      |               | the immutable receipt.    |
   |                      |               | When license.revocability |
   |                      |               | == "none", at least one   |
   |                      |               | of this field (non-empty) |
   |                      |               | or work.artifacts (non-   |
   |                      |               | empty array) MUST be      |
   |                      |               | present — see the         |
   |                      |               | revocability row below.   |
   +----------------------+---------------+---------------------------+
   | work.artifacts       | conditionally | Array of {role, platform, |
   |                      | required      | filename, size_bytes,     |
   |                      |               | sha256} objects; an at-   |
   |                      |               | purchase snapshot, not a  |
   |                      |               | live index.  Same         |
   |                      |               | revocability: "none"      |
   |                      |               | conditional as            |
   |                      |               | work.artifact_series,     |
   |                      |               | above.                    |
   +----------------------+---------------+---------------------------+

                           Table 4: work object

Martinalli               Expires 7 February 2027                [Page 8]
Internet-Draft          attest Purchase Receipts             August 2026

   +=========================+===============+=========================+
   | Field                   | Required      | Semantics               |
   +=========================+===============+=========================+
   | grant                   | REQUIRED      | Enum perpetual |        |
   |                         |               | subscription.           |
   +-------------------------+---------------+-------------------------+
   | revocability            | REQUIRED      | Enum none |             |
   |                         |               | refund_window |         |
   |                         |               | policy; governs         |
   |                         |               | revocation-record       |
   |                         |               | effectiveness           |
   |                         |               | (Section 8).  When      |
   |                         |               | revocability ==         |
   |                         |               | "none", the schema      |
   |                         |               | additionally REQUIRES   |
   |                         |               | drm == "drm-free",      |
   |                         |               | redownload_right ==     |
   |                         |               | true under              |
   |                         |               | survivability, and at   |
   |                         |               | least one of a non-     |
   |                         |               | empty                   |
   |                         |               | work.artifact_series    |
   |                         |               | or a non-empty          |
   |                         |               | work.artifacts array;   |
   |                         |               | a revocability: "none"  |
   |                         |               | payload violating any   |
   |                         |               | of these is schema-     |
   |                         |               | invalid, never merely   |
   |                         |               | a warning.              |
   +-------------------------+---------------+-------------------------+
   | revocation_window_days  | conditionally | REQUIRED iff            |
   |                         | required      | revocability ==         |
   |                         |               | "refund_window".        |
   |                         |               | Integer, 1 to 3650;     |
   |                         |               | the window is anchored  |
   |                         |               | to issued_at and        |
   |                         |               | evaluated against a     |
   |                         |               | revocation record's     |
   |                         |               | own signed time, never  |
   |                         |               | the verifier's clock.   |
   +-------------------------+---------------+-------------------------+
   | transferable            | REQUIRED      | Boolean.  This          |
   |                         |               | document defines no     |
   |                         |               | meaning for this        |
   |                         |               | field: it is reserved,  |
   |                         |               | exactly as in the       |
   |                         |               | living specification's  |
   |                         |               | own core profile.       |

Martinalli               Expires 7 February 2027                [Page 9]
Internet-Draft          attest Purchase Receipts             August 2026

   |                         |               | Meaning is assigned to  |
   |                         |               | it only by the living   |
   |                         |               | specification's Stage   |
   |                         |               | 3 transfer profile,     |
   |                         |               | which is out of this    |
   |                         |               | document's normative    |
   |                         |               | scope (Section 12).     |
   +-------------------------+---------------+-------------------------+
   | not_transferable_before | OPTIONAL      | ISO-8601 UTC string.    |
   |                         |               | Reserved in the same    |
   |                         |               | sense as transferable;  |
   |                         |               | carries no meaning      |
   |                         |               | under the material      |
   |                         |               | this document           |
   |                         |               | specifies alone.        |
   +-------------------------+---------------+-------------------------+
   | drm                     | REQUIRED      | Enum drm-free | drm-    |
   |                         |               | bound; a drm-bound      |
   |                         |               | receipt otherwise       |
   |                         |               | verifies, with a        |
   |                         |               | mandatory warning —     |
   |                         |               | except under            |
   |                         |               | revocability: "none"    |
   |                         |               | (above), where drm-     |
   |                         |               | bound is schema-        |
   |                         |               | invalid, not merely     |
   |                         |               | warned.                 |
   +-------------------------+---------------+-------------------------+
   | terms_uri               | REQUIRED      | URI; annotation-only    |
   |                         |               | in schema terms, see    |
   |                         |               | Section 4.              |
   +-------------------------+---------------+-------------------------+
   | legal_text_sha256       | REQUIRED      | Lowercase hex SHA-256   |
   |                         |               | of the license text at  |
   |                         |               | terms_uri, hash-        |
   |                         |               | binding it into the     |
   |                         |               | signed payload.         |
   +-------------------------+---------------+-------------------------+
   | jurisdiction_flags      | OPTIONAL      | Object, boolean-        |
   |                         |               | valued, open            |
   |                         |               | vocabulary.             |
   +-------------------------+---------------+-------------------------+

                          Table 5: license object

Martinalli               Expires 7 February 2027               [Page 10]
Internet-Draft          attest Purchase Receipts             August 2026

      +=======================+==========+=========================+
      | Field                 | Required | Semantics               |
      +=======================+==========+=========================+
      | redownload_right      | REQUIRED | Boolean.                |
      +-----------------------+----------+-------------------------+
      | mirror_policy_uri     | OPTIONAL |                         |
      +-----------------------+----------+-------------------------+
      | mirror_policy_sha256  | OPTIONAL | Hash-binds the mirror   |
      |                       |          | policy text into the    |
      |                       |          | signed payload.         |
      +-----------------------+----------+-------------------------+
      | end_of_life           | REQUIRED | Non-empty string, open  |
      |                       |          | versioned vocabulary;   |
      |                       |          | an unrecognized value   |
      |                       |          | is valid, with warning. |
      +-----------------------+----------+-------------------------+
      | eol_commitment_uri    | OPTIONAL | String or null.         |
      +-----------------------+----------+-------------------------+
      | eol_commitment_sha256 | OPTIONAL | Hash-binds a future     |
      |                       |          | end-of-life commitment  |
      |                       |          | document.               |
      +-----------------------+----------+-------------------------+

                      Table 6: survivability object

3.2.  Buyer commitment and binding

   Two layered mechanisms bind a receipt to its buyer without signing a
   plaintext identifier.

   *Commitment (always present).* With domain label the ASCII string
   "Attest-buyer-commitment-v1":

   P = UTF8("Attest-buyer-commitment-v1") || 0x00
       || UTF8(identifier_type) || 0x00 || UTF8(normalize(identifier))
   commitment = scrypt(P, salt, N=32768, r=8, p=1, dkLen=32)

   salt MUST be exactly 16 raw bytes, generated per receipt by the
   issuer, hashed as raw bytes (never as base64url text).  The scrypt
   parameters above are fixed by this profile and MUST NOT be
   configurable per issuer.  normalize() is: for identifier_type ==
   "email", strip ASCII whitespace from both ends, apply Unicode NFC
   normalization, then lowercase ASCII A-Z only (byte-deterministic, no
   locale case-folding); for identifier_type == "issuer-account", apply
   Unicode NFC normalization only.  In both cases the normalized string
   MUST NOT contain the byte 0x00.

Martinalli               Expires 7 February 2027               [Page 11]
Internet-Draft          attest Purchase Receipts             August 2026

   Revealing (identifier, salt) to a verifier is a replayable bearer
   proof that also hands over the identifier itself; per-receipt salts
   confine this to one receipt's commitment path.  A verifier MUST treat
   a disclosed identifier as personal data not to be retained beyond the
   verification.

   *Key binding (buyer.pubkey, optional).* The stronger path: an Ed25519
   public key bound into the signed payload, proven via non-replayable
   challenge-response, with domain label the ASCII string "Attest-
   binding-challenge-v1":

   verifier sends nonce (>= 16 random bytes,
                         freshly generated per challenge)
   buyer signs: UTF8("Attest-binding-challenge-v1") || 0x00
                || receipt_id || 0x00 || nonce

   The nonce MUST be at least 16 bytes and MUST be freshly generated per
   challenge: without that freshness requirement, a captured transcript
   could be replayed and the challenge-response would not be the non-
   replayable proof this binding path depends on.  receipt_id here is
   the receipt's own payload.receipt_id, encoded as UTF-8 text, not
   decoded and re-encoded.  A verifier MUST NOT treat buyer.pubkey
   equality across two receipts as proof of buyer identity; keys SHOULD
   be per-receipt.

4.  Canonicalization

   The signature input for a receipt is exactly JCS(payload) — the
   output of the attest-JCS profile below — encoded as UTF-8 bytes.
   attest-JCS follows [RFC8785] (JSON Canonicalization Scheme) with one
   deliberate deviation by restriction: full JCS permits any I-JSON
   number, canonicalized via a rounding algorithm every implementation
   must reproduce identically to stay interoperable. attest-JCS removes
   that cross-language interoperability risk entirely by restricting
   numbers to integers only, with magnitude strictly less than 2^53.  A
   conforming canonicalizer MUST accept a JSON number if and only if it
   is such an integer, and MUST reject any float, any NaN/Infinity/-
   Infinity construct, and any integer at or beyond that magnitude.
   This is a restriction of, never an incompatible extension to,
   [RFC8785]: every attest-JCS output is also a valid JCS output.

   Additional parse-time requirements, applied before any signature or
   schema step: the input MUST be valid UTF-8; a JSON object containing
   a duplicate member name MUST be rejected outright, never silently
   deduplicated; object keys MUST be serialized in the order produced by
   sorting their UTF-16BE code-unit sequences; and lone UTF-16
   surrogates, whether arriving as literal bytes or as \uXXXX escapes,
   MUST be rejected.  An over-magnitude integer is rejected at this

Martinalli               Expires 7 February 2027               [Page 12]
Internet-Draft          attest Purchase Receipts             August 2026

   canonicalization stage, before schema validation ever runs: the
   corresponding verification result reports signature: "invalid" and
   schema: "not_checked" (Section 7), never a schema-validation failure.

   Encodings: signatures, commitments, salts, and public keys MUST be
   base64url without padding ([RFC4648] Section 5); SHA-256 hashes MUST
   be lowercase hexadecimal; receipt_id and supersedes are ULIDs
   (Crockford base32, 26 characters).  format: "uri" fields in the JSON
   Schema are annotation-only: a conforming validator is not required to
   assert URI well-formedness as a failure; integrity of a referenced
   document is guaranteed by its accompanying SHA-256 hash binding,
   never by URI syntax.

5.  Signature Profiles

5.1.  The ed25519 profile (attest_version "0.1")

   signatures MUST be a JSON array with exactly one entry, carrying kid
   (string), sig (base64url, 64 decoded bytes), and alg equal to the
   literal string "Ed25519".  A verifier MUST reject any other alg
   value, and MUST NOT use alg to select a verification primitive: the
   algorithm for attest_version: "0.1" is fixed by this profile, never
   by dispatching on the signature block.

   Verification uses [RFC8032] Edwards-Curve Digital Signature Algorithm
   (EdDSA) under a pinned ruleset.  A conforming verifier MUST perform
   cofactorless (strict) [RFC8032] verification and additionally MUST:
   reject a signature whose scalar S is non-canonical (S >= L, the
   Ed25519 group order — the SUF-CMA property); and reject small-order
   or non-canonical encodings of the public key and of the signature's R
   component (the SBS property).  This pinned ruleset exists so that
   implementations built on different cryptographic backends disagree
   loudly, at conformance-test time, rather than silently accepting a
   malleable signature in the field.

   A receipt hash, SHA-256(JCS(payload)), is defined for bundle and
   deduplication purposes; it MUST NOT be computed over the whole
   envelope, which contains the unsigned, malleable delivery member.
   This hash is distinct from — and MUST NOT be confused with — the
   transparency-log commitment the living specification's Stage 2
   profile defines; that construction is out of this document's
   normative scope (Section 12).

Martinalli               Expires 7 February 2027               [Page 13]
Internet-Draft          attest Purchase Receipts             August 2026

5.2.  The hybrid ed25519+ml-dsa-65 profile (attest_version "0.2")

   The hybrid profile pairs the classical Ed25519 primitive with ML-
   DSA-65 [FIPS204], a NIST security category 3 module-lattice signature
   scheme, so that forging a receipt requires breaking _both_
   primitives: an attacker who breaks only Ed25519 (for example, via a
   future cryptographically-relevant quantum computer) or only ML-DSA-65
   (for example, via a classical cryptanalytic advance) still cannot
   forge a signature.

   A hybrid envelope has the same three-member shape as Section 3; the
   differences are inside payload.attest_version (which MUST equal the
   literal string "0.2") and signatures, which MUST be a JSON array
   containing exactly two entries, in this fixed order: entry 0 with alg
   == "Ed25519", entry 1 with alg == "ML-DSA-65".  A verifier MUST
   reject any other order, count, or alg value.  Both entries MUST carry
   the same kid — the hybrid pair is one signer, not two independently
   resolved keys — and both signatures MUST be computed over the
   identical JCS(payload) canonical bytes: one signature input, signed
   twice with two different keys.

   *Composite key binding lives in the manifest.* Because kid itself
   carries no algorithm information, the binding between a hybrid
   signer's two public keys is established entirely by the key manifest
   (Section 6): a key-entry object carrying pub_ml_dsa_65 (base64url,
   1952 decoded bytes) is what makes that entry hybrid, and a verifier
   MUST NOT accept a hybrid signature against an entry lacking it.  A
   manifest's own manifest_signature gains an optional sig_ml_dsa_65
   member (base64url, 3309 decoded bytes), REQUIRED if and only if the
   signing key's own entry is hybrid.  This is AND-verified, fail-closed
   in both directions: a hybrid signer's manifest signature missing its
   ML-DSA-65 leg MUST be treated as invalid (a downgrade attempt), and
   an Ed25519-only signer's manifest signature carrying a stray ML-
   DSA-65 leg MUST likewise be treated as invalid.  The same AND-rule
   extends, unchanged in shape, to revocation records (Section 8) and
   artifact manifests (Section 6): a hybrid-keyed issuer's side-document
   carrying only an Ed25519 signature is never honored.

Martinalli               Expires 7 February 2027               [Page 14]
Internet-Draft          attest Purchase Receipts             August 2026

   *Mixed-keyset prohibition.* An issuer that declares the hybrid
   profile MUST NOT hold an Ed25519-only key in state active: doing so
   would silently downgrade the issuer's claimed hybrid protection back
   to classical-only for any receipt an attacker chooses to forge under
   the still-active classical-only sibling.  Migration from an
   Ed25519-only key to a hybrid key MUST retire (or otherwise move out
   of active) every Ed25519-only key in the same manifest-version step
   that introduces the hybrid key; there is no sanctioned intermediate
   coexistence state.  A verifier resolving a manifest exhibiting the
   mixed-keyset condition for a hybrid receipt it is verifying MUST emit
   the exact warning literal mixed_keyset_active_ed_only_sibling.

   Verification substitutes an eleven-step hybrid check for steps 1 and
   4 of Section 7 whenever payload.attest_version == "0.2": signature-
   count, block shape, algorithm-and-order, shared-kid, and type checks
   on the signature block; the issuer-binding and key-status checks of
   Section 7, unchanged; a check that the resolved key entry carries
   pub_ml_dsa_65; then independent verification of the Ed25519 leg and
   the ML-DSA-65 leg.  This is AND semantics: both legs MUST
   independently verify, or the receipt is invalid.  Only if both legs
   verify does verification continue to the schema, revocation, and
   binding steps of Section 7, unchanged.  The result vocabulary and the
   definition of ok (Section 7) are unchanged by this profile: it
   introduces no new result value, only new ways to arrive at signature:
   "invalid".  A conforming implementation SHOULD surface the nine
   pinned hybrid error literals of the living specification's v0.2 §3.1
   verbatim (or a superset containing them), so that cross-
   implementation conformance testing can match on literal text; Table 7
   transcribes that table verbatim.

Martinalli               Expires 7 February 2027               [Page 15]
Internet-Draft          attest Purchase Receipts             August 2026

     +==========================+====================================+
     | Literal (verbatim)       | Emitted when                       |
     +==========================+====================================+
     | hybrid envelope requires | signatures length ≠ 2.             |
     | exactly two signatures   |                                    |
     +--------------------------+------------------------------------+
     | malformed signature      | either signature entry is not an   |
     | block                    | object.                            |
     +--------------------------+------------------------------------+
     | hybrid envelope requires | entry 0/1 alg is not exactly       |
     | algs Ed25519 and ML-     | ["Ed25519", "ML-DSA-65"] in that   |
     | DSA-65 in order          | order (includes a duplicated alg). |
     +--------------------------+------------------------------------+
     | hybrid envelope          | the two entries' kid values        |
     | signatures must share a  | differ.                            |
     | single kid               |                                    |
     +--------------------------+------------------------------------+
     | malformed signature      | the shared kid is not a string.    |
     | block: 'kid' must be a   |                                    |
     | string                   |                                    |
     +--------------------------+------------------------------------+
     | malformed signature      | either signature entry's sig is    |
     | block: 'sig' must be a   | not a string.                      |
     | string                   |                                    |
     +--------------------------+------------------------------------+
     | key entry for kid        | the resolved manifest key entry    |
     | {kid!r} has no ML-DSA-65 | lacks pub_ml_dsa_65.               |
     | public key               |                                    |
     +--------------------------+------------------------------------+
     | signature verification   | the Ed25519 leg fails to verify    |
     | failed                   | (unchanged literal from v0.1).     |
     +--------------------------+------------------------------------+
     | ML-DSA-65 signature      | the ML-DSA-65 leg fails to verify. |
     | verification failed      |                                    |
     +--------------------------+------------------------------------+

           Table 7: Hybrid error-literal table (verbatim, living
                          specification v0.2 §3.1)

6.  Trust Manifests, Rotation, and Continuity

   An issuer's identity is its DNS domain.  An issuer SHOULD publish its
   key manifest at https://<issuer.id>/.well-known/attest.json.

Martinalli               Expires 7 February 2027               [Page 16]
Internet-Draft          attest Purchase Receipts             August 2026

    +====================+==========+=================================+
    | Field              | Required | Semantics                       |
    +====================+==========+=================================+
    | issuer             | REQUIRED | DNS domain; MUST equal the      |
    |                    |          | domain prefix of every listed   |
    |                    |          | kid.                            |
    +--------------------+----------+---------------------------------+
    | manifest_version   | REQUIRED | Integer, monotonically          |
    |                    |          | increasing per issuer; rotation |
    |                    |          | continuity keys off N -> N+1.   |
    +--------------------+----------+---------------------------------+
    | issued_at          | REQUIRED |                                 |
    +--------------------+----------+---------------------------------+
    | keys               | REQUIRED | Array of key-entry objects; see |
    |                    |          | below.                          |
    +--------------------+----------+---------------------------------+
    | manifest_signature | REQUIRED | Ed25519 signature (plus, for a  |
    |                    |          | hybrid signer, an sig_ml_dsa_65 |
    |                    |          | member, Section 5.2) over       |
    |                    |          | JCS(manifest) with this member  |
    |                    |          | removed.                        |
    +--------------------+----------+---------------------------------+

                        Table 8: Key manifest fields

Martinalli               Expires 7 February 2027               [Page 17]
Internet-Draft          attest Purchase Receipts             August 2026

    +===============+==========+======================================+
    | Field         | Required | Semantics                            |
    +===============+==========+======================================+
    | kid           | REQUIRED | String, <issuer-                     |
    |               |          | domain>/keys/<label>#<name>; domain  |
    |               |          | prefix MUST equal issuer.            |
    +---------------+----------+--------------------------------------+
    | pub           | REQUIRED | base64url, 32 decoded bytes; Ed25519 |
    |               |          | public key.                          |
    +---------------+----------+--------------------------------------+
    | pub_ml_dsa_65 | required | base64url, 1952 decoded bytes,       |
    |               | for a    | Section 5.2.                         |
    |               | hybrid   |                                      |
    |               | signer's |                                      |
    |               | entry    |                                      |
    +---------------+----------+--------------------------------------+
    | valid_from    | REQUIRED |                                      |
    +---------------+----------+--------------------------------------+
    | valid_to      | OPTIONAL | Absent or null means open-ended.     |
    +---------------+----------+--------------------------------------+
    | status        | REQUIRED | Enum active | retired | compromised. |
    +---------------+----------+--------------------------------------+

                         Table 9: Key-entry object

   *Rotation continuity is normative, not best-effort.* A manifest at
   manifest_version N+1 is auto-trusted only if it was signed by a key
   that was active in the version-N manifest already trusted; version
   gaps are bridgeable only by validating every intermediate manifest.
   On a discontinuous rotation, or on conflicting manifests for the same
   issuer, a verifier MUST report trust: "unverified_rotation" and MUST
   NOT auto-accept the manifest.  For a hybrid signer, this continuity
   check is enforced through the hybrid manifest signature
   (Section 5.2): a rotation manifest whose signer key is hybrid but
   whose manifest signature has been downgraded to Ed25519-only fails
   the AND-verified check and is therefore not validly signed for
   continuity purposes, even though the receipt's own hybrid signature
   may independently verify cleanly against the manifest in use.

   *Key compromise fails closed.* A key marked compromised invalidates
   all signatures ever made with it, regardless of issued_at, because
   issued_at lives inside the signed payload and is controlled by
   whoever holds the key.  A verifier MUST reject any receipt signature
   resolving to a compromised key unconditionally.  The same fail-closed
   rule governs revocation records (Section 8): one signed by a key that
   is not status == "active" in its resolving manifest MUST be ignored,
   with a warning.

Martinalli               Expires 7 February 2027               [Page 18]
Internet-Draft          attest Purchase Receipts             August 2026

   *Artifact manifests* are separate signed side-documents, same signing
   discipline as key manifests, carrying issuer, series, version, an
   optional (required on manifests produced after this profile's
   currency amendment) manifest_version, released_at, the current
   artifacts array (Section 3.1 shape), and a manifest_signature.

   *Acceptance is conditional on authentication, not on being merely
   issuer-signed.* A verifier MUST treat an artifact manifest as
   authenticated only if: its resolving key manifest is itself self-
   consistent; the manifest's signing key resolves, in that key
   manifest, to a key-entry with status == "active"; released_at falls
   within that key's validity window; the artifact manifest's own issuer
   field equals the resolving key manifest's issuer field; and the
   signature verifies (for a hybrid signer, under the AND-rule of
   Section 5.2).  An unauthenticated artifact manifest contributes
   nothing to currency and MUST be ignored, with a warning, never
   treated as the newest-seen state on the strength of being merely
   issuer-signed.  Two distinct, differently-content manifests presented
   at the same (issuer, artifact_series, manifest_version) tuple are an
   equivocation and MUST be rejected, not silently accepted as if one
   superseded the other.

   Currency is scoped per (issuer, artifact_series) pair: a verifier
   holding persistent trust state MUST NOT accept, for that pair, a
   manifest with a lower manifest_version than the newest already
   accepted; on regression it MUST report trust: "unverified_rotation",
   the identical value the key- manifest rotation rule above uses.  A
   manifest lacking manifest_version entirely (a legacy manifest) has no
   currency ordering to violate and is never rejected on these grounds,
   only warned.

   *Offline verification and trust bootstrapping.* Offline verification
   works from a local trust store of key manifests.  A manifest obtained
   from the issuer's own domain over TLS is the root of trust: a
   verifier that resolved a manifest this way MUST report trust:
   "verified" (absent a discontinuous rotation).  A manifest that
   arrived by any other path — for example, embedded in an export
   bundle, never independently fetched over TLS — is unauthenticated
   trust-on-first-use and MUST be reported as trust:
   "unauthenticated_tofu", never silently upgraded.

Martinalli               Expires 7 February 2027               [Page 19]
Internet-Draft          attest Purchase Receipts             August 2026

7.  Verification Algorithm

   A verifier executes verify(envelope, trust_store,
   revocation_view=None, disclosure=None), returning a layered
   VerificationResult.  Steps are executed in order; a step that rejects
   the input short-circuits the remaining steps, and the result's
   revocation and binding components take their safe stub values
   ("unknown" and "not_checked") whenever not reached.

   The living specification numbers these steps 0 through 7; that
   numbering is preserved below as explicit labels rather than by list
   auto-numbering, since step 0 is not step 1.

      *Step 0 (preconditions):* parse the input once per Section 4 (UTF-
      8, attest-JCS-conformant, no duplicate keys).  Every later step
      operates on this single parsed object, never on the raw
      transmitted bytes.

      *Step 1 (envelope well-formedness):* attest_version is a version
      this verifier supports; the signature block shape matches the
      selected profile (Section 5).

      *Step 2 (issuer binding):* resolve the signing key only from the
      trust store's manifest for payload.issuer.id; both the kid's
      domain prefix and the resolved manifest's own issuer field MUST
      equal it.

      *Step 3 (key checks):* the key MUST be present in the resolved
      manifest and MUST NOT be "compromised"; issued_at MUST fall within
      its validity window.  A "retired" key still verifies, with a
      warning.

      *Step 4 (signature verification):* per the selected profile
      (Section 5).

      *Step 5 (schema validation):* the parsed payload MUST satisfy the
      structural constraints of the core receipt schema
      ([ATTEST-SCHEMA], attest-receipt.schema.json revision-pinned at
      tag v0.4.0), EXCEPT its Stage 3 (v0.2 §17.8) holder-binding
      conditional on license.transferable and buyer.pubkey, which
      belongs to the Stage 3 extension (Section 12) and is OUTSIDE this
      profile's normative scope — a verifier conforming to this
      document's core/hybrid profile alone is not required to enforce
      it.  This document's own payload field registry (Section 3.1) is a
      descriptive summary of that same structure, not a second,
      independent normative source: the schema, minus that one named
      conditional, is the precise structural authority this step checks
      against.

Martinalli               Expires 7 February 2027               [Page 20]
Internet-Draft          attest Purchase Receipts             August 2026

      *Step 6 (revocation):* only if revocation_view is supplied, and
      only if signature and schema both succeeded; see Section 8.

      *Step 7 (binding):* only if disclosure is supplied, and only if
      signature and schema both succeeded; see Section 3.2.

           +============+======================================+
           | Component  | Allowed values                       |
           +============+======================================+
           | signature  | valid | invalid                      |
           +------------+--------------------------------------+
           | schema     | valid | invalid | not_checked        |
           +------------+--------------------------------------+
           | revocation | unknown | not_revoked_as_of:<T> |    |
           |            | revoked | invalid_revocation_ignored |
           +------------+--------------------------------------+
           | binding    | proven | not_proven | not_checked    |
           +------------+--------------------------------------+
           | trust      | verified | unauthenticated_tofu |    |
           |            | unverified_rotation                  |
           +------------+--------------------------------------+

                        Table 10: Result vocabulary

   trust is resolved as early as possible and reported at its best-
   available value even when a later step rejects the receipt; a
   verifier MUST NOT silently reset it on later failure. ok is defined
   as: signature == "valid" _and_ schema == "valid" _and_ revocation !=
   "revoked" _and_ the result carries no errors.
   invalid_revocation_ignored, unknown, and any not_revoked_as_of:<T>
   value do _not_ affect ok.

   An unrecognized top-level payload field is allowed and signed, but
   MUST be reported as a warning, never an error.  A conforming verifier
   MUST also warn on: a signing key resolved as "retired"; a "drm-bound"
   license; an end_of_life value outside the seed vocabulary; a
   revocation record that matched this receipt but failed
   authentication, or that was ignored because revocability == "none",
   or that authenticated but fell outside a refund_window.  Offline
   verifiers with no revocation_view report revocation: "unknown"
   honestly rather than failing the whole receipt closed.

   *Structural ceilings.* A verifier MUST bound the resource a hostile
   envelope or manifest can force it to spend before any cryptographic
   or schema work runs, and these ceilings bind every attest_version
   this document defines, including the hybrid envelopes and manifests
   of Section 5.2.  The revocation-view record ceiling (Section 8) is a
   distinct bound and is NOT one of these structural ceilings: it

Martinalli               Expires 7 February 2027               [Page 21]
Internet-Draft          attest Purchase Receipts             August 2026

   applies only at Step 6, to the untrusted revocation_view input, never
   before cryptographic or schema work runs, and is evaluated
   independently of the table below.

     +=====================+==============+==========================+
     | Ceiling             | Value        | Class                    |
     +=====================+==============+==========================+
     | Raw envelope size   | 1,048,576    | New; MUST accept within, |
     |                     | bytes (2^20) | MAY reject beyond        |
     +---------------------+--------------+--------------------------+
     | Parsed envelope     | 256          | Pre-existing parser      |
     | tree nesting depth  |              | bound; unconditional     |
     +---------------------+--------------+--------------------------+
     | Issuer key manifest | 256 entries  | New; MUST accept within, |
     | keys[] length       |              | MAY reject beyond        |
     +---------------------+--------------+--------------------------+
     | Artifact manifest   | 4,096        | New; MUST accept within, |
     | artifacts[] length  | entries      | MAY reject beyond        |
     +---------------------+--------------+--------------------------+

                       Table 11: Structural ceilings

8.  Revocation

   A revocation record is a minimal, issuer-signed side-document:
   receipt_id (the receipt it refers to), status (only the literal value
   "revoked" carries revocation meaning under the material this document
   specifies alone), revoked_at (the record's own signed time — window
   checks are evaluated against this, never the verifier's local clock),
   and signature (Ed25519, plus for a hybrid-keyed issuer an
   sig_ml_dsa_65 leg per the AND-rule of Section 5.2, over JCS(record)
   with this member removed).

   *Authentication.* A verifier MUST treat a revocation record as
   authenticated only if: its resolving key manifest is itself self-
   consistent; its signing key resolves to a key-entry with status ==
   "active" — a compromised or retired key's signature MUST be rejected
   exactly as it would be on a receipt; revoked_at falls within that
   key's validity window; and the signature verifies.  A verifier MUST
   fail closed (treat as unauthenticated) on any malformed, wrong-typed,
   or missing input, rather than raising.  An unauthenticated record
   that nonetheless matches this receipt's receipt_id MUST be ignored,
   with a warning, never honored.

Martinalli               Expires 7 February 2027               [Page 22]
Internet-Draft          attest Purchase Receipts             August 2026

   +======================+============================================+
   | license.revocability | Effect of an authenticated, matching       |
   |                      | record                                     |
   +======================+============================================+
   | none                 | Ignored: the record is itself treated      |
   |                      | as invalid, revocation:                    |
   |                      | "invalid_revocation_ignored", a            |
   |                      | warning is emitted, ok is unaffected.      |
   |                      | This is the irrevocability guarantee:      |
   |                      | without it, the revocation mechanism       |
   |                      | would falsify every revocability:          |
   |                      | "none" receipt's own claim.                |
   +----------------------+--------------------------------------------+
   | refund_window        | Honored only if revoked_at falls at        |
   |                      | or before issued_at +                      |
   |                      | revocation_window_days: revocation:        |
   |                      | "revoked", ok becomes false.  A            |
   |                      | record that matches and authenticates      |
   |                      | but falls outside the window is            |
   |                      | ignored, with a warning.                   |
   +----------------------+--------------------------------------------+
   | policy               | Honored as-is: revocation: "revoked",      |
   |                      | ok becomes false.  A verifier cannot       |
   |                      | itself evaluate the referenced policy      |
   |                      | terms, so a correctly signed record        |
   |                      | is trusted.                                |
   +----------------------+--------------------------------------------+

                       Table 12: Revocation-by-class

   *Freshness anchor.* T, in not_revoked_as_of:<T>, MUST be computed as
   the maximum revoked_at across all authenticated records the verifier
   consulted in the supplied revocation view, regardless of which
   receipt_id they target — it describes how current the verifier's
   authenticated feed is, restricted to authenticated records so that a
   forged far-future revoked_at cannot inflate the reported freshness.
   With zero authenticated records the result MUST be the bare literal
   unknown.

   *Revocation-view ceiling.* A verifier MUST bound the number of
   records it will evaluate from an untrusted revocation view to 10,000.
   An oversized view is never truncated and never raises; it fails
   closed for revocable receipts (an untrusted view too large to
   evaluate cannot rule out a revocation, so it MUST NOT certify the
   receipt: this is recorded as an error, ok becomes false), while for
   revocability: "none" receipts an oversized view is a non-fatal
   warning instead, since a revocation record can never affect ok for
   that class regardless of view size.  This bound exists independently

Martinalli               Expires 7 February 2027               [Page 23]
Internet-Draft          attest Purchase Receipts             August 2026

   of the Structural ceilings of Section 7: it is a per-call record-
   count cap on trusted-input-shaped-as-untrusted data (the
   revocation_view parameter), evaluated at Step 6, never a wire-format
   or manifest-shape structural bound checked before cryptographic or
   schema work runs.

9.  Security Considerations

   This section distills the security-relevant properties of the
   material specified above; the maintained, living threat model
   (docs/spec/attest-threat-model.md in the attest source repository) is
   the normative-in-intent, continuously updated companion analysis and
   takes precedence over this summary.

   *Offline verification is trust-on-first-use unless a TLS root is
   established.* A verifier that has never fetched an issuer's key
   manifest over TLS from that issuer's own domain reports trust:
   "unauthenticated_tofu", never "verified", and this document defines
   no registry or alternate root of trust that upgrades that value; a
   relying party that requires stronger provenance than TOFU must obtain
   the manifest over TLS itself.

   *Key compromise and rotation continuity are fail-closed.* A key
   marked compromised invalidates every signature ever made with it, and
   a discontinuous manifest rotation is reported, never silently auto-
   accepted (Section 6).  Both properties depend on the verifier
   actually holding, or being able to validate, a continuous manifest
   chain; a verifier that bootstraps trust from an isolated manifest
   with no chain cannot detect a rollback to an earlier, since-
   compromised state on its own.

   *Canonicalization strictness is deliberate.* attest-JCS's duplicate-
   member rejection, integer-only number restriction, and lone-surrogate
   rejection (Section 4) exist so that two independent implementations
   parsing the same bytes always compute the identical signature input;
   a canonicalizer bug is a silent signature mismatch, not a loud parse
   error, which is why cross-language parity on the conformance corpus
   (Section 1) is load-bearing for this property rather than merely a
   testing convenience.

   *Signature malleability is closed by a pinned ruleset, not by
   trusting a library's defaults.* The Ed25519 verification rules of
   Section 5.1 (non-canonical scalar rejection, small-order/non-
   canonical point rejection) exist because different cryptographic
   backends do not agree on malleability handling by default;
   implementations built on different backends are expected to disagree
   loudly at conformance-test time rather than silently accepting a
   malleable signature in the field.

Martinalli               Expires 7 February 2027               [Page 24]
Internet-Draft          attest Purchase Receipts             August 2026

   *The hybrid profile's post-quantum rationale is explicit and
   bounded.* Section 5.2 defends against an attacker who can break
   exactly one of its two primitives, whether that break is a future
   cryptographically-relevant quantum computer against Ed25519 or a
   classical cryptanalytic advance against ML-DSA-65; it makes no claim
   about an attacker capable of breaking both simultaneously, and the
   mixed-keyset prohibition (Section 5.2) exists because an issuer that
   adopts the hybrid profile but leaves an Ed25519-only key active would
   otherwise silently forfeit that guarantee for any receipt forged
   under the still-active classical-only sibling.

   *A signature establishes what was signed, never why.* Nothing in the
   material this document specifies distinguishes a voluntary signature
   from a coerced one: a revocation record produced under compulsion
   authenticates identically to one produced voluntarily.  This is a
   permanent boundary of what a signature scheme can prove, not a gap
   this document proposes to close.

   *Structural ceilings bound resource exhaustion before cryptographic
   work runs.* The ceilings of Section 7 (envelope size, manifest array
   lengths, parse-tree nesting depth) exist so that a hostile envelope
   or manifest cannot force a verifier to spend unbounded resources
   parsing or evaluating it before any signature or schema check has a
   chance to reject it outright.  The revocation-view record ceiling
   (Section 8) bounds a distinct, later-evaluated input — the untrusted
   revocation_view supplied at Step 6, only once signature and schema
   have both already succeeded — and exists independently of the pre-
   crypto ceilings above, for the same resource-exhaustion reason
   applied to that later, separately-supplied input.

10.  Privacy Considerations

   This section distills the privacy-relevant properties of the material
   specified above; the maintained, living privacy considerations
   document (docs/spec/attest-privacy.md in the attest source
   repository) is the continuously updated companion analysis, using the
   RFC 6973 threat vocabulary, and takes precedence over this summary.

   *The buyer object's specified fields carry no plaintext identifier.*
   The buyer object's specified properties are commitment,
   identifier_type, and pubkey (Section 3.1); none of the three is a
   plaintext identifier — commitment is a salted commitment, pubkey a
   pseudonymous public key, and identifier_type a non-personal enum
   (issuer-account or email).  This is a bounded property, scoped to
   that specified set, not a guarantee about the buyer object as a
   whole: Section 3.1 permits every payload object, buyer included, to
   carry additional, unlisted properties, and a verifier running
   Section 7 MUST accept a nested unrecognized buyer member as signed

Martinalli               Expires 7 February 2027               [Page 25]
Internet-Draft          attest Purchase Receipts             August 2026

   and valid, with no mandated warning.  Such a member is outside this
   guarantee and MAY carry a plaintext identifier.  Restricted to the
   specified field set, a verifier running Section 7 without a
   disclosure never sees a plaintext identifier there, and binding is
   not itself a component of ok.  The per-receipt salt required by
   Section 3.2 means the same identifier commits to an unrelated value
   in each receipt, confining a leaked salt's exposure to that one
   receipt's commitment path.

   *buyer.pubkey is pseudonymous, not an identity credential.* It is
   optional, defaults to null for client-less flows, and a verifier MUST
   NOT treat its equality across two receipts as proof of buyer identity
   (Section 3.2); this is a property this document requires of a
   conforming verifier's own verdict, not a property the wire format
   itself enforces against a party that chooses to reuse a key.

   *Per-receipt salting is a commitment-path control, not a general
   unlinkability guarantee.* Two receipts issued to the same buyer carry
   unrelated commitment values, and that is the entire effect of the
   salting requirement.  Every other stable value in a pair of receipts
   — a reused buyer.pubkey, a non-null supersedes pointer, an account
   handle placed in work.identifiers, a shared kid, or a per-buyer
   terms_uri — remains exactly what it was, and a party holding two such
   receipts can still join them by any of these means.  This document
   does not claim, and implementers MUST NOT represent, unlinkability
   across a buyer's whole receipt set as a property the format provides;
   what it provides is that the commitment values themselves do not
   supply the join.

   Pseudonymity toward third parties is not anonymity toward the issuer:
   the issuer itself computes the commitment from an identifier it holds
   and generates the salt, so, for as long as it retains or can derive
   that material, it can recompute and recognize the corresponding
   commitments.  Neither this profile nor the living specification
   requires or forbids that retention.

11.  IANA Considerations

   This document has no IANA actions.

12.  Extensions

   This section is entirely _non-normative_. It points at material the
   living specification defines and normatively governs; nothing stated
   in this section imposes a requirement, and neither of the two
   profiles it summarizes is part of this document's own conformance
   surface (Section 1).  A conforming implementation of this document
   alone implements neither.

Martinalli               Expires 7 February 2027               [Page 26]
Internet-Draft          attest Purchase Receipts             August 2026

12.1.  Transparency logging and anchoring (pointer only)

   The living specification (docs/spec/attest-v0.2.md, Sections 7
   through 16) defines an additive transparency layer: a static,
   mirrorable append-only log substrate documented as a subset of the
   C2SP tlog-tiles and tlog-checkpoint conventions, built on an RFC
   6962-style Merkle tree; hybrid Ed25519+ML-DSA-65-signed checkpoints;
   an OpenTimestamps-based anchoring mechanism that proves a checkpoint
   existed no later than a fixed, externally verifiable point in time;
   and three purely informational VerificationResult components —
   transparency, corroboration, and manifest_freshness — that a verifier
   implementing that layer may additionally populate.  The living
   specification states, as a central and load-bearing property, that
   this layer never upgrades trust and, with two narrowly scoped
   exceptions it defines precisely, never changes signature, schema,
   revocation, binding, or ok for any receipt.  None of this is
   specified normatively by the present document; a reader who needs the
   exact mechanism is directed to the living specification.

12.2.  Issuer-mediated transfer (pointer only)

   The living specification (docs/spec/attest-v0.2.md, Section 17)
   defines an issuer-mediated protocol that gives the reserved
   license.transferable field (Section 3.1) its first assigned meaning:
   an issuer-signed transfer record, logged in the transparency layer of
   Section 12.1, that extinguishes an old receipt via a new reachable
   value of the revocation component (revocation: "transferred") and
   issues a new receipt to an incoming holder, with a separate chain-of-
   title audit surface for tracing a receipt's transfer history.  This
   profile is layered on top of the transparency layer of Section 12.1
   and requires it.  None of this is specified normatively by the
   present document, including the one new reachable revocation value it
   introduces; a reader who needs the exact mechanism, its consent gate,
   or its chain-of-title diagnostics is directed to the living
   specification.

13.  Normative References

   [RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
              Requirement Levels", BCP 14, RFC 2119,
              DOI 10.17487/RFC2119, March 1997,
              <https://www.rfc-editor.org/info/rfc2119>.

   [RFC8174]  Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
              2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
              May 2017, <https://www.rfc-editor.org/info/rfc8174>.

Martinalli               Expires 7 February 2027               [Page 27]
Internet-Draft          attest Purchase Receipts             August 2026

   [RFC8785]  Rundgren, A., Jordan, B., and S. Erdtman, "JSON
              Canonicalization Scheme (JCS)", RFC 8785,
              DOI 10.17487/RFC8785, June 2020,
              <https://www.rfc-editor.org/info/rfc8785>.

   [RFC8032]  Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital
              Signature Algorithm (EdDSA)", RFC 8032,
              DOI 10.17487/RFC8032, January 2017,
              <https://www.rfc-editor.org/info/rfc8032>.

   [RFC4648]  Josefsson, S., "The Base16, Base32, and Base64 Data
              Encodings", RFC 4648, DOI 10.17487/RFC4648, October 2006,
              <https://www.rfc-editor.org/info/rfc4648>.

   [FIPS204]  National Institute of Standards and Technology, "Module-
              Lattice-Based Digital Signature Standard", FIPS 204,
              August 2024, <https://csrc.nist.gov/pubs/fips/204/final>.

   [ATTEST-SCHEMA]
              Martinalli, S., "attest receipt JSON Schema (attest-
              receipt.schema.json), revision-pinned", 2026,
              <https://github.com/bernalli/attest/blob/v0.4.0/docs/spec/
              schema/attest-receipt.schema.json>.  The machine-readable
              structural authority this profile's Step 5 (Section 7)
              validates against, pinned at tag v0.4.0 — the released
              specification state carrying the mirrored v0.1 revision 5
              / v0.2 revision 6 (Section 1.1).  The tag is an immutable
              target, so a later change to the repository's working
              schema cannot silently alter this document's normative
              payload constraints.  Its Stage 3 (v0.2 §17.8) holder-
              binding conditional is outside this profile's normative
              surface (see Step 5).

14.  Informative References

   [RFC9334]  Birkholz, H., Thaler, D., Richardson, M., Smith, N., and
              W. Pan, "Remote ATtestation procedureS (RATS)
              Architecture", RFC 9334, DOI 10.17487/RFC9334, January
              2023, <https://www.rfc-editor.org/info/rfc9334>.

   [RFC9943]  IETF SCITT Working Group, "An Architecture for Trustworthy
              and Transparent Digital Supply Chains", RFC 9943, June
              2026, <https://www.rfc-editor.org/info/rfc9943>.  Cited in
              this document for its architectural definition of a SCITT
              "receipt" as inclusion evidence, distinguished in
              Section 2 and Appendix A from attest's own, unrelated use
              of the word "receipt."

Martinalli               Expires 7 February 2027               [Page 28]
Internet-Draft          attest Purchase Receipts             August 2026

   [RFC7515]  Jones, M., Bradley, J., and N. Sakimura, "JSON Web
              Signature (JWS)", RFC 7515, DOI 10.17487/RFC7515, May
              2015, <https://www.rfc-editor.org/info/rfc7515>.

   [RFC9052]  Schaad, J., "CBOR Object Signing and Encryption (COSE):
              Structures and Process", RFC 9052, DOI 10.17487/RFC9052,
              August 2022, <https://www.rfc-editor.org/info/rfc9052>.

   [W3C.VC-DATA-MODEL]
              World Wide Web Consortium (W3C), "Verifiable Credentials
              Data Model v2.0", 2025,
              <https://www.w3.org/TR/vc-data-model-2.0/>.

   [C2PA]     Coalition for Content Provenance and Authenticity (C2PA),
              "C2PA Technical Specification", 2025,
              <https://c2pa.org/specifications/specifications/2.2/specs/
              C2PA_Specification.html>.

   [ATTEST-REPO]
              Martinalli, S., "attest: Portable, Offline-Verifiable
              Digital Purchase Receipts (source repository)", 2026,
              <https://github.com/bernalli/attest>.  The living,
              normative specification (docs/spec/attest-v0.1.md,
              docs/spec/attest-v0.2.md), the JSON Schema (referenced
              normatively and revision-pinned as [ATTEST-SCHEMA]), the
              standards-relationship annex (docs/spec/attest-standards-
              relationship.md), the maintained threat model and privacy
              considerations, and the cross-language conformance vector
              corpus this document distills all live in this repository.
              This document mirrors specific, declared revisions of it;
              see Section 1.1.

Appendix A.  Relationship to Existing Standards

   This appendix is a compressed distillation of a companion document in
   the attest source repository, docs/spec/attest-standards-
   relationship.md ([ATTEST-REPO]), which is the canonical source for
   this material and states each boundary at full length, including what
   a future bridge to a given standard could look like.  This appendix
   is necessarily incomplete relative to that document and defers to it
   on any point of detail.

   *W3C Verifiable Credentials [W3C.VC-DATA-MODEL].* The VC Data Model
   describes an open-world claim shape secured by a plurality of
   possible proof suites. attest's payload has a fixed required core
   with a single mandatory canonicalization profile (Section 4), made
   cross-language-safe by a shared conformance corpus rather than by
   proof-suite negotiation; the two models trade in opposite directions

Martinalli               Expires 7 February 2027               [Page 29]
Internet-Draft          attest Purchase Receipts             August 2026

   on purpose.  An attest envelope could in principle be carried inside
   a credential's subject as an opaque, independently verifiable object;
   neither specification depends on the other.

   *eIDAS 2.0 and the EUDI Wallet.* eIDAS 2.0 attests to identity and
   regulated attributes through a wallet-mediated, legally tiered
   apparatus (qualified and public-sector electronic attestations of
   attributes). attest attests to a merchant's grant of a license,
   verified entirely offline with no wallet, attestation provider, or
   Member State supervision anywhere in its path.  The two frameworks
   attest to different kinds of fact for different kinds of relying
   party.

   *JOSE/JWS [RFC7515] and COSE [RFC9052].* Both formats sign the
   producer's own serialized payload bytes as transmitted (or a
   deterministic re-encoding derived from them), which is exactly what
   makes detached content a first-class feature of each. attest inverts
   that relationship: its signature input is the canonical re-derivation
   of the parsed payload (Section 4), recomputable from a parsed JSON
   object in any language with no side channel and no preserved wire
   form to keep — at the cost of requiring every implementation to
   canonicalize identically, which is exactly the cross-language risk
   the conformance corpus (Section 1) exists to make checkable rather
   than merely asserted.

   *RFC 8785 (JCS).* attest-JCS (Section 4) is a restriction of, not an
   alternative to, [RFC8785]: every attest-JCS output is also a valid
   RFC 8785 output.  What attest-JCS adds — the integer-only number
   restriction, duplicate- member rejection, and lone-surrogate
   rejection — narrows the accepted input set within RFC 8785's own
   envelope; it never departs from it.

   *C2PA [C2PA].* A C2PA manifest answers what an asset is and how it
   was produced. attest answers a different, adjacent question about the
   same asset: that a license to hold or use a copy of it was granted,
   by whom, to whom, and under what terms.  A C2PA-signed asset and an
   attest receipt for the same underlying artifact are not in tension
   and can coexist without any new mechanism in either specification.

Martinalli               Expires 7 February 2027               [Page 30]
Internet-Draft          attest Purchase Receipts             August 2026

   *SCITT and RFC 9943.* A SCITT "receipt" is a transparency service's
   proof that a signed statement was registered — evidence of inclusion
   in an append-only log.  An attest "receipt" is the signed purchase
   statement itself (Section 2).  The two protocols use one word for two
   different things.  Where the living specification's Stage 2 log
   substrate (Section 12.1) genuinely does touch SCITT's territory — an
   append-only, Merkle-tree-based registration and inclusion-proof
   substrate — that overlap is real: attest's log applies the same class
   of transparency-log machinery to a narrower, purchase-evidence-
   specific evidence model, keeping the log strictly corroborative and
   never authoritative over a receipt's own signature-based validity.

   *RATS (RFC 9334).* Restated from Section 2: "attest" is a project
   name chosen with no relationship to the RATS architecture, and this
   document's protocol makes no RATS claim — no Attester, Verifier, or
   Relying Party role mapping in the RATS sense, and no Evidence,
   Attestation Results, or execution-environment endorsement semantics.

Author's Address

   Samuele Martinalli
   Email: bernalli@proton.me

Martinalli               Expires 7 February 2027               [Page 31]