aere-quantum/verify-layer/jurnal-arbore.mjs

90 lines
7.0 KiB
JavaScript

// 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` };
return { ok: true };
} catch (x) { return { ok: false, motiv: 'the proof cannot be read: ' + x.message }; }
}