diff --git a/README.md b/README.md index 0f92aff..674fea0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Aere Quantum -Self-hosted post-quantum infrastructure from Aere Network. Four components, each a few files with **no dependencies**: -Node.js 24 and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry. +Self-hosted post-quantum infrastructure from Aere Network. Each component is a few files with **no dependencies**: Node.js 24 +and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry. The one exception is the notarization +command of the verification layer, which needs `ethers`. | component | what it does | |---|---| @@ -9,6 +10,8 @@ Node.js 24 and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a pac | [`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 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) | +| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content | Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them. @@ -24,6 +27,8 @@ 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 | 34/34 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF= node proba-sidecar.mjs`); without it 30 run, 4 are reported as skipped and the exit code is 2 | 6/6 in this repository (`node control-negativ-sidecar.mjs`; its seventh case compares with the version from the development history and is skipped here) | +| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test | Code comments, most function and variable names (also many exported between the files of a component), test names and control messages are in Romanian, and so are the two command words of the KMS HSM tool (explained in its README). Error codes, error @@ -32,4 +37,4 @@ in English. ## Licence -MIT, see [LICENSE](LICENSE). Files: 66 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42). +MIT, see [LICENSE](LICENSE). Files: 77 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3). diff --git a/proof-kinds/README.md b/proof-kinds/README.md new file mode 100644 index 0000000..d192c5a --- /dev/null +++ b/proof-kinds/README.md @@ -0,0 +1,21 @@ +# proof-kinds: one builder for every AIP-23 proof kind + +A generic builder of AERE Proof Protocol envelopes (AIP-23) for the standard proof kinds: `data`, `execution`, `identity`, +`compliance`, `runtime`, `location`, `device`, `payment`, `ownership`, `time`, `block`, `authorization`, `settlement` and +`provenance`. Every kind produces the same envelope (`statement` plus `statementHash`, the SHA-256 of the canonical statement +text), so the same AIP-23 reference verifier checks all of them with no kind-specific verification code. + +``` +import { buildProof, KINDS } from './proof-kinds.mjs'; +const envelope = buildProof('runtime', { host, artifactContent, attested, matches: true, createdAt }); +``` + +A kind is a schema: its required fields, some of them digests, plus `createdAt`. Raw content given for a digest field is hashed +(bytes as they are, strings as UTF-8, other values as their JSON text), and a value that is already a 32-byte digest is kept, so +an envelope carries digests, never the raw content. A missing required field stops the build with an error instead of producing +an incomplete envelope. Notarization and post-quantum finality come from AIP-23, not from this builder. + +Test: `node proba-proof-kinds.mjs` (24 checks: every kind built and found `VALID` by the reference verifier, six negative +controls such as content changed after hashing or a required field missing, no raw content in any envelope, bytes hashed as +bytes). It needs the AIP-23 reference verifier: `AERE_VERIFY_PROOF=` (from the `aere-node` +repository, after `npm install` there); without it the test reports that it did not measure and exits with 2. Node.js 24. diff --git a/proof-kinds/proba-proof-kinds.mjs b/proof-kinds/proba-proof-kinds.mjs new file mode 100644 index 0000000..5811df8 --- /dev/null +++ b/proof-kinds/proba-proof-kinds.mjs @@ -0,0 +1,75 @@ +'use strict'; +// Proba "Proof of everything": pentru FIECARE fel construieste un plic, il verifica cu ACELASI verificator AIP-23 (zero cod nou), si +// controale negative: continut atins dupa hash -> INVALID; camp obligatoriu lipsa -> eroare la constructie; niciun continut brut in plic. +// node proba-proof-kinds.mjs -> 0 toate cum trebuia, 1 altfel + +import fs from 'node:fs'; import crypto from 'node:crypto'; import os from 'node:os'; import path from 'node:path'; +import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { buildProof, KINDS } from './proof-kinds.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs'); +if (!fs.existsSync(VERIFY)) { console.log(`NEMASURAT: verificatorul AIP-23 nu e la ${VERIFY}; dati AERE_VERIFY_PROOF (de ex. verify-proof.mjs din aere-node/tools)`); process.exit(2); } +const T = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-')); +let rele = 0; const cer = (n, c) => { console.log(` [${c ? 'OK ' : 'RAU '}] ${n}`); if (!c) rele++; }; +function verdict(plicPath) { try { return JSON.parse(execFileSync(process.execPath, [VERIFY, plicPath, '--json'], { encoding: 'utf8' })).verdict; } catch (e) { try { return JSON.parse((e.stdout || '')).verdict; } catch { return '?'; } } } + +// intrari de proba pentru fiecare fel (continut brut, ca sa verific ca nu se scurge) +const BRUT = 'CONTINUT-BRUT-BRUT-42'; +const INTRARI = { + data: { name: 'raport.csv', content: BRUT, createdAt: '2026-09-26T09:00:00Z' }, + execution: { program: 'aere-node', input: BRUT, output: BRUT + 'o', exitCode: 0, createdAt: '2026-09-26T09:00:00Z' }, + identity: { subjectId: 'agent-1', publicKey: BRUT, method: 'ml-dsa-65', createdAt: '2026-09-26T09:00:00Z' }, + compliance: { subject: 'org-x', policy: 'pq-ready', result: 'pass', evidence: BRUT, createdAt: '2026-09-26T09:00:00Z' }, + runtime: { host: 'h1', artifactContent: BRUT, attested: BRUT, matches: true, createdAt: '2026-09-26T09:00:00Z' }, + location: { subject: 'srv-eu', region: 'eu-central', evidence: BRUT, createdAt: '2026-09-26T09:00:00Z' }, + device: { deviceId: 'dev-9', attestation: BRUT, publicKey: BRUT, posture: 'secure-boot', createdAt: '2026-09-26T09:00:00Z' }, + payment: { from: '0xa', to: '0xb', amount: '100', asset: 'AERE', tx: BRUT, createdAt: '2026-09-26T09:00:00Z' }, + ownership: { owner: '0xowner', assetId: 'nft-7', asset: BRUT, createdAt: '2026-09-26T09:00:00Z' }, + time: { subject: BRUT, source: 'aere-anchor', at: '2026-09-26T09:00:00Z', createdAt: '2026-09-26T09:00:00Z' }, + block: { blockHash: '0x' + 'ab'.repeat(32), stateRoot: '0x' + 'cd'.repeat(32), anchorHeight: 20255488, certificateDigest: '0x' + 'ef'.repeat(32), createdAt: '2026-09-26T09:00:00Z' }, + authorization: { grantor: 'org-x', grantee: 'agent-7', scope: 'payments:send<=100', policy: BRUT, expiresAt: '2026-12-31T00:00:00Z', createdAt: '2026-09-28T09:00:00Z' }, + settlement: { chainId: 2800, txHash: '0x' + '12'.repeat(32), blockHash: '0x' + '34'.repeat(32), from: '0xa', to: '0xb', amount: '100', asset: 'AERE', instruction: BRUT, createdAt: '2026-09-28T09:00:00Z' }, + provenance: { subject: BRUT, parents: [BRUT + '1', BRUT + '2'], process: BRUT + 'p', actor: 'build-bot', createdAt: '2026-09-28T09:00:00Z' }, +}; + +try { + cer('schema acopera cele 14 feluri (10 + bloc + autorizare, decontare, provenienta)', KINDS.length >= 14 && ['authorization', 'settlement', 'provenance'].every((k) => KINDS.includes(k))); + for (const k of KINDS) { + // fiecare fel se judeca separat: un fel care nu se mai poate construi e un RAU numit, nu o exceptie care opreste proba (2026-09-28) + let plic; try { plic = buildProof(k, INTRARI[k]); } catch (e) { cer(`${k}: constructia a cazut (${e.message})`, false); continue; } + const f = path.join(T, k + '.json'); fs.writeFileSync(f, JSON.stringify(plic, null, 1)); + const v = verdict(f); + const faraBrut = !JSON.stringify(plic).includes(BRUT); + cer(`${k}: verificatorul AIP-23 il valideaza si nu contine continut brut`, v === 'VALID' && faraBrut); + } + // CONTROL NEGATIV 1: statement atins dupa hash -> INVALID (pe 'data') + const p = buildProof('data', INTRARI.data); p.statement.name = 'ALTCEVA'; + const fr = path.join(T, 'rau.json'); fs.writeFileSync(fr, JSON.stringify(p, null, 1)); + cer('CONTROL: statement schimbat dupa hash -> INVALID', verdict(fr) === 'INVALID'); + // CONTROL NEGATIV 2: camp obligatoriu lipsa -> eroare la constructie + let aAruncat = false; try { buildProof('data', { name: 'x', createdAt: '2026-09-26T09:00:00Z' }); } catch { aAruncat = true; } + cer('CONTROL: camp obligatoriu lipsa (sha256) -> eroare la constructie', aAruncat); + // CONTROL NEGATIV 3: fel necunoscut -> eroare + let aAruncat2 = false; try { buildProof('inexistent', { createdAt: '2026-09-26T09:00:00Z' }); } catch { aAruncat2 = true; } + cer('CONTROL: fel necunoscut -> eroare', aAruncat2); + // 2026-09-28: felurile noi isi refuza formele gresite la constructie, nu produc un plic gresit + const arunca = (f) => { try { f(); return false; } catch { return true; } }; + cer('CONTROL: decontare cu txHash care nu e hash de 32 de octeti -> eroare', arunca(() => buildProof('settlement', { ...INTRARI.settlement, txHash: '0x1234' }))); + cer('CONTROL: provenienta cu lista de parinti goala -> eroare', arunca(() => buildProof('provenance', { ...INTRARI.provenance, parents: [] }))); + cer('CONTROL: autorizare fara scope -> eroare', arunca(() => buildProof('authorization', { ...INTRARI.authorization, scope: undefined }))); + const pv = buildProof('provenance', INTRARI.provenance); + cer('provenienta: fiecare parinte e digestul continutului lui, in ordine', pv.statement.parents.length === 2 && pv.statement.parents[0] === '0x' + crypto.createHash('sha256').update(BRUT + '1', 'utf8').digest('hex')); + // OCTETII unui Buffer (2026-09-27): digestul continutului dat ca Buffer = sha256 al octetilor (cel pe care il reface sha256sum pe + // fisier) = digestul aceluiasi continut dat ca sir. Pana azi un Buffer se hashuia ca JSON {"type":"Buffer",...}. + const oct = Buffer.from('ARTEFACT-DESFASURAT-\u0000\u00ff', 'latin1'); + const asteptat = '0x' + crypto.createHash('sha256').update(oct).digest('hex'); + const pb = buildProof('data', { name: 'a', content: oct, createdAt: '2026-09-26T09:00:00Z' }); + const pu = buildProof('data', { name: 'a', content: new Uint8Array(oct), createdAt: '2026-09-26T09:00:00Z' }); + const ps = buildProof('data', { name: 'a', content: 'abc', createdAt: '2026-09-26T09:00:00Z' }); + cer('Buffer/Uint8Array: digestul = sha256 al octetilor (reproductibil cu sha256sum)', pb.statement.sha256 === asteptat && pu.statement.sha256 === asteptat); + cer('sirurile raman ca inainte (vectorii publicati neschimbati)', ps.statement.sha256 === '0x' + crypto.createHash('sha256').update('abc', 'utf8').digest('hex')); +} finally { fs.rmSync(T, { recursive: true, force: true }); } +const total = KINDS.length + 10; +console.log(`\nProof of everything: ${total - rele}/${total} cum trebuia (${KINDS.length} feluri + 6 controale negative + acoperire + Buffer + parinti)`); +process.exit(rele ? 1 : 0); diff --git a/proof-kinds/proof-kinds.mjs b/proof-kinds/proof-kinds.mjs new file mode 100644 index 0000000..987dd21 --- /dev/null +++ b/proof-kinds/proof-kinds.mjs @@ -0,0 +1,86 @@ +'use strict'; +// AERE Proof of everything (roadmap #56-66): un constructor generic de plicuri AERE Proof Protocol (AIP-23) pentru felurile de +// dovada standard - date, executie, identitate, conformitate, runtime, locatie, dispozitiv, plata, proprietate, timp, bloc, si din +// 2026-09-28 autorizare, decontare, provenienta. Toate produc +// ACELASI plic (statement + statementHash SHA-256 al textului canonic) pe care il verifica ACELASI verificator (tools/aere-proof- +// protocol/verify.mjs), fara niciun cod nou de verificare. Notarizarea + finalitatea post-cuantica vin din AIP-23. Numai Node 24. +// +// Un fel de dovada = o schema: campurile obligatorii (unele DIGESTURI, ca nimic sensibil sa nu intre brut) + createdAt. Constructorul +// hashuieste continutul brut daca i se da, sau accepta un digest gata (0x+64). Un camp obligatoriu lipsa -> eroare (nu un plic gol). + +import crypto from 'node:crypto'; + +const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); +export const isDigest = (s) => typeof s === 'string' && /^0x[0-9a-fA-F]{64}$/.test(s); +// hashuieste un continut in digest; daca e deja digest, il pastreaza +// OCTETII unui Buffer/Uint8Array se hashuiesc ca atare. Platit 2026-09-27: un Buffer trecea prin JSON.stringify si se hashuia +// forma {"type":"Buffer","data":[...]}, deci digestul unui artefact dat ca fisier (sidecar record --kind deployment) nu se mai +// putea reface din fisier cu sha256sum. Sirurile si obiectele raman exact ca inainte (vectorii AIP-23 publicati nu se schimba). +const dg = (v) => (v == null ? null : (isDigest(v) ? v.toLowerCase() + : (Buffer.isBuffer(v) || v instanceof Uint8Array) ? sha256(Buffer.from(v)) + : sha256(Buffer.from(typeof v === 'string' ? v : JSON.stringify(v), 'utf8')))); + +// schema fiecarui fel: campuri de tip 'digest' (se hashuiesc), 'plain' (raman ca atare), 'bool', 'list'. Toate obligatorii daca in `req`. +export const SCHEME = { + 'data': { digest: { sha256: 'content' }, plain: { name: 'subject' }, req: ['sha256', 'name'] }, + 'execution': { digest: { inputHash: 'input', outputHash: 'output', programSha256: 'programContent' }, plain: { program: 'program', exitCode: 'exitCode' }, req: ['program', 'inputHash', 'outputHash'] }, + 'identity': { digest: { publicKeyHash: 'publicKey', subjectHash: 'subject' }, plain: { method: 'method', subjectId: 'subjectId' }, req: ['subjectId', 'publicKeyHash'] }, + 'compliance': { digest: { evidenceHash: 'evidence' }, plain: { subject: 'subject', policy: 'policy', result: 'result' }, req: ['subject', 'policy', 'result'] }, + 'runtime': { digest: { artifactSha256: 'artifactContent', attestedSha256: 'attested' }, plain: { host: 'host', unit: 'unit', matches: 'matches' }, req: ['host', 'artifactSha256'] }, + 'location': { digest: { evidenceHash: 'evidence' }, plain: { subject: 'subject', region: 'region', method: 'method' }, req: ['subject', 'region'] }, + 'device': { digest: { attestationHash: 'attestation', publicKeyHash: 'publicKey' }, plain: { deviceId: 'deviceId', posture: 'posture' }, req: ['deviceId', 'attestationHash'] }, + 'payment': { digest: { txHash: 'tx' }, plain: { from: 'from', to: 'to', amount: 'amount', asset: 'asset' }, req: ['from', 'to', 'amount'] }, + 'ownership': { digest: { assetHash: 'asset' }, plain: { owner: 'owner', assetId: 'assetId' }, req: ['owner', 'assetId'] }, + 'time': { digest: { subjectHash: 'subject' }, plain: { source: 'source', at: 'at' }, req: ['subjectHash', 'at'] }, + // A4: plicul de consens al unui bloc (blockHash, stateRoot ancorate de certificatul de ancora PQ). Dovada de executie per bloc + // (stateTransitionProof) e optionala si vine cand exista proverul; fara ea, plicul poarta increderea CONSENSULUI, nu a executiei. + 'block': { digest: { stateTransitionProof: 'stateTransitionProof' }, plain: { blockHash: 'blockHash', stateRoot: 'stateRoot', anchorHeight: 'anchorHeight', certificateDigest: 'certificateDigest' }, req: ['blockHash', 'stateRoot', 'certificateDigest'] }, + // 2026-09-28 (roadmap master, punctul 11): cele trei feluri numite in plan care lipseau. `hash` = campuri care SUNT deja un hash de + // 32 de octeti (0x+64 hex; o valoare de alta forma e refuzata, nu hashuita), `list` = o lista nevida de digesturi (fiecare element + // brut se hashuieste, un digest ramane). Felurile vechi nu au nici una, deci plicurile si vectorii lor raman octet cu octet aceiasi. + // authorization: cine (grantor) a dat cui (grantee) dreptul de a face ce (scope), sub ce politica, pana cand + 'authorization': { digest: { policyHash: 'policy' }, plain: { grantor: 'grantor', grantee: 'grantee', scope: 'scope', expiresAt: 'expiresAt' }, req: ['grantor', 'grantee', 'scope'] }, + // settlement: o decontare incheiata pe un lant: tranzactia, blocul care o contine, suma; finalitatea blocului o da AIP-23 (notarizare + // + ancora) sau clientul usor PQ (interop_core::pq), nu plicul + 'settlement': { digest: { instructionHash: 'instruction' }, hash: ['txHash', 'blockHash'], plain: { chainId: 'chainId', from: 'from', to: 'to', amount: 'amount', asset: 'asset' }, req: ['chainId', 'txHash', 'blockHash', 'amount', 'asset'] }, + // provenance: din ce a iesit un artefact (parents), prin ce proces, cine l-a produs + 'provenance': { digest: { subjectHash: 'subject', processHash: 'process' }, list: { parents: 'parentsContent' }, plain: { actor: 'actor' }, req: ['subjectHash', 'parents'] }, +}; +export const KINDS = Object.keys(SCHEME); + +/** + * Construieste un plic AIP-23 pentru un fel de dovada. + * @param {string} kind unul din KINDS + * @param {object} p campurile (brute sau digesturi); vezi SCHEME + * @returns {object} plicul { v, kind: 'aere-proof-of--attestation', statement, statementHash } + */ +export function buildProof(kind, p = {}) { + const s = SCHEME[kind]; + if (!s) throw new Error('proof-kinds: unknown kind "' + kind + '" (' + KINDS.join(', ') + ')'); + if (!p.createdAt) throw new Error('proof-kinds: createdAt is required (RFC 3339)'); + const statement = { v: 1, kind: 'aere-proof-of-' + kind }; + // campuri digest: se hashuiesc din sursa (numele sursei din schema) sau se iau gata din campul de digest + for (const [camp, sursa] of Object.entries(s.digest || {})) { + statement[camp] = p[camp] !== undefined ? dg(p[camp]) : dg(p[sursa]); + } + for (const camp of s.hash || []) { + if (p[camp] === undefined || p[camp] === null) continue; + if (!isDigest(p[camp])) throw new Error('proof-kinds: ' + kind + ' needs "' + camp + '" to be a 32-byte hash (0x + 64 hex)'); + statement[camp] = p[camp].toLowerCase(); + } + for (const [camp, sursa] of Object.entries(s.list || {})) { + const v = p[camp] !== undefined ? p[camp] : p[sursa]; + if (v === undefined || v === null) continue; + if (!Array.isArray(v) || v.length === 0) throw new Error('proof-kinds: ' + kind + ' needs "' + camp + '" to be a non-empty list'); + statement[camp] = v.map(dg); + } + for (const [camp] of Object.entries(s.plain || {})) { + if (p[camp] !== undefined) statement[camp] = p[camp]; + } + statement.createdAt = p.createdAt; + for (const r of s.req) { + if (statement[r] === undefined || statement[r] === null) throw new Error('proof-kinds: ' + kind + ' needs the field "' + r + '"'); + } + const text = JSON.stringify(statement); // canonic AIP-23 + return { v: 1, kind: 'aere-proof-of-' + kind + '-attestation', statement, statementHash: sha256(Buffer.from(text, 'utf8')) }; +} diff --git a/verify-layer/README.md b/verify-layer/README.md new file mode 100644 index 0000000..153fdac --- /dev/null +++ b/verify-layer/README.md @@ -0,0 +1,105 @@ +# AERE Verification Layer (sidecar) + +A sidecar that runs next to a deployment, on any cloud or on premises, and keeps an **audit log** of it. Every entry is an +AERE Proof Protocol envelope (AIP-23), built by the same `proof-kinds` builder and checked by the same AIP-23 reference +verifier as every other AERE proof, and every entry covers the hash of the one before it (an append-only hash chain, +`hash = sha256(seq | prev | envelope)`, genesis `0x00..00`). The head of the chain can be notarized on Aere Network, which +gives the history up to that head post-quantum finality that anyone can check. + +Node.js 24, no dependencies; `notarize-head` alone needs `ethers` (`npm install` in this directory). + +## What it proves, and what it does not + +- **The content of the entries is what the host says.** The sidecar runs on the host and reads the export the operator makes. + An entry proves that the host *declared* a container was running, with this image and this configuration, at the time it + declared (`--at` is also a declaration). It does not prove that the declaration is true. +- **The chain has no key.** Whoever can write the log file can rebuild the whole chain from genesis with any content, and + `verify-log` on that file alone reports it intact. A partial edit is caught (the chain breaks at the edited entry and + `verify-log` names it), a full rewrite is not. +- **What is checkable without trusting the host is the history before a published head.** `attest-head` turns the current + head into an envelope; `notarize-head` records its hash on the AereNotary contract; the AIP-23 reference verifier then gives + it post-quantum finality (the most recent certified anchor, the parent header bound by hash, its state root, and a Merkle + proof of `firstSeen[statementHash]`). `verify-log --attested ` requires that today's log **continues** that head: + the same first `count` entries, the entry `count - 1` hashing to the attested head. A rewritten or shortened history fails. + +So: publish heads often (every deployment, or on a timer), and judge a log against the latest head you hold from outside the +host, never on its own. + +## Commands + + node sidecar.mjs record --kind runtime --artifact --attested 0x [--host h] [--log p] [--at T] + node sidecar.mjs record --kind deployment --name --version --content-file [--host h] [--log p] [--at T] + node sidecar.mjs record --kind proof --proof-file # any AIP-23 envelope, after checking its form and hash + node sidecar.mjs scan --source docker|kubernetes --input [--host h] [--log p] + node sidecar.mjs verify-log --log p [--attested head.json] # 0 intact (and continues the head), 1 broken or not continued + node sidecar.mjs attest-head --log p [--host h] [--out head.json] # the head of the chain as an aere-audit-head envelope + node sidecar.mjs notarize-head --head head.json --rpc --key-file [--notary 0x..] [--out notarized.json] + node sidecar.mjs bundle --log p [--out bundle.json] # {host, count, head, chainOk, entries[]} for a console + +The default log is `aere-audit.log` in the current directory. Writers take a lock file beside the log (`.lock`, holding +the writer's process id), so two writers on the same host do not break the chain; a lock left by a process that no longer +exists is removed after 10 seconds. A log that is already broken is never appended to. + +`record --kind runtime` records that a file on the host (the binary that runs) has the digest you expected: `matches` says +whether it does. `record --kind deployment` records the digest of a deployed artifact, never its content. + +## The runtime adapter + +`scan` reads an export the operator makes on the host, so the sidecar needs no access to the Docker daemon, the cluster, or +any cloud credential: + + docker inspect $(docker ps -q) > export.json # then: scan --source docker --input export.json + kubectl get pods -A -o json > export.json # then: scan --source kubernetes --input export.json + +For every container that is **running**, it writes a canonical descriptor (image and image id, name, namespace and pod on +Kubernetes, start time, restart count, the **hash** of the command line, the **names** of the environment variables, the +destinations of the mounts) and records the descriptor's digest as a deployment entry. No environment value, no argument in +clear and no host path of a mount is stored. The descriptors themselves are written beside the log +(`.descriptori.jsonl`), so an auditor can re-hash each one and compare it with the chain. An export with no running +container, or one that is not in the expected shape, is refused rather than recorded as "zero deployments". + +A hash does not hide a guessable secret: if a password is passed on a command line, anyone who knows the rest of the command +can test guesses against `commandSha256`. Pass secrets through files or the environment, not arguments. + +## Notarization + +`notarize-head` reads the chain id from the RPC endpoint (it does not trust an argument), refuses an attestation whose +statement does not match its hash before sending anything, and sends nothing if the head is already notarized. On chain 2800 +(Aere Network mainnet) the notarization is a real transaction paid by the key's account, so it is sent only with +`AERE_CONFIRM_MAINNET=yes`. The key is read from a file (`d=` or `PRIVATE_KEY=0x`) and never printed; error +messages are cut before any long hex string. + +`dovezi-notarizare-testnet/` holds a real run on the public testnet (chain 28001): a log of three test deployments and its +head, notarized in block 3,699,939 on 2026-09-27. Anyone can check both halves: + + node sidecar.mjs verify-log --log dovezi-notarizare-testnet/audit.log --attested dovezi-notarizare-testnet/cap-notarizat-28001.json + node verify-proof.mjs dovezi-notarizare-testnet/cap-notarizat-28001.json # the AIP-23 reference verifier (aere-node/tools) + +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`. + +## Tests + + node proba-sidecar.mjs # 34 checks + node control-negativ-sidecar.mjs # puts each guard back to its absent form and requires the named check to fail + +`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 +`verify-log` alone (the stated limit) but fails against the attested head, as do a shortened log and a modified attestation, +while a log that only grew passes; that two writers appending 150 entries each at the same time leave an intact chain of 300; +that a line that is not JSON is reported as broken at its seq; that the Docker and Kubernetes adapters record only running +containers and no secret value; and that every envelope is `VALID` for the AIP-23 reference verifier. The verifier is looked +for at `AERE_VERIFY_PROOF` (for example `verify-proof.mjs` from the `aere-node` repository, after `npm install` there); without +it, the four checks that need it are reported as skipped and the exit code is 2, not 0. + +`control-negativ-sidecar.mjs` measured 7 of 7 on 2026-09-29: the version before the review (taken from the development +history, skipped where that history is absent) and six plantings, each disabling one guard (the writer lock, the head +comparison, the length check, the attestation hash check, the handling of an unreadable line, the digest check of `--attested`). +`proba-notarizare-testnet.mjs --key-file ` repeats the on-chain run above (9 checks, it needs a funded testnet key). + +## What it is not (yet) + +It does not report to a live console over the network, and it has no per-cloud adapters that read deployments from the AWS, +Azure or GCP APIs with credentials: the runtime adapter above, without credentials, is the first one. It does not sign entries +with a host key, so it cannot tell two hosts apart by itself; the host name in an entry is also a declaration. No third party +has reviewed it. diff --git a/verify-layer/control-negativ-sidecar.mjs b/verify-layer/control-negativ-sidecar.mjs new file mode 100644 index 0000000..b142119 --- /dev/null +++ b/verify-layer/control-negativ-sidecar.mjs @@ -0,0 +1,79 @@ +// control-negativ-sidecar.mjs (2026-09-29, revizuirea adversariala, pista B): controlul negativ al verificarilor R, C si N din +// proba-sidecar.mjs. Trei stari pe caz: PRINS (proba a ajuns la capat si verificarile numite sunt rosii), SCAPAT, STRICAT (copia nu +// s-a incarcat, ancora nu apare exact o data, sau proba nu a ajuns la capat). +// V0 sidecar-ul de dinainte, din git (sarit si spus daca revizia nu e in istoric) -> R CONTROL x3, N, N CONTROL, C rosii +// P1 lacatul scos (scriitorii nu se mai asteapta) -> C rosu +// P2 capul atestat nu se mai compara cu intrarea count-1 -> R CONTROL (istoria rescrisa) rosu +// P3 lungimea jurnalului nu se mai compara cu count -> R CONTROL (intrari scoase) rosu +// P4 statementHash-ul capului atestat nu se mai reface -> R CONTROL (cap manipulat) rosu +// P5 un rand care nu e JSON arunca din nou (cadere) -> N rosu +// P6 --attested nu se mai cere digest -> N CONTROL rosu +// Copiile stau LANGA original (importul lui proof-kinds e relativ) si se sterg; originalul trebuie sa ramana octet cu octet. +// node control-negativ-sidecar.mjs -> 0 toate prinse, 1 una scapata, 2 STRICAT +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const SRC = path.join(AICI, 'sidecar.mjs'); +const PROBA = path.join(AICI, 'proba-sidecar.mjs'); +const REV = process.env.AERE_SIDECAR_REV_VECHE || 'b2b08a5e'; +const sha = (f) => crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex'); +const inainte = sha(SRC); +let prinse = 0, stricate = 0, total = 0; + +const T = { + refacut: 'R CONTROL: acelasi lant refacut fata de capul atestat', + taiat: 'R CONTROL: jurnal taiat sub capul atestat', + capMan: 'R CONTROL: cap atestat manipulat', + necitibil: 'N: rand care nu e JSON', + digest: 'N CONTROL: --attested care nu e digest', + concurent: 'C: doi scriitori concurenti', +}; + +function caz(id, text, tinte) { + total++; + const copie = path.join(AICI, `.plantat-sidecar-${id}.mjs`); + try { + fs.writeFileSync(copie, text); + const r = spawnSync(process.execPath, [PROBA], { encoding: 'utf8', timeout: 400000, env: { ...process.env, AERE_SIDECAR_MODUL: copie } }); + const out = (r.stdout || '') + (r.stderr || ''); + if (!/B3 sidecar \(verify layer\): \d+\/\d+ cum trebuia/.test(out) || !out.includes(copie)) { stricate++; console.log(` STRICAT ${id}: proba nu a ajuns la capat pe copie (${out.trim().split('\n').pop()?.slice(0, 140)})`); return; } + const rosii = out.split('\n').filter((l) => l.includes('[RAU ]')); + const lipsa = tinte.filter((x) => !rosii.some((l) => l.includes(x))); + if (r.status === 1 && !lipsa.length) { prinse++; console.log(` PRINS ${id}: ${rosii.length} verificari rosii, intre ele: ${tinte.map((t) => t.split(':')[0]).join(', ')}`); } + else console.log(` SCAPAT ${id}: cod ${r.status}, au ramas verzi: ${lipsa.join(' | ') || '-'}`); + } finally { fs.rmSync(copie, { force: true }); fs.rmSync(copie + '.lock', { force: true }); } +} +const planta = (t, a, b) => (t.split(a).length === 2 ? t.replace(a, b) : null); + +const CALE = `${REV}:tools/aere-verify-layer/sidecar.mjs`; +if (spawnSync('git', ['cat-file', '-e', CALE], { cwd: AICI }).status !== 0) console.log(` SARIT V0: revizia ${REV} nu e in istoricul acestui depozit; NEMASURAT aici`); +else { + const g = spawnSync('git', ['-c', 'core.autocrlf=false', 'show', CALE], { cwd: AICI, encoding: 'utf8' }); + if (g.status !== 0 || g.stdout.includes('verificaFataDeCap')) { stricate++; total++; console.log(' STRICAT V0: revizia veche nu se citeste sau are deja reparatia'); } + else caz('V0', g.stdout, [T.refacut, T.taiat, T.capMan, T.necitibil, T.digest]); +} +const nou = fs.readFileSync(SRC, 'utf8'); +const NU = "process.env.AERE_PLANTA_NICIODATA === 'da'"; +const P = [ + ['P1', " try { return fn(); } finally { try { fs.unlinkSync(lacat); } catch { /* deja ridicat */ } }\n}", ` try { return fn(); } finally { try { fs.unlinkSync(lacat); } catch { /* deja ridicat */ } }\n}\nconst cuLacatAdevarat = cuLacat;\ncuLacat = (p, fn) => (${NU} ? cuLacatAdevarat(p, fn) : fn());`, [T.concurent]], + ['P2', 'if (h !== st.head.toLowerCase()) return', `if (h !== st.head.toLowerCase() && ${NU}) return`, [T.refacut]], + ['P3', 'if (intrari.length < st.count) return', `if (intrari.length < st.count && ${NU}) return`, [T.taiat]], + ['P4', "if (!eDigest(cap.statementHash) || sha256(Buffer.from(JSON.stringify(st), 'utf8')) !== cap.statementHash.toLowerCase()) return", `if (!eDigest(cap.statementHash) || (sha256(Buffer.from(JSON.stringify(st), 'utf8')) !== cap.statementHash.toLowerCase() && ${NU})) return`, [T.capMan]], + ['P5', "try { return JSON.parse(l); } catch { return { necitibil: true }; }", `try { return JSON.parse(l); } catch (e) { if (${NU}) return { necitibil: true }; throw e; }`, [T.necitibil]], + ['P6', 'if (!eDigest(attested)) {', `if (!eDigest(attested) && ${NU}) {`, [T.digest]], +]; +for (const [id, a, b, tinte] of P) { + let t = planta(nou, a, b); + // P1 cere ca `cuLacat` sa poata fi reasignat: declaratia de functie devine o variabila + if (t && id === 'P1') t = planta(t, 'function cuLacat(p, fn) {', 'let cuLacat = function (p, fn) {'); + if (!t) { stricate++; total++; console.log(` STRICAT ${id}: ancora nu apare exact o data`); continue; } + caz(id, t, tinte); +} +const ramase = fs.readdirSync(AICI).filter((f) => f.startsWith('.plantat-sidecar-')); +if (sha(SRC) !== inainte || ramase.length) { stricate++; console.log(' STRICAT originalul s-a schimbat sau au ramas copii: ' + ramase.join(',')); } +console.log(`\ncontrolul negativ al sidecar-ului: prinse ${prinse}/${total}, stricate ${stricate}`); +process.exitCode = stricate ? 2 : prinse === total ? 0 : 1; diff --git a/verify-layer/dovezi-notarizare-testnet/audit.log b/verify-layer/dovezi-notarizare-testnet/audit.log new file mode 100644 index 0000000..504657e --- /dev/null +++ b/verify-layer/dovezi-notarizare-testnet/audit.log @@ -0,0 +1,3 @@ +{"seq":0,"prev":"0x0000000000000000000000000000000000000000000000000000000000000000","proof":{"v":1,"kind":"aere-proof-of-data-attestation","statement":{"v":1,"kind":"aere-proof-of-data","sha256":"0xce70eb0a265ba614ae99939189c7b826e910951c2a747bd94e2503925e3e031b","name":"serviciu-proba@1.0.1","createdAt":"2026-09-27T16:57:56.073Z"},"statementHash":"0x13245267c558776462c6ac7e81626b4ed8c9ee166f748215fca8465f86178d18"},"hash":"0xca1a3ad2e530b30830e26168bb997de3651032a858a9545f194ff55603420e71"} +{"seq":1,"prev":"0xca1a3ad2e530b30830e26168bb997de3651032a858a9545f194ff55603420e71","proof":{"v":1,"kind":"aere-proof-of-data-attestation","statement":{"v":1,"kind":"aere-proof-of-data","sha256":"0x7c0762253eecffb9c7854af39ea3bbb4b907aafb6c883cbab4cf5d3fec4a39a1","name":"serviciu-proba@1.0.2","createdAt":"2026-09-27T16:57:56.318Z"},"statementHash":"0xfef0c6b0edaf2db63305ce791c4878e48d782618bdf1a7422b4524674af73134"},"hash":"0x54c52801d1ead44c2886f4d2e69a0f9d206e5873497f34ab9cf17976a027966e"} +{"seq":2,"prev":"0x54c52801d1ead44c2886f4d2e69a0f9d206e5873497f34ab9cf17976a027966e","proof":{"v":1,"kind":"aere-proof-of-data-attestation","statement":{"v":1,"kind":"aere-proof-of-data","sha256":"0x12be5c7cead026d63e1ea076713648391b6ae801c7083c2bbefd4f825647b1ef","name":"serviciu-proba@1.0.3","createdAt":"2026-09-27T16:57:56.592Z"},"statementHash":"0xc6448b16bf1aab05ed67bea64da8f0e399573caae6c0c13166291ffee08da46f"},"hash":"0xaa8c81d9e98167a6d36610749991cfb68b6610c6dae60fbd3c4adb4d2be79190"} diff --git a/verify-layer/dovezi-notarizare-testnet/cap-notarizat-28001.json b/verify-layer/dovezi-notarizare-testnet/cap-notarizat-28001.json new file mode 100644 index 0000000..f91f2a7 --- /dev/null +++ b/verify-layer/dovezi-notarizare-testnet/cap-notarizat-28001.json @@ -0,0 +1,20 @@ +{ + "v": 1, + "kind": "aere-audit-head-attestation", + "statement": { + "v": 1, + "kind": "aere-audit-head", + "host": "proba-b3", + "count": 3, + "head": "0xaa8c81d9e98167a6d36610749991cfb68b6610c6dae60fbd3c4adb4d2be79190", + "createdAt": "2026-09-27T16:57:57.125Z" + }, + "statementHash": "0x403be87131d5668f7650efa5f624744834f0c7826ad0954b38a9a6d59a8e3d4b", + "notarization": { + "chainId": 28001, + "notary": "0x70099E62735500AA2F85B60C518551a57B202d54", + "firstSeen": 1790528299, + "txHash": "0x5d66c837b188fa0646401cee8e262126ecf37c3cba3c3b5196a82e4e6a1fd2fb", + "block": 3699939 + } +} diff --git a/verify-layer/package.json b/verify-layer/package.json new file mode 100644 index 0000000..5f6cac4 --- /dev/null +++ b/verify-layer/package.json @@ -0,0 +1,8 @@ +{ + "name": "aere-verify-layer", + "private": true, + "description": "AERE Verification Layer: an audit-log sidecar whose head can be notarized on Aere Network", + "license": "MIT", + "engines": { "node": ">=24" }, + "dependencies": { "ethers": "6.16.0" } +} diff --git a/verify-layer/proba-notarizare-testnet.mjs b/verify-layer/proba-notarizare-testnet.mjs new file mode 100644 index 0000000..1aedc6e --- /dev/null +++ b/verify-layer/proba-notarizare-testnet.mjs @@ -0,0 +1,101 @@ +// Proba B3 milestone 3, PE LANT: jurnalul de audit al unei desfasurari -> capul lui atestat -> notarizat pe AereNotary de pe +// testnetul public 28001 -> verify-proof ii da finalitate POST-CUANTICA (ancora certificata -> radacina de stare -> dovada Merkle a +// lui firstSeen). Cu cheia de DEZVOLTATOR a testnetului (valoare de test, niciodata tiparita), niciodata o cheie de mainnet. +// node proba-notarizare-testnet.mjs --key-file +// Cazuri, fiecare cu perechea lui: +// A inainte de notarizare, verify-proof pe cap: finalitatea NU e post-cuantica, absenta e DOVEDITA ("not notarized") +// B notarize-head: tranzactie reusita, firstSeen > 0, plicul iesit declara notarizarea +// C verify-proof pe plicul notarizat: finality PASSED post-quantum, cu timpul = firstSeen (asteapta ancora certificata de dupa) +// D CONTROL: o intrare din jurnal manipulata -> verify-log rupt la seq-ul ei, attest-head refuza +// E CONTROL: capul altui jurnal (o intrare in plus) -> verify-proof: NEnotarizat, nu post-cuantic +// F CONTROL: plic cu statement schimbat si statementHash vechi -> notarize-head REFUZA, fara tranzactie +// G CONTROL: RPC-ul mainnetului (2800) fara AERE_CONFIRM_MAINNET=yes -> REFUZ inainte de cheie, cod 3 +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const SIDECAR = path.join(AICI, 'sidecar.mjs'); +const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : path.resolve(AICI, '..', '..', 'verificator-falcon', 'verify-proof.mjs'); +const RPC = 'https://testnet-rpc.aere.network'; +const arg = (n) => { const i = process.argv.indexOf('--' + n); return i > 0 ? process.argv[i + 1] : null; }; +const KEY = arg('key-file'); +if (!KEY || !fs.existsSync(KEY)) { console.error('cer --key-file '); process.exitCode = 2; } +const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-b3-notar-')); +let ok = 0, rau = 0; const linii = []; +const cere = (c, ce) => { linii.push((c ? 'OK ' : 'RAU ') + ce); c ? ok++ : rau++; }; +const taie = (s) => String(s ?? '').replace(/(0x)?[0-9a-fA-F]{40,}/g, (m) => (m.length === 66 ? m.slice(0, 12) + '..' : '')).slice(0, 220); +function run(script, args, env = {}) { + try { return { cod: 0, out: execFileSync(process.execPath, [script, ...args], { encoding: 'utf8', env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'pipe'] }) }; } + catch (e) { return { cod: e.status ?? 2, out: String(e.stdout || '') + String(e.stderr || '') }; } +} +const verifica = (f) => { const r = run(VERIFY, [f, '--rpc', RPC, '--chain', '28001', '--json']); try { return JSON.parse(r.out); } catch { return { verdict: '?', levels: [], brut: r.out }; } }; +const fin = (v) => (v.levels || []).find((l) => l.level === 'finality') || {}; +const dormi = (ms) => new Promise((r) => setTimeout(r, ms)); + +if (KEY && fs.existsSync(KEY)) try { + const log = path.join(T, 'audit.log'); + const creazaJurnal = (logf, n) => { + for (let i = 1; i <= n; i++) { + const f = path.join(T, `artefact-${i}.bin`); fs.writeFileSync(f, `desfasurarea ${i} a serviciului de proba, ${T}`); + const r = run(SIDECAR, ['record', '--kind', 'deployment', '--name', 'serviciu-proba', '--version', `1.0.${i}`, '--content-file', f, '--log', logf, '--host', 'proba-b3']); + if (r.cod !== 0) throw new Error('record a cazut: ' + taie(r.out)); + } + }; + creazaJurnal(log, 3); + cere(run(SIDECAR, ['verify-log', '--log', log]).cod === 0, 'jurnalul de 3 desfasurari e intreg'); + const head = path.join(T, 'cap.json'); + cere(run(SIDECAR, ['attest-head', '--log', log, '--out', head, '--host', 'proba-b3']).cod === 0 && fs.existsSync(head), 'capul lantului atestat (aere-audit-head)'); + + // A: absenta DOVEDITA inainte + const vA = verifica(head); const fA = fin(vA); + cere(fA.state !== 'PASSED' && /not notarized|ABSENT|no record/i.test(String(fA.detail) + fA.state), `A inainte de notarizare: finality ${fA.state} (${taie(fA.detail)})`); + + // B: notarizarea + const notar = path.join(T, 'cap-notarizat.json'); + const rB = run(SIDECAR, ['notarize-head', '--head', head, '--rpc', RPC, '--key-file', KEY, '--out', notar]); + const pB = fs.existsSync(notar) ? JSON.parse(fs.readFileSync(notar, 'utf8')) : null; + cere(rB.cod === 0 && pB && pB.notarization && pB.notarization.firstSeen > 0 && pB.notarization.chainId === 28001, `B notarize-head: ${taie(rB.out.trim().split('\n').pop())}`); + + // C: finalitatea post-cuantica apare dupa urmatoarea ancora certificata (asteptare marginita, 5 minute) + let vC = null, fC = {}, incercari = 0; + for (const t0 = Date.now(); Date.now() - t0 < 300000; incercari++) { + vC = verifica(notar); fC = fin(vC); + if (fC.state === 'PASSED' && /post-quantum/.test(fC.detail || '')) break; + await dormi(20000); + } + const timp = pB && pB.notarization ? String(pB.notarization.firstSeen) : 'X'; + cere(vC.verdict === 'VALID' && fC.state === 'PASSED' && /post-quantum/.test(fC.detail || '') && String(fC.detail).includes(timp), `C verify-proof: finality ${fC.state} post-quantum cu firstSeen ${timp} (dupa ${incercari} reincercari): ${taie(fC.detail)}`); + + // D: jurnal manipulat + const logD = path.join(T, 'audit-manipulat.log'); fs.copyFileSync(log, logD); + const ld = fs.readFileSync(logD, 'utf8').split('\n'); const o = JSON.parse(ld[1]); o.proof.statement.name = 'serviciu-strain'; ld[1] = JSON.stringify(o); fs.writeFileSync(logD, ld.join('\n')); + const rD = run(SIDECAR, ['verify-log', '--log', logD]); const rD2 = run(SIDECAR, ['attest-head', '--log', logD, '--out', path.join(T, 'x.json')]); + cere(rD.cod === 1 && /seq 1/.test(rD.out) && rD2.cod === 1, `D CONTROL: intrare manipulata -> ${taie(rD.out.trim())}; attest-head refuza`); + + // E: capul altui jurnal + const logE = path.join(T, 'audit-altul.log'); fs.copyFileSync(log, logE); creazaJurnal(logE, 1); + const headE = path.join(T, 'cap-altul.json'); run(SIDECAR, ['attest-head', '--log', logE, '--out', headE, '--host', 'proba-b3']); + const fE = fin(verifica(headE)); + cere(fE.state !== 'PASSED', `E CONTROL: capul unui jurnal cu o intrare in plus NU e notarizat: finality ${fE.state} (${taie(fE.detail)})`); + + // F: plic manipulat + const pF = JSON.parse(fs.readFileSync(head, 'utf8')); pF.statement.count = pF.statement.count + 1; const headF = path.join(T, 'cap-manipulat.json'); fs.writeFileSync(headF, JSON.stringify(pF)); + const rF = run(SIDECAR, ['notarize-head', '--head', headF, '--rpc', RPC, '--key-file', KEY]); + cere(rF.cod === 1 && /modified attestation/.test(rF.out) && !/notarized on chain/.test(rF.out), `F CONTROL: statement schimbat -> notarize-head refuza fara tranzactie (${taie(rF.out.trim())})`); + + // G: garda de mainnet + const rG = run(SIDECAR, ['notarize-head', '--head', head, '--rpc', 'https://rpc.aere.network', '--key-file', KEY], { AERE_CONFIRM_MAINNET: '' }); + cere(rG.cod === 3 && /2800/.test(rG.out), `G CONTROL: RPC de mainnet fara DA -> cod ${rG.cod}, refuzat (${taie(rG.out.trim())})`); + + // pastreaza dovada: capul notarizat si jurnalul lui, fara nimic secret + const dov = path.join(AICI, 'dovezi-notarizare-testnet'); fs.mkdirSync(dov, { recursive: true }); + if (pB) fs.writeFileSync(path.join(dov, 'cap-notarizat-28001.json'), JSON.stringify(pB, null, 1) + '\n'); + fs.copyFileSync(log, path.join(dov, 'audit.log')); +} catch (e) { linii.push('RAU proba nu a putut rula: ' + taie(e.message)); rau++; } +finally { try { fs.rmSync(T, { recursive: true, force: true }); } catch {} } +for (const l of linii) console.log(l); +console.log(`B3 notarizare pe testnet: ${ok}/${ok + rau} cum trebuia`); +if (!process.exitCode) process.exitCode = rau ? 1 : 0; diff --git a/verify-layer/proba-sidecar.mjs b/verify-layer/proba-sidecar.mjs new file mode 100644 index 0000000..ca62733 --- /dev/null +++ b/verify-layer/proba-sidecar.mjs @@ -0,0 +1,191 @@ +'use strict'; +// Proba sidecar-ului B3: inregistreaza evenimente, verifica lantul, si controale NEGATIVE - o intrare manipulata rupe lantul exact la +// seq-ul ei, un hash schimbat se prinde, adaugarea peste un lant rupt e refuzata. Plus moatul: fiecare plic inregistrat e VALID +// pentru verificatorul AIP-23 comun (zero cod de verificare nou). Din 2026-09-29 (revizuirea adversariala, pista B), si: +// R un lant REFACUT de la geneza de cine poate scrie fisierul iese INTREG la verify-log (limita spusa), dar e prins fata de capul +// atestat (verify-log --attested); la fel un jurnal TAIAT si un cap atestat manipulat; un jurnal care continua capul trece +// C doi scriitori concurenti nu rup lantul (lacatul) +// N un rand care nu e JSON iese RUPT la seq-ul lui, nu cadere; --attested care nu e digest e refuzat +// node proba-sidecar.mjs -> 0 toate cum trebuia, 1 cel putin una rea, 2 cel putin una SARITA (verificatorul AIP-23 lipseste) +// Mediu: AERE_SIDECAR_MODUL (copia masurata, pentru controlul negativ), AERE_VERIFY_PROOF (verificatorul AIP-23; implicit cel din +// depozitul de dezvoltare, tools/aere-proof-protocol/verify.mjs, sau verify-proof.mjs din aere-node/tools intr-o copie publica). + +import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; +import { execFileSync, spawn } from 'node:child_process'; import { fileURLToPath, pathToFileURL } from 'node:url'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const SIDE = process.env.AERE_SIDECAR_MODUL ? path.resolve(process.env.AERE_SIDECAR_MODUL) : path.join(AICI, 'sidecar.mjs'); +const { verificaJurnal, hashIntrare } = await import(pathToFileURL(SIDE).href); +const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs'); +const areVerificator = fs.existsSync(VERIFY); +const T = fs.mkdtempSync(path.join(os.tmpdir(), 'avl-')); +let rele = 0, bune = 0, sarite = 0; +const cer = (n, c) => { console.log(` [${c ? 'OK ' : 'RAU '}] ${n}`); if (c) bune++; else rele++; }; +const sari = (n) => { console.log(` [SARIT] ${n} (verificatorul AIP-23 nu e la ${VERIFY}; dati AERE_VERIFY_PROOF)`); sarite++; }; +function side(args) { try { return { cod: 0, out: execFileSync(process.execPath, [SIDE, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) }; } catch (e) { return { cod: e.status ?? 1, out: (e.stdout || '') + (e.stderr || '') }; } } +const linii = (p) => fs.readFileSync(p, 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l)); +const valid = (f) => { try { execFileSync(process.execPath, [VERIFY, f], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return true; } catch { return false; } }; +const pk = await import(pathToFileURL(path.resolve(path.dirname(SIDE), '..', 'proof-kinds', 'proof-kinds.mjs')).href).catch(() => import(pathToFileURL(path.resolve(AICI, '..', 'proof-kinds', 'proof-kinds.mjs')).href)); + +try { + const log = path.join(T, 'audit.log'); + const bin = path.join(T, 'aere-node'); fs.writeFileSync(bin, 'BINAR-VIU'); + const sh = '0x' + crypto.createHash('sha256').update(fs.readFileSync(bin)).digest('hex'); + const artefact = path.join(T, 'artefact.tar'); fs.writeFileSync(artefact, 'ARTEFACT-DESF'); + + cer('record runtime -> seq 0', side(['record', '--kind', 'runtime', '--artifact', bin, '--attested', sh, '--host', 'h1', '--log', log, '--at', '2026-09-26T09:00:00Z']).out.includes('seq=0')); + cer('record deployment -> seq 1', side(['record', '--kind', 'deployment', '--name', 'app', '--version', '2.0', '--content-file', artefact, '--log', log, '--at', '2026-09-26T09:01:00Z']).out.includes('seq=1')); + cer('verify-log -> INTACT cod 0', side(['verify-log', '--log', log]).cod === 0); + + // moatul: fiecare plic e VALID pentru verificatorul AIP-23 + const intrari = linii(log); + if (areVerificator) { + let toateValide = true; + for (const e of intrari) { const f = path.join(T, `e${e.seq}.json`); fs.writeFileSync(f, JSON.stringify(e.proof)); if (!valid(f)) toateValide = false; } + cer('moat: fiecare plic inregistrat e VALID pentru verificatorul AIP-23 comun', toateValide); + } else sari('moat: fiecare plic inregistrat e VALID pentru verificatorul AIP-23 comun'); + + // CONTROL NEGATIV 1: manipulez CONTINUTUL unei intrari trecute (fara sa refac hash-ul) -> lantul se rupe la ea + const man1 = intrari.map((e) => ({ ...e })); man1[0].proof = { ...man1[0].proof, statement: { ...man1[0].proof.statement, host: 'ATACATOR' } }; + cer('CONTROL: continut manipulat -> lant RUPT la seq 0', verificaJurnal(man1).rupt === 0); + + // CONTROL NEGATIV 2: schimb un hash din mijloc -> ruptura la intrarea urmatoare (prev nu mai leaga) + const man2 = intrari.map((e) => ({ ...e })); man2[0].hash = '0x' + 'ff'.repeat(32); + cer('CONTROL: hash schimbat -> lant RUPT', verificaJurnal(man2).ok === false); + + // CONTROL NEGATIV 3: adaugarea peste un lant rupt e REFUZATA + const logRupt = path.join(T, 'rupt.log'); fs.writeFileSync(logRupt, JSON.stringify(man1[0]) + '\n'); + cer('CONTROL: record peste lant rupt -> refuzat (cod != 0)', side(['record', '--kind', 'runtime', '--artifact', bin, '--attested', sh, '--log', logRupt, '--at', '2026-09-26T09:02:00Z']).cod !== 0); + + // bundle: chainOk true pe jurnalul bun + const bout = path.join(T, 'bundle.json'); + cer('bundle -> chainOk true', side(['bundle', '--log', log, '--out', bout, '--host', 'h1', '--at', '2026-09-26T09:03:00Z']).cod === 0 && JSON.parse(fs.readFileSync(bout, 'utf8')).chainOk === true); + + // attest-head: capul devine un plic AIP-23 valid pentru verificatorul comun; notarizabil -> finalitate PQ + const hout = path.join(T, 'head.json'); + cer('attest-head -> plic scris', side(['attest-head', '--log', log, '--out', hout, '--host', 'h1', '--at', '2026-09-26T09:04:00Z']).cod === 0 && fs.existsSync(hout)); + const ph = JSON.parse(fs.readFileSync(hout, 'utf8')); ph.statement.head = '0x' + 'ee'.repeat(32); + const hrau = path.join(T, 'head-rau.json'); fs.writeFileSync(hrau, JSON.stringify(ph)); + if (areVerificator) { + cer('attest-head: capul e VALID pentru verificatorul AIP-23 (integritate)', valid(hout)); + // CONTROL NEGATIV 4: manipulez capul din plic dupa hash -> verificatorul da integritate PICAT + cer('CONTROL: cap manipulat in plic -> verificator INVALID', !valid(hrau)); + } else { sari('attest-head: capul e VALID pentru verificatorul AIP-23 (integritate)'); sari('CONTROL: cap manipulat in plic -> verificator INVALID'); } + + // ---- R: lantul fata de un cap atestat -------------------------------------------------------------------------------------- + cer('R: jurnalul neatins CONTINUA capul atestat (cod 0)', side(['verify-log', '--log', log, '--attested', hout]).cod === 0); + // un scriitor cu acces la fisier reface TOT lantul de la geneza cu o intrare schimbata: verify-log singur nu are cum sa vada + const refacut = []; let prev = '0x' + '00'.repeat(32); + for (const e of intrari) { + const proof = e.seq === 0 ? { ...e.proof, statement: { ...e.proof.statement, host: 'ATACATOR' } } : e.proof; + const hash = hashIntrare(e.seq, prev, proof); refacut.push({ seq: e.seq, prev, proof, hash }); prev = hash; + } + const logRef = path.join(T, 'refacut.log'); fs.writeFileSync(logRef, refacut.map((e) => JSON.stringify(e)).join('\n') + '\n'); + cer('R: lant refacut de la geneza -> verify-log singur iese INTACT (limita, spusa in README)', side(['verify-log', '--log', logRef]).cod === 0); + const rR = side(['verify-log', '--log', logRef, '--attested', hout]); + cer('R CONTROL: acelasi lant refacut fata de capul atestat -> NU continua, istoria rescrisa (cod 1)', rR.cod === 1 && /rewritten/.test(rR.out)); + const logTaiat = path.join(T, 'taiat.log'); fs.writeFileSync(logTaiat, JSON.stringify(intrari[0]) + '\n'); + const rT = side(['verify-log', '--log', logTaiat, '--attested', hout]); + cer('R CONTROL: jurnal taiat sub capul atestat -> NU continua, intrari scoase (cod 1)', rT.cod === 1 && /removed/.test(rT.out)); + const logMai = path.join(T, 'continuat.log'); fs.copyFileSync(log, logMai); + side(['record', '--kind', 'deployment', '--name', 'app', '--version', '2.1', '--content-file', artefact, '--log', logMai, '--at', '2026-09-26T09:05:00Z']); + const rC = side(['verify-log', '--log', logMai, '--attested', hout]); + cer('R: jurnal continuat dupa cap -> continua, o intrare scrisa dupa (cod 0)', rC.cod === 0 && /1 written after/.test(rC.out)); + const capM = JSON.parse(fs.readFileSync(hout, 'utf8')); capM.statement.count = 1; const capMf = path.join(T, 'cap-man.json'); fs.writeFileSync(capMf, JSON.stringify(capM)); + const rM = side(['verify-log', '--log', log, '--attested', capMf]); + cer('R CONTROL: cap atestat manipulat (count schimbat, statementHash vechi) -> refuzat (cod 1)', rM.cod === 1 && /modified attestation/.test(rM.out)); + + // ---- N: intrari nevalide ------------------------------------------------------------------------------------------------- + const logN = path.join(T, 'necitibil.log'); fs.writeFileSync(logN, JSON.stringify(intrari[0]) + '\n{nu e json\n'); + const rN = side(['verify-log', '--log', logN]); + cer('N: rand care nu e JSON -> RUPT la seq 1, nu cadere (cod 1)', rN.cod === 1 && /BROKEN at seq 1/.test(rN.out)); + cer('N CONTROL: --attested care nu e digest -> refuzat (cod 2), nimic scris', side(['record', '--kind', 'runtime', '--artifact', bin, '--attested', 'abc', '--log', path.join(T, 'n2.log')]).cod === 2 && !fs.existsSync(path.join(T, 'n2.log'))); + + // ---- C: doi scriitori concurenti pe acelasi jurnal ------------------------------------------------------------------------ + // fiecare scriitor e un proces care adauga in bucla stransa prin modul (o pornire de proces pe inregistrare ar lasa fereastra de + // cursa prea rara ca sa se vada: masurat, controlul fara lacat iesea verde asa); la prima adaugare refuzata scriitorul iese cu 1 + const logC = path.join(T, 'concurent.log'); const N = 150; + const scriitor = (id) => new Promise((rez) => { + const cod = `const s = await import(${JSON.stringify(pathToFileURL(SIDE).href)}); + for (let i = 0; i < ${N}; i++) { try { s.adaugaPlicuri(${JSON.stringify(logC)}, [{ v: 1, kind: 'proba-concurenta', scriitor: '${id}', i }]); } catch { process.exitCode = 1; break; } }`; + const p = spawn(process.execPath, ['--input-type=module', '-e', cod], { stdio: 'ignore' }); + p.on('exit', (c) => rez(c)); + }); + const coduri = await Promise.all([scriitor('a'), scriitor('b')]); + const vC = verificaJurnal(fs.readFileSync(logC, 'utf8').split('\n').filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return { necitibil: true }; } })); + cer(`C: doi scriitori concurenti x ${N} -> lant INTACT cu ${2 * N} intrari, niciun scriitor refuzat`, coduri.every((c) => c === 0) && vC.ok && vC.count === 2 * N && !fs.existsSync(logC + '.lock')); + + // record --kind proof: leaga plicuri AIP-23 gata facute (provenienta agentilor AI in jurnalul inviolabil); cu Proof-of-AI si + // decizia de agent cand modulele lor sunt alaturi (depozitul de dezvoltare), altfel cu doua plicuri proof-kinds + const log2 = path.join(T, 'agent.log'); + const poaiP = path.resolve(AICI, '..', 'proof-of-ai', 'proof-of-ai.mjs'); const polP = path.resolve(AICI, '..', 'agent-policy', 'agent-policy.mjs'); + let plic1, plic2; + if (fs.existsSync(poaiP) && fs.existsSync(polP)) { + const { buildProofOfAi } = await import(pathToFileURL(poaiP).href); + const { definePolicy, checkAction, decisionEnvelope } = await import(pathToFileURL(polP).href); + plic1 = buildProofOfAi({ model: { name: 'claude', version: 'opus-4.8' }, prompt: 'rezuma', output: 'rezumat', tools: ['search'], agentId: 'agent-7', createdAt: '2026-09-26T09:10:00Z' }); + const { policy, policyHash } = definePolicy({ agentId: 'agent-7', spend: { amount: '100', windowSeconds: 3600, asset: 'AERE' }, tools: ['search'], recipients: null }); + const act = { kind: 'payment', to: '0x1', amount: '50', at: 1 }; + plic2 = decisionEnvelope({ policyHash, action: act, decision: checkAction(policy, act, []), createdAt: '2026-09-26T09:11:00Z' }); + } else { + plic1 = pk.buildProof('execution', { program: 'agent-7', input: 'rezuma', output: 'rezumat', createdAt: '2026-09-26T09:10:00Z' }); + plic2 = pk.buildProof('identity', { subjectId: 'agent-7', publicKey: 'cheie-publica-de-proba', createdAt: '2026-09-26T09:11:00Z' }); + } + const p1F = path.join(T, 'p1.json'); fs.writeFileSync(p1F, JSON.stringify(plic1)); + const p2F = path.join(T, 'p2.json'); fs.writeFileSync(p2F, JSON.stringify(plic2)); + cer('record --kind proof (primul plic) -> seq 0', side(['record', '--kind', 'proof', '--proof-file', p1F, '--host', 'agent', '--log', log2, '--at', '2026-09-26T09:10:00Z']).out.includes('seq=0')); + cer('record --kind proof (al doilea plic) -> seq 1', side(['record', '--kind', 'proof', '--proof-file', p2F, '--host', 'agent', '--log', log2, '--at', '2026-09-26T09:11:00Z']).out.includes('seq=1')); + cer('lantul de provenienta al agentului e INTACT', side(['verify-log', '--log', log2]).cod === 0); + // CONTROL NEGATIV 5: plic manipulat (statementHash nu se potriveste) -> record refuzat + const rauP = JSON.parse(JSON.stringify(plic1)); rauP.statement.createdAt = '2030-01-01T00:00:00Z'; + const rauPF = path.join(T, 'p-rau.json'); fs.writeFileSync(rauPF, JSON.stringify(rauP)); + cer('CONTROL: record --kind proof cu plic manipulat -> refuzat', side(['record', '--kind', 'proof', '--proof-file', rauPF, '--log', log2, '--at', '2026-09-26T09:12:00Z']).cod !== 0); + + // ---- adaptorul de runtime (scan docker | kubernetes) ---------------------------------------------------------------------- + const SECRET = 'AERE-SINTETIC-supersecret-XYZ-123'; const PAROLA = 'AERE-SINTETIC-hunter2-parola'; + const dockerExport = [ + { Name: '/app-api', Image: 'sha256:' + 'ab'.repeat(32), Path: '/usr/local/bin/api', Args: ['--data-path=/var/lib/api', `--db-password=${PAROLA}`], + Config: { Image: 'example/api:3', Env: [`SECRET_TOKEN=${SECRET}`, 'JAVA_OPTS=-Xmx4g', 'PATH=/usr/bin'] }, + Mounts: [{ Source: '/root/chei', Destination: '/var/lib/api' }], State: { Running: true, StartedAt: '2026-09-27T00:00:00Z' }, RestartCount: 0 }, + { Name: '/sidecar-ntp', Image: 'sha256:' + 'cd'.repeat(32), Path: 'chronyd', Args: [], Config: { Image: 'ntp:4', Env: [] }, Mounts: [], State: { Running: true, StartedAt: '2026-09-27T00:01:00Z' }, RestartCount: 2 }, + { Name: '/oprit', Image: 'sha256:' + 'ef'.repeat(32), Path: 'x', Args: [], Config: { Image: 'x:1', Env: [] }, Mounts: [], State: { Running: false } }, + ]; + const dExp = path.join(T, 'docker.json'); fs.writeFileSync(dExp, JSON.stringify(dockerExport)); + const log3 = path.join(T, 'runtime.log'); const des3 = log3 + '.descriptori.jsonl'; + const s1 = side(['scan', '--source', 'docker', '--input', dExp, '--host', 'gazda-1', '--log', log3, '--at', '2026-09-27T01:00:00Z']); + cer('scan docker -> 2 plicuri (numai containerele care ruleaza; cel oprit NU)', s1.cod === 0 && linii(log3).length === 2 && !fs.readFileSync(des3, 'utf8').includes('oprit')); + cer('scan: jurnalul de runtime e INTACT', side(['verify-log', '--log', log3]).cod === 0); + if (areVerificator) { + let valide3 = true; + for (const e of linii(log3)) { const f = path.join(T, `r${e.seq}.json`); fs.writeFileSync(f, JSON.stringify(e.proof)); if (!valid(f)) valide3 = false; } + cer('scan: fiecare plic de runtime e VALID pentru verificatorul AIP-23 comun', valide3); + } else sari('scan: fiecare plic de runtime e VALID pentru verificatorul AIP-23 comun'); + const textLog = fs.readFileSync(log3, 'utf8'); const textDes = fs.readFileSync(des3, 'utf8'); + cer('CONFIDENTIALITATE: valoarea de mediu si parola din argumente NU apar nici in jurnal, nici in descriptori', !textLog.includes(SECRET) && !textDes.includes(SECRET) && !textLog.includes(PAROLA) && !textDes.includes(PAROLA)); + cer('control pozitiv al confidentialitatii: NUMELE variabilei (SECRET_TOKEN) e in descriptor', textDes.includes('SECRET_TOKEN') && !textDes.includes('/root/chei')); + const desLinii = textDes.split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l)); + const shaDe = (o) => '0x' + crypto.createHash('sha256').update(Buffer.from(JSON.stringify(o, Object.keys(o).sort()), 'utf8')).digest('hex'); + cer('descriptorii re-hashati = digestul din plicurile lantului', desLinii.length === 2 && desLinii.every((d) => shaDe(d.descriptor) === d.sha256 && linii(log3)[d.seq].proof.statement.sha256 === d.sha256)); + // CONTROL NEGATIV 6: un descriptor schimbat dupa inregistrare nu mai are digestul din lant + const falsificat = { ...desLinii[0].descriptor, imageId: 'sha256:' + '00'.repeat(32) }; + cer('CONTROL: descriptor falsificat -> digest diferit de cel din lant', shaDe(falsificat) !== linii(log3)[0].proof.statement.sha256); + // kubernetes: un pod cu doua containere, unul ruleaza, unul asteapta + const k8s = { items: [{ metadata: { namespace: 'prod', name: 'api-7f' }, + spec: { containers: [{ name: 'api', image: 'api:3', command: ['node'], args: ['srv.js', `--token=${PAROLA}`], env: [{ name: 'DB_PASS', value: SECRET }], volumeMounts: [{ mountPath: '/data' }] }, { name: 'init-side', image: 'side:1' }] }, + status: { containerStatuses: [{ name: 'api', image: 'api:3', imageID: 'docker-pullable://api@sha256:' + '12'.repeat(32), restartCount: 1, state: { running: { startedAt: '2026-09-27T00:05:00Z' } } }, + { name: 'init-side', image: 'side:1', imageID: '', restartCount: 0, state: { waiting: { reason: 'PodInitializing' } } }] } }] }; + const kExp = path.join(T, 'k8s.json'); fs.writeFileSync(kExp, JSON.stringify(k8s)); + const log4 = path.join(T, 'k8s.log'); + const s2 = side(['scan', '--source', 'kubernetes', '--input', kExp, '--host', 'cluster-a', '--log', log4, '--at', '2026-09-27T01:01:00Z']); + const t4 = fs.readFileSync(log4 + '.descriptori.jsonl', 'utf8'); + cer('scan kubernetes -> 1 plic (containerul care asteapta NU), fara valori, cu numele DB_PASS', s2.cod === 0 && linii(log4).length === 1 && !t4.includes(SECRET) && !t4.includes(PAROLA) && t4.includes('DB_PASS')); + // CONTROL NEGATIV 7: un export fara niciun container care ruleaza nu se inregistreaza ca "zero desfasurari" + const gol = path.join(T, 'gol.json'); fs.writeFileSync(gol, JSON.stringify([dockerExport[2]])); + cer('CONTROL: export fara containere care ruleaza -> refuzat (cod 2), nimic scris', side(['scan', '--source', 'docker', '--input', gol, '--log', path.join(T, 'gol.log')]).cod === 2 && !fs.existsSync(path.join(T, 'gol.log'))); + // CONTROL NEGATIV 8: forma gresita (un obiect in loc de tabloul lui docker inspect) -> refuzat + const rau = path.join(T, 'rau.json'); fs.writeFileSync(rau, JSON.stringify({ items: [] })); + cer('CONTROL: intrare care nu e docker inspect -> refuzata (cod 2)', side(['scan', '--source', 'docker', '--input', rau, '--log', path.join(T, 'rau.log')]).cod === 2); +} catch (e) { console.log(` [RAU ] proba a cazut: ${e.message}`); rele++; } +finally { fs.rmSync(T, { recursive: true, force: true }); } +console.log(`\nB3 sidecar (verify layer): ${bune}/${bune + rele + sarite} cum trebuia${sarite ? `, ${sarite} SARITE` : ''} (sidecar: ${SIDE})`); +process.exitCode = rele ? 1 : sarite ? 2 : 0; diff --git a/verify-layer/sidecar.mjs b/verify-layer/sidecar.mjs new file mode 100644 index 0000000..a447bcc --- /dev/null +++ b/verify-layer/sidecar.mjs @@ -0,0 +1,344 @@ +#!/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] -> 0 intreg (si continua capul atestat), 1 rupt sau necontinuat +// sidecar attest-head --log p [--out f] -> capul lantului ca plic AIP-23 aere-audit-head +// 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) { + 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)' }; + 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 }; +} + +// ---- 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'); + if (af) { + let cap; + try { cap = JSON.parse(fs.readFileSync(af, 'utf8')); } catch (e) { console.log(`cannot read the attestation ${af}: ${e.message}`); return 2; } + const r = verificaFataDeCap(intrari, cap); + if (r.ok) { console.log(`log CONTINUES the attested head: its first ${r.count} entries are the attested ones, ${r.extra} written after`); 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 }; + const plic = { v: 1, kind: 'aere-audit-head-attestation', statement, statementHash: sha256(Buffer.from(JSON.stringify(statement), 'utf8')) }; + 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 '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] sidecar attest-head --log p --out f'); + 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; }); +}