verify-layer: the audit log as a Merkle tree (RFC 9162) - signed tree heads, and inclusion and consistency proofs anyone checks without the log

This commit is contained in:
Aere Network 2026-09-30 10:13:10 +03:00
parent f75c33454a
commit 082b862d78
7 changed files with 487 additions and 4 deletions

View File

@ -10,7 +10,7 @@ notarization command of the verification layer, and the agents' x402 wallet (EIP
| [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 |
| [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL |
| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, PEM blocks, dependency manifests): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow |
| [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network for post-quantum finality; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth) |
| [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network for post-quantum finality; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth); the same log as a Merkle tree (RFC 9162): signed tree heads, and inclusion and consistency proofs of a few hashes that anyone checks without the log |
| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content |
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
| [`control-plane/`](control-plane/) | from findings to a finished migration: a prioritized plan from the inventory and the scanner, its execution through the gateway, KMS and PKI with consent per action and a measured proof afterwards, recipes for the servers the products do not touch and a rescan that judges them, a compliance report against NIST IR 8547, the EU roadmap and optionally CNSA 2.0, and a console that checks it all again |
@ -31,7 +31,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
| pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`test/control-negativ-hsm-incredere.mjs`) |
| pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) |
| crypto-inventory | 37/37 (`node test/proba.mjs`); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 18/18 (`node test/control-negativ.mjs`); cost 3/3 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) |
| verify-layer | 44/44 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere-node> node proba-sidecar.mjs`); without it 39 run, 5 are reported as skipped and the exit code is 2 | 9/9 in this repository (`node control-negativ-sidecar.mjs`; its tenth case compares with the version from the development history and is skipped here) |
| verify-layer | 44/44 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere-node> node proba-sidecar.mjs`); without it 39 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 19/19 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 9/9 in this repository (`node control-negativ-sidecar.mjs`; its tenth case compares with the version from the development history and is skipped here); the tree 13/13 (`node control-negativ-arbore.mjs`) |
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
| control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) |
@ -45,4 +45,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ...
## Licence
MIT, see [LICENSE](LICENSE). Files: 150 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 19, agents 39, identity 11, readiness 4).
MIT, see [LICENSE](LICENSE). Files: 154 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 12, proof-kinds 3, control-plane 19, agents 39, identity 11, readiness 4).

View File

@ -85,10 +85,32 @@ head, notarized in block 3,699,939 on 2026-09-27. Anyone can check both halves:
The first says the log is the one that was notarized; the second, measured on 2026-09-29, reports `finality: PASSED
post-quantum` under a certified anchor of the testnet, and `VALID`.
## The log as a Merkle tree (RFC 9162)
The same entries, seen as a Merkle tree in the manner of Certificate Transparency 2.0 (RFC 9162, section 2.1): leaf i is the hash
the chain already gives entry i, with the RFC's separation between leaves (`0x00`) and nodes (`0x01`). With the chain alone, checking
that one entry is in the log, or that a new head extends an old one, means receiving and replaying the whole log; with the tree, a
proof of about log2(n) hashes is enough and shows no other entry.
node sidecar.mjs tree-head --log audit.log --sign-key head.key.pem --out tree-head.json # size and root, an AIP-23 envelope
node sidecar.mjs prove-inclusion --log audit.log --seq 7 --out inclusion.json
node sidecar.mjs verify-inclusion --proof inclusion.json --head tree-head.json --signer head.pub.pem # no log needed
node sidecar.mjs prove-consistency --log audit.log --from 12 --out consistency.json
node sidecar.mjs verify-consistency --proof consistency.json --old head-12.json --new head-20.json --signer head.pub.pem
The tree head is signed with the operator's key in the same form as the chain head (so the AIP-23 verifier checks its signature),
and can be notarized. An inclusion proof carries the entry itself: the verifier recomputes its hash from its content and checks its
envelope, so an internal node cannot be passed off as an entry. A proof names a tree by its size and root; bind it to a head you
received (`--head`), not to the numbers in the proof. Two heads of the same log with no consistency proof between them mean the
history was rewritten. What it does not prove, as for the chain: that the entries are true (the host writes them), or that no
other log with other heads was shown to someone else; whoever receives heads from both, or notarized heads, sees it.
## Tests
node proba-sidecar.mjs # 44 checks
node control-negativ-sidecar.mjs # puts each guard back to its absent form and requires the named check to fail
node proba-arbore.mjs # 19: the tree (the reference roots of Certificate Transparency for 1..8 leaves, every proof up to 64 leaves, each attack)
node control-negativ-arbore.mjs # 13 guards removed in a copy, each turns its named test red
`proba-sidecar.mjs` records runtime, deployment and envelope entries; checks that a modified entry breaks the chain at its
seq, a changed hash is caught, and nothing is appended to a broken chain; that a chain **rebuilt from genesis** passes

View File

@ -0,0 +1,104 @@
// 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; }
}

View File

@ -0,0 +1,64 @@
// Controlul negativ al arborelui Merkle al jurnalului (arbore-merkle.mjs, jurnal-arbore.mjs, proba-arbore.mjs): fiecare paznic
// scos intr-o COPIE trebuie sa inroseasca proba NUMITA, cu proba chiar rulata; pe copia neatinsa, verde. Trei stari: un tipar care
// nu apare exact o data sau o proba care nu ajunge la rezumat e STRICAT si se numara esec.
// node control-negativ-arbore.mjs iesire 0 = martorul verde si toate plantarile rosii pe proba lor
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path';
import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const M = 'arbore-merkle.mjs', J = 'jurnal-arbore.mjs';
const PLANTARI = [
// [nume, fisier, [[tipar, inlocuire], ...], proba (inceputul numelui ei)]
['fara separarea frunza / nod', M, [['const Z = Buffer.from([0x00]), U = Buffer.from([0x01]);', 'const Z = Buffer.alloc(0), U = Buffer.alloc(0);']], 'CONTROL: o frunza nu poate fi data drept nod'],
['includerea nu mai cere ca drumul sa se termine la varf (sn == 0)', M, [[" return sn === 0 && hx(r) === String(rootHash);", " return hx(r) === String(rootHash);"]], 'CONTROL: un nod intern dat drept frunza'],
['includerea nu mai coboara pe ramura dreapta (forma gresita a algoritmului)', M, [[" r = nod(pb, r);\n if (!lsb(fn)) while (!lsb(fn) && fn !== 0) { fn >>= 1; sn >>= 1; }", ' r = nod(pb, r);']], 'fiecare dovada de includere'],
['consistenta nu mai cere radacina veche', M, [["return sn === 0 && hx(fr) === firstRoot && hx(sr) === secondRoot;", 'return sn === 0 && hx(sr) === secondRoot;']], 'ATAC: istoria rescrisa'],
['consistenta fara radacina veche pusa in fata la o putere a lui 2', M, [["if ((firstSize & (firstSize - 1)) === 0) pc.unshift(dinHex(firstRoot, 'first root'));", '']], 'fiecare dovada de consistenta'],
['marimi egale acceptate cu orice drum', M, [['if (firstSize === secondSize) return path.length === 0 && firstRoot === secondRoot;', 'if (firstSize === secondSize) return true;']], 'CONTROL: marimi egale'],
['intrarea din dovada nu isi mai reface hash-ul', J, [["if (hashIntrare(e.seq, e.prev, e.proof) !== e.hash) return", 'if (false) return']], 'ATAC: intrarea din dovada schimbata'],
['plicul intrarii nu mai trebuie sa fie intreg', J, [["if (!e.proof || !e.proof.statement || sha256(Buffer.from(JSON.stringify(e.proof.statement), 'utf8')) !== String(e.proof.statementHash).toLowerCase()) return", 'if (false) return']], 'ATAC: un plic stricat scris de gazda'],
['marimea si radacina capului nu se mai compara cu dovada', J, [['if (cap.statement.treeSize !== d.treeSize || cap.statement.rootHash !== d.rootHash) return', 'if (false) return']], 'ATAC: dovada de includere pentru un arbore de alta marime'],
['capul atins primit (statementHash nerefacut)', J, [["if (!H.test(String(cap.statementHash)) || sha256(Buffer.from(JSON.stringify(st), 'utf8')) !== cap.statementHash) return", 'if (false) return']], 'ATAC: capul de arbore atins'],
['semnatarul cerut nu mai trece in verificare', J, [['const s = verificaSemnaturaCap(cap, semnatar);', 'const s = verificaSemnaturaCap(cap, null);']], 'includere: intrarea 7'],
['dovada de consistenta nu mai trebuie sa porneasca de la capul vechi', J, [['if (vechi.statement.treeSize !== d.firstSize || vechi.statement.rootHash !== d.firstRoot) return', 'if (false) return']], 'ATAC: capetele inversate'],
['un jurnal rupt primeste cap', J, [['if (!v.ok) throw new Error(`tree: the log is broken', 'if (false) throw new Error(`tree: the log is broken']], 'un jurnal rupt'],
];
const FISIERE = [M, J, 'sidecar.mjs', 'proba-arbore.mjs'];
function copie() {
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-arb-ctl-'));
fs.mkdirSync(path.join(t, 'aere-verify-layer')); fs.mkdirSync(path.join(t, 'proof-kinds'));
for (const f of FISIERE) fs.copyFileSync(path.join(AICI, f), path.join(t, 'aere-verify-layer', f));
fs.copyFileSync(path.join(AICI, '..', 'proof-kinds', 'proof-kinds.mjs'), path.join(t, 'proof-kinds', 'proof-kinds.mjs'));
return t;
}
function ruleaza(t) {
return new Promise((resolve) => {
const c = spawn(process.execPath, [path.join(t, 'aere-verify-layer', 'proba-arbore.mjs')]); let out = '';
const ceas = setTimeout(() => c.kill(), 180000);
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; });
c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /aere-arbore: \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => l.startsWith(' RAU ')) }); });
});
}
async function planteaza([nume, fisier, perechi, tinta]) {
const t = copie();
try {
const f = path.join(t, 'aere-verify-layer', fisier); let src = fs.readFileSync(f, 'utf8');
for (const [din, inl] of perechi) {
if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul "${din.slice(0, 40)}" apare de ${src.split(din).length - 1} ori`];
src = src.replace(din, inl);
}
fs.writeFileSync(f, src);
const r = await ruleaza(t);
if (!r.rulat) return [false, ` STRICAT ${nume}: proba nu a ajuns la rezumat (cod ${r.cod})`];
if (r.rosii.some((l) => l.startsWith(' RAU ' + tinta))) return [true, ` ROSU cum trebuia ${nume} (proba '${tinta}' pica)`];
return [false, ` CONTROL CAZUT ${nume}: proba '${tinta}' a ramas verde (${r.rosii.length} rosii altundeva)`];
} finally { fs.rmSync(t, { recursive: true, force: true }); }
}
let rele = 0;
const t0 = copie(); const m = await ruleaza(t0); fs.rmSync(t0, { recursive: true, force: true });
if (m.rulat && m.cod === 0 && !m.rosii.length) console.log(' OK martorul: copia neatinsa verde'); else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii)`); }
const rez = new Array(PLANTARI.length); let i = 0;
await Promise.all(Array.from({ length: 4 }, async () => { while (i < PLANTARI.length) { const k = i++; rez[k] = await planteaza(PLANTARI[k]); } }));
for (const [bun, linie] of rez) { console.log(linie); if (!bun) rele++; }
console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `DOVEDIT: martorul verde, ${PLANTARI.length} din ${PLANTARI.length} paznici scosi -> proba lor rosie`);
process.exitCode = rele ? 1 : 0;

View File

@ -0,0 +1,89 @@
// 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 }; }
}

View File

@ -0,0 +1,170 @@
// 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;

View File

@ -23,6 +23,11 @@
// sidecar keygen --out <dosar> -> cheia ML-DSA-65 cu care operatorul semneaza capetele (head.key.pem 0600, head.pub.pem)
// sidecar notarize-head --head cap.json --rpc URL --key-file f [--notary 0x..] [--out f]
// sidecar bundle --log p [--out f] -> buraf {host, count, head, entries[]} pentru consola planului de control
// sidecar tree-head --log p [--sign-key k] [--out f] -> capul de ARBORE (RFC 9162): marimea si radacina, plic AIP-23
// sidecar prove-inclusion --log p --seq N [--size S] [--out f] -> dovada ca intrarea N e in arbore (log2 n hash-uri)
// sidecar prove-consistency --log p --from M [--to N] [--out f] -> dovada ca arborele de M e un prefix al celui de N
// sidecar verify-inclusion --proof f [--head cap.json [--signer pub]] -> 0 inclusa, 1 nu (FARA jurnal)
// sidecar verify-consistency --proof f --old cap1.json --new cap2.json [--signer pub] -> 0 capul nou il continua pe cel vechi
// Mesajele catre utilizator sunt in engleza. Numai Node 24 (ethers numai pentru notarize-head).
import fs from 'node:fs';
@ -372,6 +377,35 @@ async function main() {
return 0;
} catch (e) { console.error('the notarization failed: ' + taie(e.shortMessage || e.message)); return 1; }
}
// 2026-09-30 (punctul 34): jurnalul si ca arbore Merkle (RFC 9162, jurnal-arbore.mjs): capul de arbore, dovezile de includere si de
// consistenta, si verificarea lor FARA jurnal
case 'tree-head':
case 'prove-inclusion':
case 'prove-consistency':
case 'verify-inclusion':
case 'verify-consistency': {
const A = await import(pathToFileURL(path.join(AICI, 'jurnal-arbore.mjs')).href);
const citeste = (f, ce) => { if (!f) throw new Error(`${cmd} needs ${ce}`); return JSON.parse(fs.readFileSync(f, 'utf8')); };
const scrie = (o) => { const out = get('--out'); const s = JSON.stringify(o, null, 1); if (out) { fs.writeFileSync(out, s); console.log('written', out); } else console.log(s); };
const semnatar = get('--signer') ? fs.readFileSync(get('--signer'), 'utf8') : null;
try {
if (cmd === 'tree-head') {
const kf = get('--sign-key');
const cap = A.capDeArbore(citesteJurnal(log), { host, createdAt: at, signKeyPem: kf ? fs.readFileSync(kf, 'utf8') : null });
scrie(cap); return 0;
}
if (cmd === 'prove-inclusion') { scrie(A.dovadaIncludere(citesteJurnal(log), Number(get('--seq')), get('--size') ? Number(get('--size')) : undefined)); return 0; }
if (cmd === 'prove-consistency') { scrie(A.dovadaConsistenta(citesteJurnal(log), Number(get('--from')), get('--to') ? Number(get('--to')) : undefined)); return 0; }
if (cmd === 'verify-inclusion') {
const r = A.verificaIncludere(citeste(get('--proof'), '--proof'), { cap: get('--head') ? citeste(get('--head'), '--head') : null, semnatar });
console.log(r.ok ? `entry INCLUDED${get('--head') ? ' in the tree of the given head' + (r.semnatDe ? `, signed by key ${r.semnatDe.slice(0, 18)}${semnatar ? ' (the expected signer)' : ' (not checked against an expected signer: --signer)'}` : ' (unsigned head)') : ' in the tree the proof names (pass --head to bind it to an attested head)'}` : `NOT included: ${r.motiv}`);
return r.ok ? 0 : 1;
}
const r = A.verificaConsistenta(citeste(get('--proof'), '--proof'), { vechi: citeste(get('--old'), '--old'), nou: citeste(get('--new'), '--new'), semnatar });
console.log(r.ok ? 'the new head EXTENDS the old one (the history up to the old head was not rewritten)' : `NOT consistent: ${r.motiv}`);
return r.ok ? 0 : 1;
} catch (e) { console.log(`${cmd}: ${String(e.message).replace(/[0-9a-fA-F]{32,}/g, '<hex>').slice(0, 200)}`); return 2; }
}
case 'bundle': {
const intrari = citesteJurnal(log);
const v = verificaJurnal(intrari);
@ -381,7 +415,7 @@ async function main() {
return v.ok ? 0 : 1;
}
default:
console.log('AERE Verification Layer (sidecar). Commands: record | scan | verify-log | attest-head | notarize-head | bundle');
console.log('AERE Verification Layer (sidecar). Commands: record | scan | verify-log | attest-head | notarize-head | bundle | tree-head | prove-inclusion | prove-consistency | verify-inclusion | verify-consistency');
console.log(' sidecar record --kind runtime --artifact f --attested 0x.. sidecar verify-log --log p [--attested cap.json [--signer head.pub.pem]] sidecar attest-head --log p --out f [--sign-key head.key.pem] sidecar keygen --out dir');
return cmd ? 1 : 0;
}