Spec: Signed Import Receipt v1
Ce contenu n’est pas encore disponible dans votre langue.
A Signed Import Receipt is the portable proof of one import: which source produced it, every recorded decision along the way (with its actor), the outcome, and a signature that lets a third party verify all of that offline — no Mildport service, no call home, no account.
Schema: import-receipt.v1.schema.json ·
current version: receiptVersion: 1
The two documents
Section titled “The two documents”A receipt travels as an envelope of three members:
{ "receipt": { "receiptVersion": 1, "...": "the plain receipt document" }, "signature": { "protected": "<base64url JWS protected header>", "signature": "<base64url JWS signature>" }, "keyId": "<key id, duplicated from the header for grep-ability>"}The receipt member stays plain, readable JSON — an auditor reads the document; the
signature travels alongside it. The receipt is redaction-safe by construction: source
and deliveries are metadata, and ledger evidence was bounded and redacted when it was
first recorded — a receipt is safe to email by default.
The receipt document
Section titled “The receipt document”The exhaustive shape is the schema; the semantics of the main fields:
| Field | Semantics |
|---|---|
receiptVersion |
Always 1 for this revision. Verifiers check it before anything else. |
receiptId |
rcpt_ + URL-safe identifier — the receipt’s own identity. |
recordId / tenantId |
Which import run, for which tenant. |
source |
Source identity as metadata: detected format, external id, page count, per-sheet dimensions. Names and shapes only — never cell values. |
decisions.questionCounts |
How many questions ended in each state. |
decisions.mapping |
Each answered mapping question: header → column key, which actor answered, when. |
decisions.references |
Each answered reference question: field, chosen target, affected rows, actor, when. |
decisions.ledger |
The full audited question trail — every judgment call as a typed question with its evidence and resolution. The schema carries the complete shape. |
outcome |
submit or discard, the row count, and the deliveries that resulted. |
issuedAt |
When the receipt was issued. |
calibration (optional) |
Identity of the signed calibration artifact that priced this import’s confidences, with its own key id — an auditor can demand and verify it independently. |
certificate (optional) |
A statistical verification certificate — see below. |
transforms, facts, validation, and changesetId are reserved: the schema owns
their names and shapes now so consumers can rely on them later, but they are optional and
may be absent.
The verification certificate
Section titled “The verification certificate”When a stratified sample review completed, the receipt carries a certificate stating the
sample design as part of the claim: per-stratum populations, recorded oversampling
weights, sample sizes and defect counts, the selection seed (so an auditor can regenerate
the exact sampled rows), and the exact Clopper–Pearson one-sided upper bound over the whole
sample.
A conforming renderer never displays this as “verified”. The honest sentence — the only
claim the certificate supports — is: “at most defectRateUpperBound defect rate, at
confidence confidence.”
Canonical JSON
Section titled “Canonical JSON”The signature does not cover the pretty-printed document — it covers the receipt’s canonical form. Any implementation must produce byte-identical canonical JSON:
- Objects: keys sorted lexicographically, recursively. Properties whose value is
undefined(absent) are dropped — matching standardJSON.stringifysemantics. - Arrays: order preserved exactly.
- Scalars: standard JSON rendering (the output is
JSON.stringifyof the sorted structure, no added whitespace). - Rejections, not coercions: non-finite numbers, circular references, and anything JSON cannot represent faithfully (functions, symbols, bigints) are errors. Objects must be plain — a value that would “roughly” serialize (a date object, a map) is rejected rather than silently flattened. A signed artifact is never approximately itself.
The signature
Section titled “The signature”The scheme is a detached JWS (RFC 7515 flattened serialization) with the unencoded payload option (RFC 7797):
- Protected header:
{ "alg": "ES256", "kid": "<key id>", "b64": false, "crit": ["b64"] }. - Payload: the UTF-8 bytes of the receipt’s canonical JSON — not base64url-encoded
(that is what
b64: falsemeans), and not embedded in the envelope; verifiers recompute it from thereceiptmember. - Algorithm: ES256 (ECDSA over P-256, SHA-256), the same algorithm the product uses for
license signing. The JWS signature is the raw 64-byte
R || Sconcatenation, base64url encoded, per RFC 7518. Public keys are exchanged as SPKI PEM.
Verifying a receipt
Section titled “Verifying a receipt”- Parse the envelope; reject unknown
receiptVersion. - Recompute the canonical JSON of the
receiptmember (rules above). - Verify the flattened JWS
{ protected, signature }against the canonical bytes as the unencoded detached payload, accepting onlyalg: ES256. - Compare the header’s
kidwith the envelope’skeyId, and check it identifies a key you trust.
A single flipped byte anywhere in the document fails step 3 — that is the point of recomputing the canonical form instead of trusting an embedded payload.
The public key comes from the operator of the instance that issued the receipt — the same trust anchoring as offline license verification. For receipts issued by Mildport’s managed cloud, the keys are published. Verification never phones home.
Two ready-made implementations of this procedure exist: verify in your browser (zero egress, on that page) or the MIT CLI.
This specification page and the linked schema are licensed CC BY 4.0 — see the spec overview for scope.