#!/usr/bin/env node 'use strict'; // B3, AERE Verification Layer: un SIDECAR care ruleaza langa desfasurarea unui client (orice cloud sau on-prem) si tine un JURNAL // DE AUDIT al ei, facut din plicuri AERE Proof Protocol (AIP-23), construite de ACELASI tools/proof-kinds si verificabile de ACELASI // verificator. Fiecare intrare acopera hash-ul celei dinainte (lant de hash-uri, append-only). // // CE DOVEDESTE SI CE NU (revizuirea adversariala 2026-09-29, pista B; forma de dinainte spunea "fara sa aiba incredere in gazda"): // - CONTINUTUL intrarilor e ce spune gazda: sidecar-ul ruleaza pe ea si citeste exportul facut de operator. Nimic de aici nu // dovedeste ca un container a rulat, doar ca gazda a declarat asta, la ora pe care a declarat-o (`--at` e tot o declaratie). // - Lantul NU are cheie: cine poate scrie fisierul poate reface tot lantul de la geneza, iar `verify-log` singur iese INTREG. // Manipularea se vede numai FATA DE UN CAP publicat in afara gazdei: `attest-head` scoate capul ca plic, `notarize-head` il pune // pe AereNotary (finalitate post-cuantica prin verificatorul AIP-23), iar `verify-log --attested ` cere ca jurnalul de acum // sa CONTINUE acel cap. Deci: integritatea istoriei de dinainte de un cap notarizat se verifica fara incredere in gazda; // adevarul ei, nu. // // sidecar record --kind runtime --artifact f --attested 0x.. [--host h] [--log p] [--at T] // sidecar record --kind deployment --name X --version V --content-file f [--host h] [--log p] [--at T] // sidecar record --kind proof --proof-file f.json (leaga orice plic AIP-23 gata facut, dupa ce ii verifica forma si integritatea) // sidecar scan --source docker|kubernetes --input export.json (fiecare container care RULEAZA; NUMAI numele variabilelor de mediu, // linia de comanda ca hash; exportul il face operatorul) // sidecar verify-log --log p [--attested cap.json [--signer head.pub.pem]] -> 0 intreg (si continua capul atestat), 1 rupt sau necontinuat // sidecar attest-head --log p [--out f] [--sign-key head.key.pem] -> capul lantului ca plic AIP-23 aere-audit-head, semnat ML-DSA-65 // sidecar keygen --out -> 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 // Mesajele catre utilizator sunt in engleza. Numai Node 24 (ethers numai pentru notarize-head). import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { fileURLToPath, pathToFileURL } from 'node:url'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const TOOLS = path.resolve(AICI, '..'); const GENEZA = '0x' + '00'.repeat(32); const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); const sha256File = (p) => sha256(fs.readFileSync(p)); const eDigest = (s) => typeof s === 'string' && /^0x[0-9a-fA-F]{64}$/.test(s); // hash-ul unei intrari acopera: seq, hash-ul precedent, si plicul canonic. Un singur loc, ca sa nu diverga scriitor de cititor. export function hashIntrare(seq, prev, proof) { return sha256(Buffer.from(`${seq}|${prev}|${JSON.stringify(proof)}`, 'utf8')); } // un rand care nu e JSON devine o intrare NECITIBILA, pe care verificaJurnal o raporteaza la locul ei (forma veche cadea intreaga) function citesteJurnal(p) { if (!fs.existsSync(p)) return []; return fs.readFileSync(p, 'utf8').split('\n').filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return { necitibil: true }; } }); } /** * Verifica lantul de hash-uri end-to-end. Returneaza {ok, count, head, rupt, motiv} - rupt = primul seq stricat sau null. * NU spune nimic despre un lant refacut in intregime: pentru asta, verificaFataDeCap. */ export function verificaJurnal(intrari) { let prev = GENEZA; for (let i = 0; i < intrari.length; i++) { const e = intrari[i]; if (!e || typeof e !== 'object' || e.necitibil) return { ok: false, count: intrari.length, head: null, rupt: i, motiv: `entry ${i} is not a JSON log entry` }; if (e.seq !== i) return { ok: false, count: intrari.length, head: null, rupt: i, motiv: `wrong seq: ${e.seq} instead of ${i}` }; if (e.prev !== prev) return { ok: false, count: intrari.length, head: null, rupt: i, motiv: `prev does not link entry ${i - 1}` }; if (hashIntrare(e.seq, e.prev, e.proof) !== e.hash) return { ok: false, count: intrari.length, head: null, rupt: i, motiv: `the hash of entry ${i} does not match its content (modified)` }; prev = e.hash; } return { ok: true, count: intrari.length, head: prev, rupt: null }; } /** * Jurnalul de acum CONTINUA un cap atestat (plicul aere-audit-head scos de attest-head, notarizat sau nu)? * Cere: plicul intreg (statementHash se reface), lantul intreg, cel putin `count` intrari, si hash-ul intrarii count-1 == head. * Returneaza {ok, motiv, count, extra} - extra = intrarile scrise dupa cap. */ export function verificaFataDeCap(intrari, cap, { semnatar = null } = {}) { const st = cap && cap.statement; if (!cap || cap.kind !== 'aere-audit-head-attestation' || !st || st.kind !== 'aere-audit-head') return { ok: false, motiv: 'the file is not an aere-audit-head attestation' }; if (!eDigest(cap.statementHash) || sha256(Buffer.from(JSON.stringify(st), 'utf8')) !== cap.statementHash.toLowerCase()) return { ok: false, motiv: 'the attestation statementHash does not match its statement (modified attestation)' }; const sg = verificaSemnaturaCap(cap, semnatar); if (!sg.ok) return { ok: false, motiv: sg.motiv }; if (!Number.isInteger(st.count) || st.count < 0 || !eDigest(st.head)) return { ok: false, motiv: 'the attestation has no valid count and head' }; const v = verificaJurnal(intrari); if (!v.ok) return { ok: false, motiv: `the log is broken at seq ${v.rupt}: ${v.motiv}` }; if (intrari.length < st.count) return { ok: false, motiv: `the log has ${intrari.length} entries and the attested head covers ${st.count}: entries were removed` }; const h = st.count === 0 ? GENEZA : intrari[st.count - 1].hash; if (h !== st.head.toLowerCase()) return { ok: false, motiv: `entry ${st.count - 1} is not the attested head: the history before the attested point was rewritten` }; return { ok: true, count: st.count, extra: intrari.length - st.count, semnatDe: sg.semnat ? sg.keyId : null }; } // ---- capul SEMNAT (2026-09-29, roadmap punctul 34, "jurnale semnate") --------------------------------------------------------- // Capul poate purta semnatura ML-DSA-65 a operatorului, in forma pe care o verifica verificatorul AIP-23 de referinta (nivelul // `signature`): { scheme: 'ml-dsa-65', publicKey: SPKI DER base64, signature: base64 peste textul canonic al declaratiei }. Semnatura // spune CINE garanteaza capul, nu CAND (asta o da notarizarea) si nu ca istoria e adevarata. Cine tine cheia poate semna si un cap // al unei istorii rescrise: semnatura leaga capul de operator, notarizarea il leaga de un moment; `--signer` cere cheia asteptata. const idCheie = (pub) => sha256(pub.export({ type: 'spki', format: 'der' })); export function semneazaCap(plic, cheiePem) { const priv = crypto.createPrivateKey(cheiePem); if (priv.asymmetricKeyType !== 'ml-dsa-65') throw new Error('the head signing key must be ML-DSA-65 (sidecar keygen)'); const pub = crypto.createPublicKey(priv); const sig = crypto.sign(null, Buffer.from(JSON.stringify(plic.statement), 'utf8'), priv); return { ...plic, signature: { scheme: 'ml-dsa-65', publicKey: pub.export({ type: 'spki', format: 'der' }).toString('base64'), signature: sig.toString('base64') } }; } /** @returns {{ok:boolean, semnat:boolean, keyId?:string, motiv?:string}} semnatarPem: cheia publica asteptata (optional) */ export function verificaSemnaturaCap(plic, semnatarPem = null) { const s = plic && plic.signature; if (!s) return semnatarPem ? { ok: false, semnat: false, motiv: 'the head is not signed, and a signer was required' } : { ok: true, semnat: false }; if (String(s.scheme).toLowerCase() !== 'ml-dsa-65' || !s.publicKey || !s.signature) return { ok: false, semnat: true, motiv: 'the head signature is not an ml-dsa-65 signature' }; let pub; try { pub = crypto.createPublicKey({ key: Buffer.from(String(s.publicKey), 'base64'), format: 'der', type: 'spki' }); } catch { return { ok: false, semnat: true, motiv: 'the head signature public key cannot be read' }; } let ok = false; try { ok = crypto.verify(null, Buffer.from(JSON.stringify(plic.statement), 'utf8'), pub, Buffer.from(String(s.signature), 'base64')); } catch { ok = false; } if (!ok) return { ok: false, semnat: true, keyId: idCheie(pub), motiv: 'the head signature does not verify over the statement' }; if (semnatarPem) { let asteptat; try { asteptat = crypto.createPublicKey(semnatarPem); } catch { return { ok: false, semnat: true, motiv: 'the expected signer key cannot be read' }; } if (idCheie(asteptat) !== idCheie(pub)) return { ok: false, semnat: true, keyId: idCheie(pub), motiv: `the head is signed by another key (${idCheie(pub).slice(0, 18)}), not the expected signer` }; } return { ok: true, semnat: true, keyId: idCheie(pub) }; } // ---- adaptorul de runtime: exportul pe care operatorul il face el insusi, fara acreditari de cloud ------------------------------ // Intrarea e `docker inspect $(docker ps -q)` sau `kubectl get pods -A -o json`. Pentru fiecare container care RULEAZA se scrie un // descriptor canonic, si el devine un plic AIP-23 de desfasurare. NICIO VALOARE de mediu nu intra: numai NUMELE variabilelor, iar // linia de comanda intra ca hash (un hash NU ascunde un secret ghicibil din argumente, spus in README). Montarile intra numai cu // destinatia din container, nu cu calea de pe gazda. const canonic = (o) => JSON.stringify(o, Object.keys(o).sort()); const numeEnv = (env) => (Array.isArray(env) ? env : []).map((e) => String(e).split('=')[0]).filter(Boolean).sort(); export function descrieDocker(inspect) { if (!Array.isArray(inspect)) throw new Error('docker: the input is not the output of `docker inspect` (an array)'); const out = []; for (const c of inspect) { if (!c || !c.State || c.State.Running !== true) continue; const d = { runtime: 'docker', name: String(c.Name || '').replace(/^\//, ''), image: c.Config?.Image || null, imageId: c.Image || null, commandSha256: sha256(Buffer.from(JSON.stringify([c.Path || null, ...(c.Args || [])]), 'utf8')), envNames: numeEnv(c.Config?.Env), mounts: (c.Mounts || []).map((m) => m.Destination).filter(Boolean).sort(), startedAt: c.State.StartedAt || null, restartCount: c.RestartCount ?? null, }; out.push({ name: `docker:${d.name}`, version: d.imageId, content: Buffer.from(canonic(d), 'utf8') }); } return out; } export function descrieKubernetes(lista) { if (!lista || !Array.isArray(lista.items)) throw new Error('kubernetes: the input is not the output of `kubectl get pods -o json` (items[])'); const out = []; for (const pod of lista.items) { const spec = new Map((pod.spec?.containers || []).map((c) => [c.name, c])); for (const st of pod.status?.containerStatuses || []) { if (!st.state || !st.state.running) continue; const c = spec.get(st.name) || {}; const d = { runtime: 'kubernetes', namespace: pod.metadata?.namespace || null, pod: pod.metadata?.name || null, name: st.name, image: st.image || c.image || null, imageId: st.imageID || null, commandSha256: sha256(Buffer.from(JSON.stringify([...(c.command || []), ...(c.args || [])]), 'utf8')), envNames: (c.env || []).map((e) => e.name).filter(Boolean).sort(), mounts: (c.volumeMounts || []).map((m) => m.mountPath).filter(Boolean).sort(), startedAt: st.state.running.startedAt || null, restartCount: st.restartCount ?? null, }; out.push({ name: `k8s:${d.namespace}/${d.pod}/${d.name}`, version: d.imageId, content: Buffer.from(canonic(d), 'utf8') }); } } return out; } // ---- scrierea: un singur scriitor odata ---------------------------------------------------------------------------------------- // Doi scriitori fara lacat citeau aceeasi lungime si scriau acelasi seq: lantul se rupea, si de atunci ORICE adaugare era refuzata // (revizuirea adversariala 2026-09-29, masurat: doi scriitori concurenti rup lantul). Lacatul e un fisier creat exclusiv langa // jurnal, cu PID-ul scriitorului; unul lasat de un proces care nu mai exista (si mai vechi de 10 s) se ridica singur. const traieste = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } }; const asteapta = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); function cuLacat(p, fn) { const lacat = p + '.lock'; const pana = Date.now() + 15000; for (;;) { try { const fd = fs.openSync(lacat, 'wx'); fs.writeSync(fd, String(process.pid)); fs.closeSync(fd); break; } catch (e) { if (e.code !== 'EEXIST') throw e; let pid = NaN; let varsta = 0; try { pid = Number(fs.readFileSync(lacat, 'utf8')); varsta = Date.now() - fs.statSync(lacat).mtimeMs; } catch { continue; } if (Number.isInteger(pid) && pid > 0 && !traieste(pid) && varsta > 10000) { try { fs.unlinkSync(lacat); } catch { /* altul l-a ridicat */ } continue; } if (Date.now() > pana) throw new Error(`the log is locked by another writer (${lacat}); if no writer is running, remove that file`); asteapta(20 + Math.floor(Math.random() * 30)); } } try { return fn(); } finally { try { fs.unlinkSync(lacat); } catch { /* deja ridicat */ } } } // adauga plicurile in ordine, sub lacat, peste un lant verificat O SINGURA DATA; `dupa(r, i)` ruleaza tot sub lacat (descriptorii) function adauga(p, plicuri, dupa) { return cuLacat(p, () => { const intrari = citesteJurnal(p); const v = verificaJurnal(intrari); if (!v.ok) throw new Error(`the existing log is broken at seq ${v.rupt} (${v.motiv}); refusing to append to a broken chain`); let seq = intrari.length; let prev = seq === 0 ? GENEZA : intrari[seq - 1].hash; const rez = []; for (let i = 0; i < plicuri.length; i++) { const hash = hashIntrare(seq, prev, plicuri[i]); fs.appendFileSync(p, JSON.stringify({ seq, prev, proof: plicuri[i], hash }) + '\n'); const r = { seq, hash }; rez.push(r); if (dupa) dupa(r, i); prev = hash; seq++; } return rez; }); } /** Adauga plicuri AIP-23 gata facute in jurnal, in ordine, sub lacat (pentru cine foloseste sidecar-ul ca modul). */ export function adaugaPlicuri(p, plicuri) { return adauga(p, plicuri); } async function incarcaEthers() { const { createRequire } = await import('node:module'); const req = createRequire(import.meta.url); try { return req('ethers'); } catch { /* nu e langa sidecar */ } try { return req(path.resolve(TOOLS, '..', 'contracts', 'node_modules', 'ethers')); } catch { /* nici in depozitul de dezvoltare */ } return null; } async function main() { const [cmd, ...rest] = process.argv.slice(2); const get = (f, d = null) => { const i = rest.indexOf(f); return i >= 0 ? rest[i + 1] : d; }; const pk = await import(pathToFileURL(path.join(TOOLS, 'proof-kinds', 'proof-kinds.mjs')).href); const log = get('--log', 'aere-audit.log'); const host = get('--host', 'sidecar'); const at = get('--at') || new Date().toISOString(); switch (cmd) { case 'record': { const kind = get('--kind'); let proof; if (kind === 'runtime') { const artifact = get('--artifact'); const attested = get('--attested'); if (!artifact || !attested) { console.error('record --kind runtime needs --artifact and --attested'); return 2; } if (!eDigest(attested)) { console.error('record --kind runtime: --attested must be a sha256 digest, 0x followed by 64 hex characters'); return 2; } const artifactSha256 = sha256File(artifact); proof = pk.buildProof('runtime', { host, artifactSha256, attestedSha256: attested, matches: artifactSha256 === attested.toLowerCase(), unit: get('--unit') || undefined, createdAt: at }); } else if (kind === 'deployment') { const name = get('--name'); const version = get('--version'); const cf = get('--content-file'); if (!name || !cf) { console.error('record --kind deployment needs --name and --content-file'); return 2; } // desfasurarea = un plic 'data' peste artefactul desfasurat (digest, nu continut brut), cu numele+versiunea proof = pk.buildProof('data', { name: `${name}@${version || '?'}`, content: fs.readFileSync(cf), createdAt: at }); } else if (kind === 'proof') { // leaga ORICE plic AIP-23 gata facut, dupa ce ii verifica forma si integritatea; un plic manipulat nu intra in lant const pf = get('--proof-file'); if (!pf) { console.error('record --kind proof needs --proof-file'); return 2; } proof = JSON.parse(fs.readFileSync(pf, 'utf8')); if (!(proof && proof.statement && eDigest(proof.statementHash))) { console.error('record --kind proof: the file is not an AIP-23 envelope {statement, statementHash}'); return 2; } if (sha256(Buffer.from(JSON.stringify(proof.statement), 'utf8')) !== proof.statementHash.toLowerCase()) { console.error('record --kind proof: statementHash does not match the statement (modified envelope); not recorded'); return 2; } } else { console.error('record: --kind runtime | deployment | proof'); return 2; } const [r] = adauga(log, [proof]); console.log(`recorded seq=${r.seq} kind=${kind} hash=${r.hash} in ${log}`); return 0; } case 'scan': { const src = get('--source'); const f = get('--input'); if (!['docker', 'kubernetes'].includes(src) || !f) { console.error('scan --source docker|kubernetes --input '); return 2; } let desc; try { const j = JSON.parse(fs.readFileSync(f, 'utf8')); desc = src === 'docker' ? descrieDocker(j) : descrieKubernetes(j); } catch (e) { console.error('scan: ' + e.message); return 2; } if (desc.length === 0) { console.error('scan: no running container in the export; a zero is not recorded'); return 2; } // plicul 'data' poarta numai digestul descriptorului; descriptorul insusi (fara valori de mediu, prin constructie) sta // alaturi, ca un auditor sa il poata re-hasha si compara cu plicul din lant; scris sub acelasi lacat const desFile = get('--descriptors', log + '.descriptori.jsonl'); const plicuri = desc.map((d) => pk.buildProof('data', { name: `${host}/${d.name}@${d.version || '?'}`, content: d.content, createdAt: at })); adauga(log, plicuri, (r, i) => { const d = desc[i]; fs.appendFileSync(desFile, JSON.stringify({ seq: r.seq, name: d.name, sha256: sha256(d.content), descriptor: JSON.parse(d.content.toString('utf8')) }) + '\n'); console.log(`recorded seq=${r.seq} ${d.name} ${String(d.version || '?').slice(0, 19)}`); }); console.log(`scan ${src}: ${desc.length} running containers, ${desc.length} envelopes in ${log}, descriptors in ${desFile}`); return 0; } case 'verify-log': { const intrari = citesteJurnal(log); const af = get('--attested'); const sf = get('--signer'); if (sf && !af) { console.log('--signer needs --attested : the signature is on the attested head'); return 2; } if (af) { let cap, semnatar = null; try { cap = JSON.parse(fs.readFileSync(af, 'utf8')); } catch (e) { console.log(`cannot read the attestation ${af}: ${e.message}`); return 2; } if (sf) { try { semnatar = fs.readFileSync(sf, 'utf8'); } catch (e) { console.log(`cannot read the signer key ${sf}: ${e.message}`); return 2; } } const r = verificaFataDeCap(intrari, cap, { semnatar }); const cine = r.semnatDe ? `the head is signed by key ${r.semnatDe.slice(0, 18)}${semnatar ? ', the expected signer' : ' (not checked against an expected signer: --signer)'}` : 'the head is not signed'; if (r.ok) { console.log(`log CONTINUES the attested head: its first ${r.count} entries are the attested ones, ${r.extra} written after; ${cine}`); return 0; } console.log(`log does NOT continue the attested head: ${r.motiv}`); return 1; } const v = verificaJurnal(intrari); if (v.ok) { console.log(`log INTACT: ${v.count} entries, head ${v.head} (a rewrite of the whole chain is detected only against an attested head: --attested)`); return 0; } console.log(`log BROKEN at seq ${v.rupt}: ${v.motiv}`); return 1; } case 'attest-head': { // capul lantului devine un plic AIP-23 (aere-audit-head), notarizabil pe AereNotary -> integritatea istoriei de pana la el // capata finalitate post-cuantica prin ACELASI verificator, fara cod nou const intrari = citesteJurnal(log); const v = verificaJurnal(intrari); if (!v.ok) { console.log(`refusing to attest a broken chain (seq ${v.rupt})`); return 1; } const statement = { v: 1, kind: 'aere-audit-head', host, count: v.count, head: v.head, createdAt: at }; let plic = { v: 1, kind: 'aere-audit-head-attestation', statement, statementHash: sha256(Buffer.from(JSON.stringify(statement), 'utf8')) }; const kf = get('--sign-key'); if (kf) { try { plic = semneazaCap(plic, fs.readFileSync(kf, 'utf8')); } catch (e) { console.log(`cannot sign the head: ${String(e.message).replace(/[0-9a-fA-F]{32,}/g, '').slice(0, 160)}`); return 2; } } const out = get('--out'); const s = JSON.stringify(plic, null, 1); if (out) { fs.writeFileSync(out, s); console.log('written', out, plic.statementHash); } else console.log(s); return 0; } case 'keygen': { // cheia ML-DSA-65 a operatorului pentru capete; cheia privata ramane in fisierul ei (0600), nu se tipareste nimic din ea const out = get('--out'); if (!out) { console.error('keygen --out '); return 2; } fs.mkdirSync(out, { recursive: true }); const kf = path.join(out, 'head.key.pem'); if (fs.existsSync(kf)) { console.error(`${kf} exists; refusing to overwrite a key`); return 2; } const { publicKey, privateKey } = crypto.generateKeyPairSync('ml-dsa-65'); fs.writeFileSync(kf, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 }); fs.writeFileSync(path.join(out, 'head.pub.pem'), publicKey.export({ type: 'spki', format: 'pem' }), { mode: 0o644 }); console.log(`head signing key written to ${out} (head.key.pem, head.pub.pem): key ${idCheie(publicKey)}`); return 0; } case 'notarize-head': { // capul jurnalului pe AereNotary, ca verificatorul AIP-23 sa ii dea finalitate post-cuantica (ancora certificata -> radacina de // stare -> dovada Merkle a lui firstSeen[statementHash]). Garzi: lantul se CITESTE de pe RPC (eth_chainId), nu se crede din // argument, si pe mainnet (2800) se cere confirmare explicita; cheia se citeste din fisier, NU se tipareste, si orice eroare e // taiata de sirurile hexa lungi inainte de afisare. // sidecar notarize-head --head plic.json --rpc URL --key-file f [--notary 0x..] [--out plic-notarizat.json] const taie = (s) => String(s ?? '').replace(/(0x)?[0-9a-fA-F]{40,}/g, '').slice(0, 300); const headF = get('--head'); const rpc = get('--rpc'); const keyF = get('--key-file'); if (!headF || !rpc || !keyF) { console.error('notarize-head --head cap.json --rpc URL --key-file f [--notary 0x..] [--out f]'); return 2; } const plic = JSON.parse(fs.readFileSync(headF, 'utf8')); if (plic.kind !== 'aere-audit-head-attestation' || !/^0x[0-9a-f]{64}$/.test(plic.statementHash || '')) { console.error('REFUSED: not an aere-audit-head attestation with a 32-byte statementHash'); return 2; } const recalc = sha256(Buffer.from(JSON.stringify(plic.statement), 'utf8')); if (recalc !== plic.statementHash) { console.error('REFUSED: statementHash does not match the statement (modified attestation)'); return 1; } const ethers = await incarcaEthers(); if (!ethers) { console.error('notarize-head needs the ethers package: run `npm install` in this directory'); return 2; } // aceleasi adrese ca NOTARY din verificatorul AIP-23 (verify-proof.mjs) const NOTARI = { 2800: '0x4aB392c4Aca7D9D4C16c0b60a9514c5025bd58c7', 28001: '0x70099E62735500AA2F85B60C518551a57B202d54' }; try { const provider = new ethers.JsonRpcProvider(rpc); const chainId = Number((await provider.getNetwork()).chainId); if (chainId === 2800 && process.env.AERE_CONFIRM_MAINNET !== 'yes') { console.error('REFUSED: this RPC serves chain 2800 (Aere Network mainnet); a notarization there is a real transaction paid by the key\'s account. Set AERE_CONFIRM_MAINNET=yes to send it.'); return 3; } const notary = get('--notary') || NOTARI[chainId]; if (!notary || !/^0x[0-9a-fA-F]{40}$/.test(notary)) { console.error(`REFUSED: no known notary on chain ${chainId}; pass --notary`); return 2; } const cod = await provider.getCode(notary); if (!cod || cod === '0x') { console.error(`REFUSED: there is no contract at ${notary} on chain ${chainId}`); return 1; } const brut = fs.readFileSync(keyF, 'utf8').split(/\r?\n/).map((l) => l.trim()); const d = (brut.find((l) => l.startsWith('d=')) || brut.find((l) => /^PRIVATE_KEY=/.test(l)) || '').replace(/^(d|PRIVATE_KEY)=/, '').replace(/^0x/, '').trim(); // un rand d= poate veni fara zerourile de la inceput (asa il scriu unele unelte), deci se completeaza la 32 de octeti if (!/^[0-9a-fA-F]{1,64}$/.test(d)) { console.error('REFUSED: the key file has no line d= or PRIVATE_KEY=0x'); return 2; } const wallet = new ethers.Wallet('0x' + d.padStart(64, '0'), provider); const c = new ethers.Contract(notary, ['function notarize(bytes32 h) external', 'function firstSeen(bytes32) view returns (uint64)'], wallet); const inainte = Number(await c.firstSeen(plic.statementHash)); let txHash = null, block = null; if (inainte === 0) { const tx = await c.notarize(plic.statementHash); const rc = await tx.wait(1, 120000); if (!rc || rc.status !== 1) { console.error('the transaction failed'); return 1; } txHash = rc.hash; block = rc.blockNumber; } const dupa = Number(await c.firstSeen(plic.statementHash)); if (!(dupa > 0)) { console.error('firstSeen is still 0 after the notarization'); return 1; } const iesire = { ...plic, notarization: { chainId, notary, firstSeen: dupa, ...(txHash ? { txHash, block } : { already: true }) } }; const out = get('--out'); const s = JSON.stringify(iesire, null, 1); if (out) fs.writeFileSync(out, s); else console.log(s); console.log(`notarized on chain ${chainId} at notary ${notary}: firstSeen ${dupa}${txHash ? `, tx ${txHash}, block ${block}` : ' (already notarized)'} by ${wallet.address}`); return 0; } catch (e) { console.error('the notarization failed: ' + taie(e.shortMessage || e.message)); return 1; } } case 'bundle': { const intrari = citesteJurnal(log); const v = verificaJurnal(intrari); const buraf = { v: 1, kind: 'aere-verify-layer-bundle', host, count: intrari.length, head: v.ok ? v.head : null, chainOk: v.ok, entries: intrari, createdAt: at }; const out = get('--out'); const s = JSON.stringify(buraf, null, 1); if (out) { fs.writeFileSync(out, s); console.log('written', out, 'chainOk=' + v.ok); } else console.log(s); return v.ok ? 0 : 1; } default: console.log('AERE Verification Layer (sidecar). Commands: record | scan | verify-log | attest-head | notarize-head | bundle'); 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; } } if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { // process.exitCode, nu process.exit(): dupa apeluri de retea (notarize-head), exit() cade in libuv pe Windows (capcana 2026-09-11) main().then((c) => { process.exitCode = c; }).catch((e) => { console.error(String(e.message).replace(/(0x)?[0-9a-fA-F]{40,}/g, '')); process.exitCode = 1; }); }