Content-blindness attestation
This describes a proof boundary, not a guarantee that the request body contains no PII. It is not encryption or zero-knowledge. The rest of this page separates what the client declares, what the gateway checks, and what a receipt cannot prove.
The content-blindness guarantee
The client and gateway perform separate steps:
- Supported clients attempt on-device tokenisation. On those paths, values the detector matches are replaced with a surrogate before egress. Detection is pattern- and dictionary-bound, so this is a recall-bounded claim, not an absolute one: structured identifiers (email, phone, national ID numbers, cards, IBAN, IP addresses, dates) are the strong measured lane, while free-form names outside the on-device dictionary can be missed. The gateway does not verify that a detector ran or that a particular client binary performed the replacement. See Detection coverage for the known limits and How privacy works for the client-side flow.
- The gateway checks a client proof, not the body for PII. It verifies a tenant-bound MAC over the submitted bytes and checks declared engine metadata. These checks do not show that an engine ran or that every identifier was replaced. The inference body is relayed as submitted, subject to route and content-part validation.
- The re-identification map stays on your device. The token→value map that turns surrogates back into real values is held only on the client. The gateway does not receive that map, so it cannot re-identify from it.
What is checked — and what is not
Provable properties (content-blindness):
- Client-side detector output — a client implementation can replace values it matches before egress. Detection is bounded by recall (see Detection coverage); unmatched values can remain. The gateway does not independently prove detector output.
- Inference proof integrity — the gateway checks the MAC over the exact body bytes and declared engine metadata. It does not verify tokenisation or PII detection.
- Map-never-leaves-device — the token→value map is held only on the client.
- Declared engine on the allowlist — the receipt records whether the engine
hash the client declared is on the published
/engine/hashesallowlist. This is a check on the declaration, not on the binary: the gateway never sees the engine, and the allowlist is public.
Do not read the guarantee as any of these (they would be false):
- ❌ “encrypted so we can’t read it” / “zero-knowledge.” The gateway forwards tokenised text that the upstream model must read. The surrogates are readable; they carry a surrogate in place of every value the on-device detector matched. This is content-blindness, not ciphertext hidden from the relay.
- ❌ “we can’t see the tokenised text.” We can see the tokenised text — we relay it. What stays on your device is the values the on-device tokeniser replaced — those stay on your device.
- ❌ “detection is perfect, so nothing identifying can ever reach us.” Anything the on-device detector does not match is relayed verbatim. The strength of the privacy claim is the strength of detection — see Detection coverage, and pre-tokenise known sensitive fields at the application layer for high-assurance use.
How the relay receipt is built
A completed relay produces a signed OpenReceipt envelope (a stream the client abandons before the first frame produces none, and if signing or persisting the receipt fails the relay is logged and no receipt is issued). The signature is computed over the canonical bytes of the envelope after the receipt is chain-linked, so the signature always covers exactly what is persisted (see the OpenReceipt protocol for the envelope and chain semantics). The top-level envelope carries:
| Field | Meaning |
|---|---|
v, alg | Envelope version and signature algorithm (ES256 / ecdsa-p256-sha256). |
jti | Unique receipt id. |
kid | Which signing key to fetch from the public JWKS to check sig. |
iss, sub, iat | Issuer, subject (tenant), issued-at. |
chain | { chain_id, seq, prev_hash } — the envelope signature binds its claimed chain position and previous-envelope hash. A standalone receipt does not prove complete chain history or that a later receipt was not omitted. |
claims | The attestation payload (below). |
sig | The ECDSA (P-256) signature over the canonical envelope bytes. |
The content-blindness claims, field by field
When a request is relayed as client-pretokenised (the content-blind path), the gateway adds these claims — these are the exact fields it emits, and each one proves a specific thing:
| Field | Value / type | What it proves |
|---|---|---|
pretokenised | true | The request was marked as client-pretokenised. This is caller metadata, not proof that a tokeniser ran or that PII was removed. |
tokenisation_mode | "client_pretokenised" | Names the content-blind relay mode explicitly (vs. any server-side mode). |
genuine_engine | true | false | Set by the gateway: true only when the client-declared engine hash is non-empty and present on the gateway’s own official-hash allowlist. A missing or unlisted hash gets false, whatever the client says about itself. It does not prove which engine ran: the gateway never sees the engine, and the allowlist is public, so a client can declare a listed hash without running that build. |
engine_version | string (present when supplied) | Client-declared engine ruleset version; it does not establish which engine ran. |
engine_binary_hash | string (present when supplied) | Client-declared engine hash; the gateway can check whether that hash is listed, not whether that binary ran. |
On some client-side surfaces the client also signs a richer v3 client
attestation before egress, carrying no_raw_pii_egress, tokenised_body_hash,
detector_categories, engine_ruleset_version, engine_binary_hash,
genuine_engine, and token_binding_hmac. The signature binds the reported
fields to the signed bytes; it does not prove detector completeness or that an
untrusted caller used the declared engine. The gateway adds its own receipt
fields for provider, model, and upstream byte counts.
See OpenReceipt protocol for both envelopes.
Verify it yourself — the independent recipe
None of the steps below need an account or trust in our servers. Every input is public and content-addressed, so you reproduce the trust anchors from source.
1. Fetch the public JWKS — the signing keys:
curl -s https://api.cloakapi.io/api/.well-known/cloakapi-receipt-pubkeys.jwks | jqPick the key whose kid matches the receipt’s kid (see Public JWKS).
2. Check the ECDSA signature. Verify sig (ECDSA P-256 / ES256) over the
receipt’s canonical bytes using that public key. If it fails, the receipt was
tampered with.
3. Check the engine hash against the public allowlist:
curl -s https://api.cloakapi.io/api/v1/engine/hashes | jq# { "published": true, "algorithm": "sha256",# "engine_binary_hashes": […,"fa837ad1…","028b5f7b…","651a5cb2…"],# "ruleset_version": "1.6.0", ... }The receipt’s engine_binary_hash must appear in engine_binary_hashes. If it
does, the client declared a published open build; that is a statement about the
declared hash, not proof of which binary ran. An unknown hash is reported
(genuine_engine=false), never silently trusted.
4. Run a verifier client-side — no account, no server:
- The in-browser verifier at app.cloakapi.io/receipt-verifier — paste the receipt JSON, no login (see Verifying a receipt).
- Or verify entirely on your own machine — the in-browser verifier at
signedreceipts.org/verifier (reference-library
source distribution is in preparation at
signedreceipts.org/source), or
checkGenuineEnginefrom@cloakapi/conformance/checks(it is not exported from the package root). Pass the fetched/engine/hashesdocument and the receipt.result.statusis"pass"only when the declared hash is on the allowlist, not withdrawn, andgenuine_engine === true. An empty allowlist or a missing hash is"unknown". An unlisted or withdrawn hash is"fail":
import { checkGenuineEngine } from '@cloakapi/conformance/checks';const engineHashes = await (await fetch('https://api.cloakapi.io/api/v1/engine/hashes')).json();const result = checkGenuineEngine([receipt], engineHashes);// result.status — "pass" | "fail" | "unknown"5. Reproduce the engine build → same hash. Because every allowlist entry is the sha-256 of a published, open artifact, the allowlist is not a trust anchor you take on faith — you rebuild it and confirm it matches what your client loaded:
# Browser WASM engine (should equal the fa837ad1… entry):sha256sum pkg/cloakapi_tokeniser_wasm_bg.wasm# Ruleset digest (should equal the c64c9f37… entry):sha256sum shared/pii-patterns/patterns.jsonSee Reproducibility for the full build-from-source and provenance story.