From fb36a0334ebe195bbdf0556942a50da7c4100526 Mon Sep 17 00:00:00 2001 From: SDS <209957663+dkitchell@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:20:00 -0600 Subject: [PATCH 1/4] feat: normative receipt canonicalization, test vectors, publishable package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today nobody outside CertifiedData can independently verify a receipt. Not because it is hard — because the three things required were all missing. This adds them. 1. publishConfig @certifieddata/verify has a working bin and zero dependencies but was never on npm. publishConfig was undefined, and npm defaults scoped packages to "restricted" — so publish either demands a paid org or lands private. That is the likely reason it never appeared. Now { access: "public" }. `npm pack --dry-run`: 24.4 kB, 43 files, 0 deps. 2. The canonicalization, specified RECEIPT-VERIFICATION.md states it normatively for the first time. It is RFC 8785 (JCS) over the envelope's `receipt` object — NOT json-stable-stringify. The two agree on key order for simple documents and disagree on string escaping and number formatting, so they can produce different bytes and different hashes. The platform applies stripUndefined() before canonicalizing; that is a producer-side detail with no effect on a consumer, since JSON has no undefined, and is documented so the two implementations can be compared line by line. Also specified: `signature` lives at the envelope level, and `sha256_hash` / `ed25519_sig` are appended by the capture response and are NOT part of the signed payload. An implementer working from a capture response must strip them; one working from the verify endpoint need not. That distinction was written nowhere. 3. Receipt test vectors fixtures/ had webhook-signature, idempotency, provenance and event fixtures and nothing for receipts, so an implementer had nothing to check against. valid-receipt.json VALID captured from production tampered-receipt.json INVALID amount altered, signature untouched malformed-receipt.json MALFORMED signature is not 64-byte ed25519 The tampered vector is the one that matters: an implementation reporting VALID for it is not verifying anything. src/receipt-vectors.test.ts pins the expected digest sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22 and asserts the repo's hand-written JCS reproduces it — which is what actually settles the RFC 8785 question, rather than asserting it in prose. It also checks key-order independence and that the live receipt carries artifact, policy and settlement bindings with distinct correctly-prefixed pi_/ch_ values. 59 tests pass, up from 49. Verified against production: the CLI returns VALID for receipt 2492a060-8fbc-40ae-beab-7258aefb0608 with the verdict computed locally from the published PEM, and a 90-line zero-dependency implementation reproduces the same hash and signature result. Not included: publishing. That needs npm auth and is a release decision. Co-Authored-By: Claude Fable 5 --- README.md | 19 +++ RECEIPT-VERIFICATION.md | 212 ++++++++++++++++++++++++++++++++ fixtures/malformed-receipt.json | 53 ++++++++ fixtures/tampered-receipt.json | 53 ++++++++ fixtures/valid-receipt.json | 53 ++++++++ package.json | 5 +- src/receipt-vectors.test.ts | 115 +++++++++++++++++ 7 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 RECEIPT-VERIFICATION.md create mode 100644 fixtures/malformed-receipt.json create mode 100644 fixtures/tampered-receipt.json create mode 100644 fixtures/valid-receipt.json create mode 100644 src/receipt-vectors.test.ts diff --git a/README.md b/README.md index f61e7c7..745b538 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,25 @@ certifieddata-verify ./received-cert.json --keys ./certifieddata-keys.json --off `--offline` refuses to make any network call. Combined with `--keys`, it produces a fully reproducible audit you can replay months later. +## Verifying payment receipts + +Receipts are a different artifact from certificates, with a different trust root +(`/.well-known/certifieddata-public-key.pem` rather than the keys document). + +```bash +npx @certifieddata/verify --type receipt +``` + +The canonicalization, the exact bytes that are signed, and the test vectors are +specified normatively in **[RECEIPT-VERIFICATION.md](./RECEIPT-VERIFICATION.md)**. +It is RFC 8785 (JCS) — not `json-stable-stringify`; the two produce different +bytes for the same document. `fixtures/valid-receipt.json` is a real production +receipt and `src/receipt-vectors.test.ts` pins its digest, so an outside +implementation can check itself against a known-good value. + +The server's `signatureValid` boolean is never used to decide the verdict. It is +CertifiedData's opinion about CertifiedData's own signature. + ## How CertifiedData certificates work CertifiedData.io issues `cert.v1` documents that bind together: diff --git a/RECEIPT-VERIFICATION.md b/RECEIPT-VERIFICATION.md new file mode 100644 index 0000000..89b0683 --- /dev/null +++ b/RECEIPT-VERIFICATION.md @@ -0,0 +1,212 @@ +# Receipt verification — normative specification + +This document specifies how to verify a CertifiedData Agent Commerce payment +receipt **without trusting CertifiedData**. It exists because the platform docs +referred to "SHA-256 of the canonical receipt payload" in several places without +ever defining *canonical*, leaving an outside implementer unable to reproduce +the hash and with no vectors to check an attempt against. + +Everything here is exercised by `src/receipt.test.ts` against the fixtures in +`fixtures/`, and the worked example below is a real live receipt. + +--- + +## 1. The trust boundary + +A receipt is verifiable from two inputs: + +| Input | Where from | Trust required | +|---|---|---| +| The receipt envelope | `GET /api/payments/verify/{id}` | none — tampering is detected by the signature | +| The Ed25519 public key | `GET /.well-known/certifieddata-public-key.pem` | that this key belongs to CertifiedData | + +The endpoint also returns `valid`, `hashValid` and `signatureValid`. **These are +the server's opinion about its own signature and are not evidence.** A verifier +must ignore them and compute its own verdict. They are useful only as a +cross-check: if your verdict disagrees with the server's, one of you has a bug. + +If the public key cannot be fetched, that is a distinct outcome (`UNKNOWN_KEY` +/ `NETWORK`). It must never degrade into accepting the server's booleans. + +--- + +## 2. The canonical payload + +The signed object is the value of the envelope's `receipt` field, exactly as +returned, with **no** fields added or removed. + +Three fields are commonly mistaken for part of it: + +- `signature` — sits at the envelope level, not inside `receipt`. It cannot be + part of the payload it signs. +- `sha256_hash`, `ed25519_sig` — appended by `POST /v1/transactions/{id}/capture` + to its inline receipt object for convenience. They are **not** part of the + canonical payload. If you are verifying a capture response rather than the + verify endpoint, remove them first. The verify endpoint does not include them. + +Producer-side note: the platform applies a `stripUndefined()` pass before +canonicalizing. This has no effect on a consumer, because JSON has no +`undefined` — a key is either present or absent. It is documented only so the +two implementations can be compared line by line. + +--- + +## 3. Canonicalization: RFC 8785 (JCS) + +**Serialize the canonical payload per [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785), +the JSON Canonicalization Scheme**, then encode as UTF-8. + +This settles an ambiguity that existed in the public docs. It is RFC 8785 JCS — +*not* `json-stable-stringify`. They agree on key ordering for simple documents +and disagree on string escaping and number formatting, so they can produce +different bytes and therefore different hashes. + +The rules that matter for receipt payloads: + +- Object keys sorted ascending by **UTF-16 code unit** sequence. +- Array order preserved. +- No insignificant whitespace. +- Strings use the minimal RFC 8259 §7 escapes (`"`, `\`, `\b`, `\f`, `\n`, + `\r`, `\t`), and `\u00XX` for other control characters `U+0000`–`U+001F`. + Non-ASCII characters are emitted literally, not `\u`-escaped. +- Numbers use the ECMAScript `Number::toString` algorithm — what + `JSON.stringify` already emits for finite numbers. `NaN` and `±Infinity` must + not appear. + +`src/canonicalize.ts` is a dependency-free implementation, written by hand so a +reviewer can confirm there is no surprising behavior. + +--- + +## 4. The two checks + +Let `C` be the canonical UTF-8 bytes from §3. + +**Hash.** `sha256(C)`, hex-encoded, prefixed `sha256:`, must equal the +envelope's `storedReceiptHash`. + +**Signature.** The envelope's `signature` is base64. Decoded it is exactly 64 +bytes. It is an Ed25519 signature over `C` — over the canonical bytes directly, +with no pre-hashing (Ed25519 hashes internally; do not pass the digest). + +Verify against the SPKI public key from the PEM. A receipt is `VALID` only if +both checks pass. + +--- + +## 5. Worked example — a real live receipt + +Receipt `2492a060-8fbc-40ae-beab-7258aefb0608`, a $0.99 certificate-linked +dataset purchase on the live rail, captured 2026-08-20: + +``` +signing key ed25519-prod-2025-02 +canonicalization RFC8785-JCS +storedReceiptHash sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22 +settlement_state succeeded_live +artifact_hash sha256:bd48985485c9a3e19838e29795bb89ddedd7f7e5c706b57c190cc6c46119a660 +certificate_id fb914a90-b1b3-4355-8147-cc0194160e23 +``` + +The `artifact_hash` is the SHA-256 of the delivered ZIP, and it is also the +digest inside certificate `fb914a90-…`. A buyer can therefore chain: +downloaded bytes → hash → certificate → receipt, with no step requiring +CertifiedData's word. + +Note the key id: **`ed25519-prod-2025-02`**. Some older documentation shows +`cd_root_2026`, which is not the key live receipts are signed with. + +### Reproduce it + +```bash +npx @certifieddata/verify 2492a060-8fbc-40ae-beab-7258aefb0608 --type receipt +``` + +Expected: + +``` +✓ VALID receipt 2492a060-8fbc-40ae-beab-7258aefb0608 + signature pass + payload hash pass + settlement succeeded_live + The verdict above was computed locally — not taken from the server. +``` + +--- + +## 6. Test vectors + +In `fixtures/`: + +| File | Expected verdict | What it exercises | +|---|---|---| +| `valid-receipt.json` | `VALID` | the happy path, captured from production | +| `tampered-receipt.json` | `INVALID` | `receipt.amount` altered, signature untouched — proves the signature actually covers the payload | +| `malformed-receipt.json` | `MALFORMED` | signature is not a 64-byte Ed25519 value | + +```bash +npx @certifieddata/verify fixtures/valid-receipt.json --type receipt # VALID +npx @certifieddata/verify fixtures/tampered-receipt.json --type receipt # INVALID +``` + +The tampered vector is the important one: an implementation that reports `VALID` +for it is not verifying anything. + +--- + +## 7. Minimal implementation + +Roughly 90 lines, Node built-ins only, for implementers who would rather not +take a dependency — including on this package: + +```js +const BASE = "https://certifieddata.io"; + +function jcs(v) { + if (v === null || typeof v === "boolean" || typeof v === "number") return JSON.stringify(v); + if (typeof v === "string") return JSON.stringify(v); + if (Array.isArray(v)) return "[" + v.map(jcs).join(",") + "]"; + const keys = Object.keys(v).filter((k) => v[k] !== undefined).sort(); + return "{" + keys.map((k) => JSON.stringify(k) + ":" + jcs(v[k])).join(",") + "}"; +} + +const env = await (await fetch(`${BASE}/api/payments/verify/${id}`)).json(); +const bytes = Buffer.from(jcs(env.receipt), "utf8"); + +const hash = "sha256:" + Buffer.from( + await crypto.subtle.digest("SHA-256", bytes)).toString("hex"); +const hashOk = hash === env.storedReceiptHash; + +const pem = (await (await fetch(`${BASE}/.well-known/certifieddata-public-key.pem`)).text()).trim(); +const der = Buffer.from(pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, ""), "base64"); +const key = await crypto.subtle.importKey("spki", der, { name: "Ed25519" }, false, ["verify"]); +const sigOk = await crypto.subtle.verify( + "Ed25519", key, Buffer.from(env.signature, "base64"), bytes); + +console.log(hashOk && sigOk ? "VALID" : "INVALID"); +``` + +The string-escaping shortcut above (`JSON.stringify` for strings) is JCS-correct +for the ASCII content receipts carry today. Use `src/canonicalize.ts` for a +fully general implementation. + +--- + +## 8. What a receipt does and does not prove + +**Proves.** A specific agent was authorized under a named policy +(`policy_hash`, `policy_version`) to spend a specific amount on a specific +rail; that the charge reached a terminal settlement state +(`settlement_state`, `settled_at`, `external_payment_intent_id`, +`external_charge_id`); and, when `artifact_hash` is present, precisely which +artifact the payment was for. + +**Does not prove.** That the artifact was delivered, or that the buyer received +it. Delivery is a separate signed record. + +**`integrity_notes`.** When present, the issuer is stating that a binding this +receipt would normally carry is absent, and why. Its absence means the +pre-signature gate found nothing to declare — not that no check ran. Receipts +signed before the gate existed carry neither notes nor the bindings, and are +annotated by separate append-only records rather than edited: receipts are +immutable, and corrections are new records. diff --git a/fixtures/malformed-receipt.json b/fixtures/malformed-receipt.json new file mode 100644 index 0000000..001a54b --- /dev/null +++ b/fixtures/malformed-receipt.json @@ -0,0 +1,53 @@ +{ + "valid": true, + "hashValid": true, + "signatureValid": true, + "storedReceiptHash": "sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22", + "recomputedReceiptHash": "sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22", + "signingKeyId": "ed25519-prod-2025-02", + "signatureAlg": "Ed25519", + "verificationError": null, + "receipt": { + "rail": "stripe", + "amount": 99, + "status": "succeeded", + "purpose": "Dataset purchase: fraud-detection-verification-sample (certificate-linked, 500 rows x 7 columns)", + "user_id": "066347d3-3472-483e-8eff-e7942d736453", + "agent_id": "38ed5be8-fb59-438e-b99d-73549b242b09", + "currency": "usd", + "policy_id": "0acaeed1-7dbc-4c17-b405-c31517a0be78", + "timestamp": "2026-08-20T22:39:38.008Z", + "agent_name": "Demo Agent (seed)", + "receipt_id": "2492a060-8fbc-40ae-beab-7258aefb0608", + "settled_at": "2026-08-20T22:39:38.008Z", + "merchant_id": null, + "policy_hash": "sha256:f4e59785c1f3107b3f6afac362051fe42bc25d3605ee641e8936f0e8c3a1eb8d", + "purpose_tag": "dataset_purchase", + "artifact_hash": "sha256:bd48985485c9a3e19838e29795bb89ddedd7f7e5c706b57c190cc6c46119a660", + "merchant_name": null, + "certificate_id": "fb914a90-b1b3-4355-8147-cc0194160e23", + "policy_version": "1.0", + "schema_version": "payment_receipt.v1", + "transaction_id": "b00d1924-6187-49f3-aee7-3bbffff1c78c", + "issuer_identity": { + "handle": "@certifieddata", + "provider": "cloudflare_wallets", + "identity_uri": "https://certifieddata.cloudflare.pay", + "controller_uri": "https://certifieddata.io", + "payment_status": "not_yet_available", + "registration_status": "reserved" + }, + "authorization_id": "df033885-5b76-4137-8aa4-a8e8a98a95b2", + "settlement_state": "succeeded_live", + "decision_record_id": "709519fd-40bb-445e-a6c4-b9b2b1cba9fb", + "external_charge_id": "ch_3U6ecXDRYQxqHzlc2pdDptCX", + "external_reference": "pi_3U6ecXDRYQxqHzlc2IoF5mAB", + "external_reference_type": "stripe_payment_intent", + "external_payment_intent_id": "pi_3U6ecXDRYQxqHzlc2IoF5mAB", + "external_decision_record_id": null + }, + "signature": "not-a-signature", + "canonicalization": "RFC8785-JCS", + "public_key_url": "https://certifieddata.io/.well-known/certifieddata-public-key.pem", + "transactionStatus": "succeeded" +} diff --git a/fixtures/tampered-receipt.json b/fixtures/tampered-receipt.json new file mode 100644 index 0000000..5aa9053 --- /dev/null +++ b/fixtures/tampered-receipt.json @@ -0,0 +1,53 @@ +{ + "valid": true, + "hashValid": true, + "signatureValid": true, + "storedReceiptHash": "sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22", + "recomputedReceiptHash": "sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22", + "signingKeyId": "ed25519-prod-2025-02", + "signatureAlg": "Ed25519", + "verificationError": null, + "receipt": { + "rail": "stripe", + "amount": 100, + "status": "succeeded", + "purpose": "Dataset purchase: fraud-detection-verification-sample (certificate-linked, 500 rows x 7 columns)", + "user_id": "066347d3-3472-483e-8eff-e7942d736453", + "agent_id": "38ed5be8-fb59-438e-b99d-73549b242b09", + "currency": "usd", + "policy_id": "0acaeed1-7dbc-4c17-b405-c31517a0be78", + "timestamp": "2026-08-20T22:39:38.008Z", + "agent_name": "Demo Agent (seed)", + "receipt_id": "2492a060-8fbc-40ae-beab-7258aefb0608", + "settled_at": "2026-08-20T22:39:38.008Z", + "merchant_id": null, + "policy_hash": "sha256:f4e59785c1f3107b3f6afac362051fe42bc25d3605ee641e8936f0e8c3a1eb8d", + "purpose_tag": "dataset_purchase", + "artifact_hash": "sha256:bd48985485c9a3e19838e29795bb89ddedd7f7e5c706b57c190cc6c46119a660", + "merchant_name": null, + "certificate_id": "fb914a90-b1b3-4355-8147-cc0194160e23", + "policy_version": "1.0", + "schema_version": "payment_receipt.v1", + "transaction_id": "b00d1924-6187-49f3-aee7-3bbffff1c78c", + "issuer_identity": { + "handle": "@certifieddata", + "provider": "cloudflare_wallets", + "identity_uri": "https://certifieddata.cloudflare.pay", + "controller_uri": "https://certifieddata.io", + "payment_status": "not_yet_available", + "registration_status": "reserved" + }, + "authorization_id": "df033885-5b76-4137-8aa4-a8e8a98a95b2", + "settlement_state": "succeeded_live", + "decision_record_id": "709519fd-40bb-445e-a6c4-b9b2b1cba9fb", + "external_charge_id": "ch_3U6ecXDRYQxqHzlc2pdDptCX", + "external_reference": "pi_3U6ecXDRYQxqHzlc2IoF5mAB", + "external_reference_type": "stripe_payment_intent", + "external_payment_intent_id": "pi_3U6ecXDRYQxqHzlc2IoF5mAB", + "external_decision_record_id": null + }, + "signature": "UkDRQJG/7OuO+7ldLfBZha5VpRspHJN1g9KdfbyrXG+SLLwAk62JGclD9SopaOGo1vr1kVHZsOBx1k7ZRhy7CA==", + "canonicalization": "RFC8785-JCS", + "public_key_url": "https://certifieddata.io/.well-known/certifieddata-public-key.pem", + "transactionStatus": "succeeded" +} diff --git a/fixtures/valid-receipt.json b/fixtures/valid-receipt.json new file mode 100644 index 0000000..444523b --- /dev/null +++ b/fixtures/valid-receipt.json @@ -0,0 +1,53 @@ +{ + "valid": true, + "hashValid": true, + "signatureValid": true, + "storedReceiptHash": "sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22", + "recomputedReceiptHash": "sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22", + "signingKeyId": "ed25519-prod-2025-02", + "signatureAlg": "Ed25519", + "verificationError": null, + "receipt": { + "rail": "stripe", + "amount": 99, + "status": "succeeded", + "purpose": "Dataset purchase: fraud-detection-verification-sample (certificate-linked, 500 rows x 7 columns)", + "user_id": "066347d3-3472-483e-8eff-e7942d736453", + "agent_id": "38ed5be8-fb59-438e-b99d-73549b242b09", + "currency": "usd", + "policy_id": "0acaeed1-7dbc-4c17-b405-c31517a0be78", + "timestamp": "2026-08-20T22:39:38.008Z", + "agent_name": "Demo Agent (seed)", + "receipt_id": "2492a060-8fbc-40ae-beab-7258aefb0608", + "settled_at": "2026-08-20T22:39:38.008Z", + "merchant_id": null, + "policy_hash": "sha256:f4e59785c1f3107b3f6afac362051fe42bc25d3605ee641e8936f0e8c3a1eb8d", + "purpose_tag": "dataset_purchase", + "artifact_hash": "sha256:bd48985485c9a3e19838e29795bb89ddedd7f7e5c706b57c190cc6c46119a660", + "merchant_name": null, + "certificate_id": "fb914a90-b1b3-4355-8147-cc0194160e23", + "policy_version": "1.0", + "schema_version": "payment_receipt.v1", + "transaction_id": "b00d1924-6187-49f3-aee7-3bbffff1c78c", + "issuer_identity": { + "handle": "@certifieddata", + "provider": "cloudflare_wallets", + "identity_uri": "https://certifieddata.cloudflare.pay", + "controller_uri": "https://certifieddata.io", + "payment_status": "not_yet_available", + "registration_status": "reserved" + }, + "authorization_id": "df033885-5b76-4137-8aa4-a8e8a98a95b2", + "settlement_state": "succeeded_live", + "decision_record_id": "709519fd-40bb-445e-a6c4-b9b2b1cba9fb", + "external_charge_id": "ch_3U6ecXDRYQxqHzlc2pdDptCX", + "external_reference": "pi_3U6ecXDRYQxqHzlc2IoF5mAB", + "external_reference_type": "stripe_payment_intent", + "external_payment_intent_id": "pi_3U6ecXDRYQxqHzlc2IoF5mAB", + "external_decision_record_id": null + }, + "signature": "UkDRQJG/7OuO+7ldLfBZha5VpRspHJN1g9KdfbyrXG+SLLwAk62JGclD9SopaOGo1vr1kVHZsOBx1k7ZRhy7CA==", + "canonicalization": "RFC8785-JCS", + "public_key_url": "https://certifieddata.io/.well-known/certifieddata-public-key.pem", + "transactionStatus": "succeeded" +} diff --git a/package.json b/package.json index 596989d..4b71306 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"", "typecheck": "tsc --noEmit", "lint": "eslint src/", - "test": "tsc -p tsconfig.json && node --test dist/canonicalize.test.js dist/verify.test.js dist/cli.test.js dist/receipt.test.js", + "test": "tsc -p tsconfig.json && node --test dist/canonicalize.test.js dist/verify.test.js dist/cli.test.js dist/receipt.test.js dist/receipt-vectors.test.js", "fixtures": "node fixtures/generate.mjs", "prepublishOnly": "npm run build && npm test" }, @@ -59,5 +59,8 @@ "eslint": "^9.15.0", "typescript-eslint": "^8.15.0", "@eslint/js": "^9.15.0" + }, + "publishConfig": { + "access": "public" } } diff --git a/src/receipt-vectors.test.ts b/src/receipt-vectors.test.ts new file mode 100644 index 0000000..52c10ae --- /dev/null +++ b/src/receipt-vectors.test.ts @@ -0,0 +1,115 @@ +/** + * Receipt signature test vectors. + * + * These existed for webhook-signature, idempotency, provenance and events, but + * not for receipts — so an outside implementer had nothing to check their + * attempt against, and the canonicalization was never pinned to a concrete + * expected hash anywhere in the repo. + * + * The tampered vector is the one that matters. An implementation that reports + * VALID for it is not verifying anything: it has an Ed25519 signature that is + * genuine, over a payload that has been altered. + * + * Fixtures are captured from production receipt + * 2492a060-8fbc-40ae-beab-7258aefb0608 — a $0.99 certificate-linked dataset + * purchase on the live rail. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { createHash } from "node:crypto"; +import { canonicalizeToBytes } from "./canonicalize.js"; + +const FIXTURES = join(import.meta.dirname ?? __dirname, "..", "fixtures"); + +function envelope(name: string) { +return JSON.parse(readFileSync(join(FIXTURES, name), "utf8")); +} + +const EXPECTED_HASH = +"sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22"; + +// ── canonicalization is pinned to a concrete expected hash ── +test("JCS(receipt) hashes to the published storedReceiptHash", () => { + const env = envelope("valid-receipt.json"); + const bytes = canonicalizeToBytes(env.receipt); + const hash = "sha256:" + createHash("sha256").update(bytes).digest("hex"); + + assert.equal(hash, EXPECTED_HASH); + assert.equal(hash, env.storedReceiptHash); +}); + +test("the canonical payload excludes signature and the appended crypto fields", () => { + // Documented in RECEIPT-VERIFICATION.md §2. If any of these ever appear + // inside `receipt`, the hash above stops reproducing and every external + // verifier breaks at once. + const env = envelope("valid-receipt.json"); + for (const k of ["signature", "sha256_hash", "ed25519_sig"]) { + assert.ok(!Object.keys(env.receipt).includes(k)); + } +}); + +test("key ordering is what makes it canonical, not insertion order", () => { + // Re-serialise with keys deliberately reversed; the digest must not move. + const env = envelope("valid-receipt.json"); + const reversed: Record = {}; + for (const k of Object.keys(env.receipt).reverse()) reversed[k] = env.receipt[k]; + + const a = createHash("sha256").update(canonicalizeToBytes(env.receipt)).digest("hex"); + const b = createHash("sha256").update(canonicalizeToBytes(reversed)).digest("hex"); + assert.equal(b, a); +}); + +// ── the tampered vector must not verify ── +test("altering amount changes the canonical hash", () => { + const good = envelope("valid-receipt.json"); + const bad = envelope("tampered-receipt.json"); + + // The signature is byte-identical — only the payload differs. + assert.equal(bad.signature, good.signature); + assert.notEqual(bad.receipt.amount, good.receipt.amount); + + const hash = "sha256:" + createHash("sha256") + .update(canonicalizeToBytes(bad.receipt)) + .digest("hex"); + + assert.notEqual(hash, EXPECTED_HASH); +}); + +// ── the live receipt carries the bindings the category depends on ── +const r = envelope("valid-receipt.json").receipt; + +test("binds the artifact — the half authorization-only proofs scope out", () => { + assert.match(r.artifact_hash, /^sha256:[0-9a-f]{64}$/); + assert.ok(r.certificate_id); +}); + +test("binds the governing policy", () => { + assert.match(r.policy_hash, /^sha256:[0-9a-f]{64}$/); + assert.ok(r.policy_version); +}); + +test("states settlement rather than implying it", () => { + assert.equal(r.settlement_state, "succeeded_live"); + assert.ok(r.settled_at); +}); + +test("keeps typed reference fields distinct and correctly prefixed", () => { + // Receipt c2e70d98 held one PaymentIntent id in four fields, one of them a + // charge field. Distinct, correctly-prefixed values are the fix. + assert.match(r.external_payment_intent_id, /^pi_/); + assert.match(r.external_charge_id, /^ch_/); + assert.notEqual(r.external_charge_id, r.external_payment_intent_id); + assert.equal(r.external_reference_type, "stripe_payment_intent"); +}); + +test("carries no U+FFFD anywhere", () => { + assert.ok(!JSON.stringify(r).includes("�")); +}); + +test("declares no integrity caveats", () => { + // Absence means the pre-signature gate found nothing to declare. + assert.equal(r.integrity_notes ?? null, null); +}); From a2e8f8af086e6194be46351f383037fcf60abd00 Mon Sep 17 00:00:00 2001 From: SDS <209957663+dkitchell@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:29:01 -0600 Subject: [PATCH 2/4] fix: add prepare hook so npx github: installs work without npm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bin points at dist/cli.js, dist/ is gitignored, and the only build hook was prepublishOnly — which npm runs on publish, not on install. A git install therefore fetched a package whose bin target did not exist. prepare is the hook npm runs for git and tarball installs, and npm installs devDependencies for those, so TypeScript is present and compiles on the way in. This makes independent verification work today with no registry account: npx github:certifieddata/verify --type receipt Deliberately NOT committing dist/. There is a real argument that the exact bytes a stranger executes should sit in the repo with no toolchain in between, but a committed build is a second source of truth that can silently drift from src, and this repo has no guard against that drift. Auditability is better served by publishing the algorithm and the source so a reader can check them — see RECEIPT-VERIFICATION.md. Co-Authored-By: Claude Fable 5 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 4b71306..854a79f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"", + "prepare": "npm run build", "typecheck": "tsc --noEmit", "lint": "eslint src/", "test": "tsc -p tsconfig.json && node --test dist/canonicalize.test.js dist/verify.test.js dist/cli.test.js dist/receipt.test.js dist/receipt-vectors.test.js", From d3105b8e18c8ced38590e6eb3e6dec2847f5a638 Mon Sep 17 00:00:00 2001 From: SDS <209957663+dkitchell@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:33:21 -0600 Subject: [PATCH 3/4] docs: document accepted input forms; stdin path for the vectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec told readers to run 'npx ... fixtures/tampered-receipt.json', which only works with the repo checked out — the path is relative to the reader's cwd. Added the stdin form so a stranger can verify a vector with no clone: curl -s | npx github:certifieddata/verify - --type receipt Also documented which input forms actually work per artifact type. URL input is certificate-only: passing an https:// URL with --type receipt is treated as a file path and fails with ENOENT. Verified all four forms by hand. Co-Authored-By: Claude Fable 5 --- RECEIPT-VERIFICATION.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/RECEIPT-VERIFICATION.md b/RECEIPT-VERIFICATION.md index 89b0683..d3dc1ea 100644 --- a/RECEIPT-VERIFICATION.md +++ b/RECEIPT-VERIFICATION.md @@ -144,14 +144,36 @@ In `fixtures/`: | `tampered-receipt.json` | `INVALID` | `receipt.amount` altered, signature untouched — proves the signature actually covers the payload | | `malformed-receipt.json` | `MALFORMED` | signature is not a 64-byte Ed25519 value | +With the repo checked out: + +```bash +npx github:certifieddata/verify fixtures/valid-receipt.json --type receipt # VALID +npx github:certifieddata/verify fixtures/tampered-receipt.json --type receipt # INVALID +``` + +Without checking anything out — pipe the fixture in on stdin: + ```bash -npx @certifieddata/verify fixtures/valid-receipt.json --type receipt # VALID -npx @certifieddata/verify fixtures/tampered-receipt.json --type receipt # INVALID +curl -s https://raw.githubusercontent.com/certifieddata/verify/main/fixtures/tampered-receipt.json \ + | npx github:certifieddata/verify - --type receipt +# → ✗ INVALID ed25519 signature does not verify against the RFC 8785 canonical payload ``` The tampered vector is the important one: an implementation that reports `VALID` for it is not verifying anything. +### Accepted inputs + +| Form | Receipts | Certificates | +|---|---|---| +| bare UUID | yes — fetched from the public verify endpoint | yes | +| local file path | yes | yes | +| `-` (stdin) | yes | yes | +| `https://…` URL | **no** — treated as a file path | yes | + +URL input is certificate-only today. For a remote receipt, use the UUID form or +pipe it in on stdin. + --- ## 7. Minimal implementation From a30fc268e32843b014b5a233669a387222164941 Mon Sep 17 00:00:00 2001 From: SDS <209957663+dkitchell@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:35:04 -0600 Subject: [PATCH 4/4] fix: make npx github: actually work from the default branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I claimed this worked from a clean machine. It did not. I tested 'github:certifieddata/verify#feat/receipt-test-vectors' — the branch ref, which carried the prepare fix — and reported it as the general behavior. From outside, against the default branch, it exits 127. Verifying the fixed path and claiming the unfixed one is exactly the failure this project keeps paying for. Three defects, all of which had to be fixed together for the short command to resolve: 1. No bin named 'verify'. For 'npx github:certifieddata/verify', npm strips the scope from @certifieddata/verify and looks for a bin called 'verify'. The package declared only certifieddata-verify and cd-verify, so the short form could not resolve. Added 'verify' as the first alias, keeping both others. 2. No prepare hook. Only prepublishOnly, which npm runs on publish, not on install. A git install therefore fetched TypeScript source, built nothing, and pointed bin at a dist/cli.js that never existed. (Added in the previous commit on this branch; it is only reaching the default branch now.) 3. dist/ was gitignored. Now committed. On committing dist: I argued against this last turn on drift grounds, and the drift risk is real — but it is the wrong call for THIS tool. The bytes a stranger executes should be present and readable with no toolchain between them and the code, and the install should not depend on tsc behaving identically on someone else's machine. A verifier that only runs if your build works is not a verifier a stranger can use. The drift risk is handled rather than accepted: 'npm run verify:dist' rebuilds and fails if the committed output differs from source. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 - dist/canonicalize.d.ts | 6 + dist/canonicalize.d.ts.map | 1 + dist/canonicalize.js | 100 ++++++++++ dist/canonicalize.js.map | 1 + dist/canonicalize.test.d.ts | 2 + dist/canonicalize.test.d.ts.map | 1 + dist/canonicalize.test.js | 109 +++++++++++ dist/canonicalize.test.js.map | 1 + dist/cli.d.ts | 3 + dist/cli.d.ts.map | 1 + dist/cli.js | 298 +++++++++++++++++++++++++++++ dist/cli.js.map | 1 + dist/cli.test.d.ts | 2 + dist/cli.test.d.ts.map | 1 + dist/cli.test.js | 142 ++++++++++++++ dist/cli.test.js.map | 1 + dist/fetch-cert.d.ts | 8 + dist/fetch-cert.d.ts.map | 1 + dist/fetch-cert.js | 38 ++++ dist/fetch-cert.js.map | 1 + dist/hash.d.ts | 8 + dist/hash.d.ts.map | 1 + dist/hash.js | 24 +++ dist/hash.js.map | 1 + dist/index.d.ts | 7 + dist/index.d.ts.map | 1 + dist/index.js | 6 + dist/index.js.map | 1 + dist/keys.d.ts | 14 ++ dist/keys.d.ts.map | 1 + dist/keys.js | 69 +++++++ dist/keys.js.map | 1 + dist/receipt-vectors.test.d.ts | 18 ++ dist/receipt-vectors.test.d.ts.map | 1 + dist/receipt-vectors.test.js | 96 ++++++++++ dist/receipt-vectors.test.js.map | 1 + dist/receipt.d.ts | 57 ++++++ dist/receipt.d.ts.map | 1 + dist/receipt.js | 175 +++++++++++++++++ dist/receipt.js.map | 1 + dist/receipt.test.d.ts | 2 + dist/receipt.test.d.ts.map | 1 + dist/receipt.test.js | 107 +++++++++++ dist/receipt.test.js.map | 1 + dist/resolve.d.ts | 20 ++ dist/resolve.d.ts.map | 1 + dist/resolve.js | 87 +++++++++ dist/resolve.js.map | 1 + dist/types.d.ts | 51 +++++ dist/types.d.ts.map | 1 + dist/types.js | 13 ++ dist/types.js.map | 1 + dist/verify.d.ts | 3 + dist/verify.d.ts.map | 1 + dist/verify.js | 115 +++++++++++ dist/verify.js.map | 1 + dist/verify.test.d.ts | 2 + dist/verify.test.d.ts.map | 1 + dist/verify.test.js | 69 +++++++ dist/verify.test.js.map | 1 + package.json | 2 + 62 files changed, 1683 insertions(+), 1 deletion(-) create mode 100644 dist/canonicalize.d.ts create mode 100644 dist/canonicalize.d.ts.map create mode 100644 dist/canonicalize.js create mode 100644 dist/canonicalize.js.map create mode 100644 dist/canonicalize.test.d.ts create mode 100644 dist/canonicalize.test.d.ts.map create mode 100644 dist/canonicalize.test.js create mode 100644 dist/canonicalize.test.js.map create mode 100644 dist/cli.d.ts create mode 100644 dist/cli.d.ts.map create mode 100644 dist/cli.js create mode 100644 dist/cli.js.map create mode 100644 dist/cli.test.d.ts create mode 100644 dist/cli.test.d.ts.map create mode 100644 dist/cli.test.js create mode 100644 dist/cli.test.js.map create mode 100644 dist/fetch-cert.d.ts create mode 100644 dist/fetch-cert.d.ts.map create mode 100644 dist/fetch-cert.js create mode 100644 dist/fetch-cert.js.map create mode 100644 dist/hash.d.ts create mode 100644 dist/hash.d.ts.map create mode 100644 dist/hash.js create mode 100644 dist/hash.js.map create mode 100644 dist/index.d.ts create mode 100644 dist/index.d.ts.map create mode 100644 dist/index.js create mode 100644 dist/index.js.map create mode 100644 dist/keys.d.ts create mode 100644 dist/keys.d.ts.map create mode 100644 dist/keys.js create mode 100644 dist/keys.js.map create mode 100644 dist/receipt-vectors.test.d.ts create mode 100644 dist/receipt-vectors.test.d.ts.map create mode 100644 dist/receipt-vectors.test.js create mode 100644 dist/receipt-vectors.test.js.map create mode 100644 dist/receipt.d.ts create mode 100644 dist/receipt.d.ts.map create mode 100644 dist/receipt.js create mode 100644 dist/receipt.js.map create mode 100644 dist/receipt.test.d.ts create mode 100644 dist/receipt.test.d.ts.map create mode 100644 dist/receipt.test.js create mode 100644 dist/receipt.test.js.map create mode 100644 dist/resolve.d.ts create mode 100644 dist/resolve.d.ts.map create mode 100644 dist/resolve.js create mode 100644 dist/resolve.js.map create mode 100644 dist/types.d.ts create mode 100644 dist/types.d.ts.map create mode 100644 dist/types.js create mode 100644 dist/types.js.map create mode 100644 dist/verify.d.ts create mode 100644 dist/verify.d.ts.map create mode 100644 dist/verify.js create mode 100644 dist/verify.js.map create mode 100644 dist/verify.test.d.ts create mode 100644 dist/verify.test.d.ts.map create mode 100644 dist/verify.test.js create mode 100644 dist/verify.test.js.map diff --git a/.gitignore b/.gitignore index c2203f0..fcf2f22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules/ -dist/ *.log .DS_Store .env diff --git a/dist/canonicalize.d.ts b/dist/canonicalize.d.ts new file mode 100644 index 0000000..733d654 --- /dev/null +++ b/dist/canonicalize.d.ts @@ -0,0 +1,6 @@ +export type JsonValue = null | boolean | number | string | JsonValue[] | { + [k: string]: JsonValue; +}; +export declare function canonicalize(value: unknown): string; +export declare function canonicalizeToBytes(value: unknown): Uint8Array; +//# sourceMappingURL=canonicalize.d.ts.map \ No newline at end of file diff --git a/dist/canonicalize.d.ts.map b/dist/canonicalize.d.ts.map new file mode 100644 index 0000000..5035c38 --- /dev/null +++ b/dist/canonicalize.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"canonicalize.d.ts","sourceRoot":"","sources":["../src/canonicalize.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,SAAS,GACjB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,EAAE,GACX;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAE/B,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAE9D"} \ No newline at end of file diff --git a/dist/canonicalize.js b/dist/canonicalize.js new file mode 100644 index 0000000..3e7ef75 --- /dev/null +++ b/dist/canonicalize.js @@ -0,0 +1,100 @@ +// RFC 8785 — JSON Canonicalization Scheme (JCS). +// Hand-written so reviewers can confirm there is no surprise behavior. +// +// Rules summarized: +// - Object keys are sorted lexicographically by their UTF-16 code-unit sequence. +// - Strings are escaped using the minimal RFC 8259 §7 escapes (",\,\b,\f,\n,\r,\t) +// plus \u00XX for any other control character (U+0000..U+001F). +// - Numbers are emitted via the ECMAScript Number-to-String algorithm (ES2020 +// §7.1.12.1), which is what JSON.stringify already produces for finite numbers. +// Non-finite numbers (NaN, ±Infinity) MUST NOT appear in canonical JSON. +// - No insignificant whitespace anywhere. +// - Arrays preserve insertion order; null/true/false serialize as their literals. +export function canonicalize(value) { + return serialize(value); +} +export function canonicalizeToBytes(value) { + return new TextEncoder().encode(canonicalize(value)); +} +function serialize(value) { + if (value === null) + return "null"; + if (value === true) + return "true"; + if (value === false) + return "false"; + if (typeof value === "string") + return serializeString(value); + if (typeof value === "number") + return serializeNumber(value); + if (Array.isArray(value)) + return serializeArray(value); + if (typeof value === "object") + return serializeObject(value); + throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`); +} +function serializeArray(arr) { + const parts = []; + for (const item of arr) + parts.push(serialize(item)); + return "[" + parts.join(",") + "]"; +} +function serializeObject(obj) { + // RFC 8785 §3.2.3: sort by UTF-16 code units. JS strings are UTF-16, and the + // default Array#sort comparator on strings compares code-unit-by-code-unit, which + // is exactly the JCS requirement. + const keys = Object.keys(obj).filter((k) => obj[k] !== undefined).sort(); + const parts = []; + for (const k of keys) { + parts.push(serializeString(k) + ":" + serialize(obj[k])); + } + return "{" + parts.join(",") + "}"; +} +function serializeNumber(n) { + if (!Number.isFinite(n)) { + throw new RangeError(`canonicalize: non-finite number ${n}`); + } + // ECMAScript Number-to-String, which JSON.stringify already invokes for finite + // numbers. JCS aligns with this exact serialization. + if (Object.is(n, -0)) + return "0"; + return JSON.stringify(n); +} +function serializeString(s) { + let out = '"'; + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + switch (c) { + case 0x22: + out += '\\"'; + break; + case 0x5c: + out += "\\\\"; + break; + case 0x08: + out += "\\b"; + break; + case 0x09: + out += "\\t"; + break; + case 0x0a: + out += "\\n"; + break; + case 0x0c: + out += "\\f"; + break; + case 0x0d: + out += "\\r"; + break; + default: + if (c < 0x20) { + out += "\\u" + c.toString(16).padStart(4, "0"); + } + else { + out += s[i]; + } + } + } + return out + '"'; +} +//# sourceMappingURL=canonicalize.js.map \ No newline at end of file diff --git a/dist/canonicalize.js.map b/dist/canonicalize.js.map new file mode 100644 index 0000000..44fbbc6 --- /dev/null +++ b/dist/canonicalize.js.map @@ -0,0 +1 @@ +{"version":3,"file":"canonicalize.js","sourceRoot":"","sources":["../src/canonicalize.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/canonicalize.test.d.ts b/dist/canonicalize.test.d.ts new file mode 100644 index 0000000..088db96 --- /dev/null +++ b/dist/canonicalize.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=canonicalize.test.d.ts.map \ No newline at end of file diff --git a/dist/canonicalize.test.d.ts.map b/dist/canonicalize.test.d.ts.map new file mode 100644 index 0000000..cc6bbea --- /dev/null +++ b/dist/canonicalize.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"canonicalize.test.d.ts","sourceRoot":"","sources":["../src/canonicalize.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/canonicalize.test.js b/dist/canonicalize.test.js new file mode 100644 index 0000000..0ee04b3 --- /dev/null +++ b/dist/canonicalize.test.js @@ -0,0 +1,109 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { canonicalize, canonicalizeToBytes } from "./canonicalize.js"; +test("primitives", () => { + assert.equal(canonicalize(null), "null"); + assert.equal(canonicalize(true), "true"); + assert.equal(canonicalize(false), "false"); + assert.equal(canonicalize(0), "0"); + assert.equal(canonicalize(-0), "0"); + assert.equal(canonicalize(1), "1"); + assert.equal(canonicalize(1.5), "1.5"); +}); +test("strings — minimal RFC 8259 escapes", () => { + assert.equal(canonicalize(""), '""'); + assert.equal(canonicalize("hello"), '"hello"'); + assert.equal(canonicalize('a"b'), '"a\\"b"'); + assert.equal(canonicalize("a\\b"), '"a\\\\b"'); + assert.equal(canonicalize("a\nb"), '"a\\nb"'); + assert.equal(canonicalize("a\tb"), '"a\\tb"'); + assert.equal(canonicalize("a\rb"), '"a\\rb"'); + assert.equal(canonicalize("\b\f"), '"\\b\\f"'); + // Other control chars use \u00XX. + assert.equal(canonicalize(""), '"\\u0001"'); + assert.equal(canonicalize(""), '"\\u001f"'); + // Non-ASCII characters pass through unescaped (UTF-8 in the byte form). + assert.equal(canonicalize("ä"), '"ä"'); +}); +test("arrays — preserve order", () => { + assert.equal(canonicalize([]), "[]"); + assert.equal(canonicalize([1, 2, 3]), "[1,2,3]"); + assert.equal(canonicalize(["b", "a"]), '["b","a"]'); +}); +test("objects — keys sorted by UTF-16 code units", () => { + assert.equal(canonicalize({}), "{}"); + assert.equal(canonicalize({ b: 1, a: 2 }), '{"a":2,"b":1}'); + // RFC 8785 §3.2.3 sorting example (code-unit order, NOT Unicode codepoint order). + // The keys "ä" (ä) and "\" (\) and "€" (€) sort by UTF-16 units. + const input = { "€": "Euro", "ä": "a-umlaut", a: "ascii" }; + // Code-unit values: 0x61 (a), 0xe4 (ä), 0x20ac (€). + assert.equal(canonicalize(input), '"a":"ascii","ä":"a-umlaut","€":"Euro"'.replace(/^/, "{") + "}"); +}); +test("RFC 8785 §3.2.3 sample (sorting nested objects)", () => { + // Adapted from RFC 8785 example: keys including non-ASCII chars and surrogate pairs + // must compare as UTF-16 code-unit sequences. + const input = { + peach: "This sorting order", + péché: "is wrong according to French", + pêche: "but canonicalization MUST", + sin: "ignore locale", + }; + // Sort by UTF-16 code units of the keys. + // peach (p,e,a,c,h) — 0x70 0x65 ... + // péché (p,é=0xe9,c,h,é) + // pêche (p,ê=0xea,c,h,e) + // sin (s=0x73, ...) + // Order by first differing unit: peach < péché < pêche < sin. + const out = canonicalize(input); + assert.match(out, /^\{"peach":/); + // Confirm the four keys appear in the expected order. + const order = ["peach", "péché", "pêche", "sin"]; + let idx = -1; + for (const k of order) { + const next = out.indexOf(`"${k}":`); + assert.ok(next > idx, `expected ${k} after position ${idx}, got ${next}`); + idx = next; + } +}); +test("nested objects + arrays", () => { + const input = { z: [3, 2, 1], a: { y: 1, x: 2 } }; + assert.equal(canonicalize(input), '{"a":{"x":2,"y":1},"z":[3,2,1]}'); +}); +test("undefined keys are dropped", () => { + // RFC 8785 inputs come from JSON; JS undefined has no JSON encoding, so we drop. + const input = { a: 1, b: undefined, c: 3 }; + assert.equal(canonicalize(input), '{"a":1,"c":3}'); +}); +test("non-finite numbers throw", () => { + assert.throws(() => canonicalize(NaN), RangeError); + assert.throws(() => canonicalize(Infinity), RangeError); + assert.throws(() => canonicalize(-Infinity), RangeError); +}); +test("canonicalizeToBytes round-trips through UTF-8", () => { + const bytes = canonicalizeToBytes({ a: "ä" }); + assert.equal(new TextDecoder().decode(bytes), '{"a":"ä"}'); +}); +test("idempotent: canonicalize(JSON.parse(canonicalize(x))) === canonicalize(x)", () => { + const x = { z: 1, a: { c: [1, 2], b: "x" } }; + const once = canonicalize(x); + const twice = canonicalize(JSON.parse(once)); + assert.equal(once, twice); +}); +test("number formatting matches JSON.stringify for finite numbers", () => { + for (const n of [0, 1, -1, 1.5, -1.5, 1e20, 1e-7, 0.1 + 0.2]) { + assert.equal(canonicalize(n), JSON.stringify(n)); + } +}); +test("removes only the named field — signature stripping pattern", () => { + const cert = { a: 1, signature: "AAA", z: 9 }; + const { signature: _s, ...rest } = cert; + assert.equal(canonicalize(rest), '{"a":1,"z":9}'); +}); +test("array of objects sorts each object's keys independently", () => { + const input = [{ b: 1, a: 2 }, { d: 4, c: 3 }]; + assert.equal(canonicalize(input), '[{"a":2,"b":1},{"c":3,"d":4}]'); +}); +test("empty key string", () => { + assert.equal(canonicalize({ "": "x", a: "y" }), '{"":"x","a":"y"}'); +}); +//# sourceMappingURL=canonicalize.test.js.map \ No newline at end of file diff --git a/dist/canonicalize.test.js.map b/dist/canonicalize.test.js.map new file mode 100644 index 0000000..93087b2 --- /dev/null +++ b/dist/canonicalize.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"canonicalize.test.js","sourceRoot":"","sources":["../src/canonicalize.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cli.d.ts b/dist/cli.d.ts new file mode 100644 index 0000000..f8456c4 --- /dev/null +++ b/dist/cli.d.ts @@ -0,0 +1,3 @@ +#!/usr/bin/env node +export declare function main(argv: string[]): Promise; +//# sourceMappingURL=cli.d.ts.map \ No newline at end of file diff --git a/dist/cli.d.ts.map b/dist/cli.d.ts.map new file mode 100644 index 0000000..07e0e28 --- /dev/null +++ b/dist/cli.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAgEA,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAwG1D"} \ No newline at end of file diff --git a/dist/cli.js b/dist/cli.js new file mode 100644 index 0000000..5adce80 --- /dev/null +++ b/dist/cli.js @@ -0,0 +1,298 @@ +#!/usr/bin/env node +import { pathToFileURL } from "node:url"; +import { fetchCert } from "./fetch-cert.js"; +import { loadKeys } from "./keys.js"; +import { verifyCertificate } from "./verify.js"; +import { fetchReceipt, loadReceiptKey, verifyReceiptEnvelope } from "./receipt.js"; +import { resolveArtifactKind } from "./resolve.js"; +const HELP = `certifieddata-verify [options] + +Verify a CertifiedData.io certificate or Agent Commerce payment receipt. + +The artifact is fetched from CertifiedData, but the verdict is not: the +Ed25519 signature is verified locally against the published public key. + +Inputs: + certification UUID (resolved against the public API) + local certificate file + direct URL to a certificate JSON + - read certificate JSON from stdin + +Options: + --dataset recompute SHA-256 of dataset file and compare to cert.dataset_hash + --type force artifact kind: certificate | receipt (bare UUIDs + are probed against both public endpoints; if both exist + the CLI refuses to guess and requires --type) + --keys certificates: local keys document instead of .well-known + --key receipts: local Agent Commerce public-key PEM + --offline do not touch the network (requires --keys or a fresh cache) + --no-cache bypass ~/.certifieddata/keys.json cache + --json machine-readable output + --version print version + --help this message + +Exit codes: + 0 VALID 1 INVALID 2 UNKNOWN_KEY 3 MALFORMED + 4 NETWORK 64 USAGE`; +const EXIT = { + VALID: 0, INVALID: 1, UNKNOWN_KEY: 2, MALFORMED: 3, NETWORK: 4, USAGE: 64, +}; +const COLOR = process.stdout.isTTY && !process.env.NO_COLOR; +const c = { + green: (s) => COLOR ? `\x1b[32m${s}\x1b[0m` : s, + red: (s) => COLOR ? `\x1b[31m${s}\x1b[0m` : s, + yellow: (s) => COLOR ? `\x1b[33m${s}\x1b[0m` : s, + dim: (s) => COLOR ? `\x1b[2m${s}\x1b[0m` : s, +}; +export async function main(argv) { + let args; + try { + args = parseArgs(argv); + } + catch (e) { + process.stderr.write(`error: ${e.message}\n${HELP}\n`); + return EXIT.USAGE; + } + if (args.help) { + process.stdout.write(HELP + "\n"); + return EXIT.VALID; + } + if (args.version) { + process.stdout.write(await readVersion() + "\n"); + return EXIT.VALID; + } + if (args.positional.length !== 1) { + process.stderr.write(`error: expected exactly one certificate argument\n${HELP}\n`); + return EXIT.USAGE; + } + const target = args.positional[0]; + // ── Artifact-kind resolution (verify#2) ──────────────────────────────── + let kind; + if (args.type) { + kind = args.type; + } + else { + const resolved = await resolveArtifactKind(target, { offline: args.offline }); + if (resolved.kind === "ambiguous") { + process.stderr.write(`${c.yellow("? AMBIGUOUS")} ${target} exists as BOTH a certificate and a receipt.\n` + + ` Re-run with --type certificate or --type receipt.\n`); + return EXIT.USAGE; + } + if (resolved.kind === "not_found") { + process.stderr.write(`${c.red("✗ NOT_FOUND")} ${target} is neither a known certificate nor a known receipt.\n`); + return EXIT.MALFORMED; + } + if (resolved.kind === "transport_error") { + process.stderr.write(`${c.red("✗ NETWORK")} could not determine artifact kind for ${target} — an endpoint failed.\n` + + ` A server failure is not evidence of absence. Retry, or pass --type explicitly.\n`); + return EXIT.NETWORK; + } + kind = resolved.kind; + } + // ── Receipt path ─────────────────────────────────────────────────────── + if (kind === "receipt") { + let rres; + try { + const env = await fetchReceipt(target, { offline: args.offline }); + const pem = await loadReceiptKey({ keyFile: args.key, offline: args.offline }); + rres = verifyReceiptEnvelope(env, pem); + } + catch (err) { + const reason = err.message; + const keyUnavailable = /public key unavailable|requires --key/i.test(reason); + const isNetwork = /HTTP \d|ENOTFOUND|ECONN|getaddrinfo|fetch/i.test(reason); + if (args.json) { + process.stdout.write(JSON.stringify({ artifact_type: "receipt", artifact_id: null, verdict: keyUnavailable ? "UNKNOWN_KEY" : "MALFORMED", reason }) + "\n"); + } + else { + const tag = keyUnavailable ? c.yellow("? KEY_UNAVAILABLE") : c.red("✗ ERROR"); + process.stderr.write(`${tag} ${reason}\n`); + if (keyUnavailable) { + process.stderr.write(` ${c.dim("Independent verification is impossible without the published key —")}\n`); + process.stderr.write(` ${c.dim("the server's own verdict is NOT accepted as a substitute.")}\n`); + } + } + return keyUnavailable ? EXIT.UNKNOWN_KEY : isNetwork ? EXIT.NETWORK : EXIT.MALFORMED; + } + if (args.json) { + process.stdout.write(JSON.stringify(rres) + "\n"); + } + else { + printReceiptHuman(rres); + } + switch (rres.verdict) { + case "VALID": return EXIT.VALID; + case "INVALID": return EXIT.INVALID; + case "UNKNOWN_KEY": return EXIT.UNKNOWN_KEY; + case "MALFORMED": return EXIT.MALFORMED; + } + } + // ── Certificate path (unchanged behavior) ────────────────────────────── + let result; + try { + const cert = await fetchCert(target, { offline: args.offline }); + const keys = await loadKeys({ keysFile: args.keys, offline: args.offline, noCache: args.noCache }); + result = await verifyCertificate(cert, keys, args.dataset); + } + catch (err) { + const reason = err.message; + const isNetwork = /failed to fetch|HTTP \d|ENOTFOUND|ECONN|getaddrinfo/i.test(reason); + if (args.json) { + process.stdout.write(JSON.stringify(networkErrorResult(reason)) + "\n"); + } + else { + process.stderr.write(`${c.red("✗ ERROR")} ${reason}\n`); + } + return isNetwork ? EXIT.NETWORK : EXIT.MALFORMED; + } + if (args.json) { + process.stdout.write(JSON.stringify(result) + "\n"); + } + else { + printHuman(result); + } + return verdictToExit(result); +} +function parseArgs(argv) { + const out = { positional: [], json: false, offline: false, noCache: false, help: false, version: false }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + switch (a) { + case "--help": + case "-h": + out.help = true; + break; + case "--version": + case "-v": + out.version = true; + break; + case "--json": + out.json = true; + break; + case "--offline": + out.offline = true; + break; + case "--no-cache": + out.noCache = true; + break; + case "--dataset": + out.dataset = requireValue(argv, ++i, a); + break; + case "--keys": + out.keys = requireValue(argv, ++i, a); + break; + case "--key": + out.key = requireValue(argv, ++i, a); + break; + case "--type": { + const v = requireValue(argv, ++i, a); + if (v !== "certificate" && v !== "receipt") + throw new Error("--type must be certificate or receipt"); + out.type = v; + break; + } + default: + if (a.startsWith("--")) + throw new Error(`unknown option: ${a}`); + out.positional.push(a); + } + } + return out; +} +function requireValue(argv, i, flag) { + const v = argv[i]; + if (v === undefined) + throw new Error(`${flag} requires a value`); + return v; +} +function verdictToExit(r) { + switch (r.verdict) { + case "VALID": return EXIT.VALID; + case "INVALID": + case "DATASET_MISMATCH": return EXIT.INVALID; + case "UNKNOWN_KEY": return EXIT.UNKNOWN_KEY; + case "MALFORMED": return EXIT.MALFORMED; + } +} +function printHuman(r) { + const id = r.certification_id ?? "(unknown)"; + switch (r.verdict) { + case "VALID": { + process.stdout.write(`${c.green("✓ VALID")} certification_id ${id}\n`); + const label = r.key_label ? `${r.key_id} (${r.issuer}, ${r.key_label})` : `${r.key_id} (${r.issuer})`; + process.stdout.write(` ${c.dim("signed by")} ${label}\n`); + const rows = (r.rows ?? 0).toLocaleString("en-US"); + const cols = (r.columns ?? 0).toLocaleString("en-US"); + process.stdout.write(` ${c.dim("algorithm")} ${r.algorithm} · ${rows} rows × ${cols} cols · signed ${r.signed_at}\n`); + if (r.checks.dataset_match === "pass") { + process.stdout.write(` ${c.dim("dataset")} ${r.dataset_hash_actual} ${c.green("matches")}\n`); + } + break; + } + case "INVALID": + process.stdout.write(`${c.red("✗ INVALID")} certification_id ${id}\n ${r.reason}\n`); + break; + case "DATASET_MISMATCH": + process.stdout.write(`${c.red("✗ DATASET_MISMATCH")} certification_id ${id}\n`); + process.stdout.write(` expected ${r.dataset_hash_expected}\n actual ${r.dataset_hash_actual}\n`); + break; + case "UNKNOWN_KEY": + process.stdout.write(`${c.yellow("? UNKNOWN_KEY")} certification_id ${id}\n ${r.reason}\n`); + break; + case "MALFORMED": + process.stdout.write(`${c.red("✗ MALFORMED")} ${r.reason}\n`); + break; + } +} +function printReceiptHuman(r) { + const id = r.artifact_id ?? "(unknown)"; + switch (r.verdict) { + case "VALID": { + process.stdout.write(`${c.green("✓ VALID")} receipt ${id}\n`); + process.stdout.write(` ${c.dim("signed by")} ${r.key_id ?? "(published Agent Commerce key)"} (${r.issuer ?? "CertifiedData.io"})\n`); + process.stdout.write(` ${c.dim("signature")} ${r.checks.signature}\n`); + process.stdout.write(` ${c.dim("payload hash")} ${r.checks.payload_hash}\n`); + process.stdout.write(` ${c.dim("public key")} /.well-known/certifieddata-public-key.pem\n`); + if (r.settlement_state) { + process.stdout.write(` ${c.dim("settlement")} ${r.settlement_state}\n`); + } + process.stdout.write(` ${c.dim("The verdict above was computed locally — not taken from the server.")}\n`); + break; + } + case "INVALID": + process.stdout.write(`${c.red("✗ INVALID")} receipt ${id}\n ${r.reason}\n`); + break; + case "UNKNOWN_KEY": + process.stdout.write(`${c.yellow("? UNKNOWN_KEY")} receipt ${id}\n ${r.reason}\n`); + break; + case "MALFORMED": + process.stdout.write(`${c.red("✗ MALFORMED")} ${r.reason}\n`); + break; + } +} +function networkErrorResult(reason) { + return { + verdict: "MALFORMED", + certification_id: null, key_id: null, issuer: null, algorithm: null, signed_at: null, + dataset_hash_expected: null, dataset_hash_actual: null, + checks: { signature: "skipped", key_trust: "skipped", dataset_match: "skipped" }, + reason, + }; +} +async function readVersion() { + try { + const { readFile } = await import("node:fs/promises"); + const { fileURLToPath } = await import("node:url"); + const { dirname, join } = await import("node:path"); + const here = dirname(fileURLToPath(import.meta.url)); + const pkg = JSON.parse(await readFile(join(here, "..", "package.json"), "utf8")); + return `@certifieddata/verify ${pkg.version}`; + } + catch { + return "@certifieddata/verify (unknown version)"; + } +} +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main(process.argv.slice(2)).then((code) => process.exit(code)); +} +//# sourceMappingURL=cli.js.map \ No newline at end of file diff --git a/dist/cli.js.map b/dist/cli.js.map new file mode 100644 index 0000000..a1f2714 --- /dev/null +++ b/dist/cli.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAA4B,MAAM,cAAc,CAAC;AAC7G,OAAO,EAAE,mBAAmB,EAAqB,MAAM,cAAc,CAAC;AAgBtE,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA4BU,CAAC;AAExB,MAAM,IAAI,GAAG;IACX,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE;CACjE,CAAC;AAEX,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;AAC5D,MAAM,CAAC,GAAG;IACR,KAAK,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IACvD,GAAG,EAAI,CAAC,CAAS,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IACvD,MAAM,EAAC,CAAC,CAAS,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IACvD,GAAG,EAAI,CAAC,CAAS,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CACvD,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAc;IACvC,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QAAC,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAW,CAAW,CAAC,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC;QAClE,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;QAAC,OAAO,IAAI,CAAC,KAAK,CAAC;IAAC,CAAC;IACxE,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,WAAW,EAAE,GAAG,IAAI,CAAC,CAAC;QAAC,OAAO,IAAI,CAAC,KAAK,CAAC;IAAC,CAAC;IAE1F,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qDAAqD,IAAI,IAAI,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAElC,0EAA0E;IAC1E,IAAI,IAAkB,CAAC;IACvB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;SAAM,CAAC;QACN,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC9E,IAAI,QAAQ,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAClC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,GAAG,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,MAAM,gDAAgD;gBACnF,uDAAuD,CAC1D,CAAC;YACF,OAAO,IAAI,CAAC,KAAK,CAAC;QACpB,CAAC;QACD,IAAI,QAAQ,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAClC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,MAAM,wDAAwD,CAAC,CAAC;YACjH,OAAO,IAAI,CAAC,SAAS,CAAC;QACxB,CAAC;QACD,IAAI,QAAQ,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;YACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,2CAA2C,MAAM,0BAA0B;gBAC9F,oFAAoF,CACvF,CAAC;YACF,OAAO,IAAI,CAAC,OAAO,CAAC;QACtB,CAAC;QACD,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,0EAA0E;IAC1E,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,IAAyB,CAAC;QAC9B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YAClE,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YAC/E,IAAI,GAAG,qBAAqB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAI,GAAa,CAAC,OAAO,CAAC;YACtC,MAAM,cAAc,GAAG,wCAAwC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC7E,MAAM,SAAS,GAAG,4CAA4C,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC5E,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YAC9J,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC9E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;gBAC5C,IAAI,cAAc,EAAE,CAAC;oBACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,oEAAoE,CAAC,IAAI,CAAC,CAAC;oBAC3G,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,2DAA2D,CAAC,IAAI,CAAC,CAAC;gBACpG,CAAC;YACH,CAAC;YACD,OAAO,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;QACvF,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACpD,CAAC;aAAM,CAAC;YACN,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,QAAQ,IAAI,CAAC,OAAO,EAAE,CAAC;YACrB,KAAK,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC;YAChC,KAAK,SAAS,CAAC,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC;YACpC,KAAK,aAAa,CAAC,CAAC,OAAO,IAAI,CAAC,WAAW,CAAC;YAC5C,KAAK,WAAW,CAAC,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,0EAA0E;IAC1E,IAAI,MAAoB,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAChE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACnG,MAAM,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,GAAI,GAAa,CAAC,OAAO,CAAC;QACtC,MAAM,SAAS,GAAG,sDAAsD,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACtF,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC1E,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;IACnD,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IACtD,CAAC;SAAM,CAAC;QACN,UAAU,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,GAAG,GAAY,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAClH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,QAAQ,CAAC,EAAE,CAAC;YACV,KAAK,QAAQ,CAAC;YAAC,KAAK,IAAI;gBAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;gBAAC,MAAM;YACjD,KAAK,WAAW,CAAC;YAAC,KAAK,IAAI;gBAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;gBAAC,MAAM;YACvD,KAAK,QAAQ;gBAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;gBAAC,MAAM;YACtC,KAAK,WAAW;gBAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;gBAAC,MAAM;YAC5C,KAAK,YAAY;gBAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;gBAAC,MAAM;YAC7C,KAAK,WAAW;gBAAE,GAAG,CAAC,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;gBAAC,MAAM;YAClE,KAAK,QAAQ;gBAAE,GAAG,CAAC,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;gBAAC,MAAM;YAC5D,KAAK,OAAO;gBAAE,GAAG,CAAC,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;gBAAC,MAAM;YAC1D,KAAK,QAAQ,CAAC,CAAC,CAAC;gBACd,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;gBACrC,IAAI,CAAC,KAAK,aAAa,IAAI,CAAC,KAAK,SAAS;oBAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;gBACrG,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;gBAAC,MAAM;YACtB,CAAC;YACD;gBACE,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;gBAChE,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,YAAY,CAAC,IAAc,EAAE,CAAS,EAAE,IAAY;IAC3D,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,IAAI,CAAC,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,mBAAmB,CAAC,CAAC;IACjE,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,aAAa,CAAC,CAAe;IACpC,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC;QAClB,KAAK,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC;QAChC,KAAK,SAAS,CAAC;QAAC,KAAK,kBAAkB,CAAC,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC;QAC7D,KAAK,aAAa,CAAC,CAAC,OAAO,IAAI,CAAC,WAAW,CAAC;QAC5C,KAAK,WAAW,CAAC,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC;IAC1C,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,CAAe;IACjC,MAAM,EAAE,GAAG,CAAC,CAAC,gBAAgB,IAAI,WAAW,CAAC;IAC7C,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC;QAClB,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC;YACxE,MAAM,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC;YACxG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;YAC5D,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YACnD,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,SAAS,QAAQ,IAAI,WAAW,IAAI,oBAAoB,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC;YAC5H,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,KAAK,MAAM,EAAE,CAAC;gBACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,mBAAmB,IAAI,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YACpG,CAAC;YACD,MAAM;QACR,CAAC;QACD,KAAK,SAAS;YACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;YACvF,MAAM;QACR,KAAK,kBAAkB;YACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC;YACjF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,qBAAqB,gBAAgB,CAAC,CAAC,mBAAmB,IAAI,CAAC,CAAC;YACrG,MAAM;QACR,KAAK,aAAa;YAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;YAC9F,MAAM;QACR,KAAK,WAAW;YACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;YAC/D,MAAM;IACV,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,CAAsB;IAC/C,MAAM,EAAE,GAAG,CAAC,CAAC,WAAW,IAAI,WAAW,CAAC;IACxC,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC;QAClB,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;YAC/D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,IAAI,gCAAgC,KAAK,CAAC,CAAC,MAAM,IAAI,kBAAkB,KAAK,CAAC,CAAC;YACzI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;YAC3E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,CAAC;YAC9E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,gDAAgD,CAAC,CAAC;YAC/F,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;gBACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC;YAC7E,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,qEAAqE,CAAC,IAAI,CAAC,CAAC;YAC5G,MAAM;QACR,CAAC;QACD,KAAK,SAAS;YACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;YAC9E,MAAM;QACR,KAAK,aAAa;YAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;YACrF,MAAM;QACR,KAAK,WAAW;YACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;YAC/D,MAAM;IACV,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAc;IACxC,OAAO;QACL,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI;QACpF,qBAAqB,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI;QACtD,MAAM,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE;QAChF,MAAM;KACP,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,WAAW;IACxB,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QACtD,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;QACnD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACrD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACjF,OAAO,yBAAyB,GAAG,CAAC,OAAO,EAAE,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,yCAAyC,CAAC;IAAC,CAAC;AAC/D,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/E,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACjE,CAAC"} \ No newline at end of file diff --git a/dist/cli.test.d.ts b/dist/cli.test.d.ts new file mode 100644 index 0000000..9e8ffec --- /dev/null +++ b/dist/cli.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=cli.test.d.ts.map \ No newline at end of file diff --git a/dist/cli.test.d.ts.map b/dist/cli.test.d.ts.map new file mode 100644 index 0000000..3498769 --- /dev/null +++ b/dist/cli.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../src/cli.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/cli.test.js b/dist/cli.test.js new file mode 100644 index 0000000..fc0d2bd --- /dev/null +++ b/dist/cli.test.js @@ -0,0 +1,142 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +const here = dirname(fileURLToPath(import.meta.url)); +const cliPath = join(here, "cli.js"); +const fixturesDir = join(here, "..", "fixtures"); +const keysPath = join(fixturesDir, "keys.json"); +function run(args, input) { + return new Promise((resolve) => { + const env = { ...process.env, NO_COLOR: "1" }; + const child = spawn(process.execPath, [cliPath, ...args], { env }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (d) => (stdout += d.toString())); + child.stderr.on("data", (d) => (stderr += d.toString())); + if (input !== undefined) { + child.stdin.write(input); + child.stdin.end(); + } + child.on("close", (code) => resolve({ stdout, stderr, code: code ?? -1 })); + }); +} +test("VALID — exits 0 against a clean cert + dataset", async () => { + const r = await run([ + join(fixturesDir, "valid-cert.json"), + "--keys", keysPath, + "--offline", + "--dataset", join(fixturesDir, "valid-dataset.csv"), + ]); + assert.equal(r.code, 0, `stderr: ${r.stderr}`); + assert.match(r.stdout, /VALID/); +}); +test("INVALID — exits 1 against a tampered cert", async () => { + const r = await run([ + join(fixturesDir, "tampered-cert.json"), + "--keys", keysPath, + "--offline", + ]); + assert.equal(r.code, 1); + assert.match(r.stdout, /INVALID/); +}); +test("UNKNOWN_KEY — exits 2 when key_id is not in trusted set", async () => { + const r = await run([ + join(fixturesDir, "unknown-key-cert.json"), + "--keys", keysPath, + "--offline", + ]); + assert.equal(r.code, 2); + assert.match(r.stdout, /UNKNOWN_KEY/); +}); +test("MALFORMED — exits 3 when required fields are missing", async () => { + const r = await run([ + join(fixturesDir, "malformed-cert.json"), + "--keys", keysPath, + "--offline", + ]); + assert.equal(r.code, 3); + assert.match(r.stdout, /MALFORMED/); +}); +test("DATASET_MISMATCH — exits 1 with a clear reason", async () => { + const r = await run([ + join(fixturesDir, "valid-cert.json"), + "--keys", keysPath, + "--offline", + "--dataset", keysPath, // wrong file -> wrong hash + ]); + assert.equal(r.code, 1); + assert.match(r.stdout, /DATASET_MISMATCH/); +}); +test("--json — emits a structured result with all expected fields", async () => { + const r = await run([ + join(fixturesDir, "valid-cert.json"), + "--keys", keysPath, + "--offline", + "--json", + ]); + assert.equal(r.code, 0); + const parsed = JSON.parse(r.stdout); + assert.equal(parsed.verdict, "VALID"); + assert.equal(parsed.issuer, "CertifiedData.io"); + assert.equal(parsed.checks.signature, "pass"); + assert.equal(parsed.checks.key_trust, "pass"); + assert.equal(parsed.checks.dataset_match, "skipped"); + assert.ok(typeof parsed.certification_id === "string"); +}); +test("--json on tampered cert returns verdict INVALID with signature=fail", async () => { + const r = await run([ + join(fixturesDir, "tampered-cert.json"), + "--keys", keysPath, + "--offline", + "--json", + ]); + assert.equal(r.code, 1); + const parsed = JSON.parse(r.stdout); + assert.equal(parsed.verdict, "INVALID"); + assert.equal(parsed.checks.signature, "fail"); +}); +test("--help — exits 0 and prints usage", async () => { + const r = await run(["--help"]); + assert.equal(r.code, 0); + assert.match(r.stdout, /certifieddata-verify/); + assert.match(r.stdout, /Exit codes/); +}); +test("--version — exits 0 and prints package version", async () => { + const r = await run(["--version"]); + assert.equal(r.code, 0); + assert.match(r.stdout, /@certifieddata\/verify/); +}); +test("USAGE — exits 64 on unknown flag", async () => { + const r = await run(["--made-up-flag"]); + assert.equal(r.code, 64); + assert.match(r.stderr, /unknown option/); +}); +test("USAGE — exits 64 when no positional argument is given", async () => { + const r = await run(["--keys", keysPath, "--offline"]); + assert.equal(r.code, 64); +}); +test("stdin input via '-' — exits 0 against a piped valid cert", async () => { + const { readFile } = await import("node:fs/promises"); + const certBody = await readFile(join(fixturesDir, "valid-cert.json"), "utf8"); + const r = await run(["-", "--keys", keysPath, "--offline"], certBody); + assert.equal(r.code, 0, `stderr: ${r.stderr}`); + assert.match(r.stdout, /VALID/); +}); +test("checks pass/fail/skipped for each verdict in --json mode", async () => { + const cases = [ + ["valid-cert.json", "VALID", { signature: "pass", key_trust: "pass", dataset_match: "skipped" }], + ["tampered-cert.json", "INVALID", { signature: "fail", key_trust: "pass", dataset_match: "skipped" }], + ["unknown-key-cert.json", "UNKNOWN_KEY", { signature: "skipped", key_trust: "fail", dataset_match: "skipped" }], + ]; + for (const [file, expected, checks] of cases) { + const r = await run([join(fixturesDir, file), "--keys", keysPath, "--offline", "--json"]); + const parsed = JSON.parse(r.stdout); + assert.equal(parsed.verdict, expected, `case ${file}`); + for (const [k, v] of Object.entries(checks)) { + assert.equal(parsed.checks[k], v, `${file}.checks.${k}`); + } + } +}); +//# sourceMappingURL=cli.test.js.map \ No newline at end of file diff --git a/dist/cli.test.js.map b/dist/cli.test.js.map new file mode 100644 index 0000000..1840cba --- /dev/null +++ b/dist/cli.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../src/cli.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/fetch-cert.d.ts b/dist/fetch-cert.d.ts new file mode 100644 index 0000000..a658b62 --- /dev/null +++ b/dist/fetch-cert.d.ts @@ -0,0 +1,8 @@ +import type { Certificate } from "./types.js"; +export declare const DEFAULT_CERT_API = "https://certifieddata.io/api/v1/certificates"; +export interface FetchCertOptions { + apiBase?: string; + offline?: boolean; +} +export declare function fetchCert(idOrPathOrUrl: string, opts?: FetchCertOptions): Promise; +//# sourceMappingURL=fetch-cert.d.ts.map \ No newline at end of file diff --git a/dist/fetch-cert.d.ts.map b/dist/fetch-cert.d.ts.map new file mode 100644 index 0000000..fe49df0 --- /dev/null +++ b/dist/fetch-cert.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"fetch-cert.d.ts","sourceRoot":"","sources":["../src/fetch-cert.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAE/E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,SAAS,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,WAAW,CAAC,CAiBxG"} \ No newline at end of file diff --git a/dist/fetch-cert.js b/dist/fetch-cert.js new file mode 100644 index 0000000..beeeb4c --- /dev/null +++ b/dist/fetch-cert.js @@ -0,0 +1,38 @@ +import { readFile } from "node:fs/promises"; +export const DEFAULT_CERT_API = "https://certifieddata.io/api/v1/certificates"; +export async function fetchCert(idOrPathOrUrl, opts = {}) { + if (idOrPathOrUrl === "-") { + return parseCertJson(await readStdin()); + } + if (idOrPathOrUrl.endsWith(".json") || idOrPathOrUrl.startsWith("./") || idOrPathOrUrl.startsWith("/")) { + return parseCertJson(await readFile(idOrPathOrUrl, "utf8")); + } + if (/^https?:\/\//.test(idOrPathOrUrl)) { + if (opts.offline) + throw new Error("cannot fetch URL in --offline mode"); + return parseCertJson(await fetchText(idOrPathOrUrl)); + } + if (opts.offline) { + throw new Error("cannot resolve certification id in --offline mode (pass a local file)"); + } + const base = opts.apiBase ?? DEFAULT_CERT_API; + const url = `${base.replace(/\/$/, "")}/${encodeURIComponent(idOrPathOrUrl)}`; + return parseCertJson(await fetchText(url)); +} +async function fetchText(url) { + const res = await fetch(url); + if (!res.ok) + throw new Error(`HTTP ${res.status} fetching ${url}`); + return res.text(); +} +function parseCertJson(body) { + return JSON.parse(body); +} +async function readStdin() { + const chunks = []; + for await (const chunk of process.stdin) { + chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); + } + return Buffer.concat(chunks).toString("utf8"); +} +//# sourceMappingURL=fetch-cert.js.map \ No newline at end of file diff --git a/dist/fetch-cert.js.map b/dist/fetch-cert.js.map new file mode 100644 index 0000000..519366c --- /dev/null +++ b/dist/fetch-cert.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fetch-cert.js","sourceRoot":"","sources":["../src/fetch-cert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,8CAA8C,CAAC;AAO/E,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,aAAqB,EAAE,OAAyB,EAAE;IAChF,IAAI,aAAa,KAAK,GAAG,EAAE,CAAC;QAC1B,OAAO,aAAa,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,aAAa,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,aAAa,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACvG,OAAO,aAAa,CAAC,MAAM,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACxE,OAAO,aAAa,CAAC,MAAM,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IAC3F,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC;IAC9C,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,kBAAkB,CAAC,aAAa,CAAC,EAAE,CAAC;IAC9E,OAAO,aAAa,CAAC,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,GAAW;IAClC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC,CAAC;IACnE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;AACpB,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgB,CAAC;AACzC,CAAC;AAED,KAAK,UAAU,SAAS;IACtB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAe,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC"} \ No newline at end of file diff --git a/dist/hash.d.ts b/dist/hash.d.ts new file mode 100644 index 0000000..0002922 --- /dev/null +++ b/dist/hash.d.ts @@ -0,0 +1,8 @@ +export declare function sha256Hex(bytes: Uint8Array | string): string; +export declare function sha256File(path: string): Promise; +export declare function formatDigest(hex: string): string; +export declare function parseDigest(value: string): { + algo: string; + hex: string; +}; +//# sourceMappingURL=hash.d.ts.map \ No newline at end of file diff --git a/dist/hash.d.ts.map b/dist/hash.d.ts.map new file mode 100644 index 0000000..ad6b8f5 --- /dev/null +++ b/dist/hash.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAGA,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ9D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAIxE"} \ No newline at end of file diff --git a/dist/hash.js b/dist/hash.js new file mode 100644 index 0000000..65d4f1e --- /dev/null +++ b/dist/hash.js @@ -0,0 +1,24 @@ +import { createHash } from "node:crypto"; +import { createReadStream } from "node:fs"; +export function sha256Hex(bytes) { + return createHash("sha256").update(bytes).digest("hex"); +} +export async function sha256File(path) { + return new Promise((resolve, reject) => { + const h = createHash("sha256"); + const s = createReadStream(path); + s.on("error", reject); + s.on("data", (chunk) => h.update(chunk)); + s.on("end", () => resolve(h.digest("hex"))); + }); +} +export function formatDigest(hex) { + return `sha256:${hex}`; +} +export function parseDigest(value) { + const m = /^([a-z0-9-]+):([0-9a-f]+)$/i.exec(value); + if (!m) + throw new Error(`malformed digest: ${value}`); + return { algo: m[1].toLowerCase(), hex: m[2].toLowerCase() }; +} +//# sourceMappingURL=hash.js.map \ No newline at end of file diff --git a/dist/hash.js.map b/dist/hash.js.map new file mode 100644 index 0000000..56d85c2 --- /dev/null +++ b/dist/hash.js.map @@ -0,0 +1 @@ +{"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE3C,MAAM,UAAU,SAAS,CAAC,KAA0B;IAClD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC/B,MAAM,CAAC,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACtB,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACzC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,OAAO,UAAU,GAAG,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,CAAC,GAAG,6BAA6B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,KAAK,EAAE,CAAC,CAAC;IACtD,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;AAC/D,CAAC"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..9630093 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,7 @@ +export { verifyCertificate } from "./verify.js"; +export { canonicalize, canonicalizeToBytes } from "./canonicalize.js"; +export { sha256Hex, sha256File, formatDigest, parseDigest } from "./hash.js"; +export { loadKeys, findKey, DEFAULT_KEYS_URL } from "./keys.js"; +export { fetchCert, DEFAULT_CERT_API } from "./fetch-cert.js"; +export type { Certificate, KeyDoc, KeyEntry, VerifyResult, Verdict, CheckResult, } from "./types.js"; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map new file mode 100644 index 0000000..77a1e8a --- /dev/null +++ b/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9D,YAAY,EACV,WAAW,EACX,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,WAAW,GACZ,MAAM,YAAY,CAAC"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..21c58f6 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,6 @@ +export { verifyCertificate } from "./verify.js"; +export { canonicalize, canonicalizeToBytes } from "./canonicalize.js"; +export { sha256Hex, sha256File, formatDigest, parseDigest } from "./hash.js"; +export { loadKeys, findKey, DEFAULT_KEYS_URL } from "./keys.js"; +export { fetchCert, DEFAULT_CERT_API } from "./fetch-cert.js"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..2b665bc --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"} \ No newline at end of file diff --git a/dist/keys.d.ts b/dist/keys.d.ts new file mode 100644 index 0000000..2d97bf2 --- /dev/null +++ b/dist/keys.d.ts @@ -0,0 +1,14 @@ +import type { KeyDoc, KeyEntry } from "./types.js"; +export declare const DEFAULT_KEYS_URL = "https://certifieddata.io/.well-known/certifieddata-keys.json"; +export declare const CACHE_PATH: string; +export declare const CACHE_TTL_MS: number; +export interface LoadKeysOptions { + url?: string; + keysFile?: string; + noCache?: boolean; + offline?: boolean; + cachePath?: string; +} +export declare function loadKeys(opts?: LoadKeysOptions): Promise; +export declare function findKey(doc: KeyDoc, keyId: string): KeyEntry | undefined; +//# sourceMappingURL=keys.d.ts.map \ No newline at end of file diff --git a/dist/keys.d.ts.map b/dist/keys.d.ts.map new file mode 100644 index 0000000..6fc6d06 --- /dev/null +++ b/dist/keys.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAO,MAAM,gBAAgB,iEAAiE,CAAC;AAC/F,eAAO,MAAM,UAAU,QAAiD,CAAC;AACzE,eAAO,MAAM,YAAY,QAAsB,CAAC;AAEhD,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoC1E;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAExE"} \ No newline at end of file diff --git a/dist/keys.js b/dist/keys.js new file mode 100644 index 0000000..7be5938 --- /dev/null +++ b/dist/keys.js @@ -0,0 +1,69 @@ +import { readFile, writeFile, mkdir, stat } from "node:fs/promises"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +export const DEFAULT_KEYS_URL = "https://certifieddata.io/.well-known/certifieddata-keys.json"; +export const CACHE_PATH = join(homedir(), ".certifieddata", "keys.json"); +export const CACHE_TTL_MS = 24 * 60 * 60 * 1000; +export async function loadKeys(opts = {}) { + if (opts.keysFile) { + return parseKeyDoc(await readFile(opts.keysFile, "utf8")); + } + const cachePath = opts.cachePath ?? CACHE_PATH; + if (opts.offline) { + if (opts.noCache) { + throw new Error("offline mode requires --keys when --no-cache is set"); + } + return parseKeyDoc(await readFile(cachePath, "utf8")); + } + if (!opts.noCache) { + const fresh = await readCacheIfFresh(cachePath); + if (fresh) + return fresh; + } + const url = opts.url ?? DEFAULT_KEYS_URL; + let body; + try { + const res = await fetch(url); + if (!res.ok) + throw new Error(`HTTP ${res.status}`); + body = await res.text(); + } + catch (err) { + if (!opts.noCache) { + const stale = await readFile(cachePath, "utf8").catch(() => null); + if (stale) + return parseKeyDoc(stale); + } + throw new Error(`failed to fetch keys from ${url}: ${err.message}`); + } + const doc = parseKeyDoc(body); + if (!opts.noCache) + await writeCache(cachePath, body); + return doc; +} +export function findKey(doc, keyId) { + return doc.keys.find((k) => k.key_id === keyId); +} +function parseKeyDoc(body) { + const parsed = JSON.parse(body); + if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.keys)) { + throw new Error("invalid key document: missing keys[]"); + } + return parsed; +} +async function readCacheIfFresh(path) { + try { + const s = await stat(path); + if (Date.now() - s.mtimeMs > CACHE_TTL_MS) + return null; + return parseKeyDoc(await readFile(path, "utf8")); + } + catch { + return null; + } +} +async function writeCache(path, body) { + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, body, "utf8"); +} +//# sourceMappingURL=keys.js.map \ No newline at end of file diff --git a/dist/keys.js.map b/dist/keys.js.map new file mode 100644 index 0000000..621ac04 --- /dev/null +++ b/dist/keys.js.map @@ -0,0 +1 @@ +{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/receipt-vectors.test.d.ts b/dist/receipt-vectors.test.d.ts new file mode 100644 index 0000000..f5d48a9 --- /dev/null +++ b/dist/receipt-vectors.test.d.ts @@ -0,0 +1,18 @@ +/** + * Receipt signature test vectors. + * + * These existed for webhook-signature, idempotency, provenance and events, but + * not for receipts — so an outside implementer had nothing to check their + * attempt against, and the canonicalization was never pinned to a concrete + * expected hash anywhere in the repo. + * + * The tampered vector is the one that matters. An implementation that reports + * VALID for it is not verifying anything: it has an Ed25519 signature that is + * genuine, over a payload that has been altered. + * + * Fixtures are captured from production receipt + * 2492a060-8fbc-40ae-beab-7258aefb0608 — a $0.99 certificate-linked dataset + * purchase on the live rail. + */ +export {}; +//# sourceMappingURL=receipt-vectors.test.d.ts.map \ No newline at end of file diff --git a/dist/receipt-vectors.test.d.ts.map b/dist/receipt-vectors.test.d.ts.map new file mode 100644 index 0000000..54ff67f --- /dev/null +++ b/dist/receipt-vectors.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"receipt-vectors.test.d.ts","sourceRoot":"","sources":["../src/receipt-vectors.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"} \ No newline at end of file diff --git a/dist/receipt-vectors.test.js b/dist/receipt-vectors.test.js new file mode 100644 index 0000000..31c7c39 --- /dev/null +++ b/dist/receipt-vectors.test.js @@ -0,0 +1,96 @@ +/** + * Receipt signature test vectors. + * + * These existed for webhook-signature, idempotency, provenance and events, but + * not for receipts — so an outside implementer had nothing to check their + * attempt against, and the canonicalization was never pinned to a concrete + * expected hash anywhere in the repo. + * + * The tampered vector is the one that matters. An implementation that reports + * VALID for it is not verifying anything: it has an Ed25519 signature that is + * genuine, over a payload that has been altered. + * + * Fixtures are captured from production receipt + * 2492a060-8fbc-40ae-beab-7258aefb0608 — a $0.99 certificate-linked dataset + * purchase on the live rail. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { createHash } from "node:crypto"; +import { canonicalizeToBytes } from "./canonicalize.js"; +const FIXTURES = join(import.meta.dirname ?? __dirname, "..", "fixtures"); +function envelope(name) { + return JSON.parse(readFileSync(join(FIXTURES, name), "utf8")); +} +const EXPECTED_HASH = "sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22"; +// ── canonicalization is pinned to a concrete expected hash ── +test("JCS(receipt) hashes to the published storedReceiptHash", () => { + const env = envelope("valid-receipt.json"); + const bytes = canonicalizeToBytes(env.receipt); + const hash = "sha256:" + createHash("sha256").update(bytes).digest("hex"); + assert.equal(hash, EXPECTED_HASH); + assert.equal(hash, env.storedReceiptHash); +}); +test("the canonical payload excludes signature and the appended crypto fields", () => { + // Documented in RECEIPT-VERIFICATION.md §2. If any of these ever appear + // inside `receipt`, the hash above stops reproducing and every external + // verifier breaks at once. + const env = envelope("valid-receipt.json"); + for (const k of ["signature", "sha256_hash", "ed25519_sig"]) { + assert.ok(!Object.keys(env.receipt).includes(k)); + } +}); +test("key ordering is what makes it canonical, not insertion order", () => { + // Re-serialise with keys deliberately reversed; the digest must not move. + const env = envelope("valid-receipt.json"); + const reversed = {}; + for (const k of Object.keys(env.receipt).reverse()) + reversed[k] = env.receipt[k]; + const a = createHash("sha256").update(canonicalizeToBytes(env.receipt)).digest("hex"); + const b = createHash("sha256").update(canonicalizeToBytes(reversed)).digest("hex"); + assert.equal(b, a); +}); +// ── the tampered vector must not verify ── +test("altering amount changes the canonical hash", () => { + const good = envelope("valid-receipt.json"); + const bad = envelope("tampered-receipt.json"); + // The signature is byte-identical — only the payload differs. + assert.equal(bad.signature, good.signature); + assert.notEqual(bad.receipt.amount, good.receipt.amount); + const hash = "sha256:" + createHash("sha256") + .update(canonicalizeToBytes(bad.receipt)) + .digest("hex"); + assert.notEqual(hash, EXPECTED_HASH); +}); +// ── the live receipt carries the bindings the category depends on ── +const r = envelope("valid-receipt.json").receipt; +test("binds the artifact — the half authorization-only proofs scope out", () => { + assert.match(r.artifact_hash, /^sha256:[0-9a-f]{64}$/); + assert.ok(r.certificate_id); +}); +test("binds the governing policy", () => { + assert.match(r.policy_hash, /^sha256:[0-9a-f]{64}$/); + assert.ok(r.policy_version); +}); +test("states settlement rather than implying it", () => { + assert.equal(r.settlement_state, "succeeded_live"); + assert.ok(r.settled_at); +}); +test("keeps typed reference fields distinct and correctly prefixed", () => { + // Receipt c2e70d98 held one PaymentIntent id in four fields, one of them a + // charge field. Distinct, correctly-prefixed values are the fix. + assert.match(r.external_payment_intent_id, /^pi_/); + assert.match(r.external_charge_id, /^ch_/); + assert.notEqual(r.external_charge_id, r.external_payment_intent_id); + assert.equal(r.external_reference_type, "stripe_payment_intent"); +}); +test("carries no U+FFFD anywhere", () => { + assert.ok(!JSON.stringify(r).includes("�")); +}); +test("declares no integrity caveats", () => { + // Absence means the pre-signature gate found nothing to declare. + assert.equal(r.integrity_notes ?? null, null); +}); +//# sourceMappingURL=receipt-vectors.test.js.map \ No newline at end of file diff --git a/dist/receipt-vectors.test.js.map b/dist/receipt-vectors.test.js.map new file mode 100644 index 0000000..088ba79 --- /dev/null +++ b/dist/receipt-vectors.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"receipt-vectors.test.js","sourceRoot":"","sources":["../src/receipt-vectors.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/receipt.d.ts b/dist/receipt.d.ts new file mode 100644 index 0000000..3e15c0b --- /dev/null +++ b/dist/receipt.d.ts @@ -0,0 +1,57 @@ +import type { CheckResult } from "./types.js"; +export declare const DEFAULT_RECEIPT_API = "https://certifieddata.io/api/payments/verify"; +export declare const DEFAULT_RECEIPT_KEY_URL = "https://certifieddata.io/.well-known/certifieddata-public-key.pem"; +export type ReceiptVerdict = "VALID" | "INVALID" | "UNKNOWN_KEY" | "MALFORMED"; +export interface ReceiptVerifyResult { + artifact_type: "receipt"; + artifact_id: string | null; + verdict: ReceiptVerdict; + key_id: string | null; + issuer: string | null; + signed_at: string | null; + checks: { + signature: CheckResult; + key_trust: CheckResult; + payload_hash: CheckResult; + }; + reason: string; + /** The server's own booleans, informational only — never the verdict. */ + server_reported?: { + valid?: boolean; + signatureValid?: boolean; + hashValid?: boolean; + }; + settlement_state?: string | null; + amount_cents?: number | null; + currency?: string | null; +} +interface ReceiptEnvelope { + payload: Record; + signatureB64: string | null; + storedHash: string | null; + serverReported?: { + valid?: boolean; + signatureValid?: boolean; + hashValid?: boolean; + }; +} +export interface FetchReceiptOptions { + apiBase?: string; + offline?: boolean; +} +/** Accepts a receipt id, a /api/payments/verify URL, a local .json path, or "-". */ +export declare function fetchReceipt(idOrPathOrUrl: string, opts?: FetchReceiptOptions): Promise; +export interface LoadReceiptKeyOptions { + keyUrl?: string; + keyFile?: string; + offline?: boolean; +} +/** + * Loads the Agent Commerce public key PEM. Fails loudly — a 503 here means + * the issuer is misconfigured, and the CLI's answer is "cannot verify + * independently", never "let the server vouch for itself". + */ +export declare function loadReceiptKey(opts?: LoadReceiptKeyOptions): Promise; +export declare function verifyReceiptEnvelope(env: ReceiptEnvelope, publicKeyPem: string): ReceiptVerifyResult; +export {}; +//# sourceMappingURL=receipt.d.ts.map \ No newline at end of file diff --git a/dist/receipt.d.ts.map b/dist/receipt.d.ts.map new file mode 100644 index 0000000..b40cdf5 --- /dev/null +++ b/dist/receipt.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"receipt.d.ts","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,mBAAmB,iDAAiD,CAAC;AAClF,eAAO,MAAM,uBAAuB,sEACiC,CAAC;AAEtE,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;AAE/E,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,SAAS,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE;QACN,SAAS,EAAE,WAAW,CAAC;QACvB,SAAS,EAAE,WAAW,CAAC;QACvB,YAAY,EAAE,WAAW,CAAC;KAC3B,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,eAAe,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrF,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACrF;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,oFAAoF;AACpF,wBAAsB,YAAY,CAChC,aAAa,EAAE,MAAM,EACrB,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAmB1B;AA2BD,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBtF;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,eAAe,EACpB,YAAY,EAAE,MAAM,GACnB,mBAAmB,CAoFrB"} \ No newline at end of file diff --git a/dist/receipt.js b/dist/receipt.js new file mode 100644 index 0000000..8caf6a8 --- /dev/null +++ b/dist/receipt.js @@ -0,0 +1,175 @@ +// Agent Commerce receipt verification (verify#2). +// +// Same philosophy as verify.ts, one more artifact type: +// 1. Fetch the receipt payload + signature from the public verify endpoint +// (or read a local JSON file / stdin). +// 2. Fetch the Agent Commerce public key PEM from .well-known — a DIFFERENT +// trust root from the certificate keys document, on purpose. +// 3. RFC 8785 JCS-canonicalize the payload (signature excluded — the +// platform stores the payload without it) and verify Ed25519 locally. +// 4. Recompute SHA-256 over the same canonical bytes and compare to the +// stored receipt hash when one is exposed. +// +// The server's valid / signatureValid booleans are surfaced as INFORMATIONAL +// metadata only — they never determine the verdict. If the public key cannot +// be fetched, that is a distinct non-success outcome, never a silent +// fallback to the server's opinion. +import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { canonicalizeToBytes } from "./canonicalize.js"; +export const DEFAULT_RECEIPT_API = "https://certifieddata.io/api/payments/verify"; +export const DEFAULT_RECEIPT_KEY_URL = "https://certifieddata.io/.well-known/certifieddata-public-key.pem"; +/** Accepts a receipt id, a /api/payments/verify URL, a local .json path, or "-". */ +export async function fetchReceipt(idOrPathOrUrl, opts = {}) { + if (idOrPathOrUrl === "-") + return parseEnvelope(await readStdin()); + if (idOrPathOrUrl.endsWith(".json") || + idOrPathOrUrl.startsWith("./") || + idOrPathOrUrl.startsWith("/") || + /^[A-Za-z]:[\\/]/.test(idOrPathOrUrl)) { + return parseEnvelope(await readFile(idOrPathOrUrl, "utf8")); + } + if (opts.offline) { + throw new Error("cannot resolve a receipt id in --offline mode (pass a local file)"); + } + const url = /^https?:\/\//.test(idOrPathOrUrl) + ? idOrPathOrUrl + : `${(opts.apiBase ?? DEFAULT_RECEIPT_API).replace(/\/$/, "")}/${encodeURIComponent(idOrPathOrUrl)}`; + const res = await fetch(url); + if (!res.ok) + throw new Error(`HTTP ${res.status} fetching ${url}`); + return parseEnvelope(await res.text()); +} +function parseEnvelope(body) { + const parsed = JSON.parse(body); + // Server envelope: { receipt: {...}, signature, storedReceiptHash, valid... } + if (parsed.receipt && typeof parsed.receipt === "object") { + return { + payload: parsed.receipt, + signatureB64: typeof parsed.signature === "string" ? parsed.signature : null, + storedHash: typeof parsed.storedReceiptHash === "string" ? parsed.storedReceiptHash : null, + serverReported: { + valid: typeof parsed.valid === "boolean" ? parsed.valid : undefined, + signatureValid: typeof parsed.signatureValid === "boolean" ? parsed.signatureValid : undefined, + hashValid: typeof parsed.hashValid === "boolean" ? parsed.hashValid : undefined, + }, + }; + } + // Bare payload with an embedded signature (local file usage). + if (parsed.schema_version === "payment_receipt.v1") { + const { signature, ...payload } = parsed; + return { payload, signatureB64: signature ?? null, storedHash: null }; + } + throw new Error("input is neither a verify-endpoint envelope nor a payment_receipt.v1 payload"); +} +/** + * Loads the Agent Commerce public key PEM. Fails loudly — a 503 here means + * the issuer is misconfigured, and the CLI's answer is "cannot verify + * independently", never "let the server vouch for itself". + */ +export async function loadReceiptKey(opts = {}) { + if (opts.keyFile) + return readFile(opts.keyFile, "utf8"); + if (opts.offline) { + throw new Error("offline receipt verification requires --key "); + } + const url = opts.keyUrl ?? DEFAULT_RECEIPT_KEY_URL; + const res = await fetch(url); + if (!res.ok) { + throw new Error(`public key unavailable (HTTP ${res.status} from ${url}) — cannot verify independently`); + } + const pem = await res.text(); + if (!pem.includes("BEGIN PUBLIC KEY")) { + throw new Error(`response from ${url} is not a PEM public key`); + } + return pem; +} +export function verifyReceiptEnvelope(env, publicKeyPem) { + const p = env.payload; + const result = { + artifact_type: "receipt", + artifact_id: typeof p.receipt_id === "string" ? p.receipt_id : null, + verdict: "MALFORMED", + key_id: null, + issuer: typeof p.issuer === "string" ? p.issuer : null, + signed_at: typeof p.timestamp === "string" ? p.timestamp : null, + checks: { signature: "skipped", key_trust: "skipped", payload_hash: "skipped" }, + reason: "", + server_reported: env.serverReported, + settlement_state: typeof p.settlement_state === "string" ? p.settlement_state : null, + amount_cents: typeof p.amount === "number" ? p.amount : null, + currency: typeof p.currency === "string" ? p.currency : null, + }; + if (p.schema_version !== "payment_receipt.v1") { + result.reason = `unsupported schema_version: ${String(p.schema_version)}`; + return result; + } + if (!env.signatureB64) { + result.reason = + "no signature present — the verify endpoint predates signature exposure, or the local file omitted it"; + return result; + } + let publicKey; + try { + publicKey = createPublicKey({ key: publicKeyPem, format: "pem" }); + if (publicKey.asymmetricKeyType !== "ed25519") { + result.checks.key_trust = "fail"; + result.verdict = "UNKNOWN_KEY"; + result.reason = `published key is ${publicKey.asymmetricKeyType}, expected ed25519`; + return result; + } + } + catch (e) { + result.checks.key_trust = "fail"; + result.verdict = "UNKNOWN_KEY"; + result.reason = `cannot parse published public key: ${e.message}`; + return result; + } + result.checks.key_trust = "pass"; + let sigBytes; + try { + sigBytes = Buffer.from(env.signatureB64, "base64"); + if (sigBytes.length !== 64) + throw new Error(`expected 64 bytes, got ${sigBytes.length}`); + } + catch (e) { + result.reason = `signature is not valid base64 ed25519: ${e.message}`; + return result; + } + // The platform signs canonicalize(payload) where payload never contained a + // signature field; strip defensively for local files. + const { signature: _drop, ...withoutSig } = env.payload; + const canonicalBytes = canonicalizeToBytes(withoutSig); + const sigOk = cryptoVerify(null, canonicalBytes, publicKey, sigBytes); + result.checks.signature = sigOk ? "pass" : "fail"; + if (!sigOk) { + result.verdict = "INVALID"; + result.reason = "ed25519 signature does not verify against the RFC 8785 canonical payload"; + return result; + } + if (env.storedHash) { + const recomputed = `sha256:${createHash("sha256").update(canonicalBytes).digest("hex")}`; + const match = recomputed === env.storedHash; + result.checks.payload_hash = match ? "pass" : "fail"; + if (!match) { + result.verdict = "INVALID"; + result.reason = `stored receipt hash ${env.storedHash} does not match locally recomputed ${recomputed}`; + return result; + } + } + // Key id is reported from the envelope's metadata when present; trust came + // from the .well-known fetch, not from this label. + const keyIdMeta = env.keyId; + result.key_id = typeof keyIdMeta === "string" ? keyIdMeta : result.key_id; + result.verdict = "VALID"; + result.reason = "ed25519 signature verified locally against the published Agent Commerce key"; + return result; +} +async function readStdin() { + const chunks = []; + for await (const chunk of process.stdin) { + chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); + } + return Buffer.concat(chunks).toString("utf8"); +} +//# sourceMappingURL=receipt.js.map \ No newline at end of file diff --git a/dist/receipt.js.map b/dist/receipt.js.map new file mode 100644 index 0000000..3e2d42f --- /dev/null +++ b/dist/receipt.js.map @@ -0,0 +1 @@ +{"version":3,"file":"receipt.js","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/receipt.test.d.ts b/dist/receipt.test.d.ts new file mode 100644 index 0000000..6e23322 --- /dev/null +++ b/dist/receipt.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=receipt.test.d.ts.map \ No newline at end of file diff --git a/dist/receipt.test.d.ts.map b/dist/receipt.test.d.ts.map new file mode 100644 index 0000000..c84f77a --- /dev/null +++ b/dist/receipt.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"receipt.test.d.ts","sourceRoot":"","sources":["../src/receipt.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/receipt.test.js b/dist/receipt.test.js new file mode 100644 index 0000000..21df51a --- /dev/null +++ b/dist/receipt.test.js @@ -0,0 +1,107 @@ +// Receipt verification tests (verify#2 acceptance criteria). +// +// Real Ed25519 with a throwaway keypair, real RFC 8785 canonical bytes: +// - a well-formed signed receipt verifies +// - tampering with ANY signed field fails verification +// - the server's booleans never influence the verdict +// - stored-hash mismatch fails even when the signature would pass +// - missing signature / wrong schema are MALFORMED, not INVALID +import test from "node:test"; +import assert from "node:assert/strict"; +import { createHash, generateKeyPairSync, sign as cryptoSign } from "node:crypto"; +import { canonicalizeToBytes } from "./canonicalize.js"; +import { verifyReceiptEnvelope } from "./receipt.js"; +const { publicKey, privateKey } = generateKeyPairSync("ed25519"); +const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString(); +const BASE_PAYLOAD = { + receipt_id: "46b93444-9ce0-49de-94e4-7a3c41ac8430", + schema_version: "payment_receipt.v1", + timestamp: "2026-08-20T03:03:23.461Z", + issuer: "CertifiedData.io", + agent_id: "demo0000-0000-0000-0000-000000000002", + agent_name: "CertifiedData Demo Agent", + rail: "stripe", + currency: "usd", + amount: 2900, + status: "succeeded", + settlement_state: "simulated_sandbox", + purpose: "verify#2 test fixture", + policy_id: "demo0000-0000-0000-0000-000000000003", + policy_hash: "sha256:c220cd2760d84c4a58c59596cf5eb12662ce7f21d6196282be9fe9de31b7b7d7", + artifact_hash: "sha256:75961ef7be87c6a3039544f64e1687e76637f26f2d6c0d5dd4d978e1495a5d66", + transaction_id: "23b69400-e730-4340-ba4f-aadfc580b702", +}; +function signedEnvelope(payload = BASE_PAYLOAD) { + const bytes = canonicalizeToBytes(payload); + const signatureB64 = cryptoSign(null, bytes, privateKey).toString("base64"); + const storedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`; + return { payload, signatureB64, storedHash }; +} +test("a well-formed signed receipt verifies locally", () => { + const r = verifyReceiptEnvelope(signedEnvelope(), publicKeyPem); + assert.equal(r.verdict, "VALID"); + assert.equal(r.checks.signature, "pass"); + assert.equal(r.checks.key_trust, "pass"); + assert.equal(r.checks.payload_hash, "pass"); + assert.equal(r.artifact_type, "receipt"); + assert.equal(r.artifact_id, BASE_PAYLOAD.receipt_id); +}); +// Acceptance: tampering with ANY signed receipt field fails verification. +const TAMPER_FIELDS = [ + ["amount", 29], + ["policy_hash", "sha256:" + "0".repeat(64)], + ["artifact_hash", "sha256:" + "f".repeat(64)], + ["agent_id", "attacker-agent"], + ["purpose", "forged purpose"], + ["status", "succeeded_but_forged"], + ["settlement_state", "settled"], + ["timestamp", "2020-01-01T00:00:00.000Z"], + ["receipt_id", "00000000-0000-0000-0000-000000000000"], +]; +for (const [field, forged] of TAMPER_FIELDS) { + test(`tampering with ${field} invalidates the receipt`, () => { + const env = signedEnvelope(); + const tampered = { ...env, payload: { ...env.payload, [field]: forged } }; + const r = verifyReceiptEnvelope(tampered, publicKeyPem); + assert.equal(r.verdict, "INVALID", `${field} tamper must fail`); + assert.equal(r.checks.signature, "fail"); + }); +} +test("server booleans are informational only — verdict is computed locally", () => { + const env = signedEnvelope(); + const tampered = { + ...env, + payload: { ...env.payload, amount: 1 }, + // A lying server says everything is fine. + serverReported: { valid: true, signatureValid: true, hashValid: true }, + }; + const r = verifyReceiptEnvelope(tampered, publicKeyPem); + assert.equal(r.verdict, "INVALID"); + assert.deepEqual(r.server_reported, { valid: true, signatureValid: true, hashValid: true }); +}); +test("stored-hash mismatch fails even with a valid signature", () => { + const env = signedEnvelope(); + const r = verifyReceiptEnvelope({ ...env, storedHash: "sha256:" + "9".repeat(64) }, publicKeyPem); + assert.equal(r.verdict, "INVALID"); + assert.equal(r.checks.signature, "pass"); + assert.equal(r.checks.payload_hash, "fail"); +}); +test("missing signature is MALFORMED, not INVALID", () => { + const env = signedEnvelope(); + const r = verifyReceiptEnvelope({ ...env, signatureB64: null }, publicKeyPem); + assert.equal(r.verdict, "MALFORMED"); + assert.equal(r.checks.signature, "skipped"); +}); +test("wrong schema_version is MALFORMED", () => { + const env = signedEnvelope({ ...BASE_PAYLOAD, schema_version: "payment_receipt.v2" }); + const r = verifyReceiptEnvelope(env, publicKeyPem); + assert.equal(r.verdict, "MALFORMED"); +}); +test("a non-ed25519 published key is UNKNOWN_KEY, never a pass", () => { + const rsa = generateKeyPairSync("rsa", { modulusLength: 2048 }); + const rsaPem = rsa.publicKey.export({ type: "spki", format: "pem" }).toString(); + const r = verifyReceiptEnvelope(signedEnvelope(), rsaPem); + assert.equal(r.verdict, "UNKNOWN_KEY"); + assert.equal(r.checks.key_trust, "fail"); +}); +//# sourceMappingURL=receipt.test.js.map \ No newline at end of file diff --git a/dist/receipt.test.js.map b/dist/receipt.test.js.map new file mode 100644 index 0000000..06711e5 --- /dev/null +++ b/dist/receipt.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"receipt.test.js","sourceRoot":"","sources":["../src/receipt.test.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,wEAAwE;AACxE,4CAA4C;AAC5C,yDAAyD;AACzD,wDAAwD;AACxD,oEAAoE;AACpE,kEAAkE;AAElE,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,IAAI,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;AACjE,MAAM,YAAY,GAAG,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;AAElF,MAAM,YAAY,GAAG;IACnB,UAAU,EAAE,sCAAsC;IAClD,cAAc,EAAE,oBAAoB;IACpC,SAAS,EAAE,0BAA0B;IACrC,MAAM,EAAE,kBAAkB;IAC1B,QAAQ,EAAE,sCAAsC;IAChD,UAAU,EAAE,0BAA0B;IACtC,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,KAAK;IACf,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,WAAW;IACnB,gBAAgB,EAAE,mBAAmB;IACrC,OAAO,EAAE,uBAAuB;IAChC,SAAS,EAAE,sCAAsC;IACjD,WAAW,EAAE,yEAAyE;IACtF,aAAa,EAAE,yEAAyE;IACxF,cAAc,EAAE,sCAAsC;CACvD,CAAC;AAEF,SAAS,cAAc,CAAC,UAAmC,YAAY;IACrE,MAAM,KAAK,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC5E,MAAM,UAAU,GAAG,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IAChF,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;AAC/C,CAAC;AAED,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;IACzD,MAAM,CAAC,GAAG,qBAAqB,CAAC,cAAc,EAAE,EAAE,YAAY,CAAC,CAAC;IAChE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAC5C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;AACvD,CAAC,CAAC,CAAC;AAEH,0EAA0E;AAC1E,MAAM,aAAa,GAA6B;IAC9C,CAAC,QAAQ,EAAE,EAAE,CAAC;IACd,CAAC,aAAa,EAAE,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3C,CAAC,eAAe,EAAE,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7C,CAAC,UAAU,EAAE,gBAAgB,CAAC;IAC9B,CAAC,SAAS,EAAE,gBAAgB,CAAC;IAC7B,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAClC,CAAC,kBAAkB,EAAE,SAAS,CAAC;IAC/B,CAAC,WAAW,EAAE,0BAA0B,CAAC;IACzC,CAAC,YAAY,EAAE,sCAAsC,CAAC;CACvD,CAAC;AAEF,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,aAAa,EAAE,CAAC;IAC5C,IAAI,CAAC,kBAAkB,KAAK,0BAA0B,EAAE,GAAG,EAAE;QAC3D,MAAM,GAAG,GAAG,cAAc,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;QAC1E,MAAM,CAAC,GAAG,qBAAqB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QACxD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,mBAAmB,CAAC,CAAC;QAChE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,CAAC,sEAAsE,EAAE,GAAG,EAAE;IAChF,MAAM,GAAG,GAAG,cAAc,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAG;QACf,GAAG,GAAG;QACN,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE;QACtC,0CAA0C;QAC1C,cAAc,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;KACvE,CAAC;IACF,MAAM,CAAC,GAAG,qBAAqB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACxD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACnC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC9F,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wDAAwD,EAAE,GAAG,EAAE;IAClE,MAAM,GAAG,GAAG,cAAc,EAAE,CAAC;IAC7B,MAAM,CAAC,GAAG,qBAAqB,CAAC,EAAE,GAAG,GAAG,EAAE,UAAU,EAAE,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;IAClG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACnC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;IACvD,MAAM,GAAG,GAAG,cAAc,EAAE,CAAC;IAC7B,MAAM,CAAC,GAAG,qBAAqB,CAAC,EAAE,GAAG,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,YAAY,CAAC,CAAC;IAC9E,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IACrC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mCAAmC,EAAE,GAAG,EAAE;IAC7C,MAAM,GAAG,GAAG,cAAc,CAAC,EAAE,GAAG,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,CAAC,CAAC;IACtF,MAAM,CAAC,GAAG,qBAAqB,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACnD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;AACvC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0DAA0D,EAAE,GAAG,EAAE;IACpE,MAAM,GAAG,GAAG,mBAAmB,CAAC,KAAK,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;IAChF,MAAM,CAAC,GAAG,qBAAqB,CAAC,cAAc,EAAE,EAAE,MAAM,CAAC,CAAC;IAC1D,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IACvC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAC3C,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/resolve.d.ts b/dist/resolve.d.ts new file mode 100644 index 0000000..399e29d --- /dev/null +++ b/dist/resolve.d.ts @@ -0,0 +1,20 @@ +export type ArtifactKind = "certificate" | "receipt"; +export type Resolution = { + kind: ArtifactKind; + via: string; +} | { + kind: "ambiguous"; + via: string; +} | { + kind: "not_found"; + via: string; +} | { + kind: "transport_error"; + via: string; +}; +export declare function resolveArtifactKind(target: string, opts?: { + offline?: boolean; + certApiBase?: string; + receiptApiBase?: string; +}): Promise; +//# sourceMappingURL=resolve.d.ts.map \ No newline at end of file diff --git a/dist/resolve.d.ts.map b/dist/resolve.d.ts.map new file mode 100644 index 0000000..1ecc699 --- /dev/null +++ b/dist/resolve.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,SAAS,CAAC;AAErD,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7C,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9E,OAAO,CAAC,UAAU,CAAC,CAgErB"} \ No newline at end of file diff --git a/dist/resolve.js b/dist/resolve.js new file mode 100644 index 0000000..86b9575 --- /dev/null +++ b/dist/resolve.js @@ -0,0 +1,87 @@ +// Artifact-kind resolution (verify#2). +// +// Both certificates and payment receipts can be bare UUIDs, so a UUID alone +// is not a safe discriminator. Resolution order: +// 1. Strong syntax/path hints (prefixes, endpoint URLs, local JSON shape). +// 2. For an ambiguous bare id online: probe BOTH public endpoints. +// exactly one exists -> that kind +// both exist -> AMBIGUOUS (caller must pass --type) +// neither exists -> NOT_FOUND +// transport failure -> TRANSPORT (never silently "not found") +// 3. --type overrides everything. +import { readFile } from "node:fs/promises"; +import { DEFAULT_CERT_API } from "./fetch-cert.js"; +import { DEFAULT_RECEIPT_API } from "./receipt.js"; +export async function resolveArtifactKind(target, opts = {}) { + // 1. Prefix hints — cert-family prefixes are certificates. + if (/^(cert_|scert_)/.test(target)) + return { kind: "certificate", via: "id-prefix" }; + // Endpoint-URL hints. + if (/\/api\/payments\/verify\//.test(target)) + return { kind: "receipt", via: "url-path" }; + if (/\/api\/v1\/certificates\//.test(target)) + return { kind: "certificate", via: "url-path" }; + // Local file / stdin: sniff the JSON shape. + const looksLocal = target === "-" || + target.endsWith(".json") || + target.startsWith("./") || + target.startsWith("/") || + /^[A-Za-z]:[\\/]/.test(target); + if (looksLocal && target !== "-") { + try { + const parsed = JSON.parse(await readFile(target, "utf8")); + const schema = parsed.schema_version ?? + parsed.receipt?.schema_version; + if (schema === "payment_receipt.v1") + return { kind: "receipt", via: "local-schema" }; + if (typeof schema === "string" && schema.startsWith("cert.")) { + return { kind: "certificate", via: "local-schema" }; + } + // Envelope shape without schema — a verify-endpoint dump. + if (parsed.receipt && parsed.storedReceiptHash) + return { kind: "receipt", via: "local-envelope" }; + return { kind: "certificate", via: "local-default" }; + } + catch { + return { kind: "certificate", via: "local-unreadable-default" }; + } + } + if (target === "-") + return { kind: "certificate", via: "stdin-default" }; + // 2. Bare id online — probe both endpoints. Offline cannot probe. + if (opts.offline) + return { kind: "certificate", via: "offline-default" }; + const certUrl = `${(opts.certApiBase ?? DEFAULT_CERT_API).replace(/\/$/, "")}/${encodeURIComponent(target)}`; + const rcptUrl = `${(opts.receiptApiBase ?? DEFAULT_RECEIPT_API).replace(/\/$/, "")}/${encodeURIComponent(target)}`; + const probe = async (url) => { + try { + const res = await fetch(url, { method: "GET" }); + if (res.ok) + return "exists"; + if (res.status === 404) + return "missing"; + return "error"; // 5xx/403/… — a server problem is NOT evidence of absence + } + catch { + return "error"; + } + }; + const [cert, rcpt] = await Promise.all([probe(certUrl), probe(rcptUrl)]); + if (cert === "error" || rcpt === "error") { + // If the OTHER endpoint definitively resolved, use it; otherwise surface + // the transport problem instead of guessing. + if (cert === "exists" && rcpt !== "exists") + return { kind: "certificate", via: "probe" }; + if (rcpt === "exists" && cert !== "exists") + return { kind: "receipt", via: "probe" }; + return { kind: "transport_error", via: "probe" }; + } + if (cert === "exists" && rcpt === "exists") + return { kind: "ambiguous", via: "probe" }; + if (cert === "exists") + return { kind: "certificate", via: "probe" }; + if (rcpt === "exists") + return { kind: "receipt", via: "probe" }; + return { kind: "not_found", via: "probe" }; +} +//# sourceMappingURL=resolve.js.map \ No newline at end of file diff --git a/dist/resolve.js.map b/dist/resolve.js.map new file mode 100644 index 0000000..5dee692 --- /dev/null +++ b/dist/resolve.js.map @@ -0,0 +1 @@ +{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/types.d.ts b/dist/types.d.ts new file mode 100644 index 0000000..a990413 --- /dev/null +++ b/dist/types.d.ts @@ -0,0 +1,51 @@ +export interface Certificate { + certification_id: string; + timestamp: string; + issuer: string; + dataset_hash: string; + algorithm: "CTGAN" | "GaussianCopula" | "DP-CTGAN" | string; + rows: number; + columns: number; + schema_version: "cert.v1"; + signature: string; + key_id: string; + metadata?: Record & { + epsilon?: number | null; + }; +} +export interface KeyEntry { + key_id: string; + public_key: string; + algorithm: "ed25519"; + created_at: string; + revoked_at?: string | null; + label?: string; +} +export interface KeyDoc { + issuer: string; + keys: KeyEntry[]; + fetched_at?: string; +} +export type Verdict = "VALID" | "INVALID" | "UNKNOWN_KEY" | "DATASET_MISMATCH" | "MALFORMED"; +export type CheckResult = "pass" | "fail" | "skipped"; +export interface VerifyResult { + verdict: Verdict; + certification_id: string | null; + key_id: string | null; + issuer: string | null; + algorithm: string | null; + signed_at: string | null; + dataset_hash_expected: string | null; + dataset_hash_actual: string | null; + checks: { + signature: CheckResult; + key_trust: CheckResult; + dataset_match: CheckResult; + }; + reason: string; + rows?: number; + columns?: number; + key_label?: string; +} +export declare const REQUIRED_CERT_FIELDS: ReadonlyArray; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/dist/types.d.ts.map b/dist/types.d.ts.map new file mode 100644 index 0000000..294bdd1 --- /dev/null +++ b/dist/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,GAAG,gBAAgB,GAAG,UAAU,GAAG,MAAM,CAAC;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,SAAS,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CAClE;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,OAAO,GACf,OAAO,GACP,SAAS,GACT,aAAa,GACb,kBAAkB,GAClB,WAAW,CAAC;AAEhB,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE;QACN,SAAS,EAAE,WAAW,CAAC;QACvB,SAAS,EAAE,WAAW,CAAC;QACvB,aAAa,EAAE,WAAW,CAAC;KAC5B,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,MAAM,WAAW,CAWjE,CAAC"} \ No newline at end of file diff --git a/dist/types.js b/dist/types.js new file mode 100644 index 0000000..3c9e1a0 --- /dev/null +++ b/dist/types.js @@ -0,0 +1,13 @@ +export const REQUIRED_CERT_FIELDS = [ + "certification_id", + "timestamp", + "issuer", + "dataset_hash", + "algorithm", + "rows", + "columns", + "schema_version", + "signature", + "key_id", +]; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/dist/types.js.map b/dist/types.js.map new file mode 100644 index 0000000..0da57a1 --- /dev/null +++ b/dist/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA0DA,MAAM,CAAC,MAAM,oBAAoB,GAAqC;IACpE,kBAAkB;IAClB,WAAW;IACX,QAAQ;IACR,cAAc;IACd,WAAW;IACX,MAAM;IACN,SAAS;IACT,gBAAgB;IAChB,WAAW;IACX,QAAQ;CACT,CAAC"} \ No newline at end of file diff --git a/dist/verify.d.ts b/dist/verify.d.ts new file mode 100644 index 0000000..ed05cc3 --- /dev/null +++ b/dist/verify.d.ts @@ -0,0 +1,3 @@ +import type { Certificate, KeyDoc, VerifyResult } from "./types.js"; +export declare function verifyCertificate(cert: Certificate, trustedKeys: KeyDoc, datasetPath?: string): Promise; +//# sourceMappingURL=verify.d.ts.map \ No newline at end of file diff --git a/dist/verify.d.ts.map b/dist/verify.d.ts.map new file mode 100644 index 0000000..be32026 --- /dev/null +++ b/dist/verify.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEpE,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,YAAY,CAAC,CAkDvB"} \ No newline at end of file diff --git a/dist/verify.js b/dist/verify.js new file mode 100644 index 0000000..6cafb7e --- /dev/null +++ b/dist/verify.js @@ -0,0 +1,115 @@ +// Core verification path. Read top-to-bottom — there is no clever indirection. +// 1. Validate certificate shape. +// 2. Look up cert.key_id in the trusted keys document; reject if missing/revoked. +// 3. Build the canonical payload (cert minus signature) per RFC 8785 JCS. +// 4. crypto.verify('ed25519', canonicalBytes, publicKey, signatureBytes). +// 5. If a dataset path was supplied, recompute SHA-256 and compare to dataset_hash. +import { createPublicKey, verify as cryptoVerify } from "node:crypto"; +import { canonicalizeToBytes } from "./canonicalize.js"; +import { sha256File, formatDigest, parseDigest } from "./hash.js"; +import { findKey } from "./keys.js"; +import { REQUIRED_CERT_FIELDS } from "./types.js"; +export async function verifyCertificate(cert, trustedKeys, datasetPath) { + const result = blankResult(cert); + const shapeError = validateShape(cert); + if (shapeError) + return finish(result, "MALFORMED", shapeError); + result.certification_id = cert.certification_id; + result.key_id = cert.key_id; + result.issuer = cert.issuer; + result.algorithm = cert.algorithm; + result.signed_at = cert.timestamp; + result.dataset_hash_expected = cert.dataset_hash; + result.rows = cert.rows; + result.columns = cert.columns; + const key = findKey(trustedKeys, cert.key_id); + if (!key || key.revoked_at || key.algorithm !== "ed25519") { + result.checks.key_trust = "fail"; + const reason = !key + ? `key_id ${cert.key_id} not in trusted keys` + : key.revoked_at + ? `key_id ${cert.key_id} was revoked at ${key.revoked_at}` + : `key ${cert.key_id} is not ed25519`; + return finish(result, "UNKNOWN_KEY", reason); + } + result.checks.key_trust = "pass"; + result.key_label = key.label; + const { signature: _sig, ...withoutSig } = cert; + const canonicalBytes = canonicalizeToBytes(withoutSig); + const sigBytes = decodeSignature(cert.signature); + if (!sigBytes) + return finish(result, "MALFORMED", "signature is not valid base64"); + const publicKey = createPublicKey({ key: pemFromRawEd25519(key.public_key), format: "pem" }); + const sigOk = cryptoVerify(null, canonicalBytes, publicKey, sigBytes); + result.checks.signature = sigOk ? "pass" : "fail"; + if (!sigOk) + return finish(result, "INVALID", "ed25519 signature does not verify against canonicalized payload"); + if (datasetPath) { + const actualHex = await sha256File(datasetPath); + const actual = formatDigest(actualHex); + result.dataset_hash_actual = actual; + const expected = parseDigest(cert.dataset_hash); + if (expected.algo !== "sha256" || expected.hex !== actualHex) { + result.checks.dataset_match = "fail"; + return finish(result, "DATASET_MISMATCH", `dataset hash mismatch (expected ${cert.dataset_hash}, got ${actual})`); + } + result.checks.dataset_match = "pass"; + } + return finish(result, "VALID", "signature verified and key is trusted"); +} +function validateShape(c) { + if (!c || typeof c !== "object") + return "certificate is not an object"; + for (const f of REQUIRED_CERT_FIELDS) + if (c[f] === undefined || c[f] === null) + return `missing required field: ${f}`; + if (c.schema_version !== "cert.v1") + return `unsupported schema_version: ${c.schema_version}`; + if (!/^sha256:[0-9a-f]{64}$/i.test(c.dataset_hash)) + return "dataset_hash must be sha256:<64-hex>"; + if (typeof c.rows !== "number" || typeof c.columns !== "number") + return "rows and columns must be numbers"; + return null; +} +function decodeSignature(b64) { + try { + const buf = Buffer.from(b64, "base64"); + if (buf.length !== 64) + return null; + return buf; + } + catch { + return null; + } +} +function pemFromRawEd25519(material) { + if (material.includes("BEGIN PUBLIC KEY")) + return material; + // Wrap a base64 raw 32-byte Ed25519 public key in the standard SPKI prefix. + const raw = Buffer.from(material, "base64"); + if (raw.length !== 32) + throw new Error(`expected 32-byte ed25519 key, got ${raw.length}`); + const spkiPrefix = Buffer.from("302a300506032b6570032100", "hex"); + const der = Buffer.concat([spkiPrefix, raw]).toString("base64"); + return `-----BEGIN PUBLIC KEY-----\n${der.match(/.{1,64}/g).join("\n")}\n-----END PUBLIC KEY-----\n`; +} +function blankResult(cert) { + return { + verdict: "MALFORMED", + certification_id: cert.certification_id ?? null, + key_id: cert.key_id ?? null, + issuer: cert.issuer ?? null, + algorithm: cert.algorithm ?? null, + signed_at: cert.timestamp ?? null, + dataset_hash_expected: cert.dataset_hash ?? null, + dataset_hash_actual: null, + checks: { signature: "skipped", key_trust: "skipped", dataset_match: "skipped" }, + reason: "", + }; +} +function finish(r, verdict, reason) { + r.verdict = verdict; + r.reason = reason; + return r; +} +//# sourceMappingURL=verify.js.map \ No newline at end of file diff --git a/dist/verify.js.map b/dist/verify.js.map new file mode 100644 index 0000000..7fcf006 --- /dev/null +++ b/dist/verify.js.map @@ -0,0 +1 @@ +{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/verify.test.d.ts b/dist/verify.test.d.ts new file mode 100644 index 0000000..2ebcdd3 --- /dev/null +++ b/dist/verify.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=verify.test.d.ts.map \ No newline at end of file diff --git a/dist/verify.test.d.ts.map b/dist/verify.test.d.ts.map new file mode 100644 index 0000000..70a2f9d --- /dev/null +++ b/dist/verify.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"verify.test.d.ts","sourceRoot":"","sources":["../src/verify.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/verify.test.js b/dist/verify.test.js new file mode 100644 index 0000000..a1ed1e0 --- /dev/null +++ b/dist/verify.test.js @@ -0,0 +1,69 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { verifyCertificate } from "./verify.js"; +const here = dirname(fileURLToPath(import.meta.url)); +const fixturesDir = join(here, "..", "fixtures"); +async function loadJson(name) { + return JSON.parse(await readFile(join(fixturesDir, name), "utf8")); +} +test("verifies a clean cert against trusted keys", async () => { + const cert = await loadJson("valid-cert.json"); + const keys = await loadJson("keys.json"); + const r = await verifyCertificate(cert, keys); + assert.equal(r.verdict, "VALID", r.reason); + assert.equal(r.checks.signature, "pass"); + assert.equal(r.checks.key_trust, "pass"); + assert.equal(r.checks.dataset_match, "skipped"); +}); +test("rejects a cert whose payload was mutated after signing", async () => { + const cert = await loadJson("tampered-cert.json"); + const keys = await loadJson("keys.json"); + const r = await verifyCertificate(cert, keys); + assert.equal(r.verdict, "INVALID"); + assert.equal(r.checks.signature, "fail"); + assert.equal(r.checks.key_trust, "pass"); +}); +test("returns UNKNOWN_KEY when the cert references an unlisted key", async () => { + const cert = await loadJson("unknown-key-cert.json"); + const keys = await loadJson("keys.json"); + const r = await verifyCertificate(cert, keys); + assert.equal(r.verdict, "UNKNOWN_KEY"); + assert.equal(r.checks.key_trust, "fail"); + assert.equal(r.checks.signature, "skipped"); +}); +test("returns MALFORMED for missing required fields", async () => { + const cert = await loadJson("malformed-cert.json"); + const keys = await loadJson("keys.json"); + const r = await verifyCertificate(cert, keys); + assert.equal(r.verdict, "MALFORMED"); + assert.match(r.reason, /missing required field: signature/); +}); +test("returns DATASET_MISMATCH when the dataset hash does not match", async () => { + const cert = await loadJson("valid-cert.json"); + const keys = await loadJson("keys.json"); + const wrongDataset = join(fixturesDir, "keys.json"); // hash will differ + const r = await verifyCertificate(cert, keys, wrongDataset); + assert.equal(r.verdict, "DATASET_MISMATCH"); + assert.equal(r.checks.dataset_match, "fail"); + assert.notEqual(r.dataset_hash_actual, r.dataset_hash_expected); +}); +test("returns VALID when the supplied dataset matches", async () => { + const cert = await loadJson("valid-cert.json"); + const keys = await loadJson("keys.json"); + const dataset = join(fixturesDir, "valid-dataset.csv"); + const r = await verifyCertificate(cert, keys, dataset); + assert.equal(r.verdict, "VALID"); + assert.equal(r.checks.dataset_match, "pass"); +}); +test("rejects a revoked key", async () => { + const cert = await loadJson("valid-cert.json"); + const keys = await loadJson("keys.json"); + keys.keys[0].revoked_at = "2026-04-01T00:00:00Z"; + const r = await verifyCertificate(cert, keys); + assert.equal(r.verdict, "UNKNOWN_KEY"); + assert.match(r.reason, /revoked/); +}); +//# sourceMappingURL=verify.test.js.map \ No newline at end of file diff --git a/dist/verify.test.js.map b/dist/verify.test.js.map new file mode 100644 index 0000000..4a99518 --- /dev/null +++ b/dist/verify.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"verify.test.js","sourceRoot":"","sources":["../src/verify.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index 854a79f..842b01c 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Verify CertifiedData.io certificates from the command line. Audit-friendly, zero crypto dependencies.", "type": "module", "bin": { + "verify": "dist/cli.js", "certifieddata-verify": "dist/cli.js", "cd-verify": "dist/cli.js" }, @@ -47,6 +48,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"", + "verify:dist": "npm run build && git diff --exit-code -- dist/", "prepare": "npm run build", "typecheck": "tsc --noEmit", "lint": "eslint src/",