UNPKG

noa-receipt

Version:

NOA Agent Action Receipt — open, offline-verifiable provenance for AI-agent actions. The governance/receipt organ only; the NOA brain is separate and proprietary.

131 lines (130 loc) • 7.37 kB
import { coseSign1, coseSign1VerifyParsed } from "./cose-sign1.js"; import { canonicalize } from "../jcs.js"; import { safeParse } from "../safe-json.js"; import { validateReceiptShapeParsed } from "../schema.js"; import { parseDocument } from "../bytes.js"; import { parseVerificationKeyring } from "../verification-keyring.js"; import { receiptHashInput } from "../canonicalize.js"; import { sha256Hex } from "../hash.js"; import { signingMessage, RECEIPT_SIG_DOMAIN } from "../signing.js"; import { verifyEd25519 } from "../keys.js"; import { arrayIncludes, mapGet, mapSet, newMap, arraySlice, arrayEvery, objectGetOwnPropertyNames, isArray, bufferFrom, bufToString, bufEquals, jsonStringify } from "../intrinsics.js"; export function receiptToCose(receipt, signer) { return coseSign1(bufferFrom(canonicalize(receipt), "utf8"), signer); } function refuse(reason, o = {}) { return { ok: false, kid: o.kid === undefined ? null : o.kid, nativeKid: o.nativeKid === undefined ? null : o.nativeKid, agentClaim: o.agentClaim === undefined ? "NOT_EVALUATED" : o.agentClaim, envelopeKid: o.envelopeKid === undefined ? null : o.envelopeKid, envelopeClaim: o.envelopeClaim === undefined ? "NOT_EVALUATED" : o.envelopeClaim, receipt: null, reason, warnings: [], }; } export function receiptFromCose(coseBytes, keyringBytes, identityManifestBytes) { const kParsed = parseVerificationKeyring(keyringBytes, "keyring"); if (!kParsed.ok) return refuse(kParsed.reason); const verification = kParsed.value; const keyring = verification.keyring; let identityManifest; if (identityManifestBytes !== undefined) { const mParsed = parseDocument(identityManifestBytes, "identityManifest"); if (!mParsed.ok) return refuse(mParsed.reason); identityManifest = mParsed.value; } const haveManifest = identityManifest !== undefined; const manifest = newMap(); if (haveManifest) { if (typeof identityManifest !== "object" || identityManifest === null || isArray(identityManifest)) { return refuse("identityManifest must be an object (agent.id -> kid[])"); } try { const aids = objectGetOwnPropertyNames(identityManifest); for (let ai = 0; ai < aids.length; ai++) { const aid = aids[ai]; const kidsLive = identityManifest[aid]; if (!isArray(kidsLive)) { return refuse(`identityManifest["${aid}"] must be an array of kid strings`); } const kids = arraySlice(kidsLive); if (!arrayEvery(kids, (k) => typeof k === "string")) { return refuse(`identityManifest["${aid}"] must be an array of kid strings`); } mapSet(manifest, aid, kids); } } catch { return refuse("identityManifest threw during validation (hostile accessor)"); } } const r = coseSign1VerifyParsed(coseBytes, keyring); if (r.kid !== null && verification.retiredKids[r.kid] === true) { return refuse(`signing key ${jsonStringify(r.kid)} is retired; signer-chosen artifact time is not an independent witness`, { kid: r.kid, envelopeClaim: "FAILED" }); } if (!r.ok || !r.payload) return refuse(r.reason ?? "COSE signature did not verify", { kid: r.kid, envelopeClaim: "FAILED" }); let parsed; try { parsed = safeParse(bufToString(r.payload, "utf8")); } catch (e) { return refuse(`payload parse: ${e.message}`, { kid: r.kid }); } let recanon; try { recanon = bufferFrom(canonicalize(parsed), "utf8"); } catch (e) { return refuse(`payload is not canonicalizable: ${e.message}`, { kid: r.kid }); } if (!bufEquals(recanon, r.payload)) { return refuse("COSE payload is not canonical JCS: it does not re-canonicalize to the signed bytes (non-canonical encoding, or invalid/lossy UTF-8) — the returned receipt would not match the bytes the signature covers", { kid: r.kid }); } const v = validateReceiptShapeParsed(parsed); if (!v.ok) return refuse(`payload is not a NOA receipt: ${v.errors[0]}`, { kid: r.kid }); const receipt = parsed; const envelopeAuthenticated = r.kidAuthenticated && r.kid !== null; const envelopeKid = envelopeAuthenticated ? r.kid : null; const envelopeClaim = envelopeAuthenticated ? "VERIFIED" : "UNAUTHENTICATED"; const warnings = []; if (!envelopeAuthenticated) { warnings[warnings.length] = `the outer COSE kid ${jsonStringify(r.kid)} is not in the signed (protected) header — it resolved the envelope key but is NOT reported as an identity (envelopeKid is null); an unprotected label is swappable between keyring aliases (H4)`; } const nativeKid = receipt.sig.kid; let hashInput; try { hashInput = receiptHashInput(receipt); } catch (e) { return refuse(`the enveloped receipt cannot be hashed: ${e.message}`, { kid: r.kid, nativeKid, envelopeKid, envelopeClaim, agentClaim: "FAILED" }); } if ("sha256:" + sha256Hex(hashInput) !== receipt.chain.hash) { return refuse("the enveloped receipt's chain.hash is not a hash of its own contents — not authentic", { kid: r.kid, nativeKid, envelopeKid, envelopeClaim, agentClaim: "FAILED" }); } if (verification.retiredKids[nativeKid] === true) { return refuse(`the receipt's own signing key ${jsonStringify(nativeKid)} is retired; signer-chosen receipt time is not an independent witness`, { kid: r.kid, nativeKid, envelopeKid, envelopeClaim, agentClaim: "FAILED" }); } const nativePub = keyring[nativeKid]; if (!nativePub) { return refuse(`the receipt's own signing key ${jsonStringify(nativeKid)} is not in the keyring — the envelope authenticates its EMITTER, never the agent inside it`, { kid: r.kid, nativeKid, envelopeKid, envelopeClaim, agentClaim: "FAILED" }); } if (!verifyEd25519(nativePub, signingMessage(RECEIPT_SIG_DOMAIN, hashInput), receipt.sig.value)) { return refuse(`the enveloped receipt's own signature does not verify under its kid ${jsonStringify(nativeKid)} — a valid envelope around an unsigned receipt`, { kid: r.kid, nativeKid, envelopeKid, envelopeClaim, agentClaim: "FAILED" }); } if (haveManifest) { const allowed = mapGet(manifest, receipt.agent.id); if (allowed === undefined || !arrayIncludes(allowed, nativeKid)) { return refuse(`agent "${receipt.agent.id}" is not authorized for signing key "${nativeKid}" (identity manifest)`, { kid: r.kid, nativeKid, envelopeKid, envelopeClaim, agentClaim: "UNAUTHORIZED" }); } return { ok: true, kid: r.kid, nativeKid, agentClaim: "VERIFIED", envelopeKid, envelopeClaim, receipt, warnings }; } warnings[warnings.length] = `no identityManifest: attribution is kid-level — ok:true proves the receipt's own key ${jsonStringify(nativeKid)} signed it and a keyring-trusted key enveloped it, NOT which agent.id (run with an identityManifest to bind, or treat receipt.agent.id as unauthenticated)`; return { ok: true, kid: r.kid, nativeKid, agentClaim: "UNBOUND", envelopeKid, envelopeClaim, receipt, warnings }; }