SignedReceipt Specification v3
Normative specification for the SignedReceipt signed privacy-attestation envelope. Client-signed before egress, optionally gateway-countersigned. JSON Canonical Form (RFC 8785), ECDSA P-256, chain linking, trust tiers, self-contained offline verification. Legacy v1/v2 envelopes continue to verify.
1. Overview
A SignedReceipt is a tamper-evident JSON document that cryptographically proves three things: (a) what data was processed on the issuing device before egress; (b) what processing occurred (tokenization level, detector categories, engine hashes); and (c) that the document has not been altered. From v3 the receipt is signed by the client — the desktop app, browser extension, or in-browser engine that actually performed the tokenization — with a locally generated ECDSA P-256 key, before any byte leaves the device. A gateway in the request path verifies the client signature and adds an additive countersignature witnessing the relay facts it alone can see (provider, model, upstream byte hashes). Receipts are chained so that the full history of a session can be verified without any central authority.
This specification is normative. Implementations that deviate from it are non-conformant
regardless of what label they carry. The conformance test suite at
signedreceipts.org/conformance is the executable form of this specification.
Legacy v1 (alg: "ES512") and v2 (integer v: 2, alg: "ES256")
envelopes are deprecated for new producers but MUST continue to verify indefinitely
(audit-retention requirement). Verifiers dispatch on the envelope's v and
alg claims; the transition is gap-free.
2. Envelope format
Receipts are UTF-8 JSON with LF line endings and no BOM. The v3 envelope is a claims-based
object: all attestation content lives inside claims, and a v3 receipt is marked by
v: "v3" together with the presence of claims["v3-attestation"]. Example:
{
"v": "v3",
"alg": "ecdsa-p256-sha256",
"kid": "device-key-01JZQ…",
"iss": "https://api.cloakapi.io",
"sub": "req_01JZQ4M7Q8E2YK7X9V3B6N1T0F",
"iat": 1778089598,
"jti": "<UUID v7>",
"chain": {
"chain_id": "<UUID v7>",
"seq": 0,
"prev_hash": null
},
"claims": {
"v3-attestation": {
"tier": "beacon-anchored",
"no_raw_pii_egress": true,
"self_check": { "fired": true, "extra_tokenised": 0 },
"tokenised_body_hash": "<sha256 hex of the exact bytes that egressed>",
"detector_categories": ["EMAIL", "CREDIT_CARD"],
"engine_ruleset_version": "v1.3.0",
"engine_binary_hash": "<sha256 hex>",
"genuine_engine": true,
"privacy_tier": "balanced",
"token_binding_hmac": "<hmac-sha256 hex>",
"attester_public_key": "04<X><Y>",
"client_destination": "chatgpt.com",
"anchoring": { "inclusion_state": "anchored", "transparency_log_leaf": "<hex>" }
}
},
"sig": "<raw r||s, base64url, 86 chars, no padding>"
} Post-canonicalization (RFC 8785) key order. Core envelope fields:
| Field | Type | Description |
|---|---|---|
v | string | Envelope version. "v3" for the current wire format. Legacy: integer 2 (v2), "1"/1 (v1). |
alg | string | Signature algorithm. "ecdsa-p256-sha256" (ECDSA P-256 with SHA-256). ML-DSA-65 reserved for a post-quantum successor. |
kid | string | Key identifier. Client device/extension key, resolved via the embedded attester public key (see §6) or the issuer JWKS. |
iss | string (URL) | Issuer base URL. MUST NOT embed PII. |
sub | string | Opaque request/session identifier. MUST NOT embed PII. |
iat | integer | Issued-at Unix epoch seconds. nbf/exp optional. |
jti | string (UUID v7) | Unique receipt identifier. |
chain | object | Chain-linking metadata. See §5. |
claims | object | v3-attestation (§3), optional routing_decision/countersignature (§4a), registered v1.1-* sub-tags and vendor x-* extensions (see the extension registry). |
sig | string (base64url) | Signature over the canonical envelope with sig removed. See §4. |
3. The v3-attestation claim — integrity principle
The claims["v3-attestation"] object records only what the signing client
can cryptographically or architecturally prove. A property that depends on a third party's
unverifiable internal behaviour is never attested: notably, a "no-training" claim is excluded —
a client can prove it set a provider's no-train flag, not that the provider honoured it.
Gateway-witnessed relay facts (provider, model, upstream hashes) live in
claims.routing_decision and claims.countersignature; a blind client
MUST NOT self-attest them.
Required fields: tier, no_raw_pii_egress, tokenised_body_hash,
detector_categories (categories only, never values), engine_ruleset_version,
engine_binary_hash, genuine_engine, privacy_tier,
token_binding_hmac, anchoring. Optional: self_check,
attester_public_key, client_destination, compliance_pack.
Field-by-field semantics are in the extension registry.
No plaintext PII, prompts, or completions appear in any field — only hashes, counts, category
identifiers, and HMAC commitments.
4. Canonicalization and signing
Before signing and before verification, the envelope MUST be serialized to JSON Canonical
Form per RFC 8785 (JCS). The sig field is removed before computing the
canonical bytes. Any implementation that produces non-canonical output is non-conformant.
The conformance fixture corpus ships byte-stable expected_canonical.bin files
against which implementations are tested.
v3 (alg: "ecdsa-p256-sha256"): ECDSA over P-256 with SHA-256 per
FIPS 186-5. The sig value is the raw r || s byte string (64 bytes)
encoded as url-safe base64 without padding (RFC 4648 §5). DER encoding is rejected.
Deterministic nonces per RFC 6979 are RECOMMENDED for byte-stable fixtures.
Legacy: v2 uses alg: "ES256" over a flat gateway-signed envelope
(JCS); v1 uses alg: "ES512" with an explicit signed_fields list and
PHP ksort canonicalization. Verifiers MUST keep accepting both.
Future reservation: ML-DSA-65 (Dilithium) is reserved for a post-quantum algorithm identifier. Dual-signing is permitted during migration windows.
Keys MUST be generated on a FIPS-validated module for FIPS-certified deployments.
4a. Trust tiers and gateway countersignature
A v3 receipt earns exactly one of three tiers, recorded in claims["v3-attestation"].tier:
| Tier | Meaning |
|---|---|
gateway-countersigned | The request went through a gateway, which verified the client signature, countersigned the relay facts it witnessed, and anchored the receipt hash. Strongest. |
beacon-anchored | Off-gateway: client-signed locally, then a content-free beacon recorded the receipt hash + token counts in the public transparency log (existence, never content). |
local-only | Fully offline: client-signed into a local per-device chain; anchoring pending, drains on reconnect. |
The countersignature is additive — the client envelope is returned untouched. The gateway signs a
witness object { v: "v3-countersign", kid, receipt_hash, timestamp, hash_upstream_response?, request_id?, provider?, model?, input_tokens?, output_tokens?, signature, signed_fields }
where receipt_hash is the SHA-256 of the JCS canonical bytes of the full signed client
envelope (including sig) — the same value anchored in the transparency log. A gateway
MUST verify the client envelope's signature before countersigning.
5. Chain linking
Each receipt's chain object carries (post-canonicalization key order):
| Field | Description |
|---|---|
chain_id | UUID v7 unique to the session/device chain. Immutable. |
seq | Monotonic integer starting at 0. MUST increment by exactly 1 per receipt. |
prev_hash | SHA-256 of the previous receipt's canonical bytes including its sig. null for the first receipt. |
chain_parent | Optional. When a request crosses a trust boundary (client → gateway), a child chain references the parent receipt's jti. |
A chain is valid iff every prev_hash matches its predecessor, seq
is gapless, and every signature verifies. Forked chains (two receipts with the same
seq and chain_id) MUST be rejected.
6. Key discovery and self-contained verification
v3 (self-contained): a v3 receipt MAY embed the signer's public key as
claims["v3-attestation"].attester_public_key (SEC1 uncompressed P-256 point,
04||X||Y, 130 hex chars — exactly the browser's
crypto.subtle.exportKey('raw', …) output). Verification then requires no network
round-trip: canonicalize the envelope minus sig, SHA-256, verify ECDSA P-256
against the embedded key. The key is inside the signed envelope, so it is tamper-evident; its
authenticity is separately cross-checkable against the transparency log or an out-of-band
device pairing.
Gateway keys: issuers publish current and retired countersigning keys as a JWKS at
<iss>/.well-known/cloakapi-receipt-pubkeys.jwks — the recommended,
canonical discovery endpoint (CloakAPI serves it at
https://api.cloakapi.io/api/.well-known/cloakapi-receipt-pubkeys.jwks). Each
JWK carries its kid so a receipt's kid resolves to the correct
verification key; unknown kids MUST be rejected. A PEM variant is published at
<iss>/.well-known/cloakapi-receipt-pubkey.pem for legacy clients.
Transport: HTTPS only. Verifiers MAY pin a local trust bundle via
--trust-bundle <path>.