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:
parent
2a2ed74d32
commit
3465550e91
11
README.md
11
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=<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
|
||||
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).
|
||||
|
||||
21
proof-kinds/README.md
Normal file
21
proof-kinds/README.md
Normal 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.
|
||||
75
proof-kinds/proba-proof-kinds.mjs
Normal file
75
proof-kinds/proba-proof-kinds.mjs
Normal 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);
|
||||
86
proof-kinds/proof-kinds.mjs
Normal file
86
proof-kinds/proof-kinds.mjs
Normal 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
105
verify-layer/README.md
Normal 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.
|
||||
79
verify-layer/control-negativ-sidecar.mjs
Normal file
79
verify-layer/control-negativ-sidecar.mjs
Normal 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;
|
||||
3
verify-layer/dovezi-notarizare-testnet/audit.log
Normal file
3
verify-layer/dovezi-notarizare-testnet/audit.log
Normal 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"}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
8
verify-layer/package.json
Normal file
8
verify-layer/package.json
Normal 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" }
|
||||
}
|
||||
101
verify-layer/proba-notarizare-testnet.mjs
Normal file
101
verify-layer/proba-notarizare-testnet.mjs
Normal 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;
|
||||
191
verify-layer/proba-sidecar.mjs
Normal file
191
verify-layer/proba-sidecar.mjs
Normal 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
344
verify-layer/sidecar.mjs
Normal 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; });
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user