diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5c36c85..eb3116b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,8 +18,31 @@ jobs: node-version: 22 registry-url: https://registry.npmjs.org cache: npm + # Node 22 bundles npm 10.x. Trusted publishing (OIDC provenance with no + # long-lived token) requires npm >= 11.5.1, so upgrade the CLI before + # publishing. Without this, `npm publish --provenance` fails on auth. + - name: Upgrade npm for trusted publishing + run: npm install -g npm@latest + - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm test + + # Fail loudly if package.json version does not match the release tag, + # rather than publishing a version nobody asked for. + - name: Check version matches release tag + run: | + TAG="${GITHUB_REF_NAME#v}" + PKG="$(node -p "require('./package.json').version")" + if [ "$TAG" != "$PKG" ]; then + echo "::error::release tag v$TAG does not match package.json version $PKG" + exit 1 + fi + - run: npm publish --provenance --access public + env: + # Belt and braces: if the package is not yet configured for trusted + # publishing on npmjs.com, this token is what authenticates. Remove + # once trusted publishing is configured. + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f138139..290ee5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/). -## [0.1.0] - Unreleased +## [0.1.0] - 2026-09-07 ### Added @@ -14,7 +14,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - RFC 8785 JCS canonicalizer (`canonicalize.ts`). - Ed25519 signature verification using `node:crypto` only — zero third-party crypto dependencies. - `cert.v1` schema support. +- `cert.v2` schema support (`cert-v2.ts`) — the schema production has issued + since 2026-02. v2 differs from v1 in three ways that matter: the signature is + detached rather than a field inside the signed document (so the whole payload + is canonicalized, with nothing stripped), the signer is named at + `payload.issuer.signing_key_id`, and the artifact digest is bare hex at + `payload.artifact_hash`. Signed bytes are Ed25519 over JCS of the payload, + confirmed empirically against the live production certificate committed as + `fixtures/valid-cert-v2.json`. +- Payment receipt verification against the published Agent Commerce key. - `--dataset`, `--json`, `--offline`, `--keys`, `--no-cache` flags. -- Trusted-keys document fetched from `https://certifieddata.io/.well-known/certifieddata-keys.json` with TTL cache at `~/.certifieddata/keys.json`. +- Trusted-keys document fetched from the issuer's `.well-known` signing-keys + document, with TTL cache at `~/.certifieddata/keys.json`. - Six exit codes documented in the README and `--help`. -- 34 tests across canonicalize, verify, and CLI suites. +- 93 tests across canonicalize, verify, cert-v2, receipt, exit-code and CLI suites. + +### Fixed + +- **The certificate path could not verify any production certificate.** Two + independent causes, both addressed here: + - Every issued certificate is `cert.v2`; the verifier implemented only + `cert.v1` and rejected v2 as `MALFORMED` on a missing `certification_id` + (v2 names it `certificate_id`). + - The pinned keys URL was `/.well-known/certifieddata-keys.json`, which + returns 404 and was never deployed, so verification exited `NETWORK` before + reaching the signature. It now points at + `/.well-known/signing-keys.json` — the document the issuer actually + publishes and that every certificate's own `public_key_url` references. +- Resolving a bare certificate id now fetches `…/signed-payload` rather than + the plain `…/api/certificates/` projection. The projection carries the + real signature bytes but the signature covers the v2 payload, not the + projection, so verifying it reported `INVALID` on untampered certificates. +- The keys-document parser accepts both published dialects + (`public_key` / `public_key_pem`, per-key `revoked_at` / top-level + `revoked[]` and `retired[]`, CRLF in PEM bodies) and compares `algorithm` + case-insensitively. Previously an `algorithm` of `"Ed25519"` — the spelling + the issuer publishes — yielded `UNKNOWN_KEY`, a security verdict, for a + difference of one capital letter. A revocation entry that cannot be parsed is + now an error rather than being skipped, so an unreadable revocation record can + never be mistaken for a good key. +- Human output no longer prints `0 rows × 0 cols` for `cert.v2`, which has no + column count. diff --git a/README.md b/README.md index c508d38..4f09dd2 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,57 @@ certifieddata-verify ce_01HXYZ123abc... --dataset path/to/data.csv ## What this verifies -- **The signature.** `cert.signature` is an Ed25519 signature over the RFC 8785 JCS canonicalization of the rest of the certificate. We re-canonicalize, re-verify, and refuse to claim a cert is valid unless the signature checks out. -- **The signer.** `cert.key_id` must appear in the issuer's published [`.well-known` keys document](https://certifieddata.io/.well-known/certifieddata-keys.json) and must not be revoked. -- **The dataset (optional).** When `--dataset ` is supplied, we stream-hash the file and refuse to claim a match unless its SHA-256 is bit-identical to `cert.dataset_hash`. +- **The signature.** An Ed25519 signature over the RFC 8785 JCS canonicalization of the certificate payload. We re-canonicalize, re-verify, and refuse to claim a cert is valid unless the signature checks out. +- **The signer.** The certificate's signing key must appear in the issuer's published [signing-keys document](https://certifieddata.io/.well-known/signing-keys.json) and must not be revoked. That URL is pinned in this package; we deliberately do **not** follow the `public_key_url` inside a certificate, because a document that has not been verified yet must not choose the keys it is verified against. +- **The dataset (optional).** When `--dataset ` is supplied, we stream-hash the file and refuse to claim a match unless its SHA-256 is bit-identical to the digest in the certificate. + +## Certificate schemas + +Both issued schemas are supported, and the CLI picks the right one from the +document itself — you never pass a flag for it. + +| | `cert.v1` | `cert.v2` (current) | +|---|---|---| +| Signature location | `cert.signature`, inside the document | detached, beside the payload | +| Canonicalized bytes | certificate **minus** `signature` | the **whole** payload | +| Signer named at | `cert.key_id` | `payload.issuer.signing_key_id` | +| Artifact digest | `cert.dataset_hash` (`sha256:…`) | `payload.artifact_hash` (bare hex) | + +A `cert.v2` document is an envelope. Note that the outer `schema_version` names +the envelope, not the certificate, and that `signature` is an **object** rather +than a bare string: + +```json +{ + "schema_version": "certifieddata.manifest.v1", + "payload": { "schema_version": "cert.v2", "certificate_id": "d6da041f-…", "…": "…" }, + "signature": { "alg": "Ed25519", "key_id": "ed25519-prod-2025-02", "value": "base64…" } +} +``` + +Because the v2 signature is detached, the payload is canonicalized exactly as +issued — nothing is removed before verification. A bare base64 `signature` +string is also accepted. + +### Which endpoint to verify against + +Resolving a bare certificate id fetches +`https://api.certifieddata.io/api/certificates//signed-payload`. + +Use that one. `…/api/certificates/` (without the suffix) returns a +`certifieddata.cert.v1`-shaped **display projection** of the same certificate. +It carries the real signature bytes, but the signature covers the v2 payload +rather than the projection, so verifying that document reports `INVALID` — +which reads as tampering when nothing has been tampered with. + +> **Note on `hashes.certificate_payload_sha256`.** Some v2 payloads carry a +> self-referential digest field. It is *not* part of the trust decision and this +> verifier ignores it: the Ed25519 signature over the canonicalized payload is +> what establishes integrity. The published value is not reproducible from the +> stored document under JCS, plain `JSON.stringify`, sorted-key stringify, or +> pretty-printed JSON — most likely it was computed over insertion-ordered JSON, +> which Postgres `jsonb` does not preserve. Do not treat a mismatch in that +> field as a verification failure. ## Why audit-friendly @@ -81,8 +129,8 @@ The non-zero exit codes fail the job automatically — a CI run will not pass if ```bash # Pre-stage a copy of the issuer's keys document, then verify with no network. -curl -O https://certifieddata.io/.well-known/certifieddata-keys.json -certifieddata-verify ./received-cert.json --keys ./certifieddata-keys.json --offline +curl -O https://certifieddata.io/.well-known/signing-keys.json +certifieddata-verify ./received-cert.json --keys ./signing-keys.json --offline ``` `--offline` refuses to make any network call. Combined with `--keys`, it produces a fully reproducible audit you can replay months later. @@ -108,13 +156,18 @@ CertifiedData's opinion about CertifiedData's own signature. ## How CertifiedData certificates work -CertifiedData.io issues `cert.v1` documents that bind together: +CertifiedData.io currently issues `cert.v2` documents (`cert.v1` is still +supported here and still verifies). Both bind together: -1. A **dataset hash** — `sha256(file_bytes)` for binary data (CSV, Parquet) or `sha256(JCS(payload))` for structured data. -2. **Provenance** — the algorithm used, row/column counts, the issuance timestamp, and an opaque `certification_id`. -3. A **signer** — `key_id`, with the public key fetched from the issuer's `.well-known` endpoint. +1. An **artifact hash** — `sha256(file_bytes)` for binary data (CSV, Parquet, ZIP) or `sha256(JCS(payload))` for structured data. +2. **Provenance** — the issuing engine, a record count, the issuance timestamp, and an opaque certificate id. +3. A **signer** — a `signing_key_id`, with the public key fetched from the issuer's pinned `.well-known` signing-keys document. -The signature is computed over the RFC 8785 JCS canonicalization of the certificate **with the `signature` field omitted** — this is the only sane way to sign a JSON document and have it round-trip through arbitrary JSON parsers. +In `cert.v1` the signature is computed over the JCS canonicalization of the +certificate **with the `signature` field omitted**. In `cert.v2` the signature is +detached and travels beside the payload, so the **whole** payload is +canonicalized with nothing stripped. Either way, nothing signs the field that +holds its own signature. We use Ed25519 because it is fast, deterministic, has small keys (32 bytes) and small signatures (64 bytes), and is built into Node's `crypto` module. We never sign the field that contains the signature, and we never claim a verdict beyond what the cert actually says — for example, we will not call a CTGAN cert "differentially private" unless the metadata explicitly carries a non-null `epsilon` and the algorithm is `DP-CTGAN`. diff --git a/dist/cert-v2.d.ts b/dist/cert-v2.d.ts new file mode 100644 index 0000000..c91728b --- /dev/null +++ b/dist/cert-v2.d.ts @@ -0,0 +1,48 @@ +import type { KeyDoc, VerifyResult } from "./types.js"; +export interface CertV2Issuer { + name?: string; + signing_key_id: string; + signature_alg?: string; + environment?: string; +} +export interface CertV2Payload { + schema_version: "cert.v2"; + certificate_id: string; + certificate_type?: string; + issued_at: string; + artifact_hash: string; + hash_method?: string; + issuer: CertV2Issuer; + subject?: Record; + manifest?: { + engine?: string; + record_count?: number; + [k: string]: unknown; + }; + [k: string]: unknown; +} +/** The signature as production actually serves it, or as a bare base64 string. */ +export type CertV2Signature = string | { + value?: string; + sig?: string; + signature?: string; + alg?: string; + key_id?: string; +}; +export interface CertV2Envelope { + payload: CertV2Payload; + /** base64 Ed25519, already unwrapped from whichever spelling arrived. */ + signature: string; + signature_alg?: string; + /** key_id as claimed by the UNSIGNED envelope. Never used to select a key. */ + envelope_key_id?: string; +} +/** True when doc is a v2 envelope or a bare v2 payload carrying a sibling signature. */ +export declare function isCertV2(doc: unknown): boolean; +/** + * Normalize either shape into an envelope. + * Accepts {payload, signature} or a bare payload with a sibling signature. + */ +export declare function toEnvelope(doc: Record): CertV2Envelope | string; +export declare function verifyCertificateV2(doc: Record, trustedKeys: KeyDoc, datasetPath?: string): Promise; +//# sourceMappingURL=cert-v2.d.ts.map \ No newline at end of file diff --git a/dist/cert-v2.d.ts.map b/dist/cert-v2.d.ts.map new file mode 100644 index 0000000..45206d8 --- /dev/null +++ b/dist/cert-v2.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cert-v2.d.ts","sourceRoot":"","sources":["../src/cert-v2.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,SAAS,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC5E,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED,kFAAkF;AAClF,MAAM,MAAM,eAAe,GACvB,MAAM,GACN;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExF,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,aAAa,CAAC;IACvB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wFAAwF;AACxF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAM9C;AAwBD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,GAAG,MAAM,CAmBhF;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,WAAW,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,YAAY,CAAC,CAmFvB"} \ No newline at end of file diff --git a/dist/cert-v2.js b/dist/cert-v2.js new file mode 100644 index 0000000..22e8158 --- /dev/null +++ b/dist/cert-v2.js @@ -0,0 +1,219 @@ +// cert.v2 verification. +// +// Differences from cert.v1 that matter for verification: +// +// 1. The signature is NOT a field inside the signed document. In v1 the +// signature lives on the certificate and is stripped before +// canonicalization; in v2 the payload is signed as-is and the signature +// travels beside it in an envelope. So v2 canonicalizes the WHOLE payload +// — nothing is removed. +// 2. The signer is named at payload.issuer.signing_key_id, not cert.key_id. +// 3. The artifact digest is bare lowercase hex at payload.artifact_hash, +// with no "sha256:" prefix. +// 4. There is no rows/columns/algorithm triple. manifest.record_count and +// manifest.engine are the nearest equivalents and are display-only. +// +// Signed bytes are Ed25519 over RFC 8785 (JCS) of the payload. Confirmed +// empirically against the live production certificate in +// fixtures/valid-cert-v2.json: of JCS(payload), JSON.stringify(payload), +// JCS(envelope minus signature) and JCS(payload minus the self-hash), only +// JCS(payload) verifies. +// +// On the envelope's signature field: production serves it as an OBJECT, not a +// bare base64 string — +// +// /api/certificates/:id/signed-payload -> {alg, key_id, value} +// /api/certificates/:id -> {alg, key_id, sig} +// +// so both spellings are accepted, as is a plain string. A verifier that only +// accepted the string form would report MALFORMED on every certificate the +// company has issued. +import { createPublicKey, verify as cryptoVerify } from "node:crypto"; +import { canonicalizeToBytes } from "./canonicalize.js"; +import { sha256File, formatDigest } from "./hash.js"; +import { findKey } from "./keys.js"; +/** True when doc is a v2 envelope or a bare v2 payload carrying a sibling signature. */ +export function isCertV2(doc) { + if (!doc || typeof doc !== "object") + return false; + const d = doc; + if (d.schema_version === "cert.v2") + return true; + const p = d.payload; + return !!p && typeof p === "object" && p.schema_version === "cert.v2"; +} +/** + * Pull the base64 signature out of whichever shape arrived. + * Returns null when there is nothing usable, so the caller can say so plainly. + */ +function unwrapSignature(raw) { + if (typeof raw === "string") { + return raw.length > 0 ? { b64: raw } : null; + } + if (raw && typeof raw === "object") { + const o = raw; + // `value` is what /signed-payload emits; `sig` is what the base route emits. + const b64 = [o.value, o.sig, o.signature].find((v) => typeof v === "string" && v.length > 0); + if (typeof b64 !== "string") + return null; + return { + b64, + alg: typeof o.alg === "string" ? o.alg : undefined, + keyId: typeof o.key_id === "string" ? o.key_id : undefined, + }; + } + return null; +} +/** + * Normalize either shape into an envelope. + * Accepts {payload, signature} or a bare payload with a sibling signature. + */ +export function toEnvelope(doc) { + const hasNested = !!doc.payload && typeof doc.payload === "object"; + const payload = (hasNested ? doc.payload : doc); + const unwrapped = unwrapSignature(doc.signature); + if (!unwrapped) { + return "cert.v2 requires a detached signature alongside the payload; none was present"; + } + if (payload.schema_version !== "cert.v2") { + return `unsupported schema_version: ${String(payload.schema_version)}`; + } + return { + payload, + signature: unwrapped.b64, + signature_alg: unwrapped.alg ?? (typeof doc.signature_alg === "string" ? doc.signature_alg : undefined), + envelope_key_id: unwrapped.keyId ?? (typeof doc.signing_key_id === "string" ? doc.signing_key_id : undefined), + }; +} +export async function verifyCertificateV2(doc, trustedKeys, datasetPath) { + const result = blankV2Result(); + const env = toEnvelope(doc); + if (typeof env === "string") + return finish(result, "MALFORMED", env); + const { payload, signature } = env; + const shapeError = validateV2Shape(payload); + if (shapeError) + return finish(result, "MALFORMED", shapeError); + result.certification_id = payload.certificate_id; + result.issuer = payload.issuer?.name ?? null; + result.signed_at = payload.issued_at; + result.algorithm = payload.manifest?.engine ?? payload.certificate_type ?? null; + result.dataset_hash_expected = formatDigest(payload.artifact_hash.toLowerCase()); + if (typeof payload.manifest?.record_count === "number") { + result.rows = payload.manifest.record_count; + } + // Key selection reads the SIGNED payload only. The envelope is not covered by + // the signature, so trusting its key_id would let anyone redirect which key + // is used to check the bytes — and then present a document that "verifies". + const keyId = payload.issuer.signing_key_id; + if (!keyId) + return finish(result, "MALFORMED", "missing issuer.signing_key_id"); + result.key_id = keyId; + // A disagreement between the signed payload and the envelope means the + // document is internally inconsistent. Refuse rather than silently + // preferring one, so the condition is visible instead of papered over. + if (env.envelope_key_id && env.envelope_key_id !== keyId) { + return finish(result, "MALFORMED", `envelope names key_id ${env.envelope_key_id} but the signed payload names ${keyId}`); + } + const key = findKey(trustedKeys, keyId); + if (!key || key.revoked_at || !isEd25519(key.algorithm)) { + result.checks.key_trust = "fail"; + const reason = !key + ? `key_id ${keyId} not in trusted keys` + : key.revoked_at + ? `key_id ${keyId} was revoked at ${key.revoked_at}` + : `key ${keyId} is not ed25519`; + return finish(result, "UNKNOWN_KEY", reason); + } + result.checks.key_trust = "pass"; + result.key_label = key.label; + const sigBytes = decodeSignature(signature); + if (!sigBytes) { + return finish(result, "MALFORMED", "signature is not 64 bytes of base64-encoded Ed25519"); + } + // v2 signs the entire payload — nothing is stripped. + const canonicalBytes = canonicalizeToBytes(payload); + 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 cert.v2 payload"); + } + if (datasetPath) { + const actualHex = await sha256File(datasetPath); + result.dataset_hash_actual = formatDigest(actualHex); + if (actualHex !== payload.artifact_hash.toLowerCase()) { + result.checks.dataset_match = "fail"; + return finish(result, "DATASET_MISMATCH", `artifact hash mismatch (expected sha256:${payload.artifact_hash.toLowerCase()}, got ${result.dataset_hash_actual})`); + } + result.checks.dataset_match = "pass"; + } + return finish(result, "VALID", "signature verified and key is trusted"); +} +/** + * Compared case-insensitively on purpose. The published keys document spells + * this "Ed25519"; this verifier's own fixtures spell it "ed25519". Treating + * that as an untrusted key would report UNKNOWN_KEY — a security verdict — for + * a cosmetic difference, which teaches users to disbelieve the tool. + */ +function isEd25519(algorithm) { + return typeof algorithm === "string" && algorithm.toLowerCase() === "ed25519"; +} +function validateV2Shape(p) { + if (!p || typeof p !== "object") + return "cert.v2 payload is not an object"; + for (const f of ["certificate_id", "issued_at", "artifact_hash", "issuer"]) { + if (p[f] === undefined || p[f] === null) + return `missing required field: ${f}`; + } + if (!p.issuer || typeof p.issuer !== "object") + return "issuer must be an object"; + if (typeof p.artifact_hash !== "string" || !/^[0-9a-f]{64}$/i.test(p.artifact_hash)) { + return "artifact_hash must be 64 hex characters"; + } + if (p.hash_method && !/^sha-?256$/i.test(p.hash_method)) { + return `unsupported hash_method: ${p.hash_method}`; + } + 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; + 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 blankV2Result() { + 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: "", + }; +} +function finish(r, verdict, reason) { + r.verdict = verdict; + r.reason = reason; + return r; +} +//# sourceMappingURL=cert-v2.js.map \ No newline at end of file diff --git a/dist/cert-v2.js.map b/dist/cert-v2.js.map new file mode 100644 index 0000000..bbce700 --- /dev/null +++ b/dist/cert-v2.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cert-v2.js","sourceRoot":"","sources":["../src/cert-v2.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cert-v2.test.d.ts b/dist/cert-v2.test.d.ts new file mode 100644 index 0000000..97dc27f --- /dev/null +++ b/dist/cert-v2.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=cert-v2.test.d.ts.map \ No newline at end of file diff --git a/dist/cert-v2.test.d.ts.map b/dist/cert-v2.test.d.ts.map new file mode 100644 index 0000000..3c1a167 --- /dev/null +++ b/dist/cert-v2.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cert-v2.test.d.ts","sourceRoot":"","sources":["../src/cert-v2.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/cert-v2.test.js b/dist/cert-v2.test.js new file mode 100644 index 0000000..0952c66 --- /dev/null +++ b/dist/cert-v2.test.js @@ -0,0 +1,183 @@ +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 { isCertV2, toEnvelope, verifyCertificateV2 } from "./cert-v2.js"; +const here = dirname(fileURLToPath(import.meta.url)); +const fixtures = join(here, "..", "fixtures"); +/** Fresh copy each time — several tests mutate the document. */ +async function loadEnvelope() { + return JSON.parse(await readFile(join(fixtures, "valid-cert-v2.json"), "utf8")); +} +async function prodKeys() { + return JSON.parse(await readFile(join(fixtures, "prod-keys.json"), "utf8")); +} +// ── detection ──────────────────────────────────────────────────────────────── +test("isCertV2 detects the production envelope", async () => { + const env = await loadEnvelope(); + // Production's OUTER schema_version is "certifieddata.manifest.v1"; only the + // payload says cert.v2. Detection that looked only at the top level would + // miss every real certificate. + assert.equal(env.schema_version, "certifieddata.manifest.v1"); + assert.equal(isCertV2(env), true); +}); +test("isCertV2 rejects v1 and non-objects", () => { + assert.equal(isCertV2({ schema_version: "cert.v1" }), false); + assert.equal(isCertV2(null), false); + assert.equal(isCertV2("cert.v2"), false); +}); +// ── the real certificate ───────────────────────────────────────────────────── +test("verifies a real production cert.v2 certificate", async () => { + const res = await verifyCertificateV2(await loadEnvelope(), await prodKeys()); + assert.equal(res.verdict, "VALID"); + assert.equal(res.checks.signature, "pass"); + assert.equal(res.checks.key_trust, "pass"); + assert.equal(res.key_id, "ed25519-prod-2025-02"); + assert.equal(res.certification_id, "d6da041f-a70c-4945-93b7-dff1e42a00d0"); +}); +// ── signature spellings ────────────────────────────────────────────────────── +// +// These are the regression tests for the bug that made the first cut of this +// feature useless: production serves `signature` as an OBJECT, and an +// implementation that required a base64 string reported MALFORMED on all 577 +// issued certificates. +test('accepts the {alg, key_id, value} signature object that /signed-payload serves', async () => { + const env = await loadEnvelope(); + assert.equal(typeof env.signature, "object", "fixture should carry the production object form"); + assert.ok(env.signature.value); + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "VALID"); +}); +test('accepts the {alg, key_id, sig} signature object that /api/certificates/:id serves', async () => { + const env = await loadEnvelope(); + const sig = env.signature; + env.signature = { alg: sig.alg, key_id: sig.key_id, sig: sig.value }; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "VALID"); +}); +test("accepts a bare base64 signature string", async () => { + const env = await loadEnvelope(); + env.signature = env.signature.value; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "VALID"); +}); +test("refuses a v2 payload with no detached signature", async () => { + const env = await loadEnvelope(); + delete env.signature; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /detached signature/); +}); +test("refuses a signature object carrying no usable value", async () => { + const env = await loadEnvelope(); + env.signature = { alg: "Ed25519", key_id: "ed25519-prod-2025-02" }; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /detached signature/); +}); +test("refuses a signature that is not 64 bytes", async () => { + const env = await loadEnvelope(); + env.signature = Buffer.from("too short").toString("base64"); + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /64 bytes/); +}); +// ── key selection is a security boundary ───────────────────────────────────── +test("selects the key named in the SIGNED payload, not the envelope", async () => { + const env = await loadEnvelope(); + const sig = env.signature; + // The envelope is not covered by the signature. If key selection trusted it, + // an attacker could point verification at a key of their choosing. + env.signature = { ...sig, key_id: "attacker-supplied-key" }; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /envelope names key_id attacker-supplied-key/); + assert.equal(res.checks.signature, "skipped"); +}); +test("tolerates the envelope omitting key_id entirely", async () => { + const env = await loadEnvelope(); + const sig = env.signature; + env.signature = { alg: sig.alg, value: sig.value }; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "VALID"); +}); +// ── trust and revocation ───────────────────────────────────────────────────── +test("rejects an unknown signing key", async () => { + const keys = { issuer: "CertifiedData.io", keys: [] }; + const res = await verifyCertificateV2(await loadEnvelope(), keys); + assert.equal(res.verdict, "UNKNOWN_KEY"); + assert.equal(res.checks.signature, "skipped"); +}); +test("rejects a revoked signing key", async () => { + const keys = await prodKeys(); + keys.keys[0].revoked_at = "2026-09-01T00:00:00Z"; + const res = await verifyCertificateV2(await loadEnvelope(), keys); + assert.equal(res.verdict, "UNKNOWN_KEY"); + assert.match(res.reason, /revoked/); +}); +test('accepts algorithm "Ed25519" as well as "ed25519"', async () => { + const keys = await prodKeys(); + // This is how the issuer's own published keys document spells it. Returning + // UNKNOWN_KEY on the capital E would be a security verdict for a cosmetic + // difference. + keys.keys[0].algorithm = "Ed25519"; + const res = await verifyCertificateV2(await loadEnvelope(), keys); + assert.equal(res.verdict, "VALID"); +}); +test("rejects a key that is not ed25519 at all", async () => { + const keys = await prodKeys(); + keys.keys[0].algorithm = "rsa"; + const res = await verifyCertificateV2(await loadEnvelope(), keys); + assert.equal(res.verdict, "UNKNOWN_KEY"); + assert.match(res.reason, /not ed25519/); +}); +// ── tamper detection ───────────────────────────────────────────────────────── +test("rejects a tampered v2 payload", async () => { + const env = await loadEnvelope(); + env.payload.artifact_hash = "0".repeat(64); + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "INVALID"); + assert.equal(res.checks.signature, "fail"); +}); +test("rejects a payload with a field appended", async () => { + const env = await loadEnvelope(); + env.payload.injected = "not in the signed bytes"; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "INVALID"); + assert.equal(res.checks.signature, "fail"); +}); +test("rejects a payload with a field removed", async () => { + const env = await loadEnvelope(); + delete env.payload.certification_scope; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "INVALID"); +}); +// ── shape validation ───────────────────────────────────────────────────────── +test("reports a missing required field rather than failing the signature", async () => { + const env = await loadEnvelope(); + delete env.payload.certificate_id; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /certificate_id/); +}); +test("rejects a non-hex artifact_hash", async () => { + const env = await loadEnvelope(); + env.payload.artifact_hash = "sha256:not-hex"; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /64 hex/); +}); +test("toEnvelope explains an unsupported schema rather than throwing", () => { + const out = toEnvelope({ payload: { schema_version: "cert.v3" }, signature: "x".repeat(88) }); + assert.equal(typeof out, "string"); + assert.match(out, /unsupported schema_version: cert.v3/); +}); +// ── dataset binding ────────────────────────────────────────────────────────── +test("--dataset mismatch is reported, not silently passed", async () => { + const res = await verifyCertificateV2(await loadEnvelope(), await prodKeys(), join(fixtures, "prod-keys.json")); + assert.equal(res.verdict, "DATASET_MISMATCH"); + assert.equal(res.checks.signature, "pass"); + assert.equal(res.checks.dataset_match, "fail"); +}); +//# sourceMappingURL=cert-v2.test.js.map \ No newline at end of file diff --git a/dist/cert-v2.test.js.map b/dist/cert-v2.test.js.map new file mode 100644 index 0000000..59c4c8e --- /dev/null +++ b/dist/cert-v2.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cert-v2.test.js","sourceRoot":"","sources":["../src/cert-v2.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cli.d.ts.map b/dist/cli.d.ts.map index 31b199e..781cf8f 100644 --- a/dist/cli.d.ts.map +++ b/dist/cli.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA+DA,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAwG1D"} \ No newline at end of file +{"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,CA6G1D"} \ No newline at end of file diff --git a/dist/cli.js b/dist/cli.js index 859e91a..636397c 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -2,6 +2,7 @@ import { fetchCert } from "./fetch-cert.js"; import { loadKeys } from "./keys.js"; import { verifyCertificate } from "./verify.js"; +import { isCertV2, verifyCertificateV2 } from "./cert-v2.js"; import { fetchReceipt, loadReceiptKey, verifyReceiptEnvelope } from "./receipt.js"; import { resolveArtifactKind } from "./resolve.js"; const HELP = `certifieddata-verify [options] @@ -126,12 +127,17 @@ export async function main(argv) { case "MALFORMED": return EXIT.MALFORMED; } } - // ── Certificate path (unchanged behavior) ────────────────────────────── + // ── Certificate path ─────────────────────────────────────────────────── + // cert.v1 and cert.v2 sign different bytes and name the signer in different + // places, so the schema decides which verifier runs. Both return the same + // VerifyResult, so output and exit codes are identical either way. let result; try { - const cert = await fetchCert(target, { offline: args.offline }); + const doc = 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); + result = isCertV2(doc) + ? await verifyCertificateV2(doc, keys, args.dataset) + : await verifyCertificate(doc, keys, args.dataset); } catch (err) { const reason = err.message; @@ -220,9 +226,15 @@ function printHuman(r) { 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`); + // cert.v2 carries no column count, so only print a shape when we have + // one. Printing "0 rows × 0 cols" for a v2 certificate states something + // false about the artifact. + const shape = r.rows !== undefined && r.columns !== undefined + ? `${r.rows.toLocaleString("en-US")} rows × ${r.columns.toLocaleString("en-US")} cols · ` + : r.rows !== undefined + ? `${r.rows.toLocaleString("en-US")} records · ` + : ""; + process.stdout.write(` ${c.dim("algorithm")} ${r.algorithm} · ${shape}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`); } diff --git a/dist/cli.js.map b/dist/cli.js.map index 877eda1..b632104 100644 --- a/dist/cli.js.map +++ b/dist/cli.js.map @@ -1 +1 @@ -{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.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 index a658b62..fc9deb7 100644 --- a/dist/fetch-cert.d.ts +++ b/dist/fetch-cert.d.ts @@ -1,8 +1,29 @@ -import type { Certificate } from "./types.js"; -export declare const DEFAULT_CERT_API = "https://certifieddata.io/api/v1/certificates"; +/** + * Resolving a bare certificate id must land on bytes that are actually + * verifiable. + * + * `https://certifieddata.io/api/v1/certificates/:id` — the path this used to + * point at — returns 404; it was never deployed. + * + * `https://api.certifieddata.io/api/certificates/:id` does respond, but it + * serves a cert.v1-shaped *display projection* of a cert.v2 certificate. It + * carries the real signature bytes while the signature covers the v2 payload, + * not the projection, so verifying that document yields INVALID — which reads + * as tampering when nothing has been tampered with. + * + * `/signed-payload` is the envelope whose signature verifies over its own + * payload, so that is what a verifier has to ask for. + */ +export declare const DEFAULT_CERT_API = "https://api.certifieddata.io/api/certificates"; +export declare const CERT_ENVELOPE_SUFFIX = "/signed-payload"; export interface FetchCertOptions { apiBase?: string; offline?: boolean; } -export declare function fetchCert(idOrPathOrUrl: string, opts?: FetchCertOptions): Promise; +/** + * Returns the raw certificate document. Shape validation belongs to the + * verifier, not the fetcher: the document may be a cert.v1 certificate or a + * cert.v2 envelope, and deciding which is the caller's job. + */ +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 index fe49df0..e9e2682 100644 --- a/dist/fetch-cert.d.ts.map +++ b/dist/fetch-cert.d.ts.map @@ -1 +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 +{"version":3,"file":"fetch-cert.d.ts","sourceRoot":"","sources":["../src/fetch-cert.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB,kDAAkD,CAAC;AAChF,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,aAAa,EAAE,MAAM,EACrB,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAqBlC"} \ No newline at end of file diff --git a/dist/fetch-cert.js b/dist/fetch-cert.js index beeeb4c..20565b3 100644 --- a/dist/fetch-cert.js +++ b/dist/fetch-cert.js @@ -1,10 +1,34 @@ import { readFile } from "node:fs/promises"; -export const DEFAULT_CERT_API = "https://certifieddata.io/api/v1/certificates"; +/** + * Resolving a bare certificate id must land on bytes that are actually + * verifiable. + * + * `https://certifieddata.io/api/v1/certificates/:id` — the path this used to + * point at — returns 404; it was never deployed. + * + * `https://api.certifieddata.io/api/certificates/:id` does respond, but it + * serves a cert.v1-shaped *display projection* of a cert.v2 certificate. It + * carries the real signature bytes while the signature covers the v2 payload, + * not the projection, so verifying that document yields INVALID — which reads + * as tampering when nothing has been tampered with. + * + * `/signed-payload` is the envelope whose signature verifies over its own + * payload, so that is what a verifier has to ask for. + */ +export const DEFAULT_CERT_API = "https://api.certifieddata.io/api/certificates"; +export const CERT_ENVELOPE_SUFFIX = "/signed-payload"; +/** + * Returns the raw certificate document. Shape validation belongs to the + * verifier, not the fetcher: the document may be a cert.v1 certificate or a + * cert.v2 envelope, and deciding which is the caller's job. + */ export async function fetchCert(idOrPathOrUrl, opts = {}) { if (idOrPathOrUrl === "-") { return parseCertJson(await readStdin()); } - if (idOrPathOrUrl.endsWith(".json") || idOrPathOrUrl.startsWith("./") || idOrPathOrUrl.startsWith("/")) { + if (idOrPathOrUrl.endsWith(".json") || + idOrPathOrUrl.startsWith("./") || + idOrPathOrUrl.startsWith("/")) { return parseCertJson(await readFile(idOrPathOrUrl, "utf8")); } if (/^https?:\/\//.test(idOrPathOrUrl)) { @@ -16,7 +40,7 @@ export async function fetchCert(idOrPathOrUrl, opts = {}) { 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)}`; + const url = `${base.replace(/\/$/, "")}/${encodeURIComponent(idOrPathOrUrl)}${CERT_ENVELOPE_SUFFIX}`; return parseCertJson(await fetchText(url)); } async function fetchText(url) { diff --git a/dist/fetch-cert.js.map b/dist/fetch-cert.js.map index 519366c..9dd0a78 100644 --- a/dist/fetch-cert.js.map +++ b/dist/fetch-cert.js.map @@ -1 +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 +{"version":3,"file":"fetch-cert.js","sourceRoot":"","sources":["../src/fetch-cert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,+CAA+C,CAAC;AAChF,MAAM,CAAC,MAAM,oBAAoB,GAAG,iBAAiB,CAAC;AAOtD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,aAAqB,EACrB,OAAyB,EAAE;IAE3B,IAAI,aAAa,KAAK,GAAG,EAAE,CAAC;QAC1B,OAAO,aAAa,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,IACE,aAAa,CAAC,QAAQ,CAAC,OAAO,CAAC;QAC/B,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC;QAC9B,aAAa,CAAC,UAAU,CAAC,GAAG,CAAC,EAC7B,CAAC;QACD,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,GAAG,oBAAoB,EAAE,CAAC;IACrG,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,CAA4B,CAAC;AACrD,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,CAAE,KAAgB,CAAC,CAAC;IAClF,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/index.d.ts b/dist/index.d.ts index 9630093..5b9fe7c 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,7 +1,9 @@ export { verifyCertificate } from "./verify.js"; +export { verifyCertificateV2, isCertV2, toEnvelope } from "./cert-v2.js"; +export type { CertV2Payload, CertV2Envelope, CertV2Signature } from "./cert-v2.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 { fetchCert, DEFAULT_CERT_API, CERT_ENVELOPE_SUFFIX } 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 index 77a1e8a..d576e14 100644 --- a/dist/index.d.ts.map +++ b/dist/index.d.ts.map @@ -1 +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 +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACnF,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,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACpF,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 index 21c58f6..1e3ddf9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,6 +1,7 @@ export { verifyCertificate } from "./verify.js"; +export { verifyCertificateV2, isCertV2, toEnvelope } from "./cert-v2.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 { fetchCert, DEFAULT_CERT_API, CERT_ENVELOPE_SUFFIX } 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 index 2b665bc..40cb0f4 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +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 +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEzE,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,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"} \ No newline at end of file diff --git a/dist/keys.d.ts b/dist/keys.d.ts index 2d97bf2..1c8104f 100644 --- a/dist/keys.d.ts +++ b/dist/keys.d.ts @@ -1,5 +1,17 @@ import type { KeyDoc, KeyEntry } from "./types.js"; -export declare const DEFAULT_KEYS_URL = "https://certifieddata.io/.well-known/certifieddata-keys.json"; +/** + * The pinned trust root. + * + * This is the document the issuer actually publishes, and the one every + * certificate's own `public_key_url` points at. It is pinned here rather than + * read out of the certificate: a URL taken from an unverified document would + * let whoever supplied the document choose the keys it is checked against. + * + * `certifieddata-keys.json` — the path this used to point at — returns 404 and + * was never deployed. Both dialects are accepted by `parseKeyDoc`, so if that + * document is published later it will work without a code change. + */ +export declare const DEFAULT_KEYS_URL = "https://certifieddata.io/.well-known/signing-keys.json"; export declare const CACHE_PATH: string; export declare const CACHE_TTL_MS: number; export interface LoadKeysOptions { diff --git a/dist/keys.d.ts.map b/dist/keys.d.ts.map index 6fc6d06..b08b3dd 100644 --- a/dist/keys.d.ts.map +++ b/dist/keys.d.ts.map @@ -1 +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 +{"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;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,2DAA2D,CAAC;AACzF,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 index 7be5938..948a2bd 100644 --- a/dist/keys.js +++ b/dist/keys.js @@ -1,7 +1,19 @@ 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"; +/** + * The pinned trust root. + * + * This is the document the issuer actually publishes, and the one every + * certificate's own `public_key_url` points at. It is pinned here rather than + * read out of the certificate: a URL taken from an unverified document would + * let whoever supplied the document choose the keys it is checked against. + * + * `certifieddata-keys.json` — the path this used to point at — returns 404 and + * was never deployed. Both dialects are accepted by `parseKeyDoc`, so if that + * document is published later it will work without a code change. + */ +export const DEFAULT_KEYS_URL = "https://certifieddata.io/.well-known/signing-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 = {}) { @@ -44,12 +56,76 @@ export async function loadKeys(opts = {}) { export function findKey(doc, keyId) { return doc.keys.find((k) => k.key_id === keyId); } +/** + * Two dialects are in circulation and both have to work: + * + * this verifier's own shape keys[].public_key, algorithm "ed25519", + * per-key revoked_at + * signing-keys.v1 (published) keys[].public_key_pem, algorithm "Ed25519", + * revocation in top-level revoked[]/retired[] + * + * Normalizing here rather than at each call site means the difference cannot + * turn into a wrong verdict. Two of these differences are security-relevant: + * a mis-read `algorithm` yields UNKNOWN_KEY on a good key, and an unmapped + * `revoked[]` would let a revoked key keep verifying. + */ 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; + const revokedAtFor = buildRevocationIndex(parsed); + const keys = parsed.keys.map((raw) => { + const keyId = String(raw.key_id ?? ""); + const material = (raw.public_key ?? raw.public_key_pem ?? raw.public_key_raw_b64url); + if (!keyId || typeof material !== "string" || material.length === 0) { + throw new Error(`invalid key document: entry ${keyId || "(no key_id)"} has no public key`); + } + return { + ...raw, + key_id: keyId, + // CRLF appears in the published PEM; node's createPublicKey is fussier + // about that than it needs to be. + public_key: material.replace(/\r\n/g, "\n"), + algorithm: String(raw.algorithm ?? "").toLowerCase(), + revoked_at: raw.revoked_at ?? revokedAtFor.get(keyId) ?? null, + }; + }); + return { ...parsed, issuer: String(parsed.issuer ?? ""), keys }; +} +/** + * signing-keys.v1 lists revocations separately from the key entries. Entries + * may be bare key_id strings or objects; anything else is refused rather than + * ignored, because silently skipping a revocation record we cannot read would + * mean treating a possibly-revoked key as good. + */ +function buildRevocationIndex(doc) { + const out = new Map(); + for (const field of ["revoked", "retired"]) { + const list = doc[field]; + if (list === undefined || list === null) + continue; + if (!Array.isArray(list)) { + throw new Error(`invalid key document: ${field} must be an array`); + } + for (const entry of list) { + if (typeof entry === "string") { + out.set(entry, `listed in ${field}[]`); + continue; + } + if (entry && typeof entry === "object") { + const e = entry; + const id = e.key_id ?? e.id; + if (typeof id === "string" && id.length > 0) { + const when = e.revoked_at ?? e.retired_at ?? e.at; + out.set(id, typeof when === "string" ? when : `listed in ${field}[]`); + continue; + } + } + throw new Error(`invalid key document: unreadable entry in ${field}[] — refusing to ignore a revocation record`); + } + } + return out; } async function readCacheIfFresh(path) { try { diff --git a/dist/keys.js.map b/dist/keys.js.map index 621ac04..f3ed1ce 100644 --- a/dist/keys.js.map +++ b/dist/keys.js.map @@ -1 +1 @@ -{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"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/resolve.d.ts.map b/dist/resolve.d.ts.map index 1ecc699..d2ab9cd 100644 --- a/dist/resolve.d.ts.map +++ b/dist/resolve.d.ts.map @@ -1 +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 +{"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,CA+ErB"} \ No newline at end of file diff --git a/dist/resolve.js b/dist/resolve.js index 86b9575..05caf11 100644 --- a/dist/resolve.js +++ b/dist/resolve.js @@ -19,7 +19,7 @@ export async function resolveArtifactKind(target, opts = {}) { // Endpoint-URL hints. if (/\/api\/payments\/verify\//.test(target)) return { kind: "receipt", via: "url-path" }; - if (/\/api\/v1\/certificates\//.test(target)) + if (/\/api\/(v1\/)?certificates?\//.test(target)) return { kind: "certificate", via: "url-path" }; // Local file / stdin: sniff the JSON shape. const looksLocal = target === "-" || @@ -30,11 +30,20 @@ export async function resolveArtifactKind(target, opts = {}) { if (looksLocal && target !== "-") { try { const parsed = JSON.parse(await readFile(target, "utf8")); + // A cert.v2 envelope names its schema on the payload, not at the top + // level — production serves the outer document as + // "certifieddata.manifest.v1" — so sniffing only the top level would + // miss every real certificate. const schema = parsed.schema_version ?? + parsed.payload?.schema_version ?? parsed.receipt?.schema_version; + const payloadSchema = parsed.payload + ?.schema_version; if (schema === "payment_receipt.v1") return { kind: "receipt", via: "local-schema" }; - if (typeof schema === "string" && schema.startsWith("cert.")) { + if (isCertificateSchema(schema) || + isCertificateSchema(payloadSchema) || + schema === "certifieddata.manifest.v1") { return { kind: "certificate", via: "local-schema" }; } // Envelope shape without schema — a verify-endpoint dump. @@ -84,4 +93,17 @@ export async function resolveArtifactKind(target, opts = {}) { return { kind: "receipt", via: "probe" }; return { kind: "not_found", via: "probe" }; } +/** + * Certificate schema names seen in the wild: + * cert.v1, cert.v2 — the payload's own schema_version + * certifieddata.cert.v1 — the public display projection + * certifieddata.manifest.v1 — the signed-payload envelope + */ +function isCertificateSchema(schema) { + if (typeof schema !== "string") + return false; + return (schema.startsWith("cert.") || + schema.startsWith("certifieddata.cert.") || + schema.startsWith("certifieddata.manifest.")); +} //# sourceMappingURL=resolve.js.map \ No newline at end of file diff --git a/dist/resolve.js.map b/dist/resolve.js.map index 5dee692..813a628 100644 --- a/dist/resolve.js.map +++ b/dist/resolve.js.map @@ -1 +1 @@ -{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,4EAA4E;AAC5E,iDAAiD;AACjD,6EAA6E;AAC7E,qEAAqE;AACrE,yCAAyC;AACzC,mEAAmE;AACnE,yCAAyC;AACzC,sEAAsE;AACtE,oCAAoC;AAEpC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAUnD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAc,EACd,OAA6E,EAAE;IAE/E,2DAA2D;IAC3D,IAAI,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC;IAErF,sBAAsB;IACtB,IAAI,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;IAC1F,IAAI,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;IAE9F,4CAA4C;IAC5C,MAAM,UAAU,GACd,MAAM,KAAK,GAAG;QACd,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QACxB,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;QACvB,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC;QACtB,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,UAAU,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAA4B,CAAC;YACrF,MAAM,MAAM,GACT,MAAM,CAAC,cAAqC;gBAC3C,MAAM,CAAC,OAA+C,EAAE,cAAqC,CAAC;YAClG,IAAI,MAAM,KAAK,oBAAoB;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC;YACrF,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC7D,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC;YACtD,CAAC;YACD,0DAA0D;YAC1D,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,iBAAiB;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC;YAClG,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,0BAA0B,EAAE,CAAC;QAClE,CAAC;IACH,CAAC;IACD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC;IAEzE,kEAAkE;IAClE,IAAI,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,iBAAiB,EAAE,CAAC;IAEzE,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;IAC7G,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,cAAc,IAAI,mBAAmB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;IAEnH,MAAM,KAAK,GAAG,KAAK,EAAE,GAAW,EAA2C,EAAE;QAC3E,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YAChD,IAAI,GAAG,CAAC,EAAE;gBAAE,OAAO,QAAQ,CAAC;YAC5B,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;gBAAE,OAAO,SAAS,CAAC;YACzC,OAAO,OAAO,CAAC,CAAC,0DAA0D;QAC5E,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAEzE,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACzC,yEAAyE;QACzE,6CAA6C;QAC7C,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;QACzF,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;QACrF,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IACnD,CAAC;IACD,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IACvF,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IACpE,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IAChE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AAC7C,CAAC"} \ No newline at end of file +{"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/fixtures/prod-keys.json b/fixtures/prod-keys.json new file mode 100644 index 0000000..e79b6fe --- /dev/null +++ b/fixtures/prod-keys.json @@ -0,0 +1,13 @@ +{ + "issuer": "CertifiedData.io", + "keys": [ + { + "key_id": "ed25519-prod-2025-02", + "public_key": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAGvAJNlssfFuZp4TNNr0eZhYNHoQSgng92z8lLjKcwLM=\n-----END PUBLIC KEY-----\n", + "algorithm": "ed25519", + "created_at": "2026-02-22T18:26:46.293Z", + "revoked_at": null, + "label": "production signing key" + } + ] +} diff --git a/fixtures/valid-cert-v2.json b/fixtures/valid-cert-v2.json new file mode 100644 index 0000000..8dd6b3e --- /dev/null +++ b/fixtures/valid-cert-v2.json @@ -0,0 +1,60 @@ +{ + "schema_version": "certifieddata.manifest.v1", + "payload": { + "hashes": { + "datasets": { + "2026-09-07.56f57741adc7.public_audit_export.v1.json": { + "sha256": "cc437f2cc9e2e58fc22a9c88bd33c001305006aadcebf9af2eb7d9bfb0b34e0e", + "size_bytes": 780 + } + }, + "certificate_payload_sha256": "d611a0115feed01a3bf866258cdac97aaf5c6c310cfea2525e3aa39d5216f08e" + }, + "issuer": { + "name": "Certified Data LLC", + "environment": "production", + "signature_alg": "Ed25519", + "signing_key_id": "ed25519-prod-2025-02" + }, + "subject": { + "user_id": "536f5e7e-7c9d-4f25-ba3a-319230b0c6cf", + "dataset_name": "2026-09-07.56f57741adc7.public_audit_export.v1.json" + }, + "manifest": { + "engine": "notary-s2s", + "outputs": { + "dataset_ephemeral": true + }, + "manifest_id": "55c1a91a-25b7-415b-9582-cad5e25a6f68", + "record_count": 1, + "manifest_mode": "notary" + }, + "artifacts": { + "pdf": { + "path": "certs/d6da041f-a70c-4945-93b7-dff1e42a00d0.pdf", + "bucket": "certificates" + }, + "verification_url": "https://certifieddata.io/verify/d6da041f-a70c-4945-93b7-dff1e42a00d0" + }, + "issued_at": "2026-09-07T00:31:13.652Z", + "provenance": { + "audit_vault_entry_ids": [], + "audit_vault_retention_years": 7 + }, + "hash_method": "SHA-256", + "artifact_hash": "cc437f2cc9e2e58fc22a9c88bd33c001305006aadcebf9af2eb7d9bfb0b34e0e", + "certificate_id": "d6da041f-a70c-4945-93b7-dff1e42a00d0", + "schema_version": "cert.v2", + "inner_artifacts": {}, + "certificate_type": "MANIFEST", + "artifact_filename": "2026-09-07.56f57741adc7.public_audit_export.v1.json", + "artifact_mime_type": "application/json", + "certification_scope": "This certificate covers the delivered ZIP artifact and the listed inner files.", + "hashing_methodology_version": "artifact-hash.v1" + }, + "signature": { + "alg": "Ed25519", + "key_id": "ed25519-prod-2025-02", + "value": "z6wuaC86LVkXnYLXtCpS6Xui3I1/piGyFyRdt66iLfuuIlip/2atKf0M7qQbJIfEAjZpTcQW6bdPhbL1g1/VBA==" + } +} diff --git a/package.json b/package.json index 7db81cc..be1d5a3 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "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 dist/exit-codes.test.js dist/bin-invocation.test.js", + "test": "tsc -p tsconfig.json && node --test dist/canonicalize.test.js dist/verify.test.js dist/cert-v2.test.js dist/cli.test.js dist/receipt.test.js dist/receipt-vectors.test.js dist/exit-codes.test.js dist/bin-invocation.test.js", "fixtures": "node fixtures/generate.mjs", "prepublishOnly": "npm run build && npm test" }, diff --git a/src/cert-v2.test.ts b/src/cert-v2.test.ts new file mode 100644 index 0000000..7f020ab --- /dev/null +++ b/src/cert-v2.test.ts @@ -0,0 +1,223 @@ +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 { isCertV2, toEnvelope, verifyCertificateV2 } from "./cert-v2.js"; +import type { KeyDoc } from "./types.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const fixtures = join(here, "..", "fixtures"); + +/** Fresh copy each time — several tests mutate the document. */ +async function loadEnvelope(): Promise> { + return JSON.parse(await readFile(join(fixtures, "valid-cert-v2.json"), "utf8")) as Record< + string, + unknown + >; +} + +async function prodKeys(): Promise { + return JSON.parse(await readFile(join(fixtures, "prod-keys.json"), "utf8")) as KeyDoc; +} + +// ── detection ──────────────────────────────────────────────────────────────── + +test("isCertV2 detects the production envelope", async () => { + const env = await loadEnvelope(); + // Production's OUTER schema_version is "certifieddata.manifest.v1"; only the + // payload says cert.v2. Detection that looked only at the top level would + // miss every real certificate. + assert.equal(env.schema_version, "certifieddata.manifest.v1"); + assert.equal(isCertV2(env), true); +}); + +test("isCertV2 rejects v1 and non-objects", () => { + assert.equal(isCertV2({ schema_version: "cert.v1" }), false); + assert.equal(isCertV2(null), false); + assert.equal(isCertV2("cert.v2"), false); +}); + +// ── the real certificate ───────────────────────────────────────────────────── + +test("verifies a real production cert.v2 certificate", async () => { + const res = await verifyCertificateV2(await loadEnvelope(), await prodKeys()); + assert.equal(res.verdict, "VALID"); + assert.equal(res.checks.signature, "pass"); + assert.equal(res.checks.key_trust, "pass"); + assert.equal(res.key_id, "ed25519-prod-2025-02"); + assert.equal(res.certification_id, "d6da041f-a70c-4945-93b7-dff1e42a00d0"); +}); + +// ── signature spellings ────────────────────────────────────────────────────── +// +// These are the regression tests for the bug that made the first cut of this +// feature useless: production serves `signature` as an OBJECT, and an +// implementation that required a base64 string reported MALFORMED on all 577 +// issued certificates. + +test('accepts the {alg, key_id, value} signature object that /signed-payload serves', async () => { + const env = await loadEnvelope(); + assert.equal(typeof env.signature, "object", "fixture should carry the production object form"); + assert.ok((env.signature as Record).value); + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "VALID"); +}); + +test('accepts the {alg, key_id, sig} signature object that /api/certificates/:id serves', async () => { + const env = await loadEnvelope(); + const sig = env.signature as Record; + env.signature = { alg: sig.alg, key_id: sig.key_id, sig: sig.value }; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "VALID"); +}); + +test("accepts a bare base64 signature string", async () => { + const env = await loadEnvelope(); + env.signature = (env.signature as Record).value; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "VALID"); +}); + +test("refuses a v2 payload with no detached signature", async () => { + const env = await loadEnvelope(); + delete env.signature; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /detached signature/); +}); + +test("refuses a signature object carrying no usable value", async () => { + const env = await loadEnvelope(); + env.signature = { alg: "Ed25519", key_id: "ed25519-prod-2025-02" }; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /detached signature/); +}); + +test("refuses a signature that is not 64 bytes", async () => { + const env = await loadEnvelope(); + env.signature = Buffer.from("too short").toString("base64"); + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /64 bytes/); +}); + +// ── key selection is a security boundary ───────────────────────────────────── + +test("selects the key named in the SIGNED payload, not the envelope", async () => { + const env = await loadEnvelope(); + const sig = env.signature as Record; + // The envelope is not covered by the signature. If key selection trusted it, + // an attacker could point verification at a key of their choosing. + env.signature = { ...sig, key_id: "attacker-supplied-key" }; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /envelope names key_id attacker-supplied-key/); + assert.equal(res.checks.signature, "skipped"); +}); + +test("tolerates the envelope omitting key_id entirely", async () => { + const env = await loadEnvelope(); + const sig = env.signature as Record; + env.signature = { alg: sig.alg, value: sig.value }; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "VALID"); +}); + +// ── trust and revocation ───────────────────────────────────────────────────── + +test("rejects an unknown signing key", async () => { + const keys: KeyDoc = { issuer: "CertifiedData.io", keys: [] }; + const res = await verifyCertificateV2(await loadEnvelope(), keys); + assert.equal(res.verdict, "UNKNOWN_KEY"); + assert.equal(res.checks.signature, "skipped"); +}); + +test("rejects a revoked signing key", async () => { + const keys = await prodKeys(); + keys.keys[0].revoked_at = "2026-09-01T00:00:00Z"; + const res = await verifyCertificateV2(await loadEnvelope(), keys); + assert.equal(res.verdict, "UNKNOWN_KEY"); + assert.match(res.reason, /revoked/); +}); + +test('accepts algorithm "Ed25519" as well as "ed25519"', async () => { + const keys = await prodKeys(); + // This is how the issuer's own published keys document spells it. Returning + // UNKNOWN_KEY on the capital E would be a security verdict for a cosmetic + // difference. + keys.keys[0].algorithm = "Ed25519" as "ed25519"; + const res = await verifyCertificateV2(await loadEnvelope(), keys); + assert.equal(res.verdict, "VALID"); +}); + +test("rejects a key that is not ed25519 at all", async () => { + const keys = await prodKeys(); + keys.keys[0].algorithm = "rsa" as "ed25519"; + const res = await verifyCertificateV2(await loadEnvelope(), keys); + assert.equal(res.verdict, "UNKNOWN_KEY"); + assert.match(res.reason, /not ed25519/); +}); + +// ── tamper detection ───────────────────────────────────────────────────────── + +test("rejects a tampered v2 payload", async () => { + const env = await loadEnvelope(); + (env.payload as Record).artifact_hash = "0".repeat(64); + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "INVALID"); + assert.equal(res.checks.signature, "fail"); +}); + +test("rejects a payload with a field appended", async () => { + const env = await loadEnvelope(); + (env.payload as Record).injected = "not in the signed bytes"; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "INVALID"); + assert.equal(res.checks.signature, "fail"); +}); + +test("rejects a payload with a field removed", async () => { + const env = await loadEnvelope(); + delete (env.payload as Record).certification_scope; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "INVALID"); +}); + +// ── shape validation ───────────────────────────────────────────────────────── + +test("reports a missing required field rather than failing the signature", async () => { + const env = await loadEnvelope(); + delete (env.payload as Record).certificate_id; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /certificate_id/); +}); + +test("rejects a non-hex artifact_hash", async () => { + const env = await loadEnvelope(); + (env.payload as Record).artifact_hash = "sha256:not-hex"; + const res = await verifyCertificateV2(env, await prodKeys()); + assert.equal(res.verdict, "MALFORMED"); + assert.match(res.reason, /64 hex/); +}); + +test("toEnvelope explains an unsupported schema rather than throwing", () => { + const out = toEnvelope({ payload: { schema_version: "cert.v3" }, signature: "x".repeat(88) }); + assert.equal(typeof out, "string"); + assert.match(out as string, /unsupported schema_version: cert.v3/); +}); + +// ── dataset binding ────────────────────────────────────────────────────────── + +test("--dataset mismatch is reported, not silently passed", async () => { + const res = await verifyCertificateV2( + await loadEnvelope(), + await prodKeys(), + join(fixtures, "prod-keys.json"), + ); + assert.equal(res.verdict, "DATASET_MISMATCH"); + assert.equal(res.checks.signature, "pass"); + assert.equal(res.checks.dataset_match, "fail"); +}); diff --git a/src/cert-v2.ts b/src/cert-v2.ts new file mode 100644 index 0000000..ede7238 --- /dev/null +++ b/src/cert-v2.ts @@ -0,0 +1,280 @@ +// cert.v2 verification. +// +// Differences from cert.v1 that matter for verification: +// +// 1. The signature is NOT a field inside the signed document. In v1 the +// signature lives on the certificate and is stripped before +// canonicalization; in v2 the payload is signed as-is and the signature +// travels beside it in an envelope. So v2 canonicalizes the WHOLE payload +// — nothing is removed. +// 2. The signer is named at payload.issuer.signing_key_id, not cert.key_id. +// 3. The artifact digest is bare lowercase hex at payload.artifact_hash, +// with no "sha256:" prefix. +// 4. There is no rows/columns/algorithm triple. manifest.record_count and +// manifest.engine are the nearest equivalents and are display-only. +// +// Signed bytes are Ed25519 over RFC 8785 (JCS) of the payload. Confirmed +// empirically against the live production certificate in +// fixtures/valid-cert-v2.json: of JCS(payload), JSON.stringify(payload), +// JCS(envelope minus signature) and JCS(payload minus the self-hash), only +// JCS(payload) verifies. +// +// On the envelope's signature field: production serves it as an OBJECT, not a +// bare base64 string — +// +// /api/certificates/:id/signed-payload -> {alg, key_id, value} +// /api/certificates/:id -> {alg, key_id, sig} +// +// so both spellings are accepted, as is a plain string. A verifier that only +// accepted the string form would report MALFORMED on every certificate the +// company has issued. + +import { createPublicKey, verify as cryptoVerify } from "node:crypto"; +import { canonicalizeToBytes } from "./canonicalize.js"; +import { sha256File, formatDigest } from "./hash.js"; +import { findKey } from "./keys.js"; +import type { KeyDoc, VerifyResult } from "./types.js"; + +export interface CertV2Issuer { + name?: string; + signing_key_id: string; + signature_alg?: string; + environment?: string; +} + +export interface CertV2Payload { + schema_version: "cert.v2"; + certificate_id: string; + certificate_type?: string; + issued_at: string; + artifact_hash: string; + hash_method?: string; + issuer: CertV2Issuer; + subject?: Record; + manifest?: { engine?: string; record_count?: number; [k: string]: unknown }; + [k: string]: unknown; +} + +/** The signature as production actually serves it, or as a bare base64 string. */ +export type CertV2Signature = + | string + | { value?: string; sig?: string; signature?: string; alg?: string; key_id?: string }; + +export interface CertV2Envelope { + payload: CertV2Payload; + /** base64 Ed25519, already unwrapped from whichever spelling arrived. */ + signature: string; + signature_alg?: string; + /** key_id as claimed by the UNSIGNED envelope. Never used to select a key. */ + envelope_key_id?: string; +} + +/** True when doc is a v2 envelope or a bare v2 payload carrying a sibling signature. */ +export function isCertV2(doc: unknown): boolean { + if (!doc || typeof doc !== "object") return false; + const d = doc as Record; + if (d.schema_version === "cert.v2") return true; + const p = d.payload as Record | undefined; + return !!p && typeof p === "object" && p.schema_version === "cert.v2"; +} + +/** + * Pull the base64 signature out of whichever shape arrived. + * Returns null when there is nothing usable, so the caller can say so plainly. + */ +function unwrapSignature(raw: unknown): { b64: string; alg?: string; keyId?: string } | null { + if (typeof raw === "string") { + return raw.length > 0 ? { b64: raw } : null; + } + if (raw && typeof raw === "object") { + const o = raw as Record; + // `value` is what /signed-payload emits; `sig` is what the base route emits. + const b64 = [o.value, o.sig, o.signature].find((v) => typeof v === "string" && v.length > 0); + if (typeof b64 !== "string") return null; + return { + b64, + alg: typeof o.alg === "string" ? o.alg : undefined, + keyId: typeof o.key_id === "string" ? o.key_id : undefined, + }; + } + return null; +} + +/** + * Normalize either shape into an envelope. + * Accepts {payload, signature} or a bare payload with a sibling signature. + */ +export function toEnvelope(doc: Record): CertV2Envelope | string { + const hasNested = !!doc.payload && typeof doc.payload === "object"; + const payload = (hasNested ? doc.payload : doc) as CertV2Payload; + + const unwrapped = unwrapSignature(doc.signature); + if (!unwrapped) { + return "cert.v2 requires a detached signature alongside the payload; none was present"; + } + if (payload.schema_version !== "cert.v2") { + return `unsupported schema_version: ${String(payload.schema_version)}`; + } + return { + payload, + signature: unwrapped.b64, + signature_alg: + unwrapped.alg ?? (typeof doc.signature_alg === "string" ? doc.signature_alg : undefined), + envelope_key_id: + unwrapped.keyId ?? (typeof doc.signing_key_id === "string" ? doc.signing_key_id : undefined), + }; +} + +export async function verifyCertificateV2( + doc: Record, + trustedKeys: KeyDoc, + datasetPath?: string, +): Promise { + const result = blankV2Result(); + + const env = toEnvelope(doc); + if (typeof env === "string") return finish(result, "MALFORMED", env); + + const { payload, signature } = env; + const shapeError = validateV2Shape(payload); + if (shapeError) return finish(result, "MALFORMED", shapeError); + + result.certification_id = payload.certificate_id; + result.issuer = payload.issuer?.name ?? null; + result.signed_at = payload.issued_at; + result.algorithm = payload.manifest?.engine ?? payload.certificate_type ?? null; + result.dataset_hash_expected = formatDigest(payload.artifact_hash.toLowerCase()); + if (typeof payload.manifest?.record_count === "number") { + result.rows = payload.manifest.record_count; + } + + // Key selection reads the SIGNED payload only. The envelope is not covered by + // the signature, so trusting its key_id would let anyone redirect which key + // is used to check the bytes — and then present a document that "verifies". + const keyId = payload.issuer.signing_key_id; + if (!keyId) return finish(result, "MALFORMED", "missing issuer.signing_key_id"); + result.key_id = keyId; + + // A disagreement between the signed payload and the envelope means the + // document is internally inconsistent. Refuse rather than silently + // preferring one, so the condition is visible instead of papered over. + if (env.envelope_key_id && env.envelope_key_id !== keyId) { + return finish( + result, + "MALFORMED", + `envelope names key_id ${env.envelope_key_id} but the signed payload names ${keyId}`, + ); + } + + const key = findKey(trustedKeys, keyId); + if (!key || key.revoked_at || !isEd25519(key.algorithm)) { + result.checks.key_trust = "fail"; + const reason = !key + ? `key_id ${keyId} not in trusted keys` + : key.revoked_at + ? `key_id ${keyId} was revoked at ${key.revoked_at}` + : `key ${keyId} is not ed25519`; + return finish(result, "UNKNOWN_KEY", reason); + } + result.checks.key_trust = "pass"; + result.key_label = key.label; + + const sigBytes = decodeSignature(signature); + if (!sigBytes) { + return finish(result, "MALFORMED", "signature is not 64 bytes of base64-encoded Ed25519"); + } + + // v2 signs the entire payload — nothing is stripped. + const canonicalBytes = canonicalizeToBytes(payload); + 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 cert.v2 payload", + ); + } + + if (datasetPath) { + const actualHex = await sha256File(datasetPath); + result.dataset_hash_actual = formatDigest(actualHex); + if (actualHex !== payload.artifact_hash.toLowerCase()) { + result.checks.dataset_match = "fail"; + return finish( + result, + "DATASET_MISMATCH", + `artifact hash mismatch (expected sha256:${payload.artifact_hash.toLowerCase()}, got ${result.dataset_hash_actual})`, + ); + } + result.checks.dataset_match = "pass"; + } + + return finish(result, "VALID", "signature verified and key is trusted"); +} + +/** + * Compared case-insensitively on purpose. The published keys document spells + * this "Ed25519"; this verifier's own fixtures spell it "ed25519". Treating + * that as an untrusted key would report UNKNOWN_KEY — a security verdict — for + * a cosmetic difference, which teaches users to disbelieve the tool. + */ +function isEd25519(algorithm: string | undefined): boolean { + return typeof algorithm === "string" && algorithm.toLowerCase() === "ed25519"; +} + +function validateV2Shape(p: CertV2Payload): string | null { + if (!p || typeof p !== "object") return "cert.v2 payload is not an object"; + for (const f of ["certificate_id", "issued_at", "artifact_hash", "issuer"] as const) { + if (p[f] === undefined || p[f] === null) return `missing required field: ${f}`; + } + if (!p.issuer || typeof p.issuer !== "object") return "issuer must be an object"; + if (typeof p.artifact_hash !== "string" || !/^[0-9a-f]{64}$/i.test(p.artifact_hash)) { + return "artifact_hash must be 64 hex characters"; + } + if (p.hash_method && !/^sha-?256$/i.test(p.hash_method)) { + return `unsupported hash_method: ${p.hash_method}`; + } + return null; +} + +function decodeSignature(b64: string): Buffer | null { + try { + const buf = Buffer.from(b64, "base64"); + if (buf.length !== 64) return null; + return buf; + } catch { + return null; + } +} + +function pemFromRawEd25519(material: string): string { + if (material.includes("BEGIN PUBLIC KEY")) return material; + 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 blankV2Result(): VerifyResult { + 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: "", + }; +} + +function finish(r: VerifyResult, verdict: VerifyResult["verdict"], reason: string): VerifyResult { + r.verdict = verdict; + r.reason = reason; + return r; +} diff --git a/src/cli.ts b/src/cli.ts index 6895598..7ed17a1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,9 +2,10 @@ import { fetchCert } from "./fetch-cert.js"; import { loadKeys } from "./keys.js"; import { verifyCertificate } from "./verify.js"; +import { isCertV2, verifyCertificateV2 } from "./cert-v2.js"; import { fetchReceipt, loadReceiptKey, verifyReceiptEnvelope, type ReceiptVerifyResult } from "./receipt.js"; import { resolveArtifactKind, type ArtifactKind } from "./resolve.js"; -import type { VerifyResult } from "./types.js"; +import type { Certificate, VerifyResult } from "./types.js"; interface CliArgs { positional: string[]; @@ -142,12 +143,17 @@ export async function main(argv: string[]): Promise { } } - // ── Certificate path (unchanged behavior) ────────────────────────────── + // ── Certificate path ─────────────────────────────────────────────────── + // cert.v1 and cert.v2 sign different bytes and name the signer in different + // places, so the schema decides which verifier runs. Both return the same + // VerifyResult, so output and exit codes are identical either way. let result: VerifyResult; try { - const cert = await fetchCert(target, { offline: args.offline }); + const doc = 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); + result = isCertV2(doc) + ? await verifyCertificateV2(doc, keys, args.dataset) + : await verifyCertificate(doc as unknown as Certificate, keys, args.dataset); } catch (err) { const reason = (err as Error).message; const isNetwork = /failed to fetch|HTTP \d|ENOTFOUND|ECONN|getaddrinfo/i.test(reason); @@ -215,9 +221,16 @@ function printHuman(r: VerifyResult): void { 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`); + // cert.v2 carries no column count, so only print a shape when we have + // one. Printing "0 rows × 0 cols" for a v2 certificate states something + // false about the artifact. + const shape = + r.rows !== undefined && r.columns !== undefined + ? `${r.rows.toLocaleString("en-US")} rows × ${r.columns.toLocaleString("en-US")} cols · ` + : r.rows !== undefined + ? `${r.rows.toLocaleString("en-US")} records · ` + : ""; + process.stdout.write(` ${c.dim("algorithm")} ${r.algorithm} · ${shape}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`); } diff --git a/src/fetch-cert.ts b/src/fetch-cert.ts index 7f0dc20..4ff1f1e 100644 --- a/src/fetch-cert.ts +++ b/src/fetch-cert.ts @@ -1,18 +1,46 @@ import { readFile } from "node:fs/promises"; -import type { Certificate } from "./types.js"; -export const DEFAULT_CERT_API = "https://certifieddata.io/api/v1/certificates"; +/** + * Resolving a bare certificate id must land on bytes that are actually + * verifiable. + * + * `https://certifieddata.io/api/v1/certificates/:id` — the path this used to + * point at — returns 404; it was never deployed. + * + * `https://api.certifieddata.io/api/certificates/:id` does respond, but it + * serves a cert.v1-shaped *display projection* of a cert.v2 certificate. It + * carries the real signature bytes while the signature covers the v2 payload, + * not the projection, so verifying that document yields INVALID — which reads + * as tampering when nothing has been tampered with. + * + * `/signed-payload` is the envelope whose signature verifies over its own + * payload, so that is what a verifier has to ask for. + */ +export const DEFAULT_CERT_API = "https://api.certifieddata.io/api/certificates"; +export const CERT_ENVELOPE_SUFFIX = "/signed-payload"; export interface FetchCertOptions { apiBase?: string; offline?: boolean; } -export async function fetchCert(idOrPathOrUrl: string, opts: FetchCertOptions = {}): Promise { +/** + * Returns the raw certificate document. Shape validation belongs to the + * verifier, not the fetcher: the document may be a cert.v1 certificate or a + * cert.v2 envelope, and deciding which is the caller's job. + */ +export async function fetchCert( + idOrPathOrUrl: string, + opts: FetchCertOptions = {}, +): Promise> { if (idOrPathOrUrl === "-") { return parseCertJson(await readStdin()); } - if (idOrPathOrUrl.endsWith(".json") || idOrPathOrUrl.startsWith("./") || idOrPathOrUrl.startsWith("/")) { + if ( + idOrPathOrUrl.endsWith(".json") || + idOrPathOrUrl.startsWith("./") || + idOrPathOrUrl.startsWith("/") + ) { return parseCertJson(await readFile(idOrPathOrUrl, "utf8")); } if (/^https?:\/\//.test(idOrPathOrUrl)) { @@ -23,7 +51,7 @@ export async function fetchCert(idOrPathOrUrl: string, opts: FetchCertOptions = 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)}`; + const url = `${base.replace(/\/$/, "")}/${encodeURIComponent(idOrPathOrUrl)}${CERT_ENVELOPE_SUFFIX}`; return parseCertJson(await fetchText(url)); } @@ -33,14 +61,14 @@ async function fetchText(url: string): Promise { return res.text(); } -function parseCertJson(body: string): Certificate { - return JSON.parse(body) as Certificate; +function parseCertJson(body: string): Record { + return JSON.parse(body) as Record; } async function readStdin(): Promise { const chunks: Buffer[] = []; for await (const chunk of process.stdin) { - chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk as Buffer); + chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : (chunk as Buffer)); } return Buffer.concat(chunks).toString("utf8"); } diff --git a/src/index.ts b/src/index.ts index ef75e1d..dbd51a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ export { verifyCertificate } from "./verify.js"; +export { verifyCertificateV2, isCertV2, toEnvelope } from "./cert-v2.js"; +export type { CertV2Payload, CertV2Envelope, CertV2Signature } from "./cert-v2.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 { fetchCert, DEFAULT_CERT_API, CERT_ENVELOPE_SUFFIX } from "./fetch-cert.js"; export type { Certificate, KeyDoc, diff --git a/src/keys.ts b/src/keys.ts index 8595384..ce8c4d7 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -3,7 +3,19 @@ import { homedir } from "node:os"; import { dirname, join } from "node:path"; import type { KeyDoc, KeyEntry } from "./types.js"; -export const DEFAULT_KEYS_URL = "https://certifieddata.io/.well-known/certifieddata-keys.json"; +/** + * The pinned trust root. + * + * This is the document the issuer actually publishes, and the one every + * certificate's own `public_key_url` points at. It is pinned here rather than + * read out of the certificate: a URL taken from an unverified document would + * let whoever supplied the document choose the keys it is checked against. + * + * `certifieddata-keys.json` — the path this used to point at — returns 404 and + * was never deployed. Both dialects are accepted by `parseKeyDoc`, so if that + * document is published later it will work without a code change. + */ +export const DEFAULT_KEYS_URL = "https://certifieddata.io/.well-known/signing-keys.json"; export const CACHE_PATH = join(homedir(), ".certifieddata", "keys.json"); export const CACHE_TTL_MS = 24 * 60 * 60 * 1000; @@ -57,12 +69,83 @@ export function findKey(doc: KeyDoc, keyId: string): KeyEntry | undefined { return doc.keys.find((k) => k.key_id === keyId); } +/** + * Two dialects are in circulation and both have to work: + * + * this verifier's own shape keys[].public_key, algorithm "ed25519", + * per-key revoked_at + * signing-keys.v1 (published) keys[].public_key_pem, algorithm "Ed25519", + * revocation in top-level revoked[]/retired[] + * + * Normalizing here rather than at each call site means the difference cannot + * turn into a wrong verdict. Two of these differences are security-relevant: + * a mis-read `algorithm` yields UNKNOWN_KEY on a good key, and an unmapped + * `revoked[]` would let a revoked key keep verifying. + */ function parseKeyDoc(body: string): KeyDoc { - const parsed = JSON.parse(body) as KeyDoc; + const parsed = JSON.parse(body) as Record; if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.keys)) { throw new Error("invalid key document: missing keys[]"); } - return parsed; + + const revokedAtFor = buildRevocationIndex(parsed); + + const keys: KeyEntry[] = (parsed.keys as Record[]).map((raw) => { + const keyId = String(raw.key_id ?? ""); + const material = (raw.public_key ?? raw.public_key_pem ?? raw.public_key_raw_b64url) as + | string + | undefined; + if (!keyId || typeof material !== "string" || material.length === 0) { + throw new Error(`invalid key document: entry ${keyId || "(no key_id)"} has no public key`); + } + return { + ...(raw as unknown as KeyEntry), + key_id: keyId, + // CRLF appears in the published PEM; node's createPublicKey is fussier + // about that than it needs to be. + public_key: material.replace(/\r\n/g, "\n"), + algorithm: String(raw.algorithm ?? "").toLowerCase() as "ed25519", + revoked_at: (raw.revoked_at as string | null | undefined) ?? revokedAtFor.get(keyId) ?? null, + }; + }); + + return { ...(parsed as unknown as KeyDoc), issuer: String(parsed.issuer ?? ""), keys }; +} + +/** + * signing-keys.v1 lists revocations separately from the key entries. Entries + * may be bare key_id strings or objects; anything else is refused rather than + * ignored, because silently skipping a revocation record we cannot read would + * mean treating a possibly-revoked key as good. + */ +function buildRevocationIndex(doc: Record): Map { + const out = new Map(); + for (const field of ["revoked", "retired"] as const) { + const list = doc[field]; + if (list === undefined || list === null) continue; + if (!Array.isArray(list)) { + throw new Error(`invalid key document: ${field} must be an array`); + } + for (const entry of list) { + if (typeof entry === "string") { + out.set(entry, `listed in ${field}[]`); + continue; + } + if (entry && typeof entry === "object") { + const e = entry as Record; + const id = e.key_id ?? e.id; + if (typeof id === "string" && id.length > 0) { + const when = e.revoked_at ?? e.retired_at ?? e.at; + out.set(id, typeof when === "string" ? when : `listed in ${field}[]`); + continue; + } + } + throw new Error( + `invalid key document: unreadable entry in ${field}[] — refusing to ignore a revocation record`, + ); + } + } + return out; } async function readCacheIfFresh(path: string): Promise { diff --git a/src/resolve.ts b/src/resolve.ts index ef7b7e1..3194df9 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -31,7 +31,7 @@ export async function resolveArtifactKind( // 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" }; + if (/\/api\/(v1\/)?certificates?\//.test(target)) return { kind: "certificate", via: "url-path" }; // Local file / stdin: sniff the JSON shape. const looksLocal = @@ -43,11 +43,26 @@ export async function resolveArtifactKind( if (looksLocal && target !== "-") { try { const parsed = JSON.parse(await readFile(target, "utf8")) as Record; + // A cert.v2 envelope names its schema on the payload, not at the top + // level — production serves the outer document as + // "certifieddata.manifest.v1" — so sniffing only the top level would + // miss every real certificate. const schema = (parsed.schema_version as string | undefined) ?? - ((parsed.receipt as Record | undefined)?.schema_version as string | undefined); + ((parsed.payload as Record | undefined)?.schema_version as + | string + | undefined) ?? + ((parsed.receipt as Record | undefined)?.schema_version as + | string + | undefined); + const payloadSchema = (parsed.payload as Record | undefined) + ?.schema_version as string | undefined; if (schema === "payment_receipt.v1") return { kind: "receipt", via: "local-schema" }; - if (typeof schema === "string" && schema.startsWith("cert.")) { + if ( + isCertificateSchema(schema) || + isCertificateSchema(payloadSchema) || + schema === "certifieddata.manifest.v1" + ) { return { kind: "certificate", via: "local-schema" }; } // Envelope shape without schema — a verify-endpoint dump. @@ -90,3 +105,18 @@ export async function resolveArtifactKind( if (rcpt === "exists") return { kind: "receipt", via: "probe" }; return { kind: "not_found", via: "probe" }; } + +/** + * Certificate schema names seen in the wild: + * cert.v1, cert.v2 — the payload's own schema_version + * certifieddata.cert.v1 — the public display projection + * certifieddata.manifest.v1 — the signed-payload envelope + */ +function isCertificateSchema(schema: string | undefined): boolean { + if (typeof schema !== "string") return false; + return ( + schema.startsWith("cert.") || + schema.startsWith("certifieddata.cert.") || + schema.startsWith("certifieddata.manifest.") + ); +}