signed-receipts/v1 › conformance
Implement signed-receipts/v1 in 5 minutes
Golden vectors and a one-command runner for the A2A receipt format. Everything here is static, served from councilof.ai, Apache-2.0. No GitHub account, no sign-up, no key.
PASS means your verifier's result for a case matches the expected result in
vectors.json. It is not a certification, an endorsement or a conformity mark,
and nobody here issues one.
The steps
- Read the receipt object and the verification rule in the specification
(SPEC.md, draft 0.2). A receipt is JSON; its bytes are RFC 8785 (JCS);
content_id is the SHA-256 of the receipt minus content_id and signature; the
Ed25519 signature covers the receipt minus signature.
- Download
vectors.json. Each case gives a receipt, a
did_documents map standing in for DID resolution (a DID missing from the map cannot be resolved), and the
expected result.
- Run your verifier over every case, resolving DIDs only through that map, and write what it returns:
{ "implementation": "my-verifier 0.1",
"results": { "valid-publickeyhex": "VALID", "unresolvable-key": "UNVERIFIABLE_KEY", … } }
- Check it. One line, Node 20 or later, no dependencies:
curl -sO https://councilof.ai/spec/signed-receipts/v1/conformance/run.mjs && node run.mjs my-results.json
Or with Python and the cryptography package:
curl -sO https://councilof.ai/spec/signed-receipts/v1/conformance/run.py && python3 run.py my-results.json
Each prints PASS or FAIL per case and exits 0 only when every core case matches. Both read the published
vectors.json by default, so no other file is needed; add --vectors <path> to use a local copy.
The three results
| Result | When |
VALID | content_id recomputes; the Ed25519 signature verifies under
signature.signer_public_key; and the DID in signature.kid resolves to a document that
lists exactly that key, not revoked. |
INVALID | Malformed; content_id mismatch; bad signature; or the DID document
resolved and does not list the key, or marks it "revoked": true. Integrity is checked first, so a
tampered receipt is INVALID even when its key is also unresolvable. |
UNVERIFIABLE_KEY | Integrity holds against the key the receipt carries, but the DID could
not be resolved (no resolver, unsupported method, network failure). Whose key it is, is unknown.
Never report this as VALID. |
The third result exists because of a defect of ours. Until 28 September 2026 our reference verifier returned
VALID when it could not resolve the signing key, so a receipt signed with anyone's key passed. IETF SCITT architecture
issue #462 cites it as the
failure case. The reference interceptor.py now returns
UNVERIFIABLE_KEY there, and the vectors hold every implementation, ours included, to it.
What the vectors cover
17 core Ed25519 cases: valid (key as publicKeyHex, publicKeyMultibase and
publicKeyJwk); an RFC 8785 edge case (UTF-16 key order, non-ASCII text, 0.5,
1e+30); tampered payload, with and without a recomputed content_id; tampered signature;
different-key forgery; three unresolvable-key cases; tampered and unresolvable; a key that only matches as a substring;
a revoked key; two wrong canonicalisations (insertion key order, and ", "/": " separators); and
a missing signature.
4 interop cases for the a2a-receipt-ml-dsa-65 profile, ML-DSA-65 (FIPS 204) over the same receipt
object. The receipts, keys and signatures are copied unmodified from
@fractalai/pqc-agent-receipts-conformance
0.3.1 (FractalAI, Apache-2.0); the expected results map that suite's own four checks onto the three codes. They are
optional: a candidate that leaves them out gets SKIP, not FAIL. The runners here have no ML-DSA-65, so those cases were
re-run with that package's own verifier (all four pass).
Not covered: time validity (draft 0.2 defines issued_at only, no validity window, so there are no
expiry cases), and issuer binding (draft 0.2 does not require issuer to equal the DID in
kid). One erratum in draft 0.2: its change notes say astral characters are escaped as surrogate pairs.
RFC 8785 emits them as UTF-8, the reference now does too (corrected 28 September 2026), and case
valid-jcs-edge tests it.
Files
- vectors.json: the cases, result definitions and the published test keys.
- run.mjs: the Node runner (WebCrypto Ed25519).
node run.mjs --self checks the
published vectors with its own verifier (python3 run.py --self does the same).
- run.py: the same runner in Python (
cryptography only).
- reference-results.json: our reference's results. All 21 match.
- example-fail-results.json: the results of our verifier as it was before
28 September 2026. It fails 4 cases (three unresolvable keys and the RFC 8785 edge case). Run it to see a FAIL.
- gen_vectors.py: regenerates
vectors.json byte for byte from the test
seeds.
Tell us you implemented it
If your implementation matches the vectors, email nicholas@csoai.org
with its name, version and a link to a run. Nobody is added to any list of implementers without their written consent,
and a listing will say only what the run shows.