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

105 lines
6.4 KiB
JavaScript

// 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; }
}