// The checks themselves, separated from the CLI so they can be unit-tested against fixtures // with no network. // // Three outcomes, and keeping them distinct is the point of this file: // // pass checked, and it held // fail checked, and it did not hold // not_checkable we cannot check this from public data at all // // Collapsing the third into either of the others is how a verifier starts lying. "Consent is on // record" is an assertion by the party under examination; reporting it as a cryptographic pass // would put our word inside a result that is supposed to contain only arithmetic. import { canonicalJson, sha256Hex } from "./canonical.ts"; import { verifyEd25519 } from "./ed25519.ts"; import { pinnedKey, PINNED_KEYS } from "./keys.ts"; import { bytesToHex, leafHash, rootFromPath } from "./merkle.ts"; export type Result = "pass" | "fail" | "not_checkable"; export interface Check { link: number; // 0 = document level, 1..5 = the five links name: string; result: Result; detail?: string; /** * Stable machine-readable reason, from the closed vocabulary in * docs/verification-spec-v1.md section 9. The offline verifier (packages/verify) refuses with * these exact codes, and the cross-implementation suite holds the two to code equality on every * tamper fixture: a refusal logged by one implementation has to be mechanically the same fact * in the other, or a third-party port inherits whichever vocabulary it happened to read first. * Carried beside the prose name on every check the spec defines, refusals included: the * refusal-vocabulary gate in packages/verify/_tests/refusal_vocabulary_test.ts fails CI on any * coded refusal the spec table does not name. The checks that carry no code are the ones * outside the spec's scope: transport-level shapes (the HTTP envelope) and page-versus-signed * drift checks that exist only in this network verifier. */ code?: string; } // deno-lint-ignore no-explicit-any type Json = any; export class Checks { readonly list: Check[] = []; add(link: number, name: string, result: Result, detail?: string, code?: string): void { this.list.push({ link, name, result, ...(detail ? { detail } : {}), ...(code ? { code } : {}) }); } get failed(): number { return this.list.filter((c) => c.result === "fail").length; } get unchecked(): number { return this.list.filter((c) => c.result === "not_checkable").length; } get verdict(): "pass" | "fail" | "incomplete" { if (this.failed > 0) return "fail"; return this.unchecked > 0 ? "incomplete" : "pass"; } } // THE SEAL. This file is bundled into web/docs/verify.js, the script a stranger is invited to run // against a document somebody handed them, and `verdict` is the single accessor that decides what // that stranger is told. An unfrozen prototype makes the verdict a dispatch anything in the // process can rewrite: one assignment and every FAIL in the list reports as a pass, with the list // itself untouched so nothing looks edited. Instances stay writable because a checks list is // written as the run proceeds; the prototype and the class do not need to be. Object.freeze(Checks.prototype); Object.freeze(Checks); /** Envelope shape. A response that is not the envelope cannot be reasoned about further. */ export function checkEnvelope(c: Checks, body: Json): boolean { if (body?.ok !== true) { c.add(0, "response envelope", "fail", `ok was ${JSON.stringify(body?.ok)}`); return false; } if (body?.contract_version !== 1) { // Not a failure of the record: a newer contract version means this verifier is the stale // party, and saying so is more useful than reporting the licence as broken. c.add( 0, "contract version", "not_checkable", `response is contract_version ${body?.contract_version}, this verifier implements 1`, ); return false; } c.add(0, "response envelope", "pass", "ok=true, contract_version=1"); return true; } /** * Verify a signature block against a PINNED key. The inline public key in the block is compared * to the pinned one and disagreement is a failure, because that is the shape a substituted-key * attack takes: a response signed with an attacker key that carries the attacker key alongside. */ export async function checkSignature( c: Checks, link: number, label: string, sig: Json, ): Promise { if (!sig) { c.add(link, `${label} signature`, "fail", "no signature block on the record", "signature_missing"); return; } // The same rule as the offline verifier, exactly: a STATED algorithm that is not Ed25519 is a // FAIL (this system signs Ed25519 only, so the claim itself is the finding), while an absent // field is not a claim and the signature below still has to verify as Ed25519 either way. // This used to be a not_checkable with its own prose, which is the divergence C4 flags: two // reference implementations refusing the same byte in two vocabularies. if (sig.algorithm !== undefined && sig.algorithm !== "Ed25519") { c.add( link, `${label} signature`, "fail", `the ${label} names algorithm ${String(sig.algorithm)}; this system signs Ed25519 only`, "unexpected_algorithm", ); return; } const keyId = String(sig.company_key_id ?? ""); const pinned = pinnedKey(keyId); if (!pinned) { // A refusal, not a failure. An unknown key id after a rotation means this verifier needs // updating; treating it as a bad signature would be a false accusation. c.add( link, `${label} key id`, "not_checkable", `record names key "${keyId}", which this verifier does not pin (known: ${ Object.keys(PINNED_KEYS).join(", ") })`, "unknown_key_id", ); return; } if (sig.public_key_spki_b64 && sig.public_key_spki_b64 !== pinned) { c.add( link, `${label} key id`, "fail", `the key served inline for "${keyId}" is not the pinned key for that id`, "substituted_key", ); return; } c.add(link, `${label} key id`, "pass", `${keyId}, matches the pinned key`); // Canonicalize the payload ourselves. If the served canonical_json disagrees with our own // canonicalization, the signature might still verify against THEIR bytes while meaning // something else, so this is checked before the signature and not after. if (!sig.signed_payload || typeof sig.signed_payload !== "object") { c.add(link, `${label} payload`, "fail", "no signed_payload to canonicalize", "payload_missing"); return; } const ours = canonicalJson(sig.signed_payload); if (typeof sig.canonical_json === "string" && sig.canonical_json !== ours) { c.add( link, `${label} canonical bytes`, "fail", "our canonicalization of signed_payload differs from the canonical_json served", "canonical_bytes_mismatch", ); return; } c.add(link, `${label} canonical bytes`, "pass", `${ours.length} bytes, reproduced locally`); const outcome = await verifyEd25519(ours, String(sig.sig_base64 ?? ""), pinned); if (outcome.ok === true) { c.add(link, `${label} signature`, "pass", "Ed25519 verified against the pinned key", "signature_valid"); } else if (outcome.ok === false) { c.add(link, `${label} signature`, "fail", outcome.reason, outcome.code); } else { c.add(link, `${label} signature`, "not_checkable", outcome.reason, "published_key_unusable"); } } /** Link 1. Nothing here is cryptographically checkable from public data, and it says so. */ export function checkPerson(c: Checks, license: Json): void { c.add( 1, "consent on record", license?.consent_on_record === true ? "not_checkable" : "fail", license?.consent_on_record === true ? "asserted by the issuer: consent evidence exists and this is not a demo record. Consent here is a signed agreement plus a verified ID, never a recording; the agreement's hash is committed inside the signature and is checked separately below" : "the record does not assert a consent record", ); const method = license?.identity_method; if (method === undefined) { c.add( 1, "identity method", "not_checkable", "absent, so NO CLAIM is made either way; this licence predates the field. Do not read it as either method", ); } else if (method === "document") { // Corrected 2026-08-07. This used to read "NO LIVENESS: ... never that the person holding it // was present", stated flatly. That became false on 2026-07-31, when the self-serve wizard // began asking the vendor for a selfie matched against the document photo: those people DID // have a live face checked. The field cannot tell the two apart, because the value is // 'document' either way, so the honest line is that this receipt does not record which check // ran. Understating was the safe direction to be wrong in, and it was still wrong. c.add( 1, "identity method", "not_checkable", "'document': a government ID was verified. Signed, but the check itself is the issuer's. " + "LIVENESS IS NOT RECORDED HERE, in either direction: the issuer runs both document-only " + "checks and checks with a selfie matched to the document, and this field does not say " + "which one this person had. Do not read it as proof a human took part, or as proof one " + "did not", ); } else if (method === "video_attestation") { c.add( 1, "identity method", "not_checkable", "'video_attestation': a named operator confirmed a spoken statement. NO government document " + "was checked, and a human watching a recording is not a vendor liveness check", ); } else { c.add(1, "identity method", "fail", `unknown identity_method "${method}"`); } // What actually binds an authorisation to this licence. Since 2026-07-30 that can be either a // scripted recording or the signed platform agreement, and the payload names which. Neither // artifact is published; only its hash is signed, which is what stops it being swapped later. const payload = license?.signature?.signed_payload ?? {}; const evidence = payload.consent_evidence; if (evidence && typeof evidence === "object") { const kind = String(evidence.kind ?? ""); const sha = String(evidence.sha256 ?? ""); if (!["consent_video", "platform_agreement"].includes(kind)) { c.add(1, "consent evidence", "fail", `unknown consent evidence kind "${kind}"`); } else if (!/^[0-9a-f]{64}$/.test(sha)) { c.add(1, "consent evidence", "fail", `consent evidence sha256 is not a sha256: "${sha.slice(0, 40)}"`); } else { c.add( 1, "consent evidence", "pass", kind === "consent_video" ? "a consent recording is committed by hash inside the signature" : "a signed platform agreement is committed by hash inside the signature", ); } // A licence carrying evidence needs nothing further from the legacy field; when it also has a // recording, the check below still runs and both must hold. if (payload.consent_video_sha256 === undefined) return; } const hash = payload.consent_video_sha256; if (typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash)) { c.add(1, "consent hash is signed", "pass", "consent_video_sha256 is inside the signed payload"); } else if (hash === undefined) { c.add( 1, "consent hash is signed", "fail", "the signed payload names no consent artifact at all, neither a recording hash nor consent_evidence", ); } else if (hash === "") { c.add( 1, "consent hash is signed", "fail", "consent_video_sha256 is the empty string in the signed payload, so the signature binds no recording", ); } else { // A signature over a placeholder is a valid signature over nothing. The licence verifies // perfectly and still fails to bind a recording, which is exactly the case a verifier has // to shout about rather than smooth over. c.add( 1, "consent hash is signed", "fail", `consent_video_sha256 is "${ String(hash).slice(0, 40) }", which is not a sha256, so the signature binds no consent recording`, ); } } /** Link 2. */ export function checkAssets(c: Checks, license: Json): void { const assets = license?.assets_licensed; if (!Array.isArray(assets) || assets.length === 0) { c.add( 2, "asset versions", "not_checkable", "the licence names no asset fingerprints, so 'which exact voice or pack' is not answerable from it. Expected on licences issued before asset fingerprinting", ); return; } const bad = assets.filter( (a: Json) => !a || !["voice", "face"].includes(a.asset_type) || !/^[0-9a-f]{64}$/.test(String(a.sha256 ?? "")) || !Number.isInteger(a.version) || a.version < 1, ); if (bad.length) { c.add(2, "asset versions", "fail", `${bad.length} of ${assets.length} fingerprints are malformed`); return; } const inPayload = license?.signature?.signed_payload?.assets_licensed; if (!inPayload) { c.add( 2, "asset versions", "fail", "the response lists assets_licensed but the signed payload does not, so the list is not covered by the signature", ); return; } if (canonicalJson(inPayload) !== canonicalJson(assets)) { c.add(2, "asset versions", "fail", "assets_licensed on the response differs from the signed copy"); return; } c.add( 2, "asset versions", "pass", `${assets.length} signed fingerprint(s): ${ assets.map((a: Json) => `${a.asset_type} v${a.version}`).join(", ") }`, ); } /** * What the platform took, if the record says. A rate, not an amount: deal pricing is never * published, so this reveals nothing about what the brand paid. * * The only interesting failure is a fee shown but not signed, or shown differently from the way * it was signed. Either means the number a talent reads is one the issuer can change at will, * which makes it a claim rather than a disclosure. Absent is not a failure: licences issued * before 2026-07-31 have no fee recorded, and "not recorded" is a different and honest answer. */ export function checkPlatformFee(c: Checks, license: Json): void { const shown = license?.platform_fee_bps; const signed = license?.signature?.signed_payload?.platform_fee_bps; if (shown === undefined && signed === undefined) { c.add(3, "platform fee", "not_checkable", "this licence records no platform fee"); return; } if (signed === undefined) { c.add( 3, "platform fee", "fail", `the response shows ${shown} bps but the signed payload does not carry it, so the issuer could change it at will`, ); return; } if (shown !== undefined && shown !== signed) { c.add( 3, "platform fee", "fail", `response says ${shown} bps, signature covers ${signed} bps`, ); return; } const bps = signed as number; c.add( 3, "platform fee", "pass", `${bps} bps (${(bps / 100).toFixed(2)} percent) taken by the platform, inside the signature`, ); } /** Link 3, the non-signature half: does the record say what it appears to say. */ export function checkLicenseFacts(c: Checks, license: Json): void { const payload = license?.signature?.signed_payload ?? {}; // A public field that is NOT inside the signed payload is a field we could change at will. // Worth stating per field rather than in prose, since a reader assumes the whole page is signed. const signedFields = ["license_id", "contract_sha256", "effective_at", "expires_at"]; const unsigned = signedFields.filter((f) => payload[f] === undefined); if (unsigned.length) { c.add(3, "core fields are signed", "fail", `not in the signed payload: ${unsigned.join(", ")}`); } else { c.add(3, "core fields are signed", "pass", signedFields.join(", ")); } for (const f of ["license_id", "contract_sha256"]) { if (payload[f] !== undefined && license[f] !== undefined && payload[f] !== license[f]) { c.add(3, `${f} agreement`, "fail", `response says ${license[f]}, signature covers ${payload[f]}`); } } // Timestamps are compared as instants, since the signed copy is normalized to // Date.toISOString() and the response may render the same instant differently. for (const f of ["effective_at", "expires_at"]) { if (payload[f] && license[f] && Date.parse(payload[f]) !== Date.parse(license[f])) { c.add(3, `${f} agreement`, "fail", `response says ${license[f]}, signature covers ${payload[f]}`); } } // Scope is the substance of what was permitted; it is signed, so confirm the published copy // is the signed one rather than a friendlier summary. const scopeFields: Array<[string, string]> = [ ["assets", "assets"], ["use_case", "use_case"], ["media_channels", "media_channels"], ["territories", "territories"], ]; const scope = payload.scope; if (!scope) { c.add(3, "scope is signed", "fail", "no scope inside the signed payload"); } else { const drift = scopeFields.filter(([resp, sc]) => license[resp] !== undefined && canonicalJson(license[resp]) !== canonicalJson(scope[sc]) ); if (drift.length) { c.add(3, "scope is signed", "fail", `published scope differs from signed scope: ${drift.map((d) => d[0]).join(", ")}`); } else { c.add(3, "scope is signed", "pass", "assets, use case, channels and territories match the signed scope"); } } const status = license?.status; const now = Date.now(); const from = Date.parse(license?.effective_at ?? ""); const to = Date.parse(license?.expires_at ?? ""); if (Number.isNaN(from) || Number.isNaN(to)) { c.add(3, "term", "fail", "effective_at or expires_at is not a parseable timestamp", "term_unparseable"); } else if (to <= from) { c.add(3, "term", "fail", "expires_at is not after effective_at", "term_inverted"); } else { const inTerm = now >= from && now < to; // Status is a mutable platform fact and the term is signed, so they can legitimately // disagree (revoked inside the term, for instance). Only report the combination. c.add( 3, "term", "pass", `${new Date(from).toISOString()} to ${new Date(to).toISOString()}; ${ inTerm ? "in term now" : "outside its term now" }, status "${status}"`, ); if (status === "active" && !inTerm) { c.add( 3, "status against term", "fail", 'status is "active" but now is outside the signed term', ); } } } /** Link 4 and 5, for a receipt fetched by output id. */ export async function checkCredential( c: Checks, out: Json, license: Json, fileBytes: Uint8Array | null, ): Promise { const cred = out?.credential; if (!cred) { c.add(5, "credential", "fail", "no credential on the response"); return; } await checkSignature(c, 5, "credential", cred.signature); const payload = cred?.signature?.signed_payload ?? {}; if (payload.output_sha256 !== cred.output_sha256) { c.add(5, "output hash is signed", "fail", "output_sha256 on the credential is not the signed one"); } else if (!/^[0-9a-f]{64}$/.test(String(cred.output_sha256 ?? ""))) { c.add(5, "output hash is signed", "fail", "output_sha256 is not a sha256"); } else { c.add(5, "output hash is signed", "pass", cred.output_sha256); } if (payload.captured !== "registered" && payload.captured !== "metered") { c.add(4, "capture basis", "fail", `captured is "${payload.captured}" in the signed payload`); } else { c.add( 4, "capture basis", "pass", payload.captured === "metered" ? "metered: generated by the platform, so the event was observed" : "registered: brand-reported filing, not observed by the platform", ); } // The actual file, if one was handed to us. This is the check a legal reviewer cares about. if (fileBytes) { const localHash = await sha256Hex(fileBytes); if (localHash === cred.output_sha256) { c.add(5, "file matches the credential", "pass", `sha256 ${localHash}`); } else { c.add( 5, "file matches the credential", "fail", `the file hashes to ${localHash}, the credential covers ${cred.output_sha256}. This is not the registered original: possibly an edit, possibly something else`, ); } } else { c.add( 5, "file matches the credential", "not_checkable", "no local file supplied; pass --file to compare bytes", ); } // Recompute in-term-at-publication rather than trusting the served boolean. const basis = cred.license_term_basis; const moment = basis === "published_at" ? Date.parse(cred.published_at ?? "") : Date.parse(cred.registered_at ?? ""); const from = Date.parse(license?.effective_at ?? ""); const to = Date.parse(license?.expires_at ?? ""); if ([moment, from, to].some(Number.isNaN)) { c.add(5, "in term at publication", "not_checkable", "a timestamp needed for the arithmetic is missing"); } else { const ours = moment >= from && moment < to; if (typeof cred.license_active_at_publication === "boolean" && cred.license_active_at_publication !== ours) { c.add( 5, "in term at publication", "fail", `served ${cred.license_active_at_publication}, our arithmetic over the signed term says ${ours}`, ); } else { c.add( 5, "in term at publication", "pass", `${ours ? "in term" : "OUTSIDE the term"} at ${new Date(moment).toISOString()} (basis: ${basis})${ basis === "registered_at" ? ", which is a fallback because no publication date was stated" : "" }`, ); } } if (cred.license_id !== license?.license_id) { c.add(5, "credential points at this licence", "fail", `credential names ${cred.license_id}`); } else { c.add(5, "credential points at this licence", "pass", cred.license_id); } // The per-licence register chain: publicly this is continuity, not recomputation. if (typeof out.register_record_hash === "string" && /^[0-9a-f]{64}$/.test(out.register_record_hash)) { c.add( 4, "register chain link", "not_checkable", "a chain hash is published, but its preimage includes unpublished fields and Postgres timestamp text, so it cannot be recomputed from public data (receipt-spec-v1 section 3.1)", ); } else { c.add(4, "register chain link", "not_checkable", "no register_record_hash published for this output"); } } /** The private block, when the caller holds their own copy. */ export async function checkPrivateBlock(c: Checks, cred: Json, blockJson: string): Promise { let block: unknown; try { block = JSON.parse(blockJson); } catch (e) { c.add(5, "private block", "fail", `not JSON: ${e instanceof Error ? e.message : String(e)}`, "private_block_unreadable"); return; } const expected = cred?.signature?.signed_payload?.private_sha256 ?? cred?.private_sha256; if (!expected) { c.add(5, "private block", "not_checkable", "the credential publishes no private_sha256 to compare against", "no_private_commitment"); return; } const got = await sha256Hex(canonicalJson(block)); if (got === expected) { c.add(5, "private block", "pass", `rehashes to the committed ${expected}`); } else { c.add( 5, "private block", "fail", `block hashes to ${got}, the credential commits to ${expected}. Either this is not the block that was committed, or it was altered`, "private_block_mismatch", ); } } /** Transparency-log inclusion, when the log is reachable. */ export async function checkLogInclusion(c: Checks, sth: Json, proof: Json): Promise { // The binding, before the signature line and before any arithmetic. checkSignature proves the // company signed SOME payload; these two comparisons are what make that payload THIS head. The // same key signs licences, credentials and receipts, so a signature block lifted off any // published instrument verifies here just as well unless the statement is required, and the // outer root_hash and tree_size the arithmetic below consumes are effectively unsigned unless // they byte-equal the signed ones. A failure stops the log check: proving inclusion under an // unbound root would print a pass about a tree nobody signed. // // A MISSING signature stops it exactly the same way. The binding is required, never // conditional on the field being present (verification-spec-v1 section 6): a served head with // the signature omitted used to skip this block, fail one missing-signature line further down, // and then run the arithmetic anyway, printing a pass about a tree nobody signed. // // Read ONCE into a local. Every use below, including the signature line, reads this capture, // so an accessor cannot show the binding one object and the signature check another. Same // idiom as packages/verify/src/log.ts. const sthSignature = sth?.signature; if (!sthSignature || typeof sthSignature !== "object") { c.add( 0, "tree head binding", "fail", "the served head carries no signature block, so the root_hash and tree_size the arithmetic below would consume are bound to nothing. No inclusion arithmetic was run", "sth_unsigned", ); return false; } { const signed = sthSignature.signed_payload ?? {}; if (signed.statement !== "transparency_log_tree_head") { c.add( 0, "tree head binding", "fail", `the head's signature was made over ${ signed.statement === undefined ? "a payload with no statement field" : `statement ${JSON.stringify(signed.statement)}` }, not over a tree head; a real company signature lifted off another instrument looks exactly like this. No inclusion arithmetic was run`, "sth_not_a_tree_head", ); return false; } // Values, not renderings (spec section 6): a signed size of 1 must bind a served "1" however // it is rendered, and a served "01" must not slip past a byte comparison of two renderings. const signedSize = Number(signed.tree_size); const servedSize = Number(sth.tree_size); if ( String(signed.root_hash) !== String(sth.root_hash) || !Number.isFinite(signedSize) || !Number.isFinite(servedSize) || signedSize !== servedSize ) { c.add( 0, "tree head binding", "fail", `the signed payload covers root ${signed.root_hash} at size ${signed.tree_size}, but the served head claims root ${sth.root_hash} at size ${sth.tree_size}: the fields the inclusion arithmetic would consume are not the signed ones. No inclusion arithmetic was run`, "sth_payload_contradicts_head", ); return false; } c.add( 0, "tree head binding", "pass", "the signed payload is a tree head and covers exactly the served root_hash and tree_size", "sth_bound", ); } await checkSignature(c, 0, "tree head", sthSignature); const size = Number(sth?.tree_size); const index = Number(proof?.leaf_index); if (!Number.isInteger(size) || !Number.isInteger(index)) { c.add(0, "log inclusion", "fail", "tree_size or leaf_index is not an integer", "inclusion_malformed"); return true; } if (typeof proof?.preimage === "string" && proof.preimage.length) { const recomputed = await sha256Hex(proof.preimage); if (recomputed !== proof.entry_hash) { c.add( 0, "ledger entry hash", "fail", `sha256 of the published preimage is ${recomputed}, the entry claims ${proof.entry_hash}`, "entry_hash_mismatch", ); return true; } c.add( 0, "ledger entry hash", "pass", "sha256 of the published preimage matches the entry hash", "entry_hash_recomputed", ); } else { c.add(0, "ledger entry hash", "not_checkable", "the log published no preimage for this entry", "no_entry_preimage"); } try { const path = (proof.audit_path ?? []).map((h: string) => hexBytes(h)); const root = await rootFromPath(await leafHash(proof.entry_hash), index, size, path); const rootHex = bytesToHex(root); if (rootHex === sth.root_hash) { c.add( 0, "log inclusion", "pass", `leaf ${index} of ${size} is under root ${rootHex.slice(0, 16)}...`, "inclusion_proven", ); } else { c.add( 0, "log inclusion", "fail", `recomputed root ${rootHex} does not match the signed root ${sth.root_hash}`, "inclusion_root_mismatch", ); } } catch (e) { c.add( 0, "log inclusion", "fail", `proof is malformed: ${e instanceof Error ? e.message : String(e)}`, "inclusion_unparseable", ); } // An anchor earns a PASS only if it is an EXTERNAL timestamp AND it actually covers this // entry. Two ways the presence of an anchor field means nothing for the record in hand, both // of which the log itself reports and this check used to throw away: // - method "signed" is our own key over our own head. That proves origin, not date, so it // rules out exactly nothing about back-dating. // - an anchor taken at ledger seq N says nothing about an entry at seq N+1, which is the // normal state for a freshly issued licence: the anchor lags the entry it would cover. // Passing on presence alone told a reader "back-dating is ruled out" when neither held. // The log now reports every anchor of the newest head under `anchors`, with `anchor` kept as // the strongest for older readers. Prefer finding rfc3161 ourselves rather than trusting the // server's idea of strongest: this verifier's job includes the case where the server is wrong. const reported = Array.isArray(sth?.anchors) ? sth.anchors : []; const anchor = reported.find((a: { method?: string }) => a?.method === "rfc3161") ?? sth?.anchor; if (!anchor?.method) { c.add( 0, "external anchor", "not_checkable", "this head carries no external timestamp, so its signature proves origin but not that it existed on the date it carries", "external_timestamp_absent", ); return true; } const anchoredSeq = Number(anchor.head_seq); const entrySeq = Number(proof?.seq); if (anchor.method !== "rfc3161") { c.add( 0, "external anchor", "not_checkable", `the newest anchor is method "${anchor.method}", our own signature over our own head rather than an ` + "external timestamp, so it proves origin and not that the tree had this shape on the stated date", "external_timestamp_absent", ); } else if (Number.isInteger(anchoredSeq) && Number.isInteger(entrySeq) && anchoredSeq < entrySeq) { c.add( 0, "external anchor", "not_checkable", `the newest external timestamp covers ledger seq ${anchoredSeq} and this entry is seq ${entrySeq}, ` + "so no anchor covers it yet; back-dating is ruled out only up to the anchored point", "anchor_predates_entry", ); } else { c.add( 0, "external anchor", "pass", `head timestamped by ${anchor.method} at ${anchor.anchored_at}, covering ledger seq ${anchor.head_seq} ` + `and so this entry at seq ${proof?.seq}, which is what rules out back-dating`, "anchor_covers_entry", ); } return true; } /** * The per-licence chain, recomputed rather than believed. * * Until chain versions 5 and 3 this check could not exist: the preimages held the brand's key * and the volume, so the only public statement available was "trust us, the hashes link". Now * every recomputable row publishes the exact string its hash was taken over, and this walks them * hashing each one and following prev_hash. A row that fails is a row whose published preimage * does not produce its published hash, which is the accusation this surface exists to answer. * * What a pass does NOT mean: that the chain is complete. A log that never wrote a row cannot be * caught by a check on the rows it did write, which is what the transparency log's inclusion * proofs and the anchors are for. Stated here because "the chain verified" is easy to over-read. */ export async function checkLicenseChain(c: Checks, chain: Json): Promise { const rows = (chain?.rows ?? []) as Json[]; if (!rows.length) { c.add(4, "licence chain", "not_checkable", "this licence has no metered or registered rows yet"); return; } const byKind = new Map(); for (const r of rows) { const k = String(r.kind ?? "unknown"); if (!byKind.has(k)) byKind.set(k, []); byKind.get(k)!.push(r); } let recomputed = 0; let skipped = 0; for (const [kind, kindRows] of byKind) { let expectedPrev = "genesis"; for (const r of kindRows) { if (!r.recomputable || typeof r.preimage !== "string") { skipped++; // The link still has to hold across a row we cannot recompute, so carry its hash // forward: an unpublishable preimage is not permission to lose the thread. expectedPrev = typeof r.record_hash === "string" ? r.record_hash : expectedPrev; continue; } const got = await sha256Hex(r.preimage); if (got !== r.record_hash) { c.add( 4, "licence chain", "fail", `${kind} row ${r.seq}: sha256 of the published preimage is ${got}, the row claims ${r.record_hash}`, ); return; } if (r.prev_hash !== expectedPrev) { c.add( 4, "licence chain", "fail", `${kind} row ${r.seq}: prev_hash is ${r.prev_hash}, but the row before it hashes to ${expectedPrev}`, ); return; } expectedPrev = String(r.record_hash); recomputed++; } } if (!recomputed) { c.add( 4, "licence chain", "not_checkable", `${skipped} row(s) predate the recomputable formulas, so none of this chain can be rehashed`, ); return; } c.add( 4, "licence chain", "pass", `${recomputed} row(s) rehashed from their published preimages and linked` + (skipped ? `; ${skipped} older row(s) not recomputable and skipped` : ""), ); } // --------------------------------------------------------------------------- // Post-contract instruments: approved variations and their riders. Neither has a licence // behind it, on purpose (the authority is a signed permission over agency-owned source // content), so these checks stand alone rather than borrowing the licence path. // --------------------------------------------------------------------------- /** * Link 5 for an approved-variation registration (credential.kind === "approved_variation"). * * What is deliberately NOT here, versus the licence-key output path: a licence lookup (no * licence exists; the permission reference is checked as signed, and a rider reference is * verifiable in its own right via --rider), the captured field (a variation register records * approval-bound filings only, so the payload states no capture basis and inventing one would * claim something the record does not), and a NOT CHECKABLE line for a file nobody handed * over (the claims checked are the credential's own; --file ADDS a check when supplied, and * its pass detail below says how). */ export async function checkVariationCredential( c: Checks, out: Json, fileBytes: Uint8Array | null, ): Promise { const cred = out?.credential; if (!cred) { c.add(5, "variation credential", "fail", "no credential on the response"); return; } await checkSignature(c, 5, "variation credential", cred.signature); const payload = cred?.signature?.signed_payload ?? {}; // The kind inside the signature is what stops a licence-key credential being re-served as an // approved variation or the reverse: the served field alone is whoever-controls-the-response. if (payload.kind !== "approved_variation" || cred.kind !== "approved_variation") { c.add( 5, "kind is signed", "fail", `signed kind is "${payload.kind}", served kind is "${cred.kind}"; both must be approved_variation`, ); } else { c.add(5, "kind is signed", "pass", "approved_variation, inside the signature"); } if (payload.output_sha256 !== cred.output_sha256) { c.add(5, "output hash is signed", "fail", "output_sha256 on the credential is not the signed one"); } else if (!/^[0-9a-f]{64}$/.test(String(cred.output_sha256 ?? ""))) { c.add(5, "output hash is signed", "fail", "output_sha256 is not a sha256"); } else { c.add( 5, "output hash is signed", "pass", `${cred.output_sha256} (pass --file to compare a local file against it)`, ); } // Link 3: which permission authorised this variation. The reference is signed; what it points // at is checked through its own receipt, which is what --rider exists for. if (typeof payload.permission_ref !== "string" || !payload.permission_ref.length) { c.add(3, "permission reference is signed", "fail", "no permission_ref in the signed payload"); } else if (cred.permission_ref !== payload.permission_ref) { c.add( 3, "permission reference is signed", "fail", `the page names "${cred.permission_ref}", the signature covers "${payload.permission_ref}"`, ); } else { c.add( 3, "permission reference is signed", "pass", `${payload.permission_ref}; a rider reference is separately verifiable with --rider`, ); } // The page-vs-signature rule (same as checkAssets): every fact served beside the signature // must be the fact inside it, or the page is quietly wider than the record. const mismatches: string[] = []; for (const field of ["variation_request_id", "likeness_display_name", "media_kind"]) { if (cred[field] !== payload[field]) mismatches.push(field); } for (const field of ["source_asset_sha256s", "fingerprints"]) { const served = cred[field]; const signed = payload[field]; if (served === undefined && signed === undefined) continue; if (canonicalJson(served ?? null) !== canonicalJson(signed ?? null)) mismatches.push(field); } for (const field of ["approved_at", "registered_at"]) { const served = cred[field]; const signed = payload[field]; if (served === undefined && signed === undefined) continue; if (Date.parse(String(served)) !== Date.parse(String(signed))) mismatches.push(field); } if (mismatches.length) { c.add( 5, "served facts match the signed payload", "fail", `these served fields differ from the signed ones: ${mismatches.join(", ")}`, ); } else { c.add( 5, "served facts match the signed payload", "pass", "request id, likeness, media kind, source hashes, fingerprints and both timestamps are the signed ones", ); } if (fileBytes) { const localHash = await sha256Hex(fileBytes); if (localHash === cred.output_sha256) { c.add(5, "file matches the credential", "pass", `sha256 ${localHash}`); } else { c.add( 5, "file matches the credential", "fail", `the file hashes to ${localHash}, the credential covers ${cred.output_sha256}. This is not the registered original: possibly an edit, possibly something else`, ); } } } /** * The registration's ledger entry against the served receipt. Inclusion (checkLogInclusion) * proves entries for this subject sit under the signed head; this proves those entries SAY * what the receipt says, which is what pins the register row publicly. The per-approval * register chain's preimage carries unpublished fields, so the ledger entry, whose preimage * is published whole, is deliberately the only public commitment to register_record_hash * (receipt-spec-v1 section 3.4). */ export function checkVariationLogEntry(c: Checks, logData: Json, out: Json): void { const entries = (logData?.entries ?? []) as Json[]; const entry = entries.find((e) => e?.event_type === "variation_asset_registered" || e?.event_type === "variation_asset_registered_imported" ); if (!entry) { c.add( 4, "registration pinned in the log", "not_checkable", "the log answers for this id but holds no variation-asset registration entry; expected for registrations made before the register reached the ledger", ); return; } const body = entry.body ?? {}; const cred = out?.credential ?? {}; const problems: string[] = []; if (body.output_sha256 !== cred.output_sha256) problems.push("output_sha256 differs"); if ( typeof out?.register_record_hash === "string" && body.register_record_hash !== out.register_record_hash ) { problems.push("register_record_hash differs"); } if (body.signature_b64 !== cred.signature?.sig_base64) problems.push("signature bytes differ"); if (problems.length) { c.add( 4, "registration pinned in the log", "fail", `the ledger entry disagrees with the served receipt: ${problems.join("; ")}`, ); return; } const retrospective = entry.event_type === "variation_asset_registered_imported"; c.add( 4, "registration pinned in the log", "pass", "the ledger entry carries the same output hash, register chain hash and signature bytes as the served receipt" + (retrospective ? `; the entry is retrospective (imported ${entry.at}), so it proves the record was held then, and the registration time inside it is as-signed rather than independently dated` : ""), ); } /** * Link 3 facts for a signed variation rider: everything served beside the signature must be * the thing inside it. The signature itself is checked by checkSignature in the caller. */ export function checkRiderFacts(c: Checks, rider: Json): void { const payload = rider?.signature?.signed_payload ?? {}; const doc = String(rider?.document_sha256 ?? ""); if (!/^[0-9a-f]{64}$/.test(doc)) { c.add(3, "rider document hash", "fail", `document_sha256 is not a sha256: "${doc}"`); } else if (payload.document_sha256 !== doc) { c.add(3, "rider document hash", "fail", "the served document_sha256 is not the signed one"); } else { c.add( 3, "rider document hash", "pass", `${doc}; hash the rider instrument you hold (UTF-8 plain text body) and compare`, ); } if (payload.rider_id !== rider?.rider_id || payload.variation_request_id !== rider?.variation_request_id) { c.add( 3, "ids are signed", "fail", `the page names rider ${rider?.rider_id} on request ${rider?.variation_request_id}, the signature covers rider ${payload.rider_id} on request ${payload.variation_request_id}`, ); } else { c.add(3, "ids are signed", "pass", `${payload.rider_id} on request ${payload.variation_request_id}`); } if (rider?.proposed_use === undefined || payload.proposed_use === undefined) { c.add(3, "approved scope is signed", "fail", "proposed_use is missing from the response or the signed payload"); } else if (canonicalJson(rider.proposed_use) !== canonicalJson(payload.proposed_use)) { c.add(3, "approved scope is signed", "fail", "the scope on the page is not the signed scope"); } else { c.add( 3, "approved scope is signed", "pass", "description, media, channels, territories and transformations are inside the signature", ); } // Omitted on both sides means the approval carried no conditions: no claim, no line, on the // spec's absent-is-no-claim rule. Present anywhere, the served value and the signed value // must agree. if (rider?.conditions_sha256 !== undefined || payload.conditions_sha256 !== undefined) { if (rider?.conditions_sha256 !== payload.conditions_sha256) { c.add(3, "conditions commitment", "fail", "the served conditions_sha256 is not the signed one"); } else if (!/^[0-9a-f]{64}$/.test(String(payload.conditions_sha256 ?? ""))) { c.add(3, "conditions commitment", "fail", "conditions_sha256 is not a sha256"); } else { c.add( 3, "conditions commitment", "pass", "committed by hash; a party holding the conditions rehashes their canonical JSON and compares. The fee inside them is never published", ); } } const moments: string[] = []; for (const field of ["responded_at", "signed_at"]) { const served = Date.parse(String(rider?.[field] ?? "")); const signed = Date.parse(String(payload[field] ?? "")); if (Number.isNaN(served) || Number.isNaN(signed) || served !== signed) moments.push(field); } if (moments.length) { c.add(3, "moments are signed", "fail", `served and signed values differ for: ${moments.join(", ")}`); } else { c.add( 3, "moments are signed", "pass", `approved ${payload.responded_at}, signed ${payload.signed_at}`, ); } } /** * Link 3 facts for a signed LICENCE REGION RIDER (`A` handle): a different instrument from a * variation rider, against a different parent, so it gets its own facts rather than being fed * through checks written for fields it does not have. Everything served beside the signature * must be the thing inside it, and one thing more is checkable here that a variation rider * cannot offer: the receipt SERVES the prose the signer read, so this verifier rehashes those * exact words against the hash inside the signature instead of trusting the server's own * stored_prose_matches flag. */ export async function checkRegionRiderFacts(c: Checks, rider: Json): Promise { const payload = rider?.signature?.signed_payload ?? {}; if (payload.rider_id !== rider?.rider_id || payload.license_id !== rider?.license_id) { c.add( 3, "ids are signed", "fail", `the page names rider ${rider?.rider_id} on licence ${rider?.license_id}, the signature covers rider ${payload.rider_id} on licence ${payload.license_id}`, ); } else { c.add(3, "ids are signed", "pass", `${payload.rider_id} on licence ${payload.license_id}`); } // THE PARENT LINK, and only that. This check used to be called "parent licence is signed", // which claimed more than it did (audit of PR #339, finding F2): `parent.payload_sha256` on the // page is served straight FROM the rider's own signed payload and is deliberately not // recomputed from the parent row, so the two agree by construction and a reader who stopped // here had checked nothing about the parent at all. What it does prove is worth a line: the // page is not showing a different parent hash beside the one inside the signature. Whether the // parent licence still hashes to that link, and whether it is itself signed, is checked by // checkRegionRiderParent against the licence fetched from ?id=. const parent = String(payload.parent_payload_sha256 ?? ""); if (!/^[0-9a-f]{64}$/.test(parent)) { c.add(3, "parent link is signed", "fail", `parent_payload_sha256 is not a sha256: "${parent}"`); } else if (rider?.parent?.payload_sha256 !== parent) { c.add( 3, "parent link is signed", "fail", "the parent block on the page does not carry the signed parent_payload_sha256", ); } else { c.add( 3, "parent link is signed", "pass", `the rider names parent payload ${parent} inside its own signature. This is WHICH licence the rider amends, not proof about that licence: fetch idl-verify?id=${ String(rider?.license_id ?? "") } and rehash its signed payload, which is the next check`, ); } if ( rider?.added_regions === undefined || payload.added_regions === undefined || canonicalJson(rider.added_regions) !== canonicalJson(payload.added_regions) ) { c.add(3, "added regions are signed", "fail", "the regions on the page are not the signed regions"); } else { c.add( 3, "added regions are signed", "pass", "the added regions, in canonical form, are inside the signature; a rider only ever adds", ); } // The words the signer read, rehashed HERE. prose_sha256 is inside the signature and // regions_prose is served verbatim, so this is the one check on this receipt a reader can // run entirely against bytes in front of them. const proseHash = String(payload.prose_sha256 ?? ""); if (!/^[0-9a-f]{64}$/.test(proseHash)) { c.add(3, "prose is signed", "fail", `prose_sha256 is not a sha256: "${proseHash}"`); } else if (rider?.prose_sha256 !== proseHash) { c.add(3, "prose is signed", "fail", "the served prose_sha256 is not the signed one"); } else if (typeof rider?.regions_prose !== "string" || !rider.regions_prose.length) { c.add(3, "prose is signed", "fail", "regions_prose is missing from the response"); } else if (await sha256Hex(rider.regions_prose) !== proseHash) { c.add( 3, "prose is signed", "fail", "the words served on this receipt do not hash to the prose_sha256 inside the signature", ); } else { c.add( 3, "prose is signed", "pass", `the served plain-English permissions hash to ${proseHash}, which is inside the signature`, ); } const pins: string[] = []; for (const field of ["permission_schema_version", "regions_prose_version"]) { if (rider?.[field] !== payload[field]) pins.push(field); } if (pins.length) { c.add(3, "version pins are signed", "fail", `served and signed values differ for: ${pins.join(", ")}`); } else { c.add( 3, "version pins are signed", "pass", `schema ${payload.permission_schema_version}, prose ${payload.regions_prose_version}; the two pins the wording is reproducible under`, ); } const moments: string[] = []; for (const field of ["effective_at", "signed_at"]) { const served = Date.parse(String(rider?.[field] ?? "")); const signed = Date.parse(String(payload[field] ?? "")); if (Number.isNaN(served) || Number.isNaN(signed) || served !== signed) moments.push(field); } if (moments.length) { c.add(3, "moments are signed", "fail", `served and signed values differ for: ${moments.join(", ")}`); } else { c.add( 3, "moments are signed", "pass", `effective ${payload.effective_at}, signed ${payload.signed_at}`, ); } // The issuer's OWN answers, said out loud rather than left in a field nobody reads. They are // not evidence: every one of them is the party under examination checking itself, which is why // a clean set is reported as not_checkable and the arithmetic is done above and below. A flag // the issuer sets to false is different in kind, because a registry admitting its own record // does not hold is a fact against interest, and it fails here. const flags = rider?.verification ?? {}; const named = ["parent_link_intact", "parent_signature_valid", "prose_reproduces", "stored_prose_matches"]; const present = named.filter((f) => typeof flags[f] === "boolean"); const denied = present.filter((f) => flags[f] === false); if (!present.length) { c.add(3, "issuer's own checks", "not_checkable", "the receipt carries no verification block"); } else if (denied.length) { c.add( 3, "issuer's own checks", "fail", `the issuer reports its own record as not holding: ${denied.join(", ")} are false`, ); } else { c.add( 3, "issuer's own checks", "not_checkable", `the issuer asserts ${present.join(", ")}; asserted by the party under examination, so it is reported and not counted`, ); } } /** * The parent licence itself, fetched from `?id=` rather than taken from the rider's own words. * * WHY THIS EXISTS (audit of PR #339, finding F2). Everything checkRegionRiderFacts can say about * the parent comes out of the rider's signature, and the server derives the page's parent block * from that same signed payload, so the two agree however wrong the pair is. A rider signed over * a parent hash that no licence in the registry hashes to would have passed every line on that * receipt. This is the check that closes it: rebuild the parent's canonical bytes, hash them, * and require the link inside the rider's signature to be that number. The parent's own Ed25519 * signature is verified alongside, because a parent whose bytes match a link but were never * signed is not authority either. */ export async function checkRegionRiderParent(c: Checks, rider: Json, licence: Json): Promise { const link = String(rider?.signature?.signed_payload?.parent_payload_sha256 ?? ""); if (!licence) { c.add( 3, "parent licence rehashes to the link", "not_checkable", `the parent licence could not be read from this registry, so the link inside the rider's signature stands unchecked; fetch idl-verify?id=${ String(rider?.license_id ?? "") } yourself and rehash its canonical signed payload`, ); return; } // The licence answered for has to be the licence the rider names, or the rehash below would be // checking the wrong instrument and passing. if (String(licence?.license_id ?? "") !== String(rider?.license_id ?? "")) { c.add( 3, "parent licence rehashes to the link", "fail", `the rider names licence ${rider?.license_id} and the registry answered for ${licence?.license_id}`, ); return; } await checkSignature(c, 3, "parent licence", licence?.signature); const signed = licence?.signature?.signed_payload; if (!signed || typeof signed !== "object") { c.add( 3, "parent licence rehashes to the link", "fail", "the parent licence carries no signed payload to rebuild", ); return; } const rebuilt = await sha256Hex(canonicalJson(signed)); if (!/^[0-9a-f]{64}$/.test(link)) { c.add( 3, "parent licence rehashes to the link", "fail", `the rider carries no usable parent_payload_sha256 to compare: "${link}"`, ); } else if (rebuilt !== link) { c.add( 3, "parent licence rehashes to the link", "fail", `the parent licence's canonical payload hashes to ${rebuilt}, and the rider is signed over parent ${link}; the rider amends an instrument this licence is no longer`, ); } else { c.add( 3, "parent licence rehashes to the link", "pass", `the parent licence's canonical signed payload hashes to ${rebuilt}, which is the parent link inside the rider's signature, rebuilt here from the licence receipt's own bytes`, ); } } /** * Link 4 for a rider: the signing event in the ledger, proved into the tree and compared to * the served receipt. Which event that is depends on which register answered: a variation * rider's events live under the variation REQUEST's ledger subject (the rider is one event in * that request's lifecycle) as variation_rider_signed, while a licence region rider is its own * instrument with its own subject and logs licence_region_rider_signed. The two also pin * different facts: a variation rider is pinned by its instrument's document_sha256, a region * rider by the prose_sha256 of the words it grants, because that is the hash its signature * carries. */ export async function checkRiderLogEntry( c: Checks, sth: Json, logData: Json, rider: Json, ): Promise { const region = rider?.kind === "licence_region_rider"; const eventType = region ? "licence_region_rider_signed" : "variation_rider_signed"; const entries = (logData?.entries ?? []) as Json[]; const entry = entries.find((e) => e?.event_type === eventType && e?.body?.rider_id === rider?.rider_id ); if (!entry) { c.add( 4, "rider signing event in the log", "not_checkable", `the log answers for this rider but holds no ${eventType} entry naming it; expected for riders signed before the log covered them`, ); return; } if (typeof entry.preimage === "string" && entry.preimage.length) { const recomputed = await sha256Hex(entry.preimage); if (recomputed !== entry.entry_hash) { c.add( 4, "rider signing event in the log", "fail", `sha256 of the published preimage is ${recomputed}, the entry claims ${entry.entry_hash}`, ); return; } } const proofs = (Array.isArray(logData?.inclusions) ? logData.inclusions : []) as Json[]; const proof = proofs.find((p) => p?.entry_hash === entry.entry_hash); if (!proof) { c.add(4, "rider signing event in the log", "not_checkable", "no audit path was served for the signing entry"); return; } const size = Number(sth?.tree_size); const index = Number(proof.leaf_index); try { const path = (proof.audit_path ?? []).map((h: string) => hexBytes(h)); const root = bytesToHex(await rootFromPath(await leafHash(entry.entry_hash), index, size, path)); if (root !== sth?.root_hash) { c.add( 4, "rider signing event in the log", "fail", `recomputed root ${root} does not match the signed root ${sth?.root_hash}`, ); return; } } catch (e) { c.add( 4, "rider signing event in the log", "fail", `proof is malformed: ${e instanceof Error ? e.message : String(e)}`, ); return; } if (region) { if (entry.body?.prose_sha256 !== rider?.prose_sha256) { c.add( 4, "rider signing event in the log", "fail", "the ledger's signing event carries a different prose hash than the served rider", ); return; } } else if (entry.body?.document_sha256 !== rider?.document_sha256) { c.add( 4, "rider signing event in the log", "fail", "the ledger's signing event carries a different document hash than the served rider", ); return; } c.add( 4, "rider signing event in the log", "pass", `entry seq ${entry.seq} (${eventType}) sits under the signed root and carries the same rider id and ${ region ? "prose" : "document" } hash as the receipt`, ); } function hexBytes(hex: string): Uint8Array { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); return out; } /** * An EGRESS RELEASE credential (`E` handle): an artifact that left the boundary through the gate. * * The third register `?output=` answers from, and the only one of the three whose record we * witnessed ourselves. An `O` record is a brand telling us what it published and a `V` record is a * party declaring a file it made; a release was generated inside the boundary, checked against a * signed PERMIT, then marked, fingerprinted and hashed by us before any byte reached the caller. * So this is the only credential carrying a `decision` block, and checking that block is most of * what is different here. * * WHAT THIS DELIBERATELY DOES NOT CLAIM. The decision receipt itself is not published, only its * hash: the receipt body belongs to the parties. So this verifies that a decision is NAMED, that * the naming is inside the signature, and that the pinned versions are ones we recognise. It does * NOT verify that the decision said PERMIT, because nothing public says so. A party holding the * receipt checks that by rehashing it, and `packages/verify` does exactly that when both are in * one bundle. Reporting it as checked here would be the kind of green tick this verifier exists * not to print. */ export async function checkEgressCredential( c: Checks, out: Json, fileBytes: Uint8Array | null, ): Promise { const cred = out?.credential; if (!cred) { c.add(5, "release credential", "fail", "no credential on the response"); return; } await checkSignature(c, 5, "release credential", cred.signature); const payload = cred?.signature?.signed_payload ?? {}; // The version inside the signature is what stops a release being read as an output registration // or an approved variation. The served `kind` is whoever controls the response. if (payload.receipt_version !== "idl-egress-receipt/1" || cred.kind !== "egress_release") { c.add( 5, "kind is signed", "fail", `signed receipt_version is "${payload.receipt_version}", served kind is "${cred.kind}"; ` + "a release must be both idl-egress-receipt/1 and egress_release", ); } else { c.add(5, "kind is signed", "pass", "idl-egress-receipt/1, inside the signature"); } if (payload.output_sha256 !== cred.output_sha256) { c.add(5, "output hash is signed", "fail", "output_sha256 on the credential is not the signed one"); } else if (!/^[0-9a-f]{64}$/.test(String(cred.output_sha256 ?? ""))) { c.add(5, "output hash is signed", "fail", "output_sha256 is not a sha256"); } else { c.add( 5, "output hash is signed", "pass", `${cred.output_sha256} (pass --file to compare a local file against it)`, ); } // Nothing leaves the gate unmarked: a media kind with no embedder is refused rather than passed // through. So a release receipt naming no watermark is a contradiction in terms. const wm = payload.watermark ?? {}; if (typeof wm.kind !== "string" || !wm.kind.length) { c.add(5, "watermarked before release", "fail", "the signed payload names no watermark"); } else { c.add( 5, "watermarked before release", "pass", `${wm.kind} ${wm.version ?? ""}, inside the signature. The gate refuses any media kind it cannot mark`, ); } // Link 3, carried rather than fetched: the release commits to the licence's OWN signature, so // the tie to the licence cannot be re-pointed afterwards. const ls = payload.license_signature ?? {}; if (typeof ls.sig_base64 !== "string" || !ls.sig_base64.length) { c.add(3, "licence tie is signed", "fail", "the release carries no licence signature"); } else { c.add( 3, "licence tie is signed", "pass", `licence ${payload.license_id}'s own signature is inside this release's signature`, ); } // Link 4. const decision = payload.decision ?? {}; const hashes = ["receipt_sha256", "descriptor_sha256"].filter((f) => !/^[0-9a-f]{64}$/.test(String(decision[f] ?? "")) ); if (hashes.length) { c.add(4, "authorising decision is signed", "fail", `not well formed: ${hashes.join(", ")}`); } else { c.add( 4, "authorising decision is signed", "pass", `decision ${String(decision.receipt_sha256).slice(0, 16)}… over descriptor ` + `${String(decision.descriptor_sha256).slice(0, 16)}…, under engine ${decision.engine_version} ` + `and schema ${decision.schema_version}`, ); } c.add( 4, "decision said PERMIT", "not_checkable", "the decision receipt is not published, only its hash: the body belongs to the parties. A holder " + "rehashes their copy against the hash above, which is what packages/verify does when both are " + "in one bundle. Nothing public says what the decision was, and asserting it here would be our " + "word inside an arithmetic result.", ); // The page-vs-signature rule, same as everywhere: every fact served beside the signature must be // the fact inside it, or the page is quietly wider than the record. const mismatches: string[] = []; for (const field of ["release_id", "license_id", "media_kind", "content_type", "model", "byte_size"]) { if (cred[field] !== payload[field]) mismatches.push(field); } for (const field of ["watermark", "decision"]) { if (canonicalJson(cred[field] ?? null) !== canonicalJson(payload[field] ?? null)) mismatches.push(field); } if (Date.parse(String(cred.released_at)) !== Date.parse(String(payload.released_at))) { mismatches.push("released_at"); } if (mismatches.length) { c.add( 5, "served facts match the signed payload", "fail", `these served fields differ from the signed ones: ${mismatches.join(", ")}`, ); } else { c.add( 5, "served facts match the signed payload", "pass", "release id, licence, media kind, content type, model, size, watermark, decision and the " + "release time are the signed ones", ); } if (fileBytes) { const localHash = await sha256Hex(fileBytes); if (localHash === cred.output_sha256) { c.add(5, "file matches the credential", "pass", `sha256 ${localHash}`); } else { c.add( 5, "file matches the credential", "fail", `the file hashes to ${localHash}, the credential covers ${cred.output_sha256}. This is not ` + "the released original: possibly an edit, possibly something else", ); } } }