aere-quantum/proof-kinds/proof-kinds.mjs

126 lines
12 KiB
JavaScript

'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'))));
// VOCABULARUL unui fel (2026-10-18, B8): `valori: { camp: [predicat, ce spune] }`, judecat de buildProof pe fiecare camp prezent in
// declaratie (un camp lipsa il judeca `req`). Un mecanism mic si general; felurile fara `valori` nu se schimba cu nimic.
// Operatiile si verdictele unui acces la un secret, intr-un singur loc (cloud-gateway/pq-secrets cere, intr-o proba, ca OPERATII lui sa
// fie exact aceasta lista).
export const SECRET_ACCESS = Object.freeze({ operations: Object.freeze(['put', 'get', 'metadata', 'rotate', 'revoke-version', 'grant', 'revoke', 'list']), verdicts: Object.freeze(['allowed', 'refused', 'failed']) });
const NUME_IN_SERVICIU = /^[a-z0-9][a-z0-9_-]{0,63}$/;
const eRegula = (r) => typeof r === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(r);
const eGrant = (g) => typeof g === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/.test(g);
const eIdSauNull = (v) => v === null || (typeof v === 'string' && /^aere-id:[0-9a-f]{40}$/.test(v));
// 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'] },
// 2026-10-18 (roadmap pista B, B8, linia Shield): un acces la un secret (cloud-gateway/pq-secrets): ce serviciu (audienta), ce secret, ce
// versiune, ce operatie, verdictul (allowed | refused) cu regulile care au refuzat, grantul care a permis, detinatorul si prezentatorul
// (id-uri aere-id, derivate din chei), digestul PREZENTARII (al legaturii semnate: ea numeste prin hash credentialul, dezvaluirile, lantul
// de delegare, publicul, nonce-ul si momentul) si al politicii grantului. NICIODATA valoarea secretului, nici un digest al ei (un digest
// al unei valori ghicibile nu o ascunde).
// 2026-10-18 (revizuirea adversariala B8): `requestHash` (digestul cererii pentru care detinatorul a semnat prezentarea, deja un hash),
// `claimedHolder` / `claimedPresenter` (id-urile PRETINSE de o prezentare a carei semnatura nu a fost verificata; `holder` si `presenter`
// poarta numai id-uri verificate), `judged` (pe un refuz: fiecare grant judecat, cu regulile pe care a refuzat) si VOCABULARUL valorilor
// (`valori`): un plic secret-access cu verdictul "banana" sau operatia "fly" nu se mai poate construi. Campurile noi lipsesc din plicurile
// de dinainte, deci acelea raman octet cu octet aceleasi.
'secret-access': { digest: { presentationHash: 'presentation', policyHash: 'policy' }, hash: ['requestHash'], plain: { service: 'service', secret: 'secret', version: 'version', operation: 'operation', verdict: 'verdict', rules: 'rules', grant: 'grant', holder: 'holder', presenter: 'presenter', claimedHolder: 'claimedHolder', claimedPresenter: 'claimedPresenter', judged: 'judged' }, req: ['service', 'operation', 'verdict', 'presentationHash'],
valori: {
service: [(v) => typeof v === 'string' && v.length >= 1 && v.length <= 256 && /^[\x21-\x7e]+$/.test(v), '1 to 256 printable ASCII characters without spaces (the audience of the service)'],
secret: [(v) => v === null || v === '*' || (typeof v === 'string' && NUME_IN_SERVICIU.test(v)), 'a secret name (^[a-z0-9][a-z0-9_-]{0,63}$), "*" for list, or null'],
version: [(v) => v === null || (Number.isSafeInteger(v) && v >= 1), 'an integer >= 1, or null'],
operation: [(v) => SECRET_ACCESS.operations.includes(v), SECRET_ACCESS.operations.join(' | ')],
verdict: [(v) => SECRET_ACCESS.verdicts.includes(v), SECRET_ACCESS.verdicts.join(' | ') + ' (failed: allowed and entered, then the change could not be stored)'],
rules: [(v) => Array.isArray(v) && v.length <= 128 && v.every(eRegula), 'a list of rule names (^[a-z][a-z0-9-]{0,63}$), at most 128'],
grant: [(v) => v === null || eGrant(v), 'a grant id (^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$), or null'],
holder: [eIdSauNull, 'an aere-id (aere-id: + 40 hex) verified from the presentation, or null'],
presenter: [eIdSauNull, 'an aere-id verified from the presentation, or null'],
claimedHolder: [eIdSauNull, 'an aere-id the presentation claims and nobody verified, or null'],
claimedPresenter: [eIdSauNull, 'an aere-id the presentation claims and nobody verified, or null'],
judged: [(v) => Array.isArray(v) && v.length <= 64 && v.every((x) => x && typeof x === 'object' && !Array.isArray(x) && Object.keys(x).sort().join(',') === 'grant,rules' && eGrant(x.grant) && Array.isArray(x.rules) && x.rules.length <= 64 && x.rules.every(eRegula)), 'a list of { grant, rules }, at most 64'],
createdAt: [(v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/.test(v) && !Number.isNaN(Date.parse(v)), 'an RFC 3339 time'],
} },
};
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 + '"');
}
for (const [camp, [bun, spune]] of Object.entries(s.valori || {})) {
if (statement[camp] !== undefined && !bun(statement[camp])) throw new Error('proof-kinds: ' + kind + ' field "' + camp + '" has a value outside its vocabulary (' + spune + ')');
}
const text = JSON.stringify(statement); // canonic AIP-23
return { v: 1, kind: 'aere-proof-of-' + kind + '-attestation', statement, statementHash: sha256(Buffer.from(text, 'utf8')) };
}