Signed receipts
Check a signed receipt
Someone handed you a Cadreen receipt. You can check that it is genuine and unchanged yourself, offline, with no account and without asking us. This page is the whole method.
What is a signed receipt, and how do I get one?
A signed receipt is a copy of one finished record that anyone can check, with no account. It proves that this Cadreen put its name to exactly that record, so a changed word is caught; it doesn’t prove the record itself is true.
To make one, open a finished record under Receipts and press Issue signed receipt. A person in the workspace does it; an API key can’t yet. You can then download the file, and the page shows where to check it. A record that already has one shows it instead of making another. If the page says signed receipts aren’t set up, whoever runs this Cadreen has to add a signing key first.
How do I check one?
- Get the issuer’s keys, and read the receipt strictly. Download
receipt-roots.jsonfrom the issuer’s own address (/.well-known/cadreen/receipt-roots.json), not from whoever sent you the receipt, and trust every key in it, not only the first: when an issuer changes its keys the old ones stay listed (as retired) so receipts issued earlier keep checking. Read the receipt as strict JSON: a repeated name, a decimal number, or text that is not in Unicode’s normal form means it is refused, not tidied up. - Check the key’s certificate. Every receipt carries a certificate in which the issuer’s root key vouches for the workspace key that signed it. Find that root in the file you downloaded, then check the certificate’s signature, that it speaks for the same deployment as the root, that its key id is the key it carries, and that it was valid when the receipt was issued (at most 90 days).
- Check the receipt’s signature. The workspace key signed the line
cadreen.receipt.v1, a newline, then the receipt’sbodywritten in one fixed way (RFC 8785 canonical JSON). If the check passes, nothing in the body changed after it was signed. - Check the body doesn’t contradict itself, and read only the body. A receipt can be signed and still be wrong about itself: say an operation succeeded when its own evidence says it failed. The checks are listed below. The
displaypart of a receipt is not signed and a check never reads it.
{
"format": "cadreen.receipt",
"roots": [
{ "deployment": "iss_cadreen_hosted_1", "root_key_id": "rr1_…", "alg": "Ed25519", "public_key": "…" }
]
}Two ways to do it
In your browser. The checker is one file. Choose the receipt and the keys file and it tells you in a sentence. It makes no network request at all; save the file and it works with no connection.
On the command line, with a second, independent checker. receipt_verify.py is written in Python from the receipt format alone, needs nothing installed beyond Python 3, and passes the same conformance receipts.
curl -O https://<their address>/.well-known/cadreen/receipt-roots.json
python3 receipt_verify.py receipt.json --roots receipt-roots.jsonIs it the receipt for my purchase? If the issuer tied the receipt to your own reference (your purchase order, invoice or customer number), give the checker that reference as well. It accepts only the receipt made for it.
python3 receipt_verify.py receipt.json --roots receipt-roots.json --recipient-reference PO-1042To write your own, the format is in docs/contracts/RECEIPT_FORMAT_V1.md, and the conformance receipts (every kind of valid and refused receipt, each with the one reason it is refused for) are in internal/signedreceipt/testdata/receipt-v1 in the Cadreen repository.
What if a model only judged the work?
When a mission finishes, the model that did the work checks its own result. That is a judgement, not proof. So the receipt for a finished mission says it was accepted but not confirmed, and its line rests on model_judged, which is never stronger than inferred.
One thing changes that. If the mission kept something it made (a file, a result, the output of a step), Cadreen’s own record is enough to say it happened as far as Cadreen recorded it. A summary the model wrote about its own work is not something made, and does not count.
A checker built before this was added refuses such a receipt (basis_not_for_source). Update it from the conformance receipts in the Cadreen repository.
What if there is more than one receipt for the same work?
An issuer can make a new receipt when the evidence changes. The old one is never edited, and it stays a true statement of what was known when it was made. Each audience has its own line of receipts (public, counterparty, auditor, internal): numbered from 1, and each new one says which receipt it replaces. A receipt made for someone else is never the one yours replaces.
Keep the highest number in your own line. If you hold two receipts for the same work and the same audience, the one with the higher number is the newer.
What does a passing check prove?
- That this issuer, through one of its workspaces, signed exactly this content, and that it has not been changed since.
- That the receipt doesn’t contradict itself: each line is no stronger than what it rests on, and the outcome is what the evidence says.
- Not that what it says happened is true. A signature says who said it. A receipt of a deployment that was itself compromised is only as good as that deployment.
- Not that the receipt is about the purchase you have in mind. It carries a pseudonymous reference to the operation, not your invoice number. Compare it with your own records.
- Not that the issuer hasn’t withdrawn the key since, unless you also give the checker a list of withdrawn keys.
Reference for writing a checker
These are the fixed tables a checker needs. They are read from the code that issues receipts, and a test fails if this page and the code differ. The full format, with the exact rules for each check, is the receipt format contract.
Key ids
A workspace key’s id is rk1_ and then the lowercase, unpadded base32 of the first 10 bytes of SHA-256 over cadreen.receipt.key.v1 followed by the 32 bytes of the public key. An issuer root’s id is rr1_ with cadreen.receipt.root.v1. Recompute the id from the key; if it differs, refuse. Keys and signatures are base64url without padding.
What the outcome says
The intended result comes from the level alone. Check that the receipt says what the table says.
| Level | Intended result |
|---|---|
| confirmed, observed | happened |
| failed | did_not_happen |
| cancelled | stopped |
| inferred, unknown | not_known_yet |
What comes next is the first rule that holds, in order. A state has ended when it is completed, completed_with_gaps, completed_with_issues, failed or cancelled (cancelled, completed, completed_with_gaps, completed_with_issues, failed).
| Order | When | What comes next |
|---|---|---|
| 1 | The level is failed or cancelled | nothing_left |
| 2 | The state is waiting_for_human | waiting_for_person |
| 3 | The state is blocked | needs_review |
| 4 | The level is inferred | waiting_for_provider |
| 5 | The state has ended and the level is unknown | needs_review |
| 6 | The state has ended | nothing_left |
| 7 | Anything else, including a state this version doesn't know | waiting_for_provider |
How strong a line may be
A line’s level can be no stronger than the most its basis can support. A failure or a stop can rest on any basis but none.
| Basis | Strongest level |
|---|---|
chain_finality | confirmed |
device_telemetry | confirmed |
provider_signed_notification | confirmed |
provider_status_and_hash | confirmed |
cadreen_record | observed |
local_accounting | observed |
person_attested | observed |
provider_api_response | observed |
caller_report | inferred |
model_judged | inferred |
provider_accepted | inferred |
none | unknown |
What each kind of line can rest on
| Source | Bases it can have |
|---|---|
local_accounting | local_accounting |
operation | cadreen_record, model_judged |
relay | cadreen_record, caller_report, none, person_attested |
routing | cadreen_record, none, person_attested, provider_accepted, provider_api_response |
settlement | cadreen_record, chain_finality, none, provider_accepted, provider_signed_notification, provider_status_and_hash |
stage | cadreen_record, none |
tool_call | cadreen_record, none, person_attested |
What was kept private, and under which class
| Path in the receipt | Class |
|---|---|
^/request/statement$ | request.text |
^/request/constraints/budget$ | request.constraints.budget |
^/request/constraints/deadline$ | request.constraints.deadline |
^/request/constraints/data_residency$ | request.constraints.data_residency |
^/authority/policy/decision$ | authority.policy.decision |
^/authority/policy/policy_hash$ | authority.policy.policy_hash |
^/authority/allowed_by_rule$ | authority.rule_name |
^/authority/routine/id$ | authority.routine.id |
^/authority/admitted_by/role$ | actor.role |
^/authority/admitted_by/label$ | actor.label |
^/decisions/[0-9]+/actor/role$ | actor.role |
^/decisions/[0-9]+/actor/label$ | actor.label |
^/evidence/[0-9]+/provider$ | provider.identity |
A class this list doesn’t include is allowed (newer versions add some) and a checker reports it as not understood rather than refusing.
Why a receipt is refused
A checker gives a stable code for each reason, so two checkers agree even when their words differ. For example: document_unreadable, not_a_receipt_document, signature_mismatch, root_not_trusted, certificate_deployment, certificate_signature, certificate_too_long, outside_certificate_validity, issuer_differs_from_certificate, key_revoked, level_above_basis, basis_not_for_source, outcome_not_the_evidence, outcome_table_mismatch, unknowns_empty, approval_not_approved, withheld_path_present, withheld_class_wrong_for_path. The full list is in the receipt format contract.