// 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; } // 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) return { ok: false, motiv: `not an ${TREE_HEAD} attestation` }; if (!H.test(String(cap.statementHash)) || sha256(Buffer.from(JSON.stringify(st), 'utf8')) !== cap.statementHash) return { ok: false, motiv: 'the tree head statementHash does not match its statement (modified head)' }; if (!Number.isInteger(st.treeSize) || st.treeSize < 1 || !H.test(String(st.rootHash))) return { ok: false, motiv: 'the tree head has no valid size and root' }; const s = verificaSemnaturaCap(cap, semnatar); if (!s.ok) return { ok: false, 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) return { ok: false, motiv: 'not an aere-audit-inclusion-proof' }; const e = d.entry; if (e.seq !== d.index) return { ok: false, motiv: `the entry is seq ${e.seq}, the proof is for index ${d.index}` }; if (hashIntrare(e.seq, e.prev, e.proof) !== e.hash) return { ok: false, motiv: '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()) return { ok: false, motiv: 'the entry\'s envelope statementHash does not match its statement' }; if (d.leafHash !== frunza(e.hash)) return { ok: false, motiv: 'the leaf is not this entry' }; if (!verifyInclusion(d)) return { ok: false, motiv: `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) return { ok: false, motiv: `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 { ok: false, motiv: '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') return { ok: false, motiv: 'not an aere-audit-consistency-proof' }; const a = capBun(vechi, semnatar); if (!a.ok) return { ok: false, motiv: 'old head: ' + a.motiv }; const b = capBun(nou, semnatar); if (!b.ok) return { ok: false, motiv: 'new head: ' + b.motiv }; if (vechi.statement.treeSize !== d.firstSize || vechi.statement.rootHash !== d.firstRoot) return { ok: false, motiv: 'the proof does not start at the old head' }; if (nou.statement.treeSize !== d.secondSize || nou.statement.rootHash !== d.secondRoot) return { ok: false, motiv: 'the proof does not end at the new head' }; if (!verifyConsistency(d)) return { ok: false, motiv: `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 { ok: false, motiv: 'the proof cannot be read: ' + x.message }; } }