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

171 lines
15 KiB
JavaScript

// Proba arborelui Merkle al jurnalului de audit (arbore-merkle.mjs, jurnal-arbore.mjs, comenzile sidecar-ului): vectorii de referinta
// ai Certificate Transparency, toate dovezile pana la 64 de frunze refacute si verificate, fiecare atac ca proba numita, drumul prin
// linia de comanda. Offline.
// node proba-arbore.mjs iesire 0 = toate cum trebuia
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { leafHash, merkleTree, verifyInclusion, verifyConsistency } from './arbore-merkle.mjs';
import { hashIntrare } from './sidecar.mjs';
import * as J from './jurnal-arbore.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const { buildProof } = await import(pathToFileURL(path.join(AICI, '..', 'proof-kinds', 'proof-kinds.mjs')).href);
let treceri = 0; const esecuri = [];
function test(nume, fn) { try { fn(); treceri++; console.log(' OK ' + nume); } catch (e) { esecuri.push(nume); console.log(' RAU ' + nume + ' -- ' + (e.message || e)); } }
const cere = (c, m) => { if (!c) throw new Error(m); };
const clon = (o) => JSON.parse(JSON.stringify(o));
// ---------------------------------------------------------------- arborele insusi
// vectorii din suita de probe a implementarii de referinta Certificate Transparency: opt frunze si radacinile arborilor de 1..8
const DATE = ['', '00', '10', '2021', '3031', '40414243', '5051525354555657', '606162636465666768696a6b6c6d6e6f'].map((h) => Buffer.from(h, 'hex'));
const RADACINI = ['6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d', 'fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125', 'aeb6bcfe274b70a14fb067a5e5578264db0fa9b51af5e0ba159158f329e06e77', 'd37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7', '4e3bbb1f7b478dcfe71fb631631519a3bca12c9aefca1612bfce4c13a86264d4', '76e67dadbcdf1e10e1b74ddc608abd2f98dfb16fbce75277b5232a127f2087ef', 'ddb89be403809e325750d3d263cd78929c2942b7942a34b77e122c9594a74c8c', '5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328'];
test('radacinile arborilor de 1..8 frunze sunt cele ale implementarii de referinta Certificate Transparency (8/8); arborele gol = SHA-256("")', () => {
const t = merkleTree(DATE.map(leafHash));
for (let n = 1; n <= 8; n++) cere(t.root(n) === '0x' + RADACINI[n - 1], `n=${n}: ${t.root(n)}`);
cere(t.root(0) === '0x' + crypto.createHash('sha256').update('').digest('hex'), 'arborele gol');
});
const MARE = Array.from({ length: 64 }, (_, i) => leafHash(Buffer.from('frunza ' + i)));
const TM = merkleTree(MARE);
test('fiecare dovada de includere, pentru fiecare frunza a fiecarui arbore de 1..64, verifica (2.080 de dovezi)', () => {
let n0 = 0; for (let n = 1; n <= 64; n++) for (let m = 0; m < n; m++) { cere(verifyInclusion(TM.inclusionProof(m, n)), `m=${m} n=${n}`); n0++; } cere(n0 === 2080, String(n0));
});
test('fiecare dovada de consistenta intre doi arbori 1 <= m <= n <= 64 verifica (2.080 de dovezi)', () => {
let n0 = 0; for (let n = 1; n <= 64; n++) for (let m = 1; m <= n; m++) { cere(verifyConsistency(TM.consistencyProof(m, n)), `m=${m} n=${n}`); n0++; } cere(n0 === 2080, String(n0));
});
test('CONTROL: orice element al drumului schimbat, alt index, alta marime sau alta radacina -> dovada de includere cade (toate cazurile pana la 32)', () => {
const alt = '0x' + 'ab'.repeat(32);
for (let n = 2; n <= 32; n++) for (let m = 0; m < n; m++) {
const d = TM.inclusionProof(m, n);
d.path.forEach((_, i) => { const x = clon(d); x.path[i] = alt; cere(!verifyInclusion(x), `drum ${i} m=${m} n=${n}`); });
cere(!verifyInclusion({ ...d, index: (m + 1) % n }), `index m=${m} n=${n}`);
// marimea se schimba impreuna cu radacina ADEVARATA a noii marimi (o pereche marime-radacina care nu e a niciunui arbore nu e un cap)
cere(!verifyInclusion({ ...d, treeSize: n + 1, rootHash: TM.root(n + 1) }) && !verifyInclusion({ ...d, rootHash: alt }) && !verifyInclusion({ ...d, path: [...d.path, alt] }), `marime/radacina/prelungire m=${m} n=${n}`);
}
});
test('CONTROL: orice element schimbat, alta radacina veche sau noua, sau marimi schimbate -> dovada de consistenta cade (toate cazurile pana la 32)', () => {
const alt = '0x' + 'cd'.repeat(32);
for (let n = 2; n <= 32; n++) for (let m = 1; m < n; m++) {
const d = TM.consistencyProof(m, n);
d.path.forEach((_, i) => { const x = clon(d); x.path[i] = alt; cere(!verifyConsistency(x), `drum ${i} m=${m} n=${n}`); });
cere(!verifyConsistency({ ...d, firstRoot: alt }) && !verifyConsistency({ ...d, secondRoot: alt }), `radacini m=${m} n=${n}`);
if (m + 1 < n) cere(!verifyConsistency({ ...d, firstSize: m + 1, firstRoot: TM.root(m + 1) }), `marime m=${m} n=${n}`);
}
});
test('CONTROL: un nod intern dat drept frunza, cu un drum mai scurt decat adancimea arborelui (drumul nu ajunge la varf), -> refuzat', () => {
const t4 = merkleTree(MARE.slice(0, 4)); const d = t4.inclusionProof(0, 4);
// H(L0, L1) dat drept frunza 0 a arborelui de 4, cu drumul [H(L2, L3)]: duce la radacina, dar se opreste la adancimea 1
const intern = merkleTree(MARE.slice(0, 2)).root();
cere(!verifyInclusion({ leafHash: intern, index: 0, treeSize: 4, path: [d.path[1]], rootHash: t4.root() }), 'nodul intern a trecut drept frunza');
});
test('CONTROL: marimi egale: numai cu drum gol si aceeasi radacina', () => {
const r = TM.root(9);
cere(verifyConsistency({ firstSize: 9, secondSize: 9, firstRoot: r, secondRoot: r, path: [] }), 'aceeasi marime si radacina, drum gol: trebuia sa treaca');
cere(!verifyConsistency({ firstSize: 9, secondSize: 9, firstRoot: r, secondRoot: TM.root(10), path: [] }) && !verifyConsistency({ firstSize: 9, secondSize: 9, firstRoot: r, secondRoot: r, path: [r] }), 'marimi egale cu alta radacina sau cu drum a trecut');
});
test('CONTROL: o frunza nu poate fi data drept nod (separarea 0x00/0x01): frunza facuta din cele doua hash-uri ale unui arbore de 2 nu are radacina lui', () => {
const la = leafHash(Buffer.from('a')), lb = leafHash(Buffer.from('b'));
const t2 = merkleTree([la, lb]);
// a doua preimagine clasica: fara separare, frunza cu datele la || lb ar avea exact radacina arborelui [a, b]
const alt = merkleTree([leafHash(Buffer.concat([Buffer.from(la.slice(2), 'hex'), Buffer.from(lb.slice(2), 'hex')]))]);
cere(alt.root() !== t2.root(), 'aceeasi radacina: un jurnal de o intrare ar trece drept jurnalul de doua');
});
// ---------------------------------------------------------------- jurnalul sidecar-ului ca arbore
function jurnal(n, sare = '') {
const e = []; let prev = '0x' + '00'.repeat(32);
for (let i = 0; i < n; i++) {
const proof = buildProof('data', { name: `app-${i}${sare}`, content: `continut ${i}${sare}`, createdAt: '2026-09-30T08:00:00Z' });
const hash = hashIntrare(i, prev, proof); e.push({ seq: i, prev, proof, hash }); prev = hash;
}
return e;
}
const L = jurnal(20);
const k1 = crypto.generateKeyPairSync('ml-dsa-65'), k2 = crypto.generateKeyPairSync('ml-dsa-65');
const pem = (k) => k.privateKey.export({ type: 'pkcs8', format: 'pem' }), pub = (k) => k.publicKey.export({ type: 'spki', format: 'pem' });
const cap12 = J.capDeArbore(L.slice(0, 12), { host: 'h1', createdAt: '2026-09-30T08:10:00Z', signKeyPem: pem(k1) });
const cap20 = J.capDeArbore(L, { host: 'h1', createdAt: '2026-09-30T08:20:00Z', signKeyPem: pem(k1) });
test('capul de arbore: marimea si radacina jurnalului, plic AIP-23 semnat ML-DSA-65 (aceeasi forma ca la capul de lant)', () => {
cere(cap20.statement.treeSize === 20 && cap20.statement.rootHash === merkleTree(L.map((e) => J.frunza(e.hash))).root() && cap20.signature.scheme === 'ml-dsa-65', JSON.stringify(cap20.statement));
});
test('includere: intrarea 7 e in arborele capului semnat, verificat FARA jurnal si cu semnatarul cerut; cu alt semnatar cerut -> refuzat', () => {
const d = J.dovadaIncludere(L, 7);
const r = J.verificaIncludere(d, { cap: cap20, semnatar: pub(k1) }); cere(r.ok && r.semnatDe, r.motiv);
const r2 = J.verificaIncludere(d, { cap: cap20, semnatar: pub(k2) }); cere(!r2.ok && /another key/.test(r2.motiv), JSON.stringify(r2));
});
test('ATAC: intrarea din dovada schimbata (plicul ei, sau hash-ul refacut peste alt plic) -> refuzata', () => {
const d = J.dovadaIncludere(L, 7);
const a = clon(d); a.entry.proof.statement.name = 'alta'; cere(!J.verificaIncludere(a, { cap: cap20 }).ok, 'plicul schimbat a trecut');
const b = clon(d); b.entry.proof = L[8].proof; b.entry.hash = hashIntrare(7, b.entry.prev, b.entry.proof); cere(!J.verificaIncludere(b, { cap: cap20 }).ok, 'intrarea refacuta a trecut');
const c = clon(d); c.entry.proof = L[8].proof; cere(/entry hash does not match/.test(J.verificaIncludere(c, { cap: cap20 }).motiv || ''), 'alt plic sub hash-ul vechi a trecut');
});
test('ATAC: un plic stricat scris de gazda in lant (lantul consecvent peste el) e inclus, dar verificarea spune ca plicul nu e intreg', () => {
const R = clon(L); R[3].proof.statement.name = 'rescris fara statementHash';
let prev = R[2].hash; for (let i = 3; i < R.length; i++) { R[i].prev = prev; R[i].hash = hashIntrare(i, prev, R[i].proof); prev = R[i].hash; }
const capR = J.capDeArbore(R, { createdAt: '2026-09-30T08:30:00Z' });
const r = J.verificaIncludere(J.dovadaIncludere(R, 3), { cap: capR });
cere(!r.ok && /envelope statementHash/.test(r.motiv), JSON.stringify(r));
});
test('ATAC: dovada de includere pentru un arbore de alta marime decat capul (sau alta radacina) -> refuzata', () => {
const d = J.dovadaIncludere(L, 7, 12);
cere(J.verificaIncludere(d, { cap: cap12 }).ok, 'fata de capul de 12 trebuia sa treaca');
cere(!J.verificaIncludere(d, { cap: cap20 }).ok, 'fata de capul de 20 a trecut');
});
test('ATAC: capul de arbore atins (radacina schimbata, semnatura veche) -> refuzat', () => {
const nes = J.capDeArbore(L, { createdAt: '2026-09-30T08:20:00Z' }); const x = clon(nes); x.statement.rootHash = J.dovadaIncludere(L, 7, 12).rootHash; x.statement.treeSize = 12;
cere(/modified head/.test(J.verificaIncludere(J.dovadaIncludere(L, 7, 12), { cap: x }).motiv || ''), 'capul nesemnat atins (statementHash nerefacut) a trecut');
const c = clon(cap20); c.statement.rootHash = '0x' + '11'.repeat(32); c.statementHash = '0x' + crypto.createHash('sha256').update(JSON.stringify(c.statement)).digest('hex');
cere(!J.verificaIncludere(J.dovadaIncludere(L, 7), { cap: c }).ok, 'capul atins a trecut');
});
test('consistenta: capul de 20 continua capul de 12, verificat FARA jurnal', () => {
const r = J.verificaConsistenta(J.dovadaConsistenta(L, 12), { vechi: cap12, nou: cap20, semnatar: pub(k1) }); cere(r.ok, r.motiv);
});
test('ATAC: istoria rescrisa (intrarea 5 schimbata, lantul refacut, capul nou semnat de operator) nu continua capul vechi', () => {
const R = jurnal(20); const alt = jurnal(20, '-rescris'); R.splice(5, 15, ...alt.slice(5));
let prev = R[4].hash; for (let i = 5; i < 20; i++) { R[i] = { ...R[i], prev }; R[i].hash = hashIntrare(i, prev, R[i].proof); prev = R[i].hash; }
const capR = J.capDeArbore(R, { host: 'h1', createdAt: '2026-09-30T08:20:00Z', signKeyPem: pem(k1) });
const d = J.dovadaConsistenta(R, 12);
const r = J.verificaConsistenta(d, { vechi: cap12, nou: capR, semnatar: pub(k1) }); cere(!r.ok, 'rescrierea a trecut: ' + JSON.stringify(r));
// si cu o dovada facuta pentru capul vechi adevarat: nu se potriveste cu capul nou rescris
const r2 = J.verificaConsistenta(J.dovadaConsistenta(L, 12), { vechi: cap12, nou: capR, semnatar: pub(k1) }); cere(!r2.ok, 'dovada veche a trecut pe capul rescris');
// drumul istoriei rescrise, cu radacinile capetelor scrise in dovada: numai calculul radacinii vechi il prinde
const r3 = J.verificaConsistenta({ ...d, firstRoot: cap12.statement.rootHash }, { vechi: cap12, nou: capR, semnatar: pub(k1) });
cere(!r3.ok && /rewritten/.test(r3.motiv), 'drumul rescris cu radacina veche declarata a trecut: ' + JSON.stringify(r3));
});
test('ATAC: capetele inversate (capul nou dat drept vechi) sau o dovada care nu porneste de la capul vechi -> refuzat', () => {
const d = J.dovadaConsistenta(L, 12);
cere(!J.verificaConsistenta(d, { vechi: cap20, nou: cap12 }).ok, 'inversate a trecut');
const cap10 = J.capDeArbore(L.slice(0, 10), { createdAt: '2026-09-30T08:05:00Z' });
cere(!J.verificaConsistenta(d, { vechi: cap10, nou: cap20 }).ok, 'alta pornire a trecut');
});
test('un jurnal rupt nu primeste cap de arbore si nici dovezi', () => {
const R = clon(L); R[3].proof.statement.name = 'x';
let m = null; try { J.capDeArbore(R); } catch (e) { m = e.message; } cere(/broken at seq 3/.test(m || ''), String(m));
});
// ---------------------------------------------------------------- linia de comanda
test('linia de comanda: tree-head, prove-inclusion, verify-inclusion (0; alt cap 1), prove-consistency, verify-consistency (0; inversat 1)', () => {
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-arbore-')); const S = path.join(AICI, 'sidecar.mjs');
const run = (...a) => { const r = spawnSync(process.execPath, [S, ...a], { cwd: T, encoding: 'utf8' }); return { cod: r.status, out: (r.stdout || '') + (r.stderr || '') }; };
try {
const lg = path.join(T, 'audit.log');
fs.writeFileSync(lg, L.slice(0, 12).map((e) => JSON.stringify(e)).join('\n') + '\n');
fs.writeFileSync(path.join(T, 'k.pem'), pem(k1)); fs.writeFileSync(path.join(T, 'p.pem'), pub(k1));
cere(run('tree-head', '--log', lg, '--sign-key', 'k.pem', '--out', 'h12.json').cod === 0, 'tree-head 12');
fs.appendFileSync(lg, L.slice(12).map((e) => JSON.stringify(e)).join('\n') + '\n');
cere(run('tree-head', '--log', lg, '--sign-key', 'k.pem', '--out', 'h20.json').cod === 0, 'tree-head 20');
cere(run('prove-inclusion', '--log', lg, '--seq', '3', '--out', 'i.json').cod === 0, 'prove-inclusion');
const v = run('verify-inclusion', '--proof', 'i.json', '--head', 'h20.json', '--signer', 'p.pem'); cere(v.cod === 0 && /INCLUDED/.test(v.out), v.out);
const v2 = run('verify-inclusion', '--proof', 'i.json', '--head', 'h12.json'); cere(v2.cod === 1 && /NOT included/.test(v2.out), v2.out);
cere(run('prove-consistency', '--log', lg, '--from', '12', '--out', 'c.json').cod === 0, 'prove-consistency');
const w = run('verify-consistency', '--proof', 'c.json', '--old', 'h12.json', '--new', 'h20.json', '--signer', 'p.pem'); cere(w.cod === 0 && /EXTENDS/.test(w.out), w.out);
const w2 = run('verify-consistency', '--proof', 'c.json', '--old', 'h20.json', '--new', 'h12.json'); cere(w2.cod === 1 && /NOT consistent/.test(w2.out), w2.out);
} finally { fs.rmSync(T, { recursive: true, force: true }); }
});
console.log(`\naere-arbore: ${treceri}/${treceri + esecuri.length} cum trebuia`);
process.exitCode = esecuri.length ? 1 : 0;