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
JavaScript
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 };
}