aere-quantum/agents/agent-ledger.mjs
Aere Network 6e62b1ad1a Add agents: a post-quantum identity, a policy and a signed action ledger for AI agents, with human approval and revocation
An agent gets an ML-DSA-65 identity and a policy (spending per time window, allowed tools and recipients, which actions need human
approval, who may revoke it). Every action it proposes is judged against the policy, signed by the agent and chained; a verifier that
does not trust the agent re-runs the policy over the whole ledger. Approvals and revocations are signed by people with their own
ML-DSA-65 keys. The ledger of an agent under a policy is one ledger: a second history is a branch, and two branches are a proof of
equivocation anyone can check with the public key alone. The README says what the verifier cannot see: entry times are bounded from
below only with a witness (anchors or a start time), and someone who sees one branch cannot know of another.

Tests: policy 23/23 with the AIP-23 reference verifier (21 run without it), ledger 51/51, approval and revocation 39/39, command line
23/23; negative control 25/25.
2026-09-29 21:59:41 +03:00

336 lines
26 KiB
JavaScript

// AERE Agent Ledger (roadmap B2 / #35-37, seam-ul care lipsea): identitatea post-cuantica a unui agent AI + registrul lui de actiuni,
// impus CRIPTOGRAFIC si verificabil de oricine, fara incredere in agent.
//
// agent-policy.mjs are checkAction PUR (nu tine stare, primeste cheltuiala din fereastra). O limita de cheltuiala "pe fereastra" nu
// inseamna nimic fara CINEVA care aduna cheltuiala reala si o impune la fiecare actiune - altfel agentul poate spune de fiecare data
// "n-am cheltuit nimic". Aici e acel cineva: un registru per identitate care
// 1. deriva cheltuiala din fereastra din PROPRIILE lui intrari permise (starea, nu vorba agentului),
// 2. cheama checkAction cu ea, deci limita e impusa PE TOATA SESIUNEA,
// 3. semneaza fiecare intrare cu cheia ML-DSA-65 a agentului si o leaga intr-un lant sha256(seq|prev|corp|semnatura),
// 4. leaga, optional, provenienta iesirii (model/versiune/prompt/unealta, digestul unui plic proof-of-ai).
// Identitatea agentului e LEGATA de cheie: agentId = 'aere-agent:' + primii 20 de octeti din sha256(SPKI-ul cheii publice). Nu poti
// pretinde alt agentId cu cheia ta, si nu poti semna in numele altui agent fara cheia lui.
//
// VERIFICATORUL (verifyLedger) NU se increde in ce scrie registrul despre sine: recalculeaza hash-ul politicii primite, reface lantul
// de hash-uri, RE-VERIFICA fiecare semnatura fata de cheia publica a agentului, si RE-RULEAZA checkAction de la inceput ca sa confirme
// ca decizia scrisa (allowed/refuz) e chiar cea pe care o da politica, si ca nicio cheltuiala permisa nu a depasit vreodata limita. O
// intrare cu suma schimbata, semnata de alta cheie, stearsa, sau cu un "allowed" mincinos peste un refuz, e prinsa.
//
// TIMPUL, spus exact (2026-09-29, B-17): momentul unei intrari e declaratia celui care tine cheia agentului. Verificatorul il margineste
// de SUS cu ceasul lui (sau cu momentul unui cap ancorat); de JOS numai daca primeste ancore de la un martor (`anchors`: capete ale
// registrului cu momentul la care martorul le-a vazut) sau un `notBefore`. Fara ele, cine tine cheia poate ANTEDATA intrari si imparti
// o cheltuiala peste ferestre trecute (masurat pe forma 1: 10 plati de 100 facute in aceeasi secunda, declarate in 10 ferestre, sub o
// limita de 100 pe ora, verificator "ok"); cu ancore, antedatarea e marginita la intervalul dintre doua ancore. Registrul cinstit
// (biblioteca de aici) refuza la scriere un moment mai departe de ceasul lui decat TOLERANTA_S, in ambele sensuri.
//
// UN SINGUR REGISTRU pe agent si politica (2026-09-29, B-17): limita "pe fereastra" era impusa pe REGISTRU, iar un registru nou se
// deschidea oricand cu o sesiune aleatoare, deci cine tine cheia putea deschide N registre si cheltui de N ori limita, fiecare
// verificat "ok" (masurat pe forma 1 prin aceeasi aprobare pentru 5000 folosita in doua registre). Acum sesiunea e DERIVATA din agent
// si politica (`sessionId`), deci un al doilea registru nu e "alt registru", ci o RAMURA a aceluiasi: doua intrari semnate de agent cu
// aceeasi sesiune si acelasi seq si continut diferit sunt o dovada de echivocare pe care o verifica oricine (`findEquivocation`,
// `verifyEquivocation`), fara incredere in nimeni. Un agent care reporneste isi continua registrul (`resumeLedger`); unul care si-a
// pierdut registrul nu il poate relua fara sa para o ramura, si asta e pretul cerut: registrul se tine durabil (de pilda in jurnalul
// stratului de verificare), sau se trece la o politica noua. Ce NU se poate: cine vede o singura ramura nu stie ca exista alta;
// o vede un martor al capului (`anchors`) sau cine primeste ambele.
//
// Numai Node 24 (crypto.sign/verify cu ML-DSA, null ca algoritm). Nu atinge reteaua, nu tine bani reali: `amount` e o unitate abstracta
// a politicii (wei AERE sau orice altceva ce numeste politica); mutarea reala a valorii ramane a stratului de plata (x402), care poate
// cere acest registru drept dovada ca actiunea a fost permisa de politica agentului.
import crypto from 'node:crypto';
import { checkAction, hashPolicy, canonical } from './agent-policy.mjs';
import { verifyApproval, verifyRevocation, validRevocations } from './agent-aprobare.mjs';
export { canonical };
export const VERSION = 'aere-agent-ledger/2 (2026-09-29)';
const ALG = 'ml-dsa-65';
const sha = (s) => crypto.createHash('sha256').update(typeof s === 'string' ? Buffer.from(s, 'utf8') : s).digest('hex');
const GEN = '0'.repeat(64);
const TOLERANTA_S = 300; // cat se poate departa momentul unei intrari de ceasul care o judeca (registrul la scriere, verificatorul la citire)
// 2026-09-28 (punctul 22): aprobarea umana si revocarea. O intrare poate purta aprobarile care i-au fost date (`body.approvals`, numai
// cand exista); registrul le verifica, numara aprobatorii distincti VALIZI si ii da lui checkAction. Un nonce de aprobare se poate
// folosi o singura data pe tot lantul: a doua aparitie nu mai numara. Revocarea semnata de proprietar opreste tot de la momentul ei;
// registrul o scrie ca intrare (`body.revocation`), iar verificatorul o primeste si SEPARAT, fiindca un agent isi poate omite revocarea
// din propriul registru.
function aprobatoriValizi(aprobari, { policy, policyHash, action, at, folosite }) {
const valizi = [], respinse = [];
for (const ap of aprobari || []) {
if (ap && folosite.has(String(ap.nonce))) { respinse.push('nonce already used'); continue; }
const r = verifyApproval(ap, { policy, policyHash, action, at });
if (r.ok) valizi.push(r.humanId); else respinse.push(r.error);
}
return { valizi, respinse };
}
const nonceuri = (aprobari) => (aprobari || []).filter((a) => a && a.nonce != null).map((a) => String(a.nonce));
/** SPKI-ul (DER) al unei chei publice, ca punct unic de adevar al identitatii. */
function spkiDer(publicKey) { return publicKey.export({ type: 'spki', format: 'der' }); }
/** agentId derivat din cheia publica: nefalsificabil fara cheie. */
export function agentIdFromKey(publicKey) { return 'aere-agent:' + sha(spkiDer(publicKey)).slice(0, 40); }
/**
* O identitate noua de agent (cheie ML-DSA-65 + agentId legat de ea).
* @returns {{agentId:string, publicKey:crypto.KeyObject, privateKey:crypto.KeyObject, publicKeyPem:string}}
*/
export function newAgentIdentity() {
const { publicKey, privateKey } = crypto.generateKeyPairSync(ALG);
return { agentId: agentIdFromKey(publicKey), publicKey, privateKey, publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }) };
}
/** Identitatea unui agent din cheia lui privata (PEM PKCS#8). */
export function agentFromPrivateKeyPem(pem) {
const privateKey = crypto.createPrivateKey(pem);
if (privateKey.asymmetricKeyType !== ALG) throw new Error('agent-ledger: the key is not ML-DSA-65');
const publicKey = crypto.createPublicKey(privateKey);
return { agentId: agentIdFromKey(publicKey), publicKey, privateKey, publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }) };
}
export function publicKeyFromPem(pem) { return crypto.createPublicKey(pem); }
/** Sesiunea registrului unui agent sub o politica: derivata, deci una singura. */
export function sessionId(agentId, policyHash) { return sha(`aere-agent-ledger-session|${agentId}|${String(policyHash).toLowerCase()}`).slice(0, 32); }
const ceasulLocal = () => Math.floor(Date.now() / 1000);
function pregateste({ identity, policy: politicaData, policyHash: hashDat }) {
if (!identity || !identity.privateKey || !identity.publicKey) throw new Error('agent-ledger: an identity with privateKey and publicKey is required');
if (identity.agentId !== agentIdFromKey(identity.publicKey)) throw new Error('agent-ledger: agentId is not derived from the identity key');
const { policy, policyHash } = hashPolicy(politicaData);
if (hashDat != null && String(hashDat).toLowerCase() !== policyHash) throw new Error('agent-ledger: the policy does not hash to the policyHash given');
if (policy.agentId !== identity.agentId) throw new Error('agent-ledger: the policy belongs to another agent');
return { policy, policyHash };
}
/**
* Deschide registrul NOU al unei identitati sub o politica (seq 0). `now` e injectabil (fereastra deterministica in probe). Politica se
* normalizeaza si hash-ul ei se RECALCULEAZA: o pereche politica+hash care nu se potriveste e refuzata aici, nu doar la verificare.
* A doua deschidere sub aceeasi politica e o RAMURA a primului registru (aceeasi sesiune): pentru o repornire se foloseste resumeLedger.
* @param {object} p {identity:{agentId,privateKey,publicKey}, policy, policyHash?, now?}
*/
export function openLedger({ identity, policy, policyHash, now = ceasulLocal }) {
return registru(identity, pregateste({ identity, policy, policyHash }), now, [], null);
}
/**
* Continua un registru exportat (repornirea agentului). Il verifica intai ca un strain (verifyLedger, pe ceasul registrului); refuza
* un registru care nu verifica sau al altei identitati, si reface starea: intrarile, nonce-urile folosite, revocarea.
* @param {object} p {identity, policy, policyHash?, ledger: exportul, now?, revocations?}
*/
export function resumeLedger({ identity, policy, policyHash, ledger, now = ceasulLocal, revocations = [] }) {
const pol = pregateste({ identity, policy, policyHash });
if (!ledger || ledger.agentId !== identity.agentId) throw new Error('agent-ledger: the ledger to resume belongs to another agent');
const v = verifyLedger(ledger, { policy: pol.policy, policyHash: pol.policyHash, maxTime: Number(now()) + TOLERANTA_S, revocations });
if (!v.ok) throw new Error(`agent-ledger: the ledger to resume does not verify${v.seq != null ? ` at seq ${v.seq}` : ''}: ${v.error}`);
return registru(identity, pol, now, JSON.parse(JSON.stringify(ledger.entries)), v.revocations.revokedAt);
}
function registru(identity, { policy, policyHash }, now, entries, revocatInitial) {
const session = sessionId(identity.agentId, policyHash);
const folosite = new Set(); // nonce-urile aprobarilor deja purtate de intrari
for (const e of entries) for (const n of nonceuri(e.body.approvals)) folosite.add(n);
let revokedAt = revocatInitial; // cea mai timpurie revocare valida primita
// cheltuielile PERMISE, cu momentul lor, pentru fereastra (numai platile allowed; checkAction filtreaza singur dupa fereastra)
function spentInWindow() {
return entries.filter((e) => e.body.decision.allowed && e.body.action.kind === 'payment')
.map((e) => ({ amount: String(e.body.action.amount), at: e.body.action.at }));
}
function scrie(corpFaraSeq) {
const seq = entries.length;
const prev = seq ? entries[seq - 1].hash : GEN;
const body = { ...corpFaraSeq, seq };
const mesaj = Buffer.from(`${seq}|${prev}|${canonical(body)}`, 'utf8');
const signature = crypto.sign(null, mesaj, identity.privateKey).toString('base64');
const hash = sha(`${seq}|${prev}|${canonical(body)}|${signature}`);
const entry = { seq, prev, body, signature, hash };
entries.push(entry);
return entry;
}
function momentul(opt) {
const at = opt.at ?? now();
if (!Number.isFinite(Number(at))) throw new Error('agent-ledger: the entry time is not a finite number');
if (entries.length && Number(at) < Number(entries[entries.length - 1].body.at)) throw new Error('agent-ledger: entry times cannot go backwards');
const acum = Number(now());
if (Number(at) > acum + TOLERANTA_S) throw new Error(`agent-ledger: the entry time is more than ${TOLERANTA_S} s ahead of the ledger clock`);
// 2026-09-29 (B-17): si in urma; o intrare antedatata muta o plata intr-o fereastra trecuta
if (Number(at) < acum - TOLERANTA_S) throw new Error(`agent-ledger: the entry time is more than ${TOLERANTA_S} s behind the ledger clock (backdated)`);
return Number(at);
}
const antet = { v: 2, agentId: identity.agentId, policyHash, session };
return {
agentId: identity.agentId,
session,
policyHash,
/**
* Revocarea agentului, semnata de proprietarul numit in politica. Se scrie ca intrare; de la `revokedAt` totul e refuzat.
* O revocare care nu verifica (alt semnatar, alta politica) e refuzata cu motivul, si nu se scrie.
*/
recordRevocation(rv, opt = {}) {
const r = verifyRevocation(rv, { policy, policyHash });
if (!r.ok) throw new Error(`agent-ledger: revocation refused: ${r.error}`);
const at = momentul(opt);
revokedAt = revokedAt == null ? r.revokedAt : Math.min(revokedAt, r.revokedAt);
const action = { kind: 'revocation', revokedAt: r.revokedAt, at };
return scrie({ ...antet, at, action, decision: { allowed: false, reason: `revocation recorded (from ${r.revokedAt})` }, provenance: null, revocation: rv });
},
/**
* Propune o actiune. Cheama checkAction cu cheltuiala reala din fereastra; scrie o intrare semnata si legata.
* @param {object} actionIn {kind:'payment'|'tool', amount?, to?, asset?, tool?, args?}
* @param {object} [opt] {provenance?: plic proof-of-ai / obiect (se leaga prin hash), at?: number, approvals?: aprobari umane}
* @returns {{allowed:boolean, reason:string, entry:object, rejectedApprovals:string[]}}
*/
record(actionIn, opt = {}) {
// 2026-09-27 (revizuire adversariala): `at` ales de apelant reseta fereastra la vointa. Timpul unei intrari e NEDESCRESCATOR si
// nu se poate departa de ceasul registrului cu mai mult de TOLERANTA_S; verificatorul cere acelasi lucru fata de ceasul LUI.
const at = momentul(opt);
// checkAction citeste "acum" din action.at si filtreaza fereastra dupa el: injectam at in actiune, si o stocam asa cum a fost judecata
const action = { ...actionIn, at };
const approvals = Array.isArray(opt.approvals) && opt.approvals.length ? opt.approvals : null;
const { valizi, respinse } = aprobatoriValizi(approvals, { policy, policyHash, action, at, folosite });
const decizie = checkAction(policy, action, spentInWindow(), { approvers: valizi, revokedAt });
for (const n of nonceuri(approvals)) folosite.add(n);
const body = { ...antet, at, action, decision: { allowed: !!decizie.allowed, reason: decizie.reason },
provenance: opt.provenance ? sha(canonical(opt.provenance)) : null };
if (approvals) body.approvals = approvals;
const entry = scrie(body);
return { allowed: entry.body.decision.allowed, reason: entry.body.decision.reason, entry, rejectedApprovals: respinse };
},
/** Registrul de export: identitate (cheie publica), politica (prin hash), sesiunea si intrarile. */
export() {
return { version: VERSION, agentId: identity.agentId, publicKeyPem: identity.publicKey.export({ type: 'spki', format: 'pem' }),
policyHash, session, entries: entries.slice() };
},
entries() { return entries.slice(); },
};
}
/**
* Verifica un registru exportat FARA sa se increada in el: politica primita (recalculata la hash), identitatea legata de cheie, fiecare
* semnatura, lantul de hash-uri, si - decisiv - RE-RULEAZA politica de la inceput ca sa confirme ca fiecare decizie scrisa e cea corecta
* si ca nicio cheltuiala permisa nu a depasit limita.
* @param {object} ledger exportul unui registru
* @param {object} o
* @param {object} o.policy politica (de la oricine: hash-ul ei se recalculeaza si trebuie sa fie al registrului)
* @param {string} [o.policyHash] hash-ul fixat de cel care verifica (de ex. dintr-o autorizare); daca lipseste, cel al politicii
* @param {number} [o.maxTime] marginea de SUS a timpului (secunde unix); implicit ceasul local + TOLERANTA_S
* @param {Array} [o.revocations] revocarile primite de la proprietar, nu din registru
* @param {Array} [o.anchors] [{seq, hash, at}]: capete ale registrului vazute de un martor la momentul `at`
* @param {number} [o.notBefore] marginea de JOS a timpului, de la un martor (de ex. deschiderea sesiunii)
* @param {number} [o.anchorTolerance] cat poate intarzia martorul fata de scriere (implicit TOLERANTA_S)
* @returns {{ok:boolean, seq?:number, error?:string, allowedPayments?:number, spent?:string, humanApproved?:number, revocations:object, time:object}}
*/
export function verifyLedger(ledger, { policy: politicaData, policyHash: hashFixat, maxTime = Math.floor(Date.now() / 1000) + TOLERANTA_S,
revocations = [], anchors = [], notBefore = null, anchorTolerance = TOLERANTA_S } = {}) {
const timp = { maxTime: Number(maxTime), notBefore: notBefore == null ? null : Number(notBefore), anchors: (anchors || []).length,
lowerBound: notBefore != null || (anchors || []).length ? 'witnessed' : 'none: entry times are the statement of the agent key holder' };
const rezultat = (x, extra = {}) => ({ ...x, ...extra, time: timp });
if (!ledger || !Array.isArray(ledger.entries)) return rezultat({ ok: false, error: 'a ledger with entries is required' });
let policy, policyHash;
try { ({ policy, policyHash } = hashPolicy(politicaData)); } catch (e) { return rezultat({ ok: false, error: `the policy given is not valid: ${e.message}` }); }
if (hashFixat != null && String(hashFixat).toLowerCase() !== policyHash) return rezultat({ ok: false, error: 'the policy given does not hash to the pinned policyHash (a different policy)' });
const ext = validRevocations(revocations, { policy, policyHash });
let revokedAt = ext.revokedAt;
const judecat = () => ({ revocations: { given: (revocations || []).length, rejected: ext.rejected.length, revokedAt, setHash: ext.setHash } });
if (String(ledger.policyHash).toLowerCase() !== policyHash) return rezultat({ ok: false, error: 'the ledger names another policy (policyHash differs)' }, judecat());
if (ledger.session !== sessionId(ledger.agentId, policyHash)) return rezultat({ ok: false, error: 'the ledger session is not the one derived from the agent and the policy' }, judecat());
let pub;
try { pub = crypto.createPublicKey(ledger.publicKeyPem); } catch { return rezultat({ ok: false, error: 'the public key cannot be read' }, judecat()); }
if (ledger.agentId !== agentIdFromKey(pub)) return rezultat({ ok: false, error: 'agentId is not derived from the public key (false identity)' }, judecat());
if (policy.agentId !== ledger.agentId) return rezultat({ ok: false, error: 'the policy belongs to another agent' }, judecat());
// ancorele: fiecare numeste o intrare a ACESTUI registru (altfel e alta ramura sau alt registru)
const anc = [];
for (const a of anchors || []) {
const s = Number(a && a.seq);
if (!Number.isInteger(s) || s < 0 || s >= ledger.entries.length) return rezultat({ ok: false, error: `anchor at seq ${a && a.seq} is outside the ledger` }, judecat());
if (!ledger.entries[s] || ledger.entries[s].hash !== a.hash) return rezultat({ ok: false, seq: s, error: `anchor at seq ${s} does not match the ledger (another branch or another ledger)` }, judecat());
if (!Number.isFinite(Number(a.at))) return rezultat({ ok: false, error: `anchor at seq ${s} has no time` }, judecat());
anc.push({ seq: s, at: Number(a.at) });
}
const tol = Number(anchorTolerance);
let prev = GEN, humanApproved = 0;
const folosite = new Set();
const permise = []; // cheltuielile permise re-derivate, pentru fereastra
for (let i = 0; i < ledger.entries.length; i++) {
const e = ledger.entries[i];
const esec = (error) => rezultat({ ok: false, seq: i, error }, judecat());
if (e.seq !== i) return esec(`seq ${e.seq} instead of ${i}`);
if (e.prev !== prev) return esec('prev does not link to the previous hash (an entry was removed or reordered)');
const b = e.body;
if (!b || b.v !== 2 || b.seq !== i || b.agentId !== ledger.agentId || b.session !== ledger.session) return esec('the entry body does not match its header (agent, session or seq)');
if (String(b.policyHash).toLowerCase() !== policyHash) return esec('the entry names another policy');
const mesaj = Buffer.from(`${i}|${prev}|${canonical(b)}`, 'utf8');
let sigOk = false;
try { sigOk = crypto.verify(null, mesaj, pub, Buffer.from(String(e.signature), 'base64')); } catch { sigOk = false; }
if (!sigOk) return esec('the signature does not verify (content changed or another key)');
const hash = sha(`${i}|${prev}|${canonical(b)}|${e.signature}`);
if (hash !== e.hash) return esec('the entry hash does not reproduce');
// timpul: nedescrescator, sub marginea de sus, peste marginea de jos si intre ancorele care il incadreaza
const atI = Number(b.at);
if (!Number.isFinite(atI) || !b.action || Number(b.action.at) !== atI) return esec('the entry time is missing or differs from the time judged');
if (i > 0 && atI < Number(ledger.entries[i - 1].body.at)) return esec('entry times go backwards');
if (atI > Number(maxTime)) return esec(`entry from the future: ${atI} > ${maxTime} (the verifier clock or an anchored head)`);
if (notBefore != null && atI < Number(notBefore)) return esec(`entry declares ${atI}, before the witnessed start ${notBefore}`);
for (const a of anc) {
if (i <= a.seq && atI > a.at + tol) return esec(`entry declares ${atI}, after the anchor at seq ${a.seq} that covers it was witnessed (${a.at})`);
if (i > a.seq && atI < a.at - tol) return esec(`entry declares ${atI}, before the anchor at seq ${a.seq} that precedes it was witnessed (${a.at}): backdated`);
}
// o intrare de revocare: revocarea din ea trebuie sa verifice, si ea nu permite nimic
if (b.revocation !== undefined) {
const r = verifyRevocation(b.revocation, { policy, policyHash });
if (!r.ok) return esec(`the revocation entry carries a revocation that does not verify (${r.error})`);
if (b.action.kind !== 'revocation' || Number(b.action.revokedAt) !== r.revokedAt || b.decision.allowed) return esec('the revocation entry does not say what it carries');
revokedAt = revokedAt == null ? r.revokedAt : Math.min(revokedAt, r.revokedAt);
prev = hash;
continue;
}
if (b.action.kind === 'revocation') return esec('a revocation entry without the revocation');
if (b.approvals !== undefined && !(Array.isArray(b.approvals) && b.approvals.length)) return esec('the approvals field is empty or not a list');
const { valizi } = aprobatoriValizi(b.approvals, { policy, policyHash, action: b.action, at: atI, folosite });
for (const n of nonceuri(b.approvals)) folosite.add(n);
const decizieReala = checkAction(policy, b.action, permise, { approvers: valizi, revokedAt });
if (!b.decision || !!decizieReala.allowed !== !!b.decision.allowed) {
return esec(`false decision: the ledger says allowed=${b.decision && b.decision.allowed}, the policy gives allowed=${decizieReala.allowed} (${decizieReala.reason})`);
}
if (decizieReala.allowed && decizieReala.approvedBy) humanApproved++;
if (decizieReala.allowed && b.action.kind === 'payment') permise.push({ amount: String(b.action.amount), at: b.action.at });
prev = hash;
}
const spent = permise.reduce((a, s) => a + BigInt(s.amount), 0n);
return rezultat({ ok: true, seq: ledger.entries.length, allowedPayments: permise.length, spent: String(spent), humanApproved }, judecat());
}
// DOVADA DE ECHIVOCARE: doua intrari semnate de aceeasi cheie de agent, cu aceeasi sesiune si acelasi seq, si continut diferit. Cum
// sesiunea e una pe agent si politica, asta inseamna ca agentul a scris doua istorii diferite ale aceluiasi registru (o aprobare
// folosita de doua ori, o cheltuiala dubla). Dovada se verifica fara incredere in cine o aduce: numai cheia publica si doua semnaturi.
function intrareSemnata(e, pub) {
if (!e || !e.body || !Number.isInteger(e.seq) || e.body.seq !== e.seq) return false;
let ok = false;
try { ok = crypto.verify(null, Buffer.from(`${e.seq}|${e.prev}|${canonical(e.body)}`, 'utf8'), pub, Buffer.from(String(e.signature), 'base64')); } catch { ok = false; }
return ok && sha(`${e.seq}|${e.prev}|${canonical(e.body)}|${e.signature}`) === e.hash;
}
/**
* Cauta, in doua exporturi ale aceluiasi agent, prima pozitie la care ele difera; daca ambele intrari de acolo sunt semnate de agent in
* aceeasi sesiune, intoarce dovada. @returns {object|null}
*/
export function findEquivocation(a, b) {
if (!a || !b || a.agentId !== b.agentId || a.session !== b.session) return null;
let pub; try { pub = crypto.createPublicKey(a.publicKeyPem); } catch { return null; }
if (agentIdFromKey(pub) !== a.agentId) return null;
const n = Math.min((a.entries || []).length, (b.entries || []).length);
for (let i = 0; i < n; i++) {
const x = a.entries[i], y = b.entries[i];
if (x.hash === y.hash) continue;
if (x.body.session !== a.session || y.body.session !== a.session || !intrareSemnata(x, pub) || !intrareSemnata(y, pub)) return null;
return { v: 1, kind: 'aere-agent-equivocation', agentId: a.agentId, session: a.session, publicKeyPem: a.publicKeyPem, seq: i, a: x, b: y };
}
return null;
}
/** @returns {{ok:boolean, error?:string}} */
export function verifyEquivocation(p) {
if (!p || p.kind !== 'aere-agent-equivocation' || p.v !== 1) return { ok: false, error: 'not an aere-agent-equivocation v1' };
let pub; try { pub = crypto.createPublicKey(p.publicKeyPem); } catch { return { ok: false, error: 'the public key cannot be read' }; }
if (agentIdFromKey(pub) !== p.agentId) return { ok: false, error: 'agentId is not derived from the public key' };
for (const e of [p.a, p.b]) {
if (!e || !e.body || e.seq !== p.seq || e.body.agentId !== p.agentId || e.body.session !== p.session) return { ok: false, error: 'the two entries are not at the same position of the same ledger' };
if (!intrareSemnata(e, pub)) return { ok: false, error: 'an entry is not signed by the agent' };
}
if (p.a.body.policyHash !== p.b.body.policyHash || p.session !== sessionId(p.agentId, p.a.body.policyHash)) return { ok: false, error: 'the session is not the one derived from the agent and the policy' };
if (p.a.hash === p.b.hash) return { ok: false, error: 'the two entries are the same' };
return { ok: true };
}