acr-verify
v0.2.0
Published
An independent, dependency-free checker for Command Control Receipts, the signed records ALEETH Command issues. Checks the signature and, offline, revocation against a signed revocation list. Single-file browser build and a command line tool.
Maintainers
Readme
acr-verify
An independent checker for Command Control Receipts, the signed records ALEETH Command issues. It has no
runtime dependencies. It needs only what current browsers and Node already ship: WebCrypto (SHA-256 and
Ed25519), TextEncoder and atob.
It checks two things, and reports VERIFIED only when both were checked:
- The signature. The receipt, and the document it covers, are byte for byte what was signed, by the key whose fingerprint you hold. This never needs a network.
- Revocation. ALEETH has not revoked or replaced the receipt. This is checked offline against a signed, dated revocation list you downloaded, or online against ALEETH's status service. Offline, the answer is "not revoked as of" the list's date.
The rules are in spec/command-control-receipt-v1.md. That page is
enough to write a checker of your own.
Verdicts
| Verdict | Meaning | CLI exit |
|---|---|---|
| VERIFIED | The record and the document are unchanged, the signature checks out against the key, and revocation was checked: not revoked as of the list's date (or the status service's answer). | 0 |
| SIGNATURE_VALID_REVOCATION_UNCHECKED | The signature checks out, but revocation was not checked: no list and no answer, an unusable or badly signed list, or a list older than the maximum age. Not a pass. | 4 |
| TAMPERED | The record or the document is not what was hashed and signed. At least one byte differs. | 1 |
| INVALID | The content matches its hash, but no trusted key stands behind the signature. | 1 |
| REVOKED | The signature is genuine, and ALEETH has recorded a revocation or a replacement. | 1 |
When more than one thing is wrong, the strongest failure wins: TAMPERED, then INVALID, then REVOKED.
A failure to check revocation is never reported as "not revoked".
Offline use, step by step
On a machine with a network, once, save four public files from ALEETH (the market host is
https://market.aleeth.com):
curl -fsS -o keys.json https://market.aleeth.com/.well-known/aleeth-command-keys.json
curl -fsS -o revocations.json https://market.aleeth.com/.well-known/aleeth-command-revocations.jsonplus the receipt JSON and the document (PDF) you were given. Then, with the network off:
npx acr-verify receipt.json --keys keys.json --revocation-list revocations.json \
--document file.pdf --fingerprint <the fingerprint fixed in your contract>or, from a clone of this repository, node dist/cli.cjs with the same arguments.
- The revocation list is signed with the same keys as the receipts. The command line relies on a list only
when it is signed by the very key that signed the receipt (and, with
--fingerprint, by the key you pinned); a list signed by any other key in the keys file is not relied on. Then it looks the receipt up in it. - The list carries the time it was issued. The checker relies on it for 7 days by default and says
not revoked as of <issued_at>. Change the limit with--max-list-age-days <n>. A list past the limit that does not name the receipt givesSIGNATURE_VALID_REVOCATION_UNCHECKED; a list of any age that does name it givesREVOKED. - To reproduce a check made earlier,
--now <RFC 3339 time>measures the list's age from that time. - Entries name receipts by the SHA-256 of the receipt id, so the list does not reveal which receipt ids exist.
Online instead: --revocation-url https://market.aleeth.com/verify/revocation asks the status service, or
--revocation-file answer.json uses an answer you saved earlier. A saved answer is dated by its
checked_at and held to the same 7-day default and future-date rule as a list, and a VERIFIED that rests
on it says as of that date. Either can be combined with --revocation-list: the status answer wins when it
is usable, the list fills in when it is not, and either one saying revoked gives REVOKED.
Compare the fingerprint the keys file lists with the one fixed in your contract, or pass it with
--fingerprint so the check refuses any other key.
Library and browser
import { verifyBundle } from "acr-verify";
const report = await verifyBundle(
{ signed, keys, artifact: pdfBytes, pinnedFingerprint, revocation: { state: "list", list: revocationList } },
{ maxListAgeSeconds: 7 * 24 * 60 * 60 },
);
console.log(report.verdict, report.revocation.state, report.revocation.checkedAt);Pass trustedListKeys (ALEETH's published keys document that you ship) to accept lists signed by any of those
keys; without it a list must be signed by the receipt's own key. verifyWithRevocationSources(input, { askService,
lists }) applies the order of spec section 9.3. verifyRevocationList({ list, keys, pinnedFingerprint }) checks a
list on its own against keys, which you must already trust. dist/acr-verify.browser.js
is one file that sets globalThis.AcrVerify. It is the file the public verify page runs.
Test vectors
vectors/vectors.json holds canonical JSON cases, fingerprint cases, manifest hashes, two RFC 8032 known
answers and labelled receipts, including signed revocation lists, with the verdict each one must produce. A
checker is conformant when it reproduces every expected verdict. The vectors use test keys that never sign a
real receipt.
Building and testing
npm install # TypeScript and @types/node, for the build and the type check only
npm run build # dist/ from src/, with the TypeScript compiler and nothing else
npm run check # fails when a committed file in dist/ is stale
npm run typecheck
npm test # node --test: every vector through the library and the CLILicence
Apache License 2.0. See LICENSE and NOTICE.
