126 lines
12 KiB
JavaScript
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')) };
|
|
}
|