Goal
Make @certifieddata/verify verify Agent Commerce receipts locally, so the public /verify story can point to a real terminal-based independent verification path instead of relying on the server's verified: true response.
This is intentionally small: extend the existing zero-dependency/audit-friendly verifier to support one more signed artifact type using the same Ed25519 + RFC 8785 JCS contract already used by receipt issuance.
Related platform integrity issue: certifieddata/certifieddata-platform#315.
Current state
The CLI is certificate-only today:
src/cli.ts says Verify a CertifiedData.io certificate.
- any bare id is resolved through
https://certifieddata.io/api/v1/certificates/<id>
VerifyResult is certificate-shaped
- key loading is certificate-key-document specific
The platform already has the required receipt surfaces:
- public receipt payload:
GET /api/payments/verify/:receiptId
- Agent Commerce public key:
GET /.well-known/certifieddata-public-key.pem
- receipts are Ed25519-signed over RFC 8785 JCS canonicalization of the receipt payload with the signature omitted
Important: do not trust the server's valid / verified / signatureValid fields. Fetch the receipt payload and public key, then recompute locally.
Design
CLI UX
Preferred:
certifieddata-verify <receipt-id>
and it auto-detects the artifact kind.
Because both certificates and current payment receipts may be UUIDs, UUID alone is not a safe discriminator. Use deterministic resolution:
- Strong syntax/path hints first:
cert_, scert_, etc. -> existing certificate path
- receipt URL
/api/payments/verify/<id> -> receipt
- local JSON -> inspect schema/shape (
payment_receipt.v1 vs cert.v1)
- For an ambiguous bare UUID online, resolve against certificate and receipt public endpoints.
- exactly one exists -> use it
- both exist -> fail as ambiguous and require explicit
--type
- neither exists -> not found
- a transport/server failure must not be silently interpreted as "not found"
- Add an explicit override:
certifieddata-verify <id> --type receipt
certifieddata-verify <id> --type certificate
This keeps the normal one-line UX while making ambiguity controllable.
Receipt verification contract
For a receipt:
- Fetch
GET https://certifieddata.io/api/payments/verify/<receiptId>.
- Extract the signed receipt payload and signature.
- Fetch
https://certifieddata.io/.well-known/certifieddata-public-key.pem.
- Remove the
signature field from the receipt payload.
- RFC 8785 JCS-canonicalize the remaining payload.
- Verify the Ed25519 signature locally with Node
crypto.
- If the response exposes a stored receipt hash, recompute SHA-256 over the same canonical bytes and compare locally.
- Report server-side booleans, if present, only as informational metadata; they do not determine the CLI verdict.
The platform signs receipts using canonicalPayloadBytes() in apps/api/src/certs/crypto.ts, which uses RFC 8785 JCS via the canonicalize package after stripping undefined values. Match that contract exactly.
Do not use plain JSON.stringify() as the cryptographic canonical form.
Result shape
Generalize VerifyResult enough to cover both certificate and receipt verification without breaking existing consumers.
Suggested additive fields:
artifact_type: "certificate" | "receipt";
artifact_id: string | null;
For backward compatibility, existing certificate fields (certification_id, dataset hash fields, algorithm, rows, columns) can remain nullable/optional for receipts.
Receipt-specific output should include at least:
{
"artifact_type": "receipt",
"artifact_id": "...",
"verdict": "VALID",
"key_id": "ed25519-prod-2025-02",
"checks": {
"signature": "pass",
"key_trust": "pass",
"payload_hash": "pass"
}
}
Use the existing exit-code philosophy: signature/hash failure is non-zero; key unavailable/unknown is distinct from malformed input/network failure.
Human output
Example:
✓ VALID receipt f41ef7b0-000f-428b-88a8-07cc2ee7a47d
signed by ed25519-prod-2025-02 (CertifiedData.io)
signature pass
payload hash pass
public key /.well-known/certifieddata-public-key.pem
The command should make the independence claim explicit in docs:
The receipt is fetched from CertifiedData, but the verdict is not. The CLI verifies the Ed25519 signature locally against the published public key.
/verify platform follow-up
The platform browser verifier currently has a related ambiguity: it treats any bare UUID as a certificate, while payment receipts are also UUIDs. Platform issue #315 should update /verify / verify-client.ts to use the same artifact-kind resolution rules or pass an explicit kind into the verifier.
Do not solve that by minting fake rcpt_ prefixes around existing UUID receipt IDs.
Also note: the browser receipt verifier currently uses JSON.stringify(rest) while receipt issuance signs RFC 8785 JCS bytes. That should be corrected in the platform issue so browser and CLI verification share the same canonicalization contract.
Acceptance criteria
Non-goals
- no new cryptographic primitive
- no dependency on the CertifiedData server's verdict
- no historical receipt mutation
- no broader Decision Ledger support in this issue unless it falls out trivially from a shared artifact resolver
- no platform repo changes from this repository
Goal
Make
@certifieddata/verifyverify Agent Commerce receipts locally, so the public/verifystory can point to a real terminal-based independent verification path instead of relying on the server'sverified: trueresponse.This is intentionally small: extend the existing zero-dependency/audit-friendly verifier to support one more signed artifact type using the same Ed25519 + RFC 8785 JCS contract already used by receipt issuance.
Related platform integrity issue: certifieddata/certifieddata-platform#315.
Current state
The CLI is certificate-only today:
src/cli.tssaysVerify a CertifiedData.io certificate.https://certifieddata.io/api/v1/certificates/<id>VerifyResultis certificate-shapedThe platform already has the required receipt surfaces:
GET /api/payments/verify/:receiptIdGET /.well-known/certifieddata-public-key.pemImportant: do not trust the server's
valid/verified/signatureValidfields. Fetch the receipt payload and public key, then recompute locally.Design
CLI UX
Preferred:
and it auto-detects the artifact kind.
Because both certificates and current payment receipts may be UUIDs, UUID alone is not a safe discriminator. Use deterministic resolution:
cert_,scert_, etc. -> existing certificate path/api/payments/verify/<id>-> receiptpayment_receipt.v1vscert.v1)--typeThis keeps the normal one-line UX while making ambiguity controllable.
Receipt verification contract
For a receipt:
GET https://certifieddata.io/api/payments/verify/<receiptId>.https://certifieddata.io/.well-known/certifieddata-public-key.pem.signaturefield from the receipt payload.crypto.The platform signs receipts using
canonicalPayloadBytes()inapps/api/src/certs/crypto.ts, which uses RFC 8785 JCS via thecanonicalizepackage after stripping undefined values. Match that contract exactly.Do not use plain
JSON.stringify()as the cryptographic canonical form.Result shape
Generalize
VerifyResultenough to cover both certificate and receipt verification without breaking existing consumers.Suggested additive fields:
For backward compatibility, existing certificate fields (
certification_id, dataset hash fields, algorithm, rows, columns) can remain nullable/optional for receipts.Receipt-specific output should include at least:
{ "artifact_type": "receipt", "artifact_id": "...", "verdict": "VALID", "key_id": "ed25519-prod-2025-02", "checks": { "signature": "pass", "key_trust": "pass", "payload_hash": "pass" } }Use the existing exit-code philosophy: signature/hash failure is non-zero; key unavailable/unknown is distinct from malformed input/network failure.
Human output
Example:
The command should make the independence claim explicit in docs:
/verifyplatform follow-upThe platform browser verifier currently has a related ambiguity: it treats any bare UUID as a certificate, while payment receipts are also UUIDs. Platform issue #315 should update
/verify/verify-client.tsto use the same artifact-kind resolution rules or pass an explicit kind into the verifier.Do not solve that by minting fake
rcpt_prefixes around existing UUID receipt IDs.Also note: the browser receipt verifier currently uses
JSON.stringify(rest)while receipt issuance signs RFC 8785 JCS bytes. That should be corrected in the platform issue so browser and CLI verification share the same canonicalization contract.Acceptance criteria
certifieddata-verify <known receipt UUID>resolves and verifies the receipt locally--type receiptand--type certificateexplicitly override detectioncrypto, not the server verdict/.well-known/certifieddata-public-key.pemNon-goals