Add verify-layer (an audit-log sidecar whose head can be notarized on Aere Network for post-quantum finality) and proof-kinds (the AIP-23 envelope builder it uses)

This commit is contained in:
Aere Network 2026-09-29 18:09:34 +03:00
parent 2a2ed74d32
commit 3465550e91
12 changed files with 1041 additions and 3 deletions

View File

@ -1,7 +1,8 @@
# Aere Quantum # Aere Quantum
Self-hosted post-quantum infrastructure from Aere Network. Four components, each a few files with **no dependencies**: Self-hosted post-quantum infrastructure from Aere Network. Each component is a few files with **no dependencies**: Node.js 24
Node.js 24 and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry. 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 | | 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-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 | | [`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 | | [`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. 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-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`) | | 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) | | 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=<verify-proof.mjs from aere-node> 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 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 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 ## 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).

21
proof-kinds/README.md Normal file
View File

@ -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=<path to verify-proof.mjs>` (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.

View File

@ -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);

View File

@ -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-<kind>-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')) };
}

105
verify-layer/README.md Normal file
View File

@ -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 <head.json>` 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 <file> --attested 0x<sha256> [--host h] [--log p] [--at T]
node sidecar.mjs record --kind deployment --name <name> --version <v> --content-file <file> [--host h] [--log p] [--at T]
node sidecar.mjs record --kind proof --proof-file <envelope.json> # any AIP-23 envelope, after checking its form and hash
node sidecar.mjs scan --source docker|kubernetes --input <export.json> [--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 <url> --key-file <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 (`<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
(`<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=<hex>` or `PRIVATE_KEY=0x<hex>`) 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 <testnet key>` 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.

View File

@ -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;

View File

@ -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"}

View File

@ -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
}
}

View File

@ -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" }
}

View File

@ -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 <CHEIE-DEZVOLTATOR-TESTNET.key>
// 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 <cheia de dezvoltator a testnetului>'); 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) + '..' : '<hex>')).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;

View File

@ -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;

344
verify-layer/sidecar.mjs Normal file
View File

@ -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 <cap>` 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 <export.json>'); 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, '<hex>').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=<hex> or PRIVATE_KEY=0x<hex>'); 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, '<hex>')); process.exitCode = 1; });
}