// jurnal-arbore.mjs: jurnalul de audit al sidecar-ului, si ca ARBORE Merkle (RFC 9162, arbore-merkle.mjs), langa lantul de hash-uri // (roadmap master punctul 34, pista B, 2026-09-30). Frunza i = hash-ul intrarii i (cel din lant, care acopera seq, prev si plicul), // deci arborele nu schimba nimic din jurnal: e o a doua vedere peste aceleasi intrari. // - capul de arbore (`aere-audit-tree-head`): marimea si radacina, plic AIP-23, semnabil ML-DSA-65 cu cheia operatorului (aceeasi // forma ca capul de lant, deci verificatorul AIP-23 ii judeca semnatura) si notarizabil; // - dovada de INCLUDERE: o intrare, cu log2(n) hash-uri, e in arborele unui cap, fara nicio alta intrare a jurnalului; // - dovada de CONSISTENTA: arborele unui cap vechi e un prefix al celui nou; doua capete ale aceluiasi jurnal fara o asemenea dovada // inseamna o istorie rescrisa. // Ce NU dovedeste, ca si lantul: ca intrarile spun adevarul (le scrie gazda) si ca nu exista un alt jurnal, cu alte capete, aratat // altcuiva; o vede cine primeste capete de la amandoi (sau capete notarizate). import crypto from 'node:crypto'; import { leafHash, merkleTree, verifyInclusion, verifyConsistency } from './arbore-merkle.mjs'; import { hashIntrare, verificaJurnal, verificaSemnaturaCap, semneazaCap } from './sidecar.mjs'; const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); const H = /^0x[0-9a-f]{64}$/; export const TREE_HEAD = 'aere-audit-tree-head'; /** Frunza unei intrari: leafHash peste cei 32 de octeti ai hash-ului ei din lant. */ export const frunza = (entryHash) => { if (!H.test(String(entryHash))) throw new Error('tree: an entry hash is not 0x + 64 hex'); return leafHash(Buffer.from(entryHash.slice(2), 'hex')); }; /** Arborele jurnalului; refuza un lant rupt (arborele unui jurnal stricat nu dovedeste nimic). */ export function arboreleJurnalului(intrari) { const v = verificaJurnal(intrari); if (!v.ok) throw new Error(`tree: the log is broken at seq ${v.rupt}: ${v.motiv}`); return merkleTree(intrari.map((e) => frunza(e.hash))); } /** Capul de arbore ca plic AIP-23, optional semnat cu cheia operatorului (PEM ML-DSA-65). */ export function capDeArbore(intrari, { host = 'sidecar', createdAt = new Date().toISOString(), signKeyPem = null } = {}) { const t = arboreleJurnalului(intrari); if (!t.size) throw new Error('tree: an empty log has no head to attest'); const statement = { v: 1, kind: TREE_HEAD, host, treeSize: t.size, rootHash: t.root(), createdAt }; const plic = { v: 1, kind: TREE_HEAD + '-attestation', statement, statementHash: sha256(Buffer.from(JSON.stringify(statement), 'utf8')) }; return signKeyPem ? semneazaCap(plic, signKeyPem) : plic; } // Every refusal of the checks below NAMES its rule (`regula`); the refusals of a head's signature carry the rules of the sidecar // (cap-*). Each check of a rule carries its marker (see sidecar.mjs). export const REGULI = ['dovada-forma', 'arbore-forma', 'arbore-hash', 'includere-index', 'includere-intrare', 'includere-plic', 'includere-frunza', 'includere-drum', 'includere-cap', 'consistenta-inceput', 'consistenta-sfarsit', 'consistenta']; const refuz = (regula, motiv) => { if (!REGULI.includes(regula)) throw new Error('tree: undeclared rule ' + regula); return { ok: false, regula, motiv }; }; // un cap de arbore intreg (statementHash se reface), semnat de cine se cere (daca se cere) function capBun(cap, semnatar) { const st = cap && cap.statement; if (!(!!cap && cap.kind === TREE_HEAD + '-attestation' && !!st && st.kind === TREE_HEAD /*PAZA:arbore-forma*/)) return refuz('arbore-forma', `not an ${TREE_HEAD} attestation`); if (!(!!cap && H.test(String(cap.statementHash)) && sha256(Buffer.from(String(JSON.stringify(st)), 'utf8')) === cap.statementHash /*PAZA:arbore-hash*/)) return refuz('arbore-hash', 'the tree head statementHash does not match its statement (modified head)'); if (!(!!st && Number.isInteger(st.treeSize) && st.treeSize >= 1 && H.test(String(st.rootHash)) /*PAZA:arbore-forma*/)) return refuz('arbore-forma', 'the tree head has no valid size and root'); const s = verificaSemnaturaCap(cap, semnatar); if (!s.ok) return { ok: false, regula: s.regula, motiv: s.motiv }; return { ok: true, semnatDe: s.semnat ? s.keyId : null }; } /** Dovada ca intrarea `seq` e in arborele de marime `treeSize` (implicit tot jurnalul); poarta si intrarea insasi. */ export function dovadaIncludere(intrari, seq, treeSize = intrari.length) { const t = arboreleJurnalului(intrari); return { v: 1, kind: 'aere-audit-inclusion-proof', entry: intrari[seq], ...t.inclusionProof(seq, treeSize) }; } /** * Verifica o dovada de includere FARA jurnal: intrarea isi reface hash-ul, frunza e a ei, drumul duce la radacina; cu `cap`, radacina * si marimea sunt ale capului (intreg, semnat de `semnatar` daca e dat). Intoarce { ok, motiv, semnatDe }. */ export function verificaIncludere(d, { cap = null, semnatar = null } = {}) { try { if (!(!!d && d.kind === 'aere-audit-inclusion-proof' && !!d.entry && typeof d.entry === 'object' /*PAZA:dovada-forma*/)) return refuz('dovada-forma', 'not an aere-audit-inclusion-proof'); const e = d.entry; if (!(e.seq === d.index /*PAZA:includere-index*/)) return refuz('includere-index', `the entry is seq ${e.seq}, the proof is for index ${d.index}`); if (!(hashIntrare(e.seq, e.prev, e.proof) === e.hash /*PAZA:includere-intrare*/)) return refuz('includere-intrare', 'the entry hash does not match its content (modified entry)'); if (!(!!e.proof && !!e.proof.statement && sha256(Buffer.from(JSON.stringify(e.proof.statement), 'utf8')) === String(e.proof.statementHash).toLowerCase() /*PAZA:includere-plic*/)) return refuz('includere-plic', 'the entry\'s envelope statementHash does not match its statement'); if (!(d.leafHash === frunza(e.hash) /*PAZA:includere-frunza*/)) return refuz('includere-frunza', 'the leaf is not this entry'); if (!(verifyInclusion(d) /*PAZA:includere-drum*/)) return refuz('includere-drum', `the path does not lead from the entry to root ${String(d.rootHash).slice(0, 18)}.. of a tree of ${d.treeSize}`); if (cap) { const c = capBun(cap, semnatar); if (!c.ok) return c; if (!(cap.statement.treeSize === d.treeSize && cap.statement.rootHash === d.rootHash /*PAZA:includere-cap*/)) return refuz('includere-cap', `the proof is for a tree of ${d.treeSize} with another root than the head's (${cap.statement.treeSize})`); return { ok: true, semnatDe: c.semnatDe }; } return { ok: true, semnatDe: null }; } catch (x) { return refuz('dovada-forma', 'the proof cannot be read: ' + x.message); } } /** Dovada ca arborele de marime `firstSize` e un prefix al celui de marime `secondSize` (implicit tot jurnalul). */ export function dovadaConsistenta(intrari, firstSize, secondSize = intrari.length) { return { v: 1, kind: 'aere-audit-consistency-proof', ...arboreleJurnalului(intrari).consistencyProof(firstSize, secondSize) }; } /** Verifica, FARA jurnal, ca un cap nou continua unul vechi: ambele capete intregi (si semnate de `semnatar`), dovada intre ele. */ export function verificaConsistenta(d, { vechi, nou, semnatar = null }) { try { if (!(!!d && d.kind === 'aere-audit-consistency-proof' /*PAZA:dovada-forma*/)) return refuz('dovada-forma', 'not an aere-audit-consistency-proof'); const a = capBun(vechi, semnatar); if (!a.ok) return { ok: false, regula: a.regula, motiv: 'old head: ' + a.motiv }; const b = capBun(nou, semnatar); if (!b.ok) return { ok: false, regula: b.regula, motiv: 'new head: ' + b.motiv }; if (!(vechi.statement.treeSize === d.firstSize && vechi.statement.rootHash === d.firstRoot /*PAZA:consistenta-inceput*/)) return refuz('consistenta-inceput', 'the proof does not start at the old head'); if (!(nou.statement.treeSize === d.secondSize && nou.statement.rootHash === d.secondRoot /*PAZA:consistenta-sfarsit*/)) return refuz('consistenta-sfarsit', 'the proof does not end at the new head'); if (!(verifyConsistency(d) /*PAZA:consistenta*/)) return refuz('consistenta', `the new head (${d.secondSize}) does not extend the old one (${d.firstSize}): the history was rewritten`); // B-25 (2026-09-30): cine semneaza fiecare cap se intoarce si se spune; fara `semnatar`, doua capete nesemnate (sau semnate de // oricine) care se continua trec, si cititorul trebuie sa vada asta, nu doar "EXTENDS" return { ok: true, semnatVechi: a.semnatDe, semnatNou: b.semnatDe }; } catch (x) { return refuz('dovada-forma', 'the proof cannot be read: ' + x.message); } }