#!/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 scan --source aws-ecs|aws-ec2|aws-lambda|azure-vm|azure-container|azure-containerapp|azure-webapp|gcp-compute|gcp-run // --input export.json [--task-definitions f] (B15, 2026-10-18: exportul CLI-ului furnizorului, facut de operator; aceeasi // forma: numai ce RULEAZA, numai NUMELE variabilelor, nicio acreditare si niciun apel la API) // 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 // 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'; 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. */ // Every refusal of the checks below NAMES its rule (`regula`); a rule that is not declared cannot be named. Each check of a rule carries // its marker, so that a conformance suite can take the rule out, in a copy, and see which of its vectors then pass. export const REGULI = ['intrare', 'seq', 'lant', 'hash', 'cap-forma', 'cap-hash', 'cap-nesemnat', 'cap-schema', 'cap-cheie', 'cap-semnatura', 'cap-semnatar', 'cap-scurt', 'cap-rescris']; const refuz = (regula, motiv, extra = {}) => { if (!REGULI.includes(regula)) throw new Error('sidecar: undeclared rule ' + regula); return { ok: false, regula, motiv, ...extra }; }; export function verificaJurnal(intrari) { if (!(Array.isArray(intrari) /*PAZA:intrare*/)) return refuz('intrare', 'the log is not a list of entries', { count: 0, head: null, rupt: 0 }); let prev = GENEZA; for (let i = 0; i < intrari.length; i++) { const e = intrari[i]; const la = { count: intrari.length, head: null, rupt: i }; if (!(!!e && typeof e === 'object' && !e.necitibil /*PAZA:intrare*/)) return refuz('intrare', `entry ${i} is not a JSON log entry`, la); if (!(e.seq === i /*PAZA:seq*/)) return refuz('seq', `wrong seq: ${e.seq} instead of ${i}`, la); if (!(e.prev === prev /*PAZA:lant*/)) return refuz('lant', `prev does not link entry ${i - 1}`, la); if (!(hashIntrare(e.seq, e.prev, e.proof) === e.hash /*PAZA:hash*/)) return refuz('hash', `the hash of entry ${i} does not match its content (modified)`, la); 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' /*PAZA:cap-forma*/)) return refuz('cap-forma', 'the file is not an aere-audit-head attestation'); if (!(!!cap && eDigest(cap.statementHash) && sha256(Buffer.from(String(JSON.stringify(st)), 'utf8')) === cap.statementHash.toLowerCase() /*PAZA:cap-hash*/)) return refuz('cap-hash', 'the attestation statementHash does not match its statement (modified attestation)'); const sg = verificaSemnaturaCap(cap, semnatar); if (!sg.ok) return sg; if (!(!!st && Number.isInteger(st.count) && st.count >= 0 && eDigest(st.head) /*PAZA:cap-forma*/)) return refuz('cap-forma', 'the attestation has no valid count and head'); const v = verificaJurnal(intrari); if (!v.ok) return refuz(v.regula, `the log is broken at seq ${v.rupt}: ${v.motiv}`); if (!(intrari.length >= st.count /*PAZA:cap-scurt*/)) return refuz('cap-scurt', `the log has ${intrari.length} entries and the attested head covers ${st.count}: entries were removed`); const ultima = intrari[st.count - 1]; const h = st.count === 0 ? GENEZA : (ultima ? ultima.hash : null); if (!(h === String(st.head).toLowerCase() /*PAZA:cap-rescris*/)) return refuz('cap-rescris', `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, regula?:string}} semnatarPem: cheia publica asteptata (optional) */ export function verificaSemnaturaCap(plic, semnatarPem = null) { const s = plic && plic.signature; if (!s) { if (!(!semnatarPem /*PAZA:cap-nesemnat*/)) return refuz('cap-nesemnat', 'the head is not signed, and a signer was required', { semnat: false }); return { ok: true, semnat: false }; } if (!(String(s.scheme).toLowerCase() === 'ml-dsa-65' && !!s.publicKey && !!s.signature /*PAZA:cap-schema*/)) return refuz('cap-schema', 'the head signature is not an ml-dsa-65 signature', { semnat: true }); let pub = null; try { pub = crypto.createPublicKey({ key: Buffer.from(String(s.publicKey), 'base64'), format: 'der', type: 'spki' }); } catch { pub = null; } if (!(pub !== null /*PAZA:cap-cheie*/)) return refuz('cap-cheie', 'the head signature public key cannot be read', { semnat: true }); // The key must BE an ML-DSA-65 key. crypto.verify(null, ...) verifies with whatever key it is handed: until 2026-10-03 a head // "signed" with an Ed25519 (or RSA, or P-256) key that declared the scheme ml-dsa-65 verified here as an ML-DSA-65 signed head. if (!(!!pub && pub.asymmetricKeyType === 'ml-dsa-65' /*PAZA:cap-schema*/)) return refuz('cap-schema', `the head declares an ml-dsa-65 signature and its public key is ${pub ? pub.asymmetricKeyType : 'unreadable'}`, { semnat: true }); const id = pub ? idCheie(pub) : null; let ok = false; try { ok = crypto.verify(null, Buffer.from(String(JSON.stringify(plic.statement)), 'utf8'), pub, Buffer.from(String(s.signature), 'base64')); } catch { ok = false; } if (!(ok /*PAZA:cap-semnatura*/)) return refuz('cap-semnatura', 'the head signature does not verify over the statement', { semnat: true, keyId: id }); if (semnatarPem) { let asteptat = null; try { asteptat = crypto.createPublicKey(semnatarPem); } catch { asteptat = null; } if (!(asteptat !== null && idCheie(asteptat) === id /*PAZA:cap-semnatar*/)) return refuz('cap-semnatar', asteptat ? `the head is signed by another key (${String(id).slice(0, 18)}), not the expected signer` : 'the expected signer key cannot be read', { semnat: true, keyId: id }); } return { ok: true, semnat: true, keyId: id }; } // ---- 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; } // ---- adaptoarele pe furnizori de cloud (B15, 2026-10-18): ACEEASI forma, onesta ------------------------------------------------ // Exportul il face operatorul cu CLI-ul furnizorului, cu acreditarile LUI, pe masina lui; sidecar-ul citeste fisierul si atat: nicio // acreditare in sidecar, niciun apel la un API. Fiecare adaptor: (1) refuza pe nume un fisier care nu e iesirea CLI-ului numit; // (2) ia numai incarcaturile pe care exportul le declara ca RULEAZA; (3) scrie identitatea (imagine + digest cand exportul il are, // altfel id-ul masinii/serviciului + imaginea declarata, `imageIdentity: 'declared'`), regiunea/zona, NUMAI numele variabilelor de // mediu (si ale etichetelor, setarilor, cheilor de metadata), NICIO valoare de userData/customData/metadata (nu se copiaza deloc). // Descriptorul e canonic (chei sortate la toate nivelurile) si devine un plic 'data' prin acelasi constructor ca Docker/Kubernetes. // Masurate pe iesirile DOCUMENTATE ale CLI-urilor (fixturi-cloud/), nu pe un cont real: nu avem conturi la AWS/Azure/GCP. const canonicAdanc = (v) => (Array.isArray(v) ? '[' + v.map((x) => canonicAdanc(x === undefined ? null : x)).join(',') + ']' : v && typeof v === 'object' ? '{' + Object.keys(v).filter((k) => v[k] !== undefined).sort().map((k) => JSON.stringify(k) + ':' + canonicAdanc(v[k])).join(',') + '}' : JSON.stringify(v === undefined ? null : v)); const numeDin = (lista, cheie = 'name') => [...new Set((Array.isArray(lista) ? lista : []).map((e) => (e && typeof e === 'object' ? e[cheie] : e)).filter((n) => typeof n === 'string' && n))].sort(); const ultimul = (url) => (typeof url === 'string' && url ? url.split('/').filter(Boolean).pop() || null : null); const grupulDin = (id) => { const m = /\/resourceGroups\/([^/]+)\//i.exec(String(id || '')); return m ? m[1] : null; }; const digestDinImagine = (img) => { const m = /@(sha256:[0-9a-fA-F]{64})$/.exec(String(img || '')); return m ? m[1].toLowerCase() : null; }; // az CLI scrie resursele ARM cu `properties` aplatizat; `az rest` si extensia containerapp le lasa imbricate: se citesc amandoua formele const aplatizat = (o) => (o && typeof o === 'object' && o.properties && typeof o.properties === 'object' ? { ...o.properties, ...o } : o); const intrare = (nume, version, d) => ({ name: nume, version: version || '?', content: Buffer.from(canonicAdanc(d), 'utf8') }); /** `aws ecs describe-tasks` (+ optional `aws ecs describe-task-definition`, una sau o lista, pentru numele variabilelor si comanda) */ export function descrieAwsEcs(j, definitii = null) { if (!(!!j && !Array.isArray(j) && Array.isArray(j.tasks) /*FORMA:aws-ecs*/)) throw new Error('aws-ecs: the input is not the output of `aws ecs describe-tasks` (an object with tasks[])'); const defs = new Map(); for (const d of definitii == null ? [] : Array.isArray(definitii) ? definitii : [definitii]) { const td = d && d.taskDefinition; if (!(!!td && typeof td.taskDefinitionArn === 'string' && Array.isArray(td.containerDefinitions) /*FORMA:aws-ecs-definitii*/)) throw new Error('aws-ecs: --task-definitions is not the output of `aws ecs describe-task-definition` (taskDefinition.containerDefinitions[])'); defs.set(td.taskDefinitionArn, td); } const out = []; for (const t of j.tasks) { if (!(!!t && t.lastStatus === 'RUNNING' /*RULEAZA:aws-ecs*/)) continue; const td = defs.get(t.taskDefinitionArn) || null; const region = String(t.taskArn || '').split(':')[3] || null; for (const c of t.containers || []) { // si containerul trebuie sa ruleze: un task RUNNING poate avea un container neesential deja oprit if (!(!!c && c.lastStatus === 'RUNNING' /*RULEAZA:aws-ecs-container*/)) continue; const cd = td ? td.containerDefinitions.find((x) => x && x.name === c.name) || null : null; const over = (t.overrides?.containerOverrides || []).find((x) => x && x.name === c.name) || null; const d = { runtime: 'aws-ecs', region, clusterArn: t.clusterArn || null, taskArn: t.taskArn || null, taskDefinitionArn: t.taskDefinitionArn || null, container: c.name, image: c.image || cd?.image || null, imageDigest: c.imageDigest || null, imageIdentity: c.imageDigest ? 'digest' : 'declared', runtimeId: c.runtimeId || null, launchType: t.launchType || null, availabilityZone: t.availabilityZone || null, startedAt: t.startedAt || null, group: t.group || null, taskVersion: t.version ?? null, commandSha256: cd ? sha256(Buffer.from(JSON.stringify([...(cd.entryPoint || []), ...(over?.command || cd.command || [])]), 'utf8')) : null, // numele din definitie (environment + secrets, al caror valueFrom e un ARN, nu o valoare) si din overrides; valorile nu intra envNames: numeDin([...(cd?.environment || []), ...(cd?.secrets || []), ...(over?.environment || [])] /*NUME:aws-ecs*/), envSource: td ? (over?.environment ? 'task-definition+overrides' : 'task-definition') : over?.environment ? 'overrides-only' : 'not-in-export', mounts: (cd?.mountPoints || []).map((m) => m && m.containerPath).filter(Boolean).sort(), }; out.push(intrare(`ecs:${ultimul(t.clusterArn)}/${ultimul(t.taskArn)}/${c.name}`, c.imageDigest || c.image, d)); } } return out; } /** `aws ec2 describe-instances` */ export function descrieAwsEc2(j) { if (!(!!j && !Array.isArray(j) && Array.isArray(j.Reservations) /*FORMA:aws-ec2*/)) throw new Error('aws-ec2: the input is not the output of `aws ec2 describe-instances` (an object with Reservations[])'); const out = []; for (const r of j.Reservations) for (const i of r?.Instances || []) { if (!(!!i && i.State?.Name === 'running' /*RULEAZA:aws-ec2*/)) continue; const zona = i.Placement?.AvailabilityZone || null; const region = zona ? zona.replace(/[a-z]$/, '') : null; // userData nu e in describe-instances si nu se cere; valorile etichetelor nu intra (o eticheta poate purta orice) const d = { runtime: 'aws-ec2', region, availabilityZone: zona, instanceId: i.InstanceId || null, imageId: i.ImageId || null, imageIdentity: 'declared', instanceType: i.InstanceType || null, architecture: i.Architecture || null, platformDetails: i.PlatformDetails || null, virtualizationType: i.VirtualizationType || null, bootMode: i.BootMode || null, launchTime: i.LaunchTime || null, iamInstanceProfileArn: i.IamInstanceProfile?.Arn || null, rootDeviceType: i.RootDeviceType || null, imdsV2Required: i.MetadataOptions ? i.MetadataOptions.HttpTokens === 'required' : null, tagKeys: numeDin(i.Tags, 'Key' /*NUME:aws-ec2*/), }; out.push(intrare(`ec2:${region}/${i.InstanceId}`, i.ImageId, d)); } return out; } /** `aws lambda list-functions`: list-functions nu intoarce State (nota din documentatia lui); o functie e inregistrata ca desfasurata, * cu `state: 'not-in-export'`; daca exportul poarta State (get-function-configuration), numai `Active` intra */ export function descrieAwsLambda(j) { if (!(!!j && !Array.isArray(j) && Array.isArray(j.Functions) /*FORMA:aws-lambda*/)) throw new Error('aws-lambda: the input is not the output of `aws lambda list-functions` (an object with Functions[])'); const out = []; for (const f of j.Functions) { if (!(!!f && (f.State === undefined || f.State === 'Active') /*RULEAZA:aws-lambda*/)) continue; const region = String(f.FunctionArn || '').split(':')[3] || null; const d = { runtime: 'aws-lambda', region, functionArn: f.FunctionArn || null, functionName: f.FunctionName || null, packageType: f.PackageType || 'Zip', functionRuntime: f.Runtime || null, handler: f.Handler || null, codeSha256: f.CodeSha256 || null, imageIdentity: f.CodeSha256 ? 'codeSha256' : 'declared', codeSize: f.CodeSize ?? null, lastModified: f.LastModified || null, functionVersion: f.Version || null, revisionId: f.RevisionId || null, architectures: [...(f.Architectures || [])].sort(), layers: numeDin(f.Layers, 'Arn'), state: f.State ?? 'not-in-export', envNames: Object.keys(f.Environment?.Variables || {}).sort() /*NUME:aws-lambda*/, }; out.push(intrare(`lambda:${region}/${f.FunctionName}`, f.CodeSha256, d)); } return out; } /** `az vm list -d` (--show-details adauga powerState; fara el exportul nu spune daca masina merge, si e refuzat) */ export function descrieAzureVm(j) { if (!(Array.isArray(j) && j.every((v) => v && typeof v.id === 'string' && /\/providers\/Microsoft\.Compute\/virtualMachines\//i.test(v.id)) /*FORMA:azure-vm*/)) throw new Error('azure-vm: the input is not the output of `az vm list -d` (an array of Microsoft.Compute/virtualMachines resources)'); const vms = j.map(aplatizat); if (!(vms.length === 0 || vms.some((v) => typeof v.powerState === 'string') /*FORMA:azure-vm-d*/)) throw new Error('azure-vm: the export has no powerState: run `az vm list -d` (--show-details), which adds the power state of each machine'); const out = []; for (const v of vms) { if (!(v.powerState === 'VM running' /*RULEAZA:azure-vm*/)) continue; const ir = v.storageProfile?.imageReference || {}; const rg = v.resourceGroup || grupulDin(v.id); // customData, userData, adminUsername, cheile ssh, adresele: nu se copiaza; etichetele intra numai cu numele const d = { runtime: 'azure-vm', id: v.id, name: v.name || null, resourceGroup: rg, location: v.location || null, zones: [...(v.zones || [])].sort(), vmId: v.vmId || null, vmSize: v.hardwareProfile?.vmSize || null, imageReference: Object.fromEntries(['publisher', 'offer', 'sku', 'version', 'exactVersion', 'id', 'sharedGalleryImageId', 'communityGalleryImageId'].filter((k) => ir[k] != null).map((k) => [k, ir[k]])), imageIdentity: 'declared', osType: v.storageProfile?.osDisk?.osType || null, computerName: v.osProfile?.computerName || null, provisioningState: v.provisioningState || null, powerState: v.powerState, timeCreated: v.timeCreated || null, tagKeys: Object.keys(v.tags || {}).sort() /*NUME:azure-vm*/, }; out.push(intrare(`azvm:${rg}/${v.name}`, ir.exactVersion || ir.version || ir.id, d)); } return out; } /** `az container list` / `az container show` (Azure Container Instances): starea fiecarui container e in instanceView.currentState */ export function descrieAzureContainer(j) { if (!(Array.isArray(j) && j.every((g) => g && typeof g.id === 'string' && /\/providers\/Microsoft\.ContainerInstance\/containerGroups\//i.test(g.id)) /*FORMA:azure-container*/)) throw new Error('azure-container: the input is not the output of `az container list` or `az container show` (an array of Microsoft.ContainerInstance/containerGroups resources)'); const out = []; let cuStare = 0; for (const g0 of j) { const g = aplatizat(g0); const rg = g.resourceGroup || grupulDin(g.id); for (const c0 of g.containers || []) { const c = aplatizat(c0); const st = c.instanceView?.currentState || null; if (st) cuStare++; if (!(!!st && st.state === 'Running' /*RULEAZA:azure-container*/)) continue; // environmentVariables intra cu numele (value si secureValue nu), imageRegistryCredentials nu intra deloc const d = { runtime: 'azure-container', groupId: g.id, group: g.name || null, resourceGroup: rg, location: g.location || null, zones: [...(g.zones || [])].sort(), osType: g.osType || null, provisioningState: g.provisioningState || null, container: c.name || null, image: c.image || null, imageDigest: digestDinImagine(c.image), imageIdentity: digestDinImagine(c.image) ? 'digest' : 'declared', commandSha256: sha256(Buffer.from(JSON.stringify(c.command || []), 'utf8')), envNames: numeDin(c.environmentVariables /*NUME:azure-container*/), mounts: (c.volumeMounts || []).map((m) => m && m.mountPath).filter(Boolean).sort(), startedAt: st.startTime || null, restartCount: c.instanceView?.restartCount ?? null, }; out.push(intrare(`aci:${rg}/${g.name}/${c.name}`, digestDinImagine(c.image) || c.image, d)); } } if (j.length && cuStare === 0) throw new Error('azure-container: no container in the export carries instanceView.currentState (`az container list` may omit it): collect `az container show -g -n -o json` outputs into a JSON array'); return out; } /** `az containerapp list` (Azure Container Apps): runningStatus e al aplicatiei; numai `Running` intra (Progressing, Stopped, Suspended * si `Ready`, pe care specificatia il enumera fara sa il defineasca, nu); initContainers nu intra (nu ruleaza continuu) */ export function descrieAzureContainerApp(j) { if (!(Array.isArray(j) && j.every((a) => a && typeof a.id === 'string' && /\/providers\/Microsoft\.App\/containerApps\//i.test(a.id)) /*FORMA:azure-containerapp*/)) throw new Error('azure-containerapp: the input is not the output of `az containerapp list` (an array of Microsoft.App/containerApps resources)'); const out = []; for (const a0 of j) { const a = aplatizat(a0); if (!(a.runningStatus === 'Running' /*RULEAZA:azure-containerapp*/)) continue; const rg = a.resourceGroup || grupulDin(a.id); for (const c of a.template?.containers || []) { if (!c) continue; // env intra cu numele; secretRef e numele unui secret, nu valoarea lui; configuration.secrets nu se citeste deloc const d = { runtime: 'azure-containerapp', appId: a.id, app: a.name || null, resourceGroup: rg, location: a.location || null, environmentId: a.environmentId || a.managedEnvironmentId || null, provisioningState: a.provisioningState || null, runningStatus: a.runningStatus, latestReadyRevisionName: a.latestReadyRevisionName || null, latestRevisionName: a.latestRevisionName || null, workloadProfileName: a.workloadProfileName || null, container: c.name || null, image: c.image || null, imageDigest: digestDinImagine(c.image), imageIdentity: digestDinImagine(c.image) ? 'digest' : 'declared', commandSha256: sha256(Buffer.from(JSON.stringify([...(c.command || []), ...(c.args || [])]), 'utf8')), envNames: numeDin(c.env /*NUME:azure-containerapp*/), envSecretRefs: numeDin(c.env, 'secretRef'), mounts: (c.volumeMounts || []).map((m) => m && m.mountPath).filter(Boolean).sort(), }; out.push(intrare(`aca:${rg}/${a.name}/${c.name}`, digestDinImagine(c.image) || c.image, d)); } } return out; } /** `az webapp list --show-details` (App Service): state e al sitului; runtime-ul e linuxFxVersion/windowsFxVersion (DOCKER|imagine * pentru un container); appSettings si connectionStrings intra numai cu numele (API-ul le ascunde oricum la citirile obisnuite) */ export function descrieAzureWebapp(j) { if (!(Array.isArray(j) && j.every((s) => s && typeof s.id === 'string' && /\/providers\/Microsoft\.Web\/sites\//i.test(s.id)) /*FORMA:azure-webapp*/)) throw new Error('azure-webapp: the input is not the output of `az webapp list --show-details` (an array of Microsoft.Web/sites resources)'); const out = []; for (const s0 of j) { const s = aplatizat(s0); if (!(s.state === 'Running' /*RULEAZA:azure-webapp*/)) continue; const rg = s.resourceGroup || grupulDin(s.id); const sc = s.siteConfig && typeof s.siteConfig === 'object' ? s.siteConfig : null; const fx = sc?.linuxFxVersion || sc?.windowsFxVersion || null; const m = /^DOCKER\|(.+)$/i.exec(fx || ''); const image = m ? m[1] : null; const d = { runtime: 'azure-webapp', id: s.id, name: s.name || null, resourceGroup: rg, location: s.location || null, kind: s.kind || null, state: s.state, defaultHostName: s.defaultHostName || null, fxVersion: fx, image, imageDigest: digestDinImagine(image), imageIdentity: digestDinImagine(image) ? 'digest' : fx ? 'declared' : 'not-in-export', lastModifiedTimeUtc: s.lastModifiedTimeUtc || null, httpsOnly: s.httpsOnly ?? null, minTlsVersion: sc?.minTlsVersion || null, siteConfigInExport: !!sc, appSettingNames: numeDin(sc?.appSettings /*NUME:azure-webapp*/), connectionStringNames: numeDin(sc?.connectionStrings), }; out.push(intrare(`azwebapp:${rg}/${s.name}`, digestDinImagine(image) || image || fx, d)); } return out; } /** `gcloud compute instances list --format=json`: metadata.items intra numai cu cheile (startup-script, user-data, ssh-keys stau acolo) */ export function descrieGcpCompute(j) { if (!(Array.isArray(j) && j.every((i) => i && i.kind === 'compute#instance') /*FORMA:gcp-compute*/)) throw new Error('gcp-compute: the input is not the output of `gcloud compute instances list --format=json` (an array of compute#instance resources)'); const out = []; for (const i of j) { if (!(i.status === 'RUNNING' /*RULEAZA:gcp-compute*/)) continue; const boot = (i.disks || []).find((d) => d && d.boot) || null; const zona = ultimul(i.zone); const d = { runtime: 'gcp-compute', id: i.id != null ? String(i.id) : null, name: i.name || null, zone: zona, machineType: ultimul(i.machineType), status: i.status, cpuPlatform: i.cpuPlatform || null, creationTimestamp: i.creationTimestamp || null, lastStartTimestamp: i.lastStartTimestamp || null, bootDisk: boot ? { source: ultimul(boot.source), licenses: (boot.licenses || []).map(ultimul).filter(Boolean).sort(), architecture: boot.architecture || null, diskSizeGb: boot.diskSizeGb ?? null } : null, imageIdentity: 'declared', serviceAccounts: numeDin(i.serviceAccounts, 'email'), labelKeys: Object.keys(i.labels || {}).sort(), metadataKeys: numeDin(i.metadata?.items, 'key' /*NUME:gcp-compute*/), selfLink: i.selfLink || null, }; out.push(intrare(`gce:${zona}/${i.name}`, i.id != null ? String(i.id) : null, d)); } return out; } /** `gcloud run services list --format=json` (Knative Service, Cloud Run Admin API v1): ruleaza = conditia Ready e True */ export function descrieGcpRun(j) { if (!(Array.isArray(j) && j.every((s) => s && s.apiVersion === 'serving.knative.dev/v1' && s.kind === 'Service') /*FORMA:gcp-run*/)) throw new Error('gcp-run: the input is not the output of `gcloud run services list --format=json` (an array of serving.knative.dev/v1 Service resources)'); const out = []; for (const s of j) { const ready = (s.status?.conditions || []).find((c) => c && c.type === 'Ready') || null; if (!(!!ready && ready.status === 'True' /*RULEAZA:gcp-run*/)) continue; const md = s.metadata || {}; const region = md.labels?.['cloud.googleapis.com/location'] || null; const spec = s.spec?.template?.spec || {}; (spec.containers || []).forEach((c, idx) => { if (!c) return; const nume = c.name || `container-${idx}`; // env intra cu numele; value si valueFrom (referinta la Secret Manager) nu const d = { runtime: 'gcp-run', service: md.name || null, namespace: md.namespace || null, region, uid: md.uid || null, generation: md.generation ?? null, observedGeneration: s.status?.observedGeneration ?? null, latestReadyRevisionName: s.status?.latestReadyRevisionName || null, latestCreatedRevisionName: s.status?.latestCreatedRevisionName || null, url: s.status?.url || null, serviceAccountName: spec.serviceAccountName || null, container: nume, image: c.image || null, imageDigest: digestDinImagine(c.image), imageIdentity: digestDinImagine(c.image) ? 'digest' : 'declared', commandSha256: sha256(Buffer.from(JSON.stringify([...(c.command || []), ...(c.args || [])]), 'utf8')), envNames: numeDin(c.env /*NUME:gcp-run*/), mounts: (c.volumeMounts || []).map((m) => m && m.mountPath).filter(Boolean).sort(), readySince: ready.lastTransitionTime || null, }; out.push(intrare(`run:${region}/${md.name}/${nume}`, digestDinImagine(c.image) || s.status?.latestReadyRevisionName || c.image, d)); }); } return out; } /** sursa -> adaptor; `aws-ecs` primeste si definitiile de task (optional) */ export const ADAPTOARE = { docker: descrieDocker, kubernetes: descrieKubernetes, 'aws-ecs': descrieAwsEcs, 'aws-ec2': descrieAwsEc2, 'aws-lambda': descrieAwsLambda, 'azure-vm': descrieAzureVm, 'azure-container': descrieAzureContainer, 'azure-containerapp': descrieAzureContainerApp, 'azure-webapp': descrieAzureWebapp, 'gcp-compute': descrieGcpCompute, 'gcp-run': descrieGcpRun, }; // ---- 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 (!ADAPTOARE[src] || !f) { console.error(`scan --source ${Object.keys(ADAPTOARE).join('|')} --input [--task-definitions ]`); return 2; } let desc; try { const j = JSON.parse(fs.readFileSync(f, 'utf8')); const td = get('--task-definitions'); desc = src === 'aws-ecs' ? descrieAwsEcs(j, td ? JSON.parse(fs.readFileSync(td, 'utf8')) : null) : ADAPTOARE[src](j); } catch (e) { console.error('scan: ' + e.message); return 2; } if (desc.length === 0) { console.error('scan: no running workload (container, task, instance, function or service) 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 ${['docker', 'kubernetes'].includes(src) ? 'containers' : 'workloads'}, ${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; } } // 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 }); // B-25 (2026-09-30): ca la verify-inclusion, iesirea spune cine a semnat capetele si daca s-a cerut semnatarul const cine = (k) => (k ? `signed by key ${k.slice(0, 18)}` : 'unsigned'); const semn = !r.ok ? '' : semnatar ? `; both heads signed by the expected signer (${r.semnatNou.slice(0, 18)})` : `; old head ${cine(r.semnatVechi)}, new head ${cine(r.semnatNou)}${r.semnatVechi && r.semnatNou && r.semnatVechi !== r.semnatNou ? ' (DIFFERENT keys)' : ''}; no expected signer was checked (--signer): this proves only that the second head you hold extends the first one you hold`; console.log(r.ok ? `the new head EXTENDS the old one (the history up to the old head was not rewritten)${semn}` : `NOT consistent: ${r.motiv}`); return r.ok ? 0 : 1; } catch (e) { console.log(`${cmd}: ${String(e.message).replace(/[0-9a-fA-F]{32,}/g, '').slice(0, 200)}`); return 2; } } 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 | 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; } } 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; }); }