// arbore-merkle.mjs: arborele Merkle al jurnalului de audit, dupa RFC 9162 (Certificate Transparency 2.0), sectiunea 2.1: hash-ul // arborelui (MTH), dovada de INCLUDERE a unei intrari si dovada de CONSISTENTA intre doua capete (roadmap master punctul 34, pista B, // 2026-09-30). Ce adauga fata de lantul de hash-uri al sidecar-ului: cu lantul, cine vrea sa stie ca o intrare e in jurnal, sau ca un // cap nou continua unul vechi, trebuie sa primeasca si sa refaca TOT jurnalul; cu arborele, o dovada de log2(n) hash-uri ajunge, iar // verificatorul nu vede nicio alta intrare. Doua capete semnate (sau notarizate) ale aceluiasi jurnal care nu au o dovada de // consistenta intre ele sunt dovada ca istoria a fost rescrisa (o ramura). // // Frunza: SHA-256(0x00 || date); nod: SHA-256(0x01 || stanga || dreapta) (separarea frunza/nod de la RFC 9162, care impiedica o // frunza sa fie luata drept nod). Arborele gol are hash-ul SHA-256 al sirului gol. Hash-urile se scriu 0x + 64 hex. // Numai node:crypto, fara dependinte. import crypto from 'node:crypto'; const sha = (...b) => crypto.createHash('sha256').update(Buffer.concat(b)).digest(); const Z = Buffer.from([0x00]), U = Buffer.from([0x01]); const hx = (b) => '0x' + b.toString('hex'); const H = /^0x[0-9a-f]{64}$/; const dinHex = (s, ce) => { if (typeof s !== 'string' || !H.test(s)) throw new Error(`merkle: ${ce} is not a 32-byte hash (0x + 64 hex)`); return Buffer.from(s.slice(2), 'hex'); }; export const leafHash = (date) => hx(sha(Z, Buffer.from(date))); const nod = (a, b) => sha(U, a, b); // cea mai mare putere a lui 2 strict mai mica decat n (n >= 2) const k2 = (n) => { let k = 1; while (k * 2 < n) k *= 2; return k; }; // MTH peste un interval de frunze [a, b), cu memorie pe interval (fiecare subarbore se calculeaza o data) function mth(frunze, a, b, mem) { const cheie = a + ':' + b; if (mem.has(cheie)) return mem.get(cheie); let r; if (b - a === 0) r = sha(Buffer.alloc(0)); else if (b - a === 1) r = frunze[a]; else { const k = k2(b - a); r = nod(mth(frunze, a, a + k, mem), mth(frunze, a + k, b, mem)); } mem.set(cheie, r); return r; } /** Arborele unor frunze date ca hash-uri de frunza (0x + 64 hex, fiecare = leafHash(date)). */ export function merkleTree(leafHashes) { const frunze = leafHashes.map((h, i) => dinHex(h, `leaf ${i}`)); const mem = new Map(); const root = (n = frunze.length) => { if (!Number.isInteger(n) || n < 0 || n > frunze.length) throw new Error(`merkle: tree size ${n} outside 0..${frunze.length}`); return hx(mth(frunze, 0, n, mem)); }; // PATH(m, D[a:b]) din RFC 9162, 2.1.3 (generarea) const drum = (m, a, b) => { if (b - a <= 1) return []; const k = k2(b - a); return m < k ? [...drum(m, a, a + k), mth(frunze, a + k, b, mem)] : [...drum(m - k, a + k, b), mth(frunze, a, a + k, mem)]; }; // SUBPROOF(m, D[a:b], b) din RFC 9162, 2.1.4 (generarea) const sub = (m, a, b, compl) => { const n = b - a; if (m === n) return compl ? [] : [mth(frunze, a, b, mem)]; const k = k2(n); return m <= k ? [...sub(m, a, a + k, compl), mth(frunze, a + k, b, mem)] : [...sub(m - k, a + k, b, false), mth(frunze, a, a + k, mem)]; }; return { size: frunze.length, root, /** dovada ca frunza `index` e in arborele de marime `treeSize` */ inclusionProof(index, treeSize = frunze.length) { if (!Number.isInteger(treeSize) || treeSize < 1 || treeSize > frunze.length) throw new Error(`merkle: tree size ${treeSize} outside 1..${frunze.length}`); if (!Number.isInteger(index) || index < 0 || index >= treeSize) throw new Error(`merkle: index ${index} outside the tree of ${treeSize}`); return { index, treeSize, leafHash: hx(frunze[index]), path: drum(index, 0, treeSize).map(hx), rootHash: root(treeSize) }; }, /** dovada ca arborele de marime `firstSize` e un prefix al celui de marime `secondSize` */ consistencyProof(firstSize, secondSize = frunze.length) { if (!Number.isInteger(secondSize) || secondSize < 1 || secondSize > frunze.length) throw new Error(`merkle: tree size ${secondSize} outside 1..${frunze.length}`); if (!Number.isInteger(firstSize) || firstSize < 1 || firstSize > secondSize) throw new Error(`merkle: first size ${firstSize} outside 1..${secondSize}`); return { firstSize, secondSize, firstRoot: root(firstSize), secondRoot: root(secondSize), path: sub(firstSize, 0, secondSize, true).map(hx) }; }, }; } const lsb = (x) => (x & 1) === 1; /** Verificarea unei dovezi de includere (RFC 9162, 2.1.3, verificarea), fara jurnal: numai frunza, indexul, marimea, drumul si radacina. */ export function verifyInclusion({ leafHash: frunza, index, treeSize, path, rootHash }) { try { if (!Number.isInteger(index) || !Number.isInteger(treeSize) || index < 0 || index >= treeSize || !Array.isArray(path)) return false; let fn = index, sn = treeSize - 1, r = dinHex(frunza, 'leaf hash'); for (const p of path) { const pb = dinHex(p, 'path element'); if (sn === 0) return false; if (lsb(fn) || fn === sn) { r = nod(pb, r); if (!lsb(fn)) while (!lsb(fn) && fn !== 0) { fn >>= 1; sn >>= 1; } } else r = nod(r, pb); fn >>= 1; sn >>= 1; } return sn === 0 && hx(r) === String(rootHash); } catch { return false; } } /** Verificarea unei dovezi de consistenta (RFC 9162, 2.1.4, verificarea): arborele vechi e un prefix al celui nou. */ export function verifyConsistency({ firstSize, secondSize, firstRoot, secondRoot, path }) { try { if (!Number.isInteger(firstSize) || !Number.isInteger(secondSize) || firstSize < 1 || firstSize > secondSize || !Array.isArray(path)) return false; dinHex(firstRoot, 'first root'); dinHex(secondRoot, 'second root'); if (firstSize === secondSize) return path.length === 0 && firstRoot === secondRoot; if (!path.length) return false; const pc = path.map((p) => dinHex(p, 'path element')); if ((firstSize & (firstSize - 1)) === 0) pc.unshift(dinHex(firstRoot, 'first root')); // marimea veche e o putere a lui 2 let fn = firstSize - 1, sn = secondSize - 1; while (lsb(fn)) { fn >>= 1; sn >>= 1; } let fr = pc[0], sr = pc[0]; for (const c of pc.slice(1)) { if (sn === 0) return false; if (lsb(fn) || fn === sn) { fr = nod(c, fr); sr = nod(c, sr); if (!lsb(fn)) while (!lsb(fn) && fn !== 0) { fn >>= 1; sn >>= 1; } } else sr = nod(sr, c); fn >>= 1; sn >>= 1; } return sn === 0 && hx(fr) === firstRoot && hx(sr) === secondRoot; } catch { return false; } }