346 lines
28 KiB
JavaScript
346 lines
28 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.publicKey.asymmetricKeyType !== ALG || identity.privateKey.asymmetricKeyType !== ALG) throw new Error('agent-ledger: the identity key is not ML-DSA-65');
|
|
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}}
|
|
*/
|
|
/** The rules verifyLedger names: every refusal carries `rule`, one of these (the helper throws on any other). Each check of a rule
|
|
* carries its marker, so that a conformance suite can take the rule out, in a copy, and see which of its vectors then pass. */
|
|
export const REGULI = ['registru', 'politica', 'alta-politica', 'sesiune', 'cheie', 'identitate', 'ancora', 'seq', 'lant', 'corp', 'semnatura', 'hash', 'timp', 'revocare', 'aprobari', 'decizie'];
|
|
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 });
|
|
const refuz = (rule, error, extra = {}, seq = undefined) => { if (!REGULI.includes(rule)) throw new Error('agent-ledger: undeclared rule ' + rule); return rezultat({ ok: false, ...(seq === undefined ? {} : { seq }), rule, error }, extra); };
|
|
if (!(!!ledger && Array.isArray(ledger.entries) /*PAZA:registru*/)) return refuz('registru', 'a ledger with entries is required');
|
|
let policy = null, policyHash = null, politicaRea = null;
|
|
try { ({ policy, policyHash } = hashPolicy(politicaData)); } catch (e) { politicaRea = e.message; }
|
|
if (!(politicaRea === null /*PAZA:politica*/)) return refuz('politica', `the policy given is not valid: ${politicaRea}`);
|
|
if (!(hashFixat == null || String(hashFixat).toLowerCase() === policyHash /*PAZA:politica*/)) return refuz('politica', '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 /*PAZA:alta-politica*/)) return refuz('alta-politica', 'the ledger names another policy (policyHash differs)', judecat());
|
|
if (!(ledger.session === sessionId(ledger.agentId, policyHash) /*PAZA:sesiune*/)) return refuz('sesiune', 'the ledger session is not the one derived from the agent and the policy', judecat());
|
|
let pub = null; try { pub = crypto.createPublicKey(ledger.publicKeyPem); } catch { pub = null; }
|
|
if (!(pub !== null /*PAZA:cheie*/)) return refuz('cheie', 'the public key cannot be read', judecat());
|
|
// The agent's key must BE an ML-DSA-65 key. The id is derived from whatever key is shown and crypto.verify(null, ...) verifies with
|
|
// whatever key it is handed: until 2026-10-03 a ledger whose "agent identity" was an Ed25519 (or RSA, or P-256) key verified here
|
|
// as the ledger of a post-quantum identity.
|
|
if (!(!!pub && pub.asymmetricKeyType === ALG /*PAZA:cheie*/)) return refuz('cheie', `the agent key is ${pub ? pub.asymmetricKeyType : 'unreadable'}, not ML-DSA-65: an agent identity is an ML-DSA-65 key`, judecat());
|
|
if (!(!!pub && ledger.agentId === agentIdFromKey(pub) /*PAZA:identitate*/)) return refuz('identitate', 'agentId is not derived from the public key (false identity)', judecat());
|
|
if (!(policy.agentId === ledger.agentId /*PAZA:alta-politica*/)) return refuz('alta-politica', '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 /*PAZA:ancora*/)) return refuz('ancora', `anchor at seq ${a && a.seq} is outside the ledger`, judecat());
|
|
if (!(!!ledger.entries[s] && !!a && ledger.entries[s].hash === a.hash /*PAZA:ancora*/)) return refuz('ancora', `anchor at seq ${s} does not match the ledger (another branch or another ledger)`, judecat(), s);
|
|
if (!(!!a && Number.isFinite(Number(a.at)) /*PAZA:ancora*/)) return refuz('ancora', `anchor at seq ${s} has no time`, judecat());
|
|
anc.push({ seq: s, at: Number(a && 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 = (rule, error) => refuz(rule, error, judecat(), i);
|
|
if (!(!!e && e.seq === i /*PAZA:seq*/)) return esec('seq', `seq ${e && e.seq} instead of ${i}`);
|
|
if (!(e.prev === prev /*PAZA:lant*/)) return esec('lant', '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 /*PAZA:corp*/)) return esec('corp', 'the entry body does not match its header (agent, session or seq)');
|
|
if (!(!!b && String(b.policyHash).toLowerCase() === policyHash /*PAZA:alta-politica*/)) return esec('alta-politica', '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 /*PAZA:semnatura*/)) return esec('semnatura', 'the signature does not verify (content changed or another key)');
|
|
const hash = sha(`${i}|${prev}|${canonical(b)}|${e.signature}`);
|
|
if (!(hash === e.hash /*PAZA:hash*/)) return esec('hash', '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 /*PAZA:timp*/)) return esec('timp', 'the entry time is missing or differs from the time judged');
|
|
if (!(i === 0 || atI >= Number(ledger.entries[i - 1].body.at) /*PAZA:timp*/)) return esec('timp', 'entry times go backwards');
|
|
if (!(atI <= Number(maxTime) /*PAZA:timp*/)) return esec('timp', `entry from the future: ${atI} > ${maxTime} (the verifier clock or an anchored head)`);
|
|
if (!(notBefore == null || atI >= Number(notBefore) /*PAZA:timp*/)) return esec('timp', `entry declares ${atI}, before the witnessed start ${notBefore}`);
|
|
for (const a of anc) {
|
|
if (!(!(i <= a.seq && atI > a.at + tol) /*PAZA:timp*/)) return esec('timp', `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) /*PAZA:timp*/)) return esec('timp', `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 /*PAZA:revocare*/)) return esec('revocare', `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 /*PAZA:revocare*/)) return esec('revocare', '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' /*PAZA:revocare*/)) return esec('revocare', 'a revocation entry without the revocation');
|
|
if (!(b.approvals === undefined || (Array.isArray(b.approvals) && b.approvals.length > 0) /*PAZA:aprobari*/)) return esec('aprobari', '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 /*PAZA:decizie*/)) {
|
|
return esec('decizie', `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 };
|
|
}
|