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:///.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, /keys/