// lant-de-incredere.mjs - AERE AI Trust Chain (roadmap master point 21, track B, 2026-10-02): the LINKS between the six facets of // trust in an AI result, each of which already exists as its own proof, joined into one bundle that one verifier judges: // identity the agent's post-quantum identity (agents/: agentId derived from its ML-DSA-65 key) // version what model ran: a Proof of Software attestation of the model artifact (weights or runtime), hybrid-signed // runtime that it ran: the host's runtime record (verify-layer sidecar, kind runtime) in its audit log, before the output // permissions that it was allowed: the agent's policy and signed ledger (agents/), the entry that links the output // provenance what came out: the Proof of AI envelope (model, prompt, input, output, tools, agent, requester: digests only) // witness when: the ledger head notarized on Aere, read under a post-quantum certified anchor (agents/agent-ancora.mjs) // Nothing here re-implements a child verifier: every envelope is judged by the AIP-23 reference verifier (verify-proof.mjs), the // ledger by verifyLedger, the audit log by the sidecar's own chain check. This module checks only the LINKS between them (equal // digests, equal identities, the order of times) and reports each level in four states (PASSED, FAILED, UNMEASURED, ABSENT) with // what it rests on. A verdict says that the bytes link and who vouches for each; it does not say the model is good, the host is // honest, or the output is true. // VALID every present level PASSED and the witness PASSED (a time somebody other than the agent fixed) // INVALID a level FAILED // PARTIAL otherwise (a level UNMEASURED, or no witness) // Node.js 24 only; the children are in this repository (agents, verify-layer, proof-of-software, proof-of-ai, verify-proof.mjs). import crypto from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { agentIdFromKey, verifyLedger, canonical } from '../agents/agent-ledger.mjs'; import { hashPolicy } from '../agents/agent-policy.mjs'; import { verificaJurnal, verificaFataDeCap } from '../verify-layer/sidecar.mjs'; import { anchorsFromNotarizedHeads } from '../agents/agent-ancora.mjs'; const AICI = path.dirname(fileURLToPath(import.meta.url)); export const FORMAT = 'aere-ai-trust-chain-1'; export const LEVELS = ['form', 'identity', 'version', 'runtime', 'permissions', 'provenance', 'witness']; export const TOLERANCE_S = 300; /** The rules this verifier names: the reason codes of its refusals. `fail` throws on any other, so the list is the whole list. */ export const REGULI = ['form', 'identity-mismatch', 'identity-key', 'version-attestation', 'version-artifact-mismatch', 'version-label-mismatch', 'runtime-record', 'runtime-artifact-mismatch', 'runtime-after-output', 'audit-log-broken', 'runtime-not-in-audit-log', 'audit-head', 'witness-head', 'policy-hash', 'ledger-invalid', 'output-not-in-ledger', 'action-refused', 'ledger-time-mismatch', 'policy', 'provenance-envelope', 'provenance-finality']; export const VERIFY_PROOF_DEFAULT = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs'); const H32 = /^0x[0-9a-f]{64}$/; const sha = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); const lower = (x) => (typeof x === 'string' ? x.toLowerCase() : x); const lvl = (state, extra = {}) => ({ state, ...extra }); /** the hash a ledger entry stores for its provenance: the ledger's sha over the canonical JSON (with or without 0x, both forms matched) */ export const provenanceHash = (envelope) => sha(Buffer.from(canonical(envelope), 'utf8')); const sameHash = (a, b) => String(a || '').replace(/^0x/, '').toLowerCase() === String(b || '').replace(/^0x/, '').toLowerCase() && String(a || '').length > 0; /** build the bundle from the pieces (files already parsed); the links are not computed here, the verifier recomputes them */ export function construieste({ ledger, policy, proofOfAi, software = null, runtime = null, witness = null, createdAt = new Date().toISOString() }) { if (!ledger || !Array.isArray(ledger.entries) || !ledger.publicKeyPem || !ledger.agentId) throw new Error('a ledger (agent-ledger export) is required'); if (!policy) throw new Error('the policy is required'); if (!proofOfAi || !proofOfAi.statement || !H32.test(String(proofOfAi.statementHash || ''))) throw new Error('a Proof of AI envelope is required'); const b = { format: FORMAT, createdAt, identity: { agentId: ledger.agentId, publicKeyPem: ledger.publicKeyPem }, permissions: { policy: policy.policy || policy, policyHash: policy.policyHash || null, ledger, revocations: [] }, provenance: { proofOfAi }, version: software ? { attestation: software.attestation || software, artifactName: software.artifactName || null } : null, runtime: runtime ? { record: runtime.record, auditLog: runtime.auditLog || null, head: runtime.head || null } : null, witness: witness ? { heads: witness.heads || [], chainId: witness.chainId || null } : null, }; if (policy.revocations) b.permissions.revocations = policy.revocations; return b; } /** run the AIP-23 reference verifier on an envelope; returns its JSON (levels as a map) or null */ export function judecatorAip23(verifyProof, envelope, { rpc = null, chain = null } = {}) { return new Promise((ok) => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-ai-trust-')); const f = path.join(d, 'plic.json'); fs.writeFileSync(f, JSON.stringify(envelope)); const args = [verifyProof, f, '--json', ...(rpc ? ['--rpc', rpc] : []), ...(chain ? ['--chain', String(chain)] : [])]; const c = spawn(process.execPath, args); let out = ''; c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', () => {}); const t = setTimeout(() => c.kill(), 300000); const gata = (j) => { clearTimeout(t); fs.rmSync(d, { recursive: true, force: true }); ok(j); }; c.on('error', () => gata(null)); c.on('close', () => { try { const j = JSON.parse(out); ok(gata({ verdict: j.verdict, levels: Object.fromEntries((j.levels || []).map((l) => [l.level, l])) })); } catch { gata(null); } }); }); } const parsed = (j, name) => (j && j.levels && j.levels[name]) ? j.levels[name] : { state: 'UNMEASURED', detail: 'the reference verifier gave no ' + name + ' level' }; const envelopeOk = (j) => j && ['form', 'integrity'].every((n) => parsed(j, n).state === 'PASSED') && (parsed(j, 'signature').state !== 'FAILED'); export async function verifica(bundle, { verifyProof = VERIFY_PROOF_DEFAULT, rpc = null, chain = null, now = Math.floor(Date.now() / 1000) } = {}) { const levels = Object.fromEntries(LEVELS.map((l) => [l, lvl('ABSENT')])); const assumptions = {}; const fail = (l, reason, detail) => { if (!REGULI.includes(reason)) throw new Error('ai-trust: undeclared rule ' + reason); levels[l] = lvl('FAILED', { reason, detail }); }; const unm = (l, reason, detail) => { levels[l] = lvl('UNMEASURED', { reason, detail }); }; // 1. form const isObj = (x) => x !== null && typeof x === 'object' && !Array.isArray(x); if (!isObj(bundle) || bundle.format !== FORMAT || !isObj(bundle.identity) || !isObj(bundle.permissions) || !isObj(bundle.provenance) || !isObj(bundle.permissions.ledger) || !isObj(bundle.provenance.proofOfAi)) { fail('form', 'form', 'not an ' + FORMAT + ' bundle with identity, permissions.ledger and provenance.proofOfAi'); return verdict(levels, assumptions); } levels.form = lvl('PASSED'); const poa = bundle.provenance.proofOfAi, st = poa.statement || {}, ledger = bundle.permissions.ledger; const outputAt = Math.floor(Date.parse(st.createdAt || '') / 1000); // 2. identity: the id is derived from the key, and the ledger, the bundle and the output name the same agent try { const derived = agentIdFromKey(crypto.createPublicKey(bundle.identity.publicKeyPem)); const same = derived === bundle.identity.agentId && derived === ledger.agentId && derived === st.agentId /*PAZA:identity*/; if (!same) fail('identity', 'identity-mismatch', `key derives ${derived}; bundle says ${bundle.identity.agentId}, ledger ${ledger.agentId}, output ${st.agentId}`); else levels.identity = lvl('PASSED', { detail: `${derived} (ML-DSA-65 key; the id is derived from the key, nobody can claim it without the key)` }); } catch (e) { fail('identity', 'identity-key', 'the public key cannot be read: ' + String(e.message).slice(0, 80)); } assumptions.identity = ['ML-DSA-65 unbroken; the agent key holder is the agent']; // 3. version: the model artifact attested (Proof of Software), and the output names that very artifact let attestedSha = null; if (bundle.version && bundle.version.attestation) { const att = bundle.version.attestation; const j = await judecatorAip23(verifyProof, att, { rpc: att.notarization ? rpc : null, chain }); if (!j) unm('version', 'version-verifier', 'the AIP-23 reference verifier did not answer for the attestation'); else if (!envelopeOk(j)) fail('version', 'version-attestation', 'the Proof of Software attestation does not verify: ' + ['form', 'integrity', 'signature'].map((n) => n + ' ' + parsed(j, n).state).join(', ')); else { const arts = (att.statement && att.statement.artifacts) || []; const w = lower(st.model && st.model.weightsSha256); const hit = arts.find((a) => lower(a.sha256) === w); const ok = !!(w && hit) /*PAZA:version*/; if (!w) unm('version', 'model-weights-not-named', 'the Proof of AI names no model.weightsSha256, so no artifact can be matched'); else if (!ok) fail('version', 'version-artifact-mismatch', `the output names model weights ${w.slice(0, 14)}.., the attestation covers ${arts.map((a) => a.name + ' ' + String(a.sha256).slice(0, 14) + '..').join(', ') || 'no artifact'}`); else { const v = att.statement.subject && att.statement.subject.version; const mv = st.model && st.model.version; if (v && mv && String(v) !== String(mv)) fail('version', 'version-label-mismatch', `the output says model version ${mv}, the attestation ${v}`); else { const h = hit || arts[0] || { name: '?', sha256: w }; attestedSha = lower(h.sha256); levels.version = lvl('PASSED', { detail: `${h.name} ${String(h.sha256).slice(0, 18)}.. attested as ${att.statement.subject ? att.statement.subject.name + '@' + att.statement.subject.version : '?'}; signature ${parsed(j, 'signature').state}${att.notarization ? ', finality ' + parsed(j, 'finality').state : ''}` }); } } } assumptions.version = ['the attestation proves what its signing keys attested (who holds them is a separate judgement: pos.mjs verify --signer); the artifact digest is what links it to the output']; } // 4. runtime: the host declared that THIS artifact ran, in its audit log, before the output if (bundle.runtime && bundle.runtime.record) { const rec = bundle.runtime.record, rs = rec.statement || {}; const j = await judecatorAip23(verifyProof, rec, { rpc: rec.notarization ? rpc : null, chain }); if (!j) unm('runtime', 'runtime-verifier', 'the AIP-23 reference verifier did not answer for the runtime record'); else if (!envelopeOk(j) || rec.kind !== 'aere-proof-of-runtime-attestation') fail('runtime', 'runtime-record', 'the runtime record is not a verified aere-proof-of-runtime-attestation'); else { const target = attestedSha || lower(st.model && st.model.weightsSha256); const ok = !!(target && lower(rs.artifactSha256) === target && rs.matches === true) /*PAZA:runtime*/; if (!target) unm('runtime', 'runtime-no-artifact', 'no attested artifact to match the runtime record against'); else if (!ok) fail('runtime', 'runtime-artifact-mismatch', `the host ran ${String(rs.artifactSha256).slice(0, 14)}.. (matches ${rs.matches}), the output names ${target.slice(0, 14)}..`); else { const runAt = Math.floor(Date.parse(rs.createdAt || '') / 1000); const ordered = Number.isFinite(runAt) && Number.isFinite(outputAt) && runAt <= outputAt + TOLERANCE_S /*PAZA:order*/; if (!ordered) fail('runtime', 'runtime-after-output', `the runtime record is dated ${rs.createdAt}, the output ${st.createdAt}`); else { let note = `host ${rs.host} declared the artifact running at ${rs.createdAt}`; const log = bundle.runtime.auditLog; if (Array.isArray(log)) { const v = verificaJurnal(log); if (!v.ok) { fail('runtime', 'audit-log-broken', `the audit log chain breaks at seq ${v.rupt}: ${v.motiv}`); } else if (!log.some((e) => e.proof && lower(e.proof.statementHash) === lower(rec.statementHash)) /*PAZA:audit*/) fail('runtime', 'runtime-not-in-audit-log', 'the runtime record is not an entry of the audit log given'); else { note += `; in the host's audit log (${log.length} entries, chain intact)`; if (bundle.runtime.head) { const c = verificaFataDeCap(log, bundle.runtime.head); if (!c.ok) fail('runtime', 'audit-head', 'the log does not continue the attested head: ' + (c.motiv || '')); else { note += `; continues the attested head (count ${bundle.runtime.head.statement && bundle.runtime.head.statement.count})`; if (bundle.runtime.head.notarization && rpc) { const hj = await judecatorAip23(verifyProof, bundle.runtime.head, { rpc, chain }); note += `; head finality ${hj ? parsed(hj, 'finality').state : 'UNMEASURED'}`; } } } } } else note += '; no audit log given (the record stands alone)'; if (levels.runtime.state !== 'FAILED') levels.runtime = lvl('PASSED', { detail: note }); } } } assumptions.runtime = ['the runtime record is the HOST\'s declaration (the sidecar reads the export the operator makes); it proves what the host said ran, not that it did; the audit log has no key and is judged against a head published outside the host']; } // 5. permissions: the ledger verifies under its policy, the output is an entry of it, allowed, at a matching time let anchors = [], witnessNote = null; if (bundle.witness && Array.isArray(bundle.witness.heads) && bundle.witness.heads.length) { if (!rpc) { unm('witness', 'witness-needs-rpc', 'the notarized heads can be read only through an endpoint of the chain (--rpc)'); } else { const a = await anchorsFromNotarizedHeads(ledger, bundle.witness.heads, { verifyProof, rpc, chain: chain || bundle.witness.chainId || null }); if (a.stare === 'OK' && a.anchors.length) { anchors = a.anchors; witnessNote = a.randuri.map((r) => r.detail).join(' | '); } else if (a.stare === 'FAILED') fail('witness', 'witness-head', a.randuri.filter((r) => !r.pass).map((r) => r.detail).join(' | ')); else unm('witness', 'witness-unmeasured', a.randuri.map((r) => r.detail).join(' | ') || 'no anchor could be read'); } } try { const { policy, policyHash } = hashPolicy(bundle.permissions.policy); if (bundle.permissions.policyHash && lower(bundle.permissions.policyHash) !== policyHash) fail('permissions', 'policy-hash', 'the bundle pins another policy hash than the policy given'); else { const v = verifyLedger(ledger, { policy, policyHash, revocations: bundle.permissions.revocations || [], anchors, maxTime: now + TOLERANCE_S }); if (!v.ok) fail('permissions', 'ledger-invalid', String(v.error || '').slice(0, 200)); else { const ph = provenanceHash(poa); const entry = ledger.entries.find((e) => e.body && sameHash(e.body.provenance, ph)) /*PAZA:output-in-ledger*/; if (!entry) fail('permissions', 'output-not-in-ledger', 'no ledger entry links this Proof of AI envelope (provenance hash ' + ph.slice(0, 14) + '..)'); else if (!(entry.body.decision && entry.body.decision.allowed === true) /*PAZA:allowed*/) fail('permissions', 'action-refused', `the ledger entry seq ${entry.seq} was refused: ${entry.body.decision && entry.body.decision.reason}`); else if (!(Number.isFinite(outputAt) && Math.abs(Number(entry.body.at) - outputAt) <= TOLERANCE_S)) fail('permissions', 'ledger-time-mismatch', `the ledger entry is at ${entry.body.at}, the output at ${outputAt} (${st.createdAt})`); else levels.permissions = lvl('PASSED', { detail: `entry seq ${entry.seq} (${entry.body.action && entry.body.action.kind}${entry.body.action && entry.body.action.tool ? ' ' + entry.body.action.tool : ''}) allowed under policy ${policyHash.slice(0, 14)}..; ${ledger.entries.length} entries verified, revocations ${v.revocations ? v.revocations.given : 0}, time ${v.time ? v.time.lowerBound : '?'}` }); if (anchors.length && levels.witness.state === 'ABSENT') levels.witness = lvl('PASSED', { detail: `${anchors.length} anchor(s) from notarized heads under a post-quantum certified state: ${witnessNote}` }); } } } catch (e) { fail('permissions', 'policy', String(e.message).slice(0, 160)); } assumptions.permissions = ['the policy given is the one pinned (its hash is recomputed); revocations come from the owner, outside the ledger, and only those given are judged']; // 6. provenance: the Proof of AI envelope itself { const j = await judecatorAip23(verifyProof, poa, { rpc: poa.notarization ? rpc : null, chain }); if (!j) unm('provenance', 'provenance-verifier', 'the AIP-23 reference verifier did not answer'); else if (poa.kind !== 'aere-proof-of-ai-attestation' || !envelopeOk(j)) fail('provenance', 'provenance-envelope', 'the Proof of AI envelope does not verify: ' + ['form', 'integrity', 'signature'].map((n) => n + ' ' + parsed(j, n).state).join(', ')); else if (poa.notarization && !rpc) unm('provenance', 'provenance-finality-needs-rpc', 'the envelope claims a notarization that only an endpoint can confirm'); else if (poa.notarization && parsed(j, 'finality').state !== 'PASSED') { const f = parsed(j, 'finality'); (f.state === 'FAILED' ? fail : unm)('provenance', 'provenance-finality', String(f.detail).slice(0, 200)); } else levels.provenance = lvl('PASSED', { detail: `model ${st.model && st.model.name}@${st.model && st.model.version}, prompt ${String(st.promptHash).slice(0, 12)}.., output ${String(st.outputHash).slice(0, 12)}.., tools [${(st.tools || []).join(', ')}], requester ${String(st.requesterHash).slice(0, 12)}..; signature ${parsed(j, 'signature').state}${poa.notarization ? ', finality ' + parsed(j, 'finality').state : ''}` }); assumptions.provenance = ['digests only: the prompt, input, output and requester are proven to exist as the bytes whose hashes these are, not shown; the envelope\'s time is its producer\'s statement unless notarized']; } if (levels.witness.state !== 'ABSENT') assumptions.witness = ['the time is the chain\'s (the notary records the block time; on 28001 and 2800 every validator is run by the Foundation): whoever notarizes can only notarize late']; else assumptions.witness = ['no witness: every time in this bundle is the statement of whoever holds the keys']; return verdict(levels, assumptions); } function verdict(levels, assumptions) { const failed = LEVELS.find((l) => levels[l].state === 'FAILED'); let v, reason = null; if (failed) { v = 'INVALID'; reason = levels[failed].reason; } else { const unmL = LEVELS.find((l) => levels[l].state === 'UNMEASURED'); if (unmL) { v = 'PARTIAL'; reason = levels[unmL].reason; } else if (levels.witness.state !== 'PASSED') { v = 'PARTIAL'; reason = 'unwitnessed'; } else v = 'VALID'; } // `rules`: the reason code of every level that failed (the verdict's `reason` is only the first) return { verdict: v, reason, rules: LEVELS.filter((l) => levels[l].state === 'FAILED').map((l) => levels[l].reason).sort(), levels: Object.fromEntries(LEVELS.map((l) => [l, levels[l].state])), details: levels, assumptions, notice: 'A verdict says that the bytes link and who vouches for each link. It does not say the model is good, the host is honest, or the output is true.' }; }