// AERE Identity, conformitatea fara supraveghere (roadmap master punctul 13, pista B, 2026-09-30): o POLITICA de conformitate a // verificatorului (ce emitenti crede, ce afirmatii cere: varsta, jurisdictie, nivel de verificare, acreditare), judecata pe o // prezentare de credential (identity.mjs), si o INREGISTRARE a judecatii ca plic AIP-23 `compliance` care NU poarta date personale: // numai hash-ul politicii, un pseudonim al detinatorului legat de verificator si digestul dovezii. Verificatorul pastreaza plicul // (notarizabil: momentul il da lantul), nu datele; datele raman la detinator si se arata numai cat cere politica. // // Ce NU este: nu e o dovada cu cunoastere zero (ce se arata se arata intreg; "peste 18" e o afirmatie emisa de emitent, nu calculata // din data nasterii fara s-o vada nimeni); nu e o verificare AML sau de sanctiuni (nu consulta nicio lista); nu face pe nimeni conform // cu vreo lege: spune ca o prezentare anume a indeplinit o politica anume, la un moment anume, judecata de verificator. // // O politica e { v:1, kind:'aere-compliance-policy', id, trustedIssuers:[id], requireStatus, maxAgeS, require:[regula] }, cu regula // { claim, equals } | { claim, in:[...] } | { claim, notIn:[...] } | { claim, atLeast:number } | { claim, present:true }. Hash-ul ei // se recalculeaza din forma canonica, deci o politica mai laxa nu poate trece sub hash-ul uneia stricte. import crypto from 'node:crypto'; import { verifyPresentation, canonical, idOf } from './identity.mjs'; const sha = (s) => '0x' + crypto.createHash('sha256').update(Buffer.from(s, 'utf8')).digest('hex'); const ID = /^aere-id:[0-9a-f]{40}$/; const OPERATORI = ['equals', 'in', 'notIn', 'atLeast', 'present']; /** Forma normala a unei politici si hash-ul ei; refuza campuri necunoscute si reguli fara inteles. */ export function definePolicy(p) { const chei = ['v', 'kind', 'id', 'trustedIssuers', 'requireStatus', 'maxAgeS', 'require']; const necunoscute = Object.keys(p || {}).filter((k) => !chei.includes(k)); if (necunoscute.length) throw new Error('compliance policy: unknown field ' + necunoscute.join(', ')); if (typeof p.id !== 'string' || !p.id) throw new Error('compliance policy: id is required'); if (!Array.isArray(p.trustedIssuers) || !p.trustedIssuers.length) throw new Error('compliance policy: trustedIssuers is required (a policy that trusts anyone proves nothing)'); const emitenti = [...new Set(p.trustedIssuers.map((x) => (typeof x === 'string' && ID.test(x) ? x : idOf(x))))].sort(); if (!Array.isArray(p.require) || !p.require.length) throw new Error('compliance policy: require is a non-empty list of rules'); const reguli = p.require.map((r) => { const op = OPERATORI.filter((o) => Object.hasOwn(r, o)); const extra = Object.keys(r).filter((k) => k !== 'claim' && !OPERATORI.includes(k)); if (typeof r.claim !== 'string' || !r.claim || op.length !== 1 || extra.length) throw new Error('compliance policy: a rule is { claim, }'); const o = op[0], v = r[o]; if ((o === 'in' || o === 'notIn') && (!Array.isArray(v) || !v.length)) throw new Error(`compliance policy: ${o} needs a non-empty list`); if (o === 'atLeast' && !Number.isFinite(v)) throw new Error('compliance policy: atLeast needs a number'); if (o === 'present' && v !== true) throw new Error('compliance policy: present must be true'); canonical(v); return { claim: r.claim, [o]: (o === 'in' || o === 'notIn') ? [...v].sort((a, b) => (canonical(a) < canonical(b) ? -1 : 1)) : v }; }).sort((a, b) => (canonical(a) < canonical(b) ? -1 : 1)); const maxAgeS = p.maxAgeS == null ? 300 : Number(p.maxAgeS); if (!Number.isInteger(maxAgeS) || maxAgeS < 1 || maxAgeS > 3600) throw new Error('compliance policy: maxAgeS must be 1..3600'); const policy = { v: 1, kind: 'aere-compliance-policy', id: p.id, trustedIssuers: emitenti, requireStatus: p.requireStatus !== false, maxAgeS, require: reguli }; return { policy, policyHash: sha(canonical(policy)) }; } function regula(r, claims) { const are = Object.hasOwn(claims, r.claim), v = claims[r.claim]; if (Object.hasOwn(r, 'present')) return are; if (!are) return false; if (Object.hasOwn(r, 'equals')) return canonical(v) === canonical(r.equals); if (Object.hasOwn(r, 'in')) return r.in.some((x) => canonical(x) === canonical(v)); if (Object.hasOwn(r, 'notIn')) return !r.notIn.some((x) => canonical(x) === canonical(v)); if (Object.hasOwn(r, 'atLeast')) return typeof v === 'number' && Number.isFinite(v) && v >= r.atLeast; return false; } /** * Judeca o prezentare dupa o politica. Cere: prezentarea VALIDA, legata de publicul si nonce-ul verificatorului, emitentul intre cei * ai politicii, starea credentialului JUDECATA (nu doar "nerevocat pentru ca nu stiu") cand politica o cere, si fiecare regula. * Intoarce { compliant, reasons, policyHash, presentation } fara afirmatiile detinatorului (le are verificatorul in `presentation.claims` * numai daca le cere explicit, cu `keepClaims`). */ export function checkCompliance(p, politica, { audience, nonce, now = new Date(), statusLists = [], revocations = [], keepClaims = false } = {}) { const { policy, policyHash } = definePolicy(politica); if (typeof audience !== 'string' || !audience || typeof nonce !== 'string' || !nonce) throw new Error('compliance: the verifier\'s audience and nonce are required (without them a presentation can be replayed)'); const v = verifyPresentation(p, { audience, nonce, now, trustedIssuers: policy.trustedIssuers, statusLists, revocations, maxAgeS: policy.maxAgeS }); const motive = []; if (!v.valid) motive.push('the presentation is not valid: ' + v.rows.filter((r) => r.pass === false).map((r) => r.name + (r.detail ? ' (' + r.detail + ')' : '')).join('; ')); const status = v.rows.find((r) => /^credential: (not revoked|status)/.test(r.name)); if (policy.requireStatus && !(status && status.pass === true)) motive.push('the policy requires the credential status to be judged: ' + (status ? status.detail || 'not judged' : 'no status row')); const claims = v.claims || {}; for (const r of policy.require) if (!regula(r, claims)) motive.push(`rule not met: ${canonical(r)}`); const holder = p && p.credential && p.credential.statement && p.credential.statement.holder ? p.credential.statement.holder.id : null; // B-29 (2026-09-30, revizuirea API-ului): digestul e al LEGATURII semnate de cel care prezinta (ea numeste prin hash credentialul intreg, // dezvaluirile, lantul de delegare, publicul, nonce-ul si momentul), nu al obiectului intreg: un camp de sus nesemnat, adaugat de // oricine, schimba obiectul fara sa schimbe nimic semnat, deci digestul vechi nu numea unic prezentarea. Semnatura nu intra: ML-DSA // semneaza aleator, deci aceeasi legatura poate avea oricate semnaturi valide. Fara legatura (prezentare stricata), obiectul intreg. const legatura = p && typeof p === 'object' && p.binding && typeof p.binding === 'object' && !Array.isArray(p.binding) ? p.binding : null; return { compliant: !motive.length, reasons: motive, policyHash, policyId: policy.id, holder, notJudged: v.notJudged, presentationHash: sha(canonical(legatura || p || null)), ...(keepClaims && !motive.length ? { claims } : {}) }; } /** * Inregistrarea judecatii ca plic AIP-23 `compliance` (proof-kinds), FARA date personale: subject = un pseudonim al detinatorului * legat de verificator, policy = hash-ul politicii, result = compliant | not-compliant, evidence = digestul prezentarii (verificatorul * o poate pastra sau sterge; digestul ramane). Pseudonimul: fara `pseudonymKey`, sha256 peste id-ul detinatorului si public, pe care il * poate reface oricine stie id-ul si publicul (tine id-ul afara din inregistrare, nu il ascunde de ei); cu `pseudonymKey` (un secret al * verificatorului), HMAC-SHA256, pe care numai verificatorul il poate reface. */ export function complianceEnvelope(rez, { audience, createdAt = new Date().toISOString(), buildProof, pseudonymKey = null }) { if (!rez || !rez.policyHash || !rez.presentationHash) throw new Error('compliance: a result of checkCompliance is required'); if (typeof audience !== 'string' || !audience) throw new Error('compliance: the audience names the verifier the pseudonym is bound to'); const intrare = `aere-compliance-subject|${rez.holder}|${audience}`; const pseudonim = 'pseudonym:' + (pseudonymKey ? crypto.createHmac('sha256', pseudonymKey).update(intrare).digest('hex') : sha(intrare).slice(2)).slice(0, 40); return buildProof('compliance', { subject: pseudonim, policy: rez.policyHash, result: rez.compliant ? 'compliant' : 'not-compliant', evidenceHash: rez.presentationHash, createdAt }); }