// Proba AERE Identity: fiecare afirmatie cu perechea ei negativa, si fiecare atac al revizuirii adversariale ca proba numita. // Offline. Plicurile AIP-23 se judeca cu verificatorul de referinta (AERE_VERIFY_PROOF=, sau, in depozitul de // dezvoltare, ../aere-proof-protocol/verify.mjs); fara el, acele probe ies NEMASURATE si codul de iesire e 2. // node proba-identity.mjs iesire 0 = toate cum trebuia, 1 = o proba rosie, 2 = verde dar cu probe nemasurate import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import zlib from 'node:zlib'; import crypto from 'node:crypto'; import { execFileSync, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import * as I from './identity.mjs'; import { buildProof } from '../proof-kinds/proof-kinds.mjs'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const VERIFY = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs'); let treceri = 0, sarite = 0; const esecuri = []; // o proba care intoarce 'SARIT' nu a masurat nimic: nu se numara nici trecuta, nici picata (se numara in `sarite`) function test(nume, fn) { try { if (fn() === 'SARIT') return; treceri++; console.log(' OK ' + nume); } catch (e) { esecuri.push(nume); console.log(' RAU ' + nume + ' -- ' + (e.message || e)); } } const cere = (c, m) => { if (!c) throw new Error(m); }; const arunca = (f) => { try { f(); return null; } catch (e) { return e.message; } }; const clon = (o) => JSON.parse(JSON.stringify(o)); const fals = (r) => r.rows.filter((x) => x.pass === false).map((x) => x.name + (x.detail ? ': ' + x.detail : '')).join(' | '); const rand = (r, re) => r.rows.find((x) => re.test(x.name)); const picaPe = (r, re) => !r.valid && r.rows.some((x) => x.pass === false && re.test(x.name)); const NOW = new Date('2026-09-30T06:00:00Z'); const AUD = 'https://shop.example', NONCE = 'n-7f3a'; const iss = I.generateKeys(), hol = I.generateKeys(), phone = I.generateKeys(), sess = I.generateKeys(), strain = I.generateKeys(), iss2 = I.generateKeys(); const CLAIMS = { employer: 'Example Ltd', name: 'Ana Pop', birthdate: '1990-01-01', age_over_18: true, role: 'engineer' }; const { credential, disclosures } = I.issueCredential({ issuer: iss, holder: hol.public, type: 'EmployeeCredential', claims: CLAIMS, disclosable: ['name', 'birthdate', 'age_over_18', 'role'], validUntil: '2027-09-30T00:00:00Z', status: { list: 'urn:example:status:1', index: 42 }, decoys: 3, now: NOW }); const LISTA = I.createStatusList({ issuer: iss, id: 'urn:example:status:1', revoked: [7], validUntil: '2026-10-07T00:00:00Z', now: NOW }); const toate = { audience: AUD, nonce: NONCE, now: NOW, trustedIssuers: [iss.id], statusLists: [LISTA] }; const prez = (o = {}) => I.present({ credential, disclosures, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW, ...o }); // resemneaza legatura unei prezentari modificate (ca atacatorul sa nu fie prins de semnatura, ci de regula masurata) const resemneaza = (p, cheie = hol) => { p.binding.credentialHash = I.credentialHash(p.credential); p.binding.disclosuresHash = '0x' + crypto.createHash('sha256').update(I.canonical(p.disclosures)).digest('hex'); p.binding.delegations = p.delegations.map(I.delegationHash); p.signature = I.signText('presentation', I.canonical(p.binding), cheie); return p; }; // un emitent rau-intentionat: resemneaza declaratia credentialului dupa ce o schimba const reemite = (c, cheie = iss) => { c.signature = I.signText('credential', I.canonical(c.statement), cheie); return c; }; // ---------------------------------------------------------------- drumul bun test('detinatorul arata numai age_over_18: VALID, afirmatiile = cele in clar + cea aratata, nimic nejudecat', () => { const r = I.verifyPresentation(prez(), toate); cere(r.valid && r.notJudged === 0, fals(r) || 'nejudecate ' + r.notJudged); cere(JSON.stringify(r.claims) === '{"age_over_18":true,"employer":"Example Ltd"}', JSON.stringify(r.claims)); }); test('momelile: sd are cate un digest pentru fiecare afirmatie dezvaluibila plus 3 momeli, si nimic nu arata care e care', () => { cere(credential.statement.sd.length === 7 && disclosures.length === 4, `${credential.statement.sd.length} digesturi, ${disclosures.length} dezvaluiri`); cere(!('name' in credential.statement.claims) && !JSON.stringify(credential.statement).includes('Ana Pop'), 'numele e in clar in credential'); }); test('fara emitenti de incredere, public si nonce: VALID, dar cele trei randuri sunt NEJUDECATE, spuse, nu trecute', () => { const r = I.verifyPresentation(prez(), { now: NOW, statusLists: [LISTA] }); cere(r.valid && r.notJudged === 3 && rand(r, /issuer trusted/).pass === null && rand(r, /audience/).pass === null && rand(r, /nonce/).pass === null, JSON.stringify(r.rows.filter((x) => x.pass !== true))); }); // ---------------------------------------------------------------- dezvaluirile test('ATAC: o dezvaluire fabricata (acelasi nume, alta sare) -> INVALID, nu e semnata de emitent', () => { const p = prez(); p.disclosures = [Buffer.from(JSON.stringify([crypto.randomBytes(16).toString('base64url'), 'age_over_18', true])).toString('base64url')]; const r = I.verifyPresentation(resemneaza(p), toate); cere(picaPe(r, /signed by the issuer/), fals(r) || 'trecut'); }); test('ATAC: valoarea unei dezvaluiri schimbata (role=admin) -> INVALID', () => { const p = prez({ reveal: ['role'] }); const [s] = JSON.parse(Buffer.from(p.disclosures[0], 'base64url').toString()); p.disclosures = [Buffer.from(JSON.stringify([s, 'role', 'admin'])).toString('base64url')]; const r = I.verifyPresentation(resemneaza(p), toate); cere(picaPe(r, /signed by the issuer/), fals(r) || 'trecut'); }); test('ATAC: aceeasi dezvaluire de doua ori -> INVALID', () => { const p = prez(); p.disclosures = [p.disclosures[0], p.disclosures[0]]; const r = I.verifyPresentation(resemneaza(p), toate); cere(picaPe(r, /shown once/), fals(r) || 'trecut'); }); test('ATAC: un emitent semneaza in sd o afirmatie pe care o are si in clar (doua valori pentru acelasi nume) -> INVALID', () => { const c = clon(credential); const d = Buffer.from(JSON.stringify([crypto.randomBytes(16).toString('base64url'), 'employer', 'Other Ltd'])).toString('base64url'); c.statement.sd = [...c.statement.sd, I.digestOf(d)].sort(); reemite(c); // present() nu o arata (afirmatia e in clar), deci prezentarea se face de mana, cum ar face-o atacatorul const p = prez(); p.credential = c; p.disclosures = [d]; const r = I.verifyPresentation(resemneaza(p), toate); cere(picaPe(r, /does not cover a plain claim/), fals(r) || 'trecut'); }); test('ATAC: un digest de doua ori in sd -> INVALID', () => { const c = clon(credential); c.statement.sd = [...c.statement.sd, c.statement.sd[0]].sort(); reemite(c); const p = I.present({ credential: c, disclosures, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW }); const r = I.verifyPresentation(p, toate); cere(picaPe(r, /digests it signs are distinct/), fals(r) || 'trecut'); }); test('ATAC: o dezvaluire cu numele __proto__ -> refuzata; emiterea cu o asemenea afirmatie -> refuzata', () => { const p = prez(); p.disclosures = [Buffer.from(JSON.stringify([crypto.randomBytes(16).toString('base64url'), '__proto__', { valid: true }])).toString('base64url')]; const r = I.verifyPresentation(resemneaza(p), toate); cere(picaPe(r, /readable/), fals(r) || 'trecut'); const m = arunca(() => I.issueCredential({ issuer: iss, holder: hol.public, type: 'T', claims: JSON.parse('{"__proto__": 1}'), validUntil: '2027-01-01T00:00:00Z', now: NOW })); cere(/not allowed/.test(m || ''), 'emiterea: ' + m); }); test('ATAC: o dezvaluire in alta codare base64url (cu umplutura, sau biti de coada schimbati) -> refuzata', () => { const e = prez({ reveal: ['role'] }).disclosures[0]; // 44 de octeti: base64url cu biti de coada liberi (45 n-ar avea) cere(/canonical|not base64url/.test(arunca(() => I.decodeDisclosure(e + '=')) || ''), 'cu umplutura trecea'); // bitii de coada sunt bitii de JOS ai ultimului caracter: acelasi caracter cu ei schimbati da aceiasi octeti const AB = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', ult = e[e.length - 1]; const alt = [1, 2, 3].map((k) => AB[AB.indexOf(ult) ^ k]).find((x) => Buffer.from(e.slice(0, -1) + x, 'base64url').equals(Buffer.from(e, 'base64url'))); cere(alt, 'nicio alta codare a acelorasi octeti: proba nu masoara nimic'); cere(/canonical/.test(arunca(() => I.decodeDisclosure(e.slice(0, -1) + alt)) || ''), 'bitii de coada schimbati treceau'); }); // ---------------------------------------------------------------- legatura prezentarii test('ATAC: reluata la alt verificator (alt public) -> INVALID; cu alt nonce -> INVALID', () => { const r1 = I.verifyPresentation(prez(), { ...toate, audience: 'https://other.example' }); cere(picaPe(r1, /made for/), fals(r1) || 'trecut'); const r2 = I.verifyPresentation(prez(), { ...toate, nonce: 'alt' }); cere(picaPe(r2, /nonce/), fals(r2) || 'trecut'); }); test('ATAC: prezentare veche (301 s) sau din viitor (301 s) -> INVALID', () => { const r1 = I.verifyPresentation(prez(), { ...toate, now: new Date(NOW.getTime() + 301000) }); cere(picaPe(r1, /within 300 s/), fals(r1) || 'veche trecuta'); const r2 = I.verifyPresentation(prez(), { ...toate, now: new Date(NOW.getTime() - 301000) }); cere(picaPe(r2, /within 300 s/), fals(r2) || 'viitoare trecuta'); }); test('ATAC: un strain prezinta credentialul detinatorului cu cheile lui -> INVALID', () => { const p = prez(); p.binding.presenter = { id: strain.id, keys: strain.public }; const r = I.verifyPresentation(resemneaza(p, strain), toate); cere(picaPe(r, /presented by the holder/), fals(r) || 'trecut'); }); test('ATAC: dezvaluiri adaugate dupa semnare -> INVALID', () => { const p = prez(); p.disclosures = [...p.disclosures, disclosures.find((d) => I.decodeDisclosure(d).name === 'name')]; const r = I.verifyPresentation(p, toate); cere(picaPe(r, /exactly these disclosures/), fals(r) || 'trecut'); }); test('ATAC: semnatura de DELEGARE a detinatorului peste textul legaturii pusa drept semnatura de prezentare -> INVALID (separarea de domeniu)', () => { const p = prez(); p.signature = I.signText('delegation', I.canonical(p.binding), hol); const r = I.verifyPresentation(p, toate); cere(picaPe(r, /signed by the presenter/), fals(r) || 'trecut'); }); // ---------------------------------------------------------------- semnatura hibrida si identitatea test('ATAC: partea ML-DSA a semnaturii emitentului facuta cu alta cheie -> INVALID (amandoua cerute)', () => { const c = clon(credential); c.signature.mldsa65 = I.signText('credential', I.canonical(c.statement), iss2).mldsa65; const r = I.verifyPresentation(I.present({ credential: c, disclosures, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW }), toate); cere(picaPe(r, /signed by aere-id/), fals(r) || 'trecut'); }); test('ATAC: partea Ed25519 lipsa sau alg retrogradat la ml-dsa-65 -> INVALID', () => { for (const f of [(c) => { delete c.signature.ed25519; }, (c) => { c.signature.alg = 'ml-dsa-65'; }]) { const c = clon(credential); f(c); const r = I.verifyPresentation(I.present({ credential: c, disclosures, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW }), toate); cere(picaPe(r, /signed by aere-id/), fals(r) || 'trecut'); } }); test('ATAC: id-ul emitentului schimbat cu id-ul unui emitent de incredere (cheile raman ale lui) -> INVALID', () => { const c = clon(credential); c.statement.issuer.id = iss2.id; reemite(c); const r = I.verifyPresentation(I.present({ credential: c, disclosures, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW }), { ...toate, trustedIssuers: [iss2.id] }); cere(picaPe(r, /issuer id is the id of its keys/), fals(r) || 'trecut'); }); test('ATAC: o cheie ML-DSA pusa in campul ed25519 (confuzie de tip) -> refuzata', () => { cere(/is a ml-dsa-65 key/.test(arunca(() => I.idOf({ alg: I.ALG, ed25519: hol.public.mldsa65, mldsa65: hol.public.mldsa65 })) || ''), 'acceptata'); }); test('CONTROL: emitent in afara celor de incredere -> INVALID; credential expirat sau inca nevalabil -> INVALID', () => { const r1 = I.verifyPresentation(prez(), { ...toate, trustedIssuers: [iss2.id] }); cere(picaPe(r1, /issuer trusted/), fals(r1) || 'trecut'); const later = new Date('2027-10-01T00:00:00Z'); const r2 = I.verifyPresentation(I.present({ credential, disclosures, reveal: [], presenter: hol, audience: AUD, nonce: NONCE, now: later }), { ...toate, now: later }); cere(picaPe(r2, /valid at/), fals(r2) || 'expirat trecut'); const early = new Date('2026-09-29T00:00:00Z'); const r3 = I.verifyPresentation(I.present({ credential, disclosures, reveal: [], presenter: hol, audience: AUD, nonce: NONCE, now: early }), { ...toate, now: early }); cere(picaPe(r3, /valid at/), fals(r3) || 'inainte trecut'); }); test('cheile: exportKeys -> importKeys pastreaza id-ul; un fisier cu partea publica a altcuiva e refuzat', () => { const j = I.exportKeys(hol); cere(I.importKeys(clon(j)).id === hol.id, 'id schimbat'); const r = clon(j); r.public = strain.public; cere(/not those of its private keys/.test(arunca(() => I.importKeys(r)) || ''), 'acceptat'); cere(!JSON.stringify(hol).includes('PRIVATE') && !('privat' in JSON.parse(JSON.stringify(hol))), 'partea privata iese la JSON.stringify'); }); // ---------------------------------------------------------------- lista de stare test('lista de stare: bitul 42 nepus -> nerevocat; o lista cu 42 pus -> INVALID ("the issuer revoked")', () => { const rev = I.createStatusList({ issuer: iss, id: 'urn:example:status:1', revoked: [42], validUntil: '2026-10-07T00:00:00Z', now: NOW }); const r = I.verifyPresentation(prez(), { ...toate, statusLists: [rev] }); cere(picaPe(r, /not revoked/), fals(r) || 'trecut'); const r2 = I.verifyPresentation(prez(), { ...toate, statusLists: [rev, LISTA] }); cere(picaPe(r2, /not revoked/), 'cu doua liste, cea care revoca a pierdut: ' + fals(r2)); }); test('lista de stare: bitul 0 e bitul cel mai semnificativ al primului octet (W3C), numarat independent', () => { const l = I.createStatusList({ issuer: iss, id: 'x', size: 16, revoked: [0, 9], validUntil: '2026-10-07T00:00:00Z', now: NOW }); const b = zlib.gunzipSync(Buffer.from(l.statement.encodedList, 'base64url')); cere(b[0] === 0x80 && b[1] === 0x40 && I.statusBit(l, 0) && I.statusBit(l, 9) && !I.statusBit(l, 1), b.toString('hex')); }); test('ATAC: o lista cu acelasi id semnata de ALT emitent (bitul nepus) -> nu e luata: NEJUDECAT, nu "nerevocat"', () => { const alta = I.createStatusList({ issuer: iss2, id: 'urn:example:status:1', revoked: [], validUntil: '2026-10-07T00:00:00Z', now: NOW }); const r = I.verifyPresentation(prez(), { ...toate, statusLists: [alta] }); const x = rand(r, /status in/); cere(x && x.pass === null && /not signed by the issuer/.test(x.detail), JSON.stringify(x)); }); test('lista expirata sau lipsa -> NEJUDECAT, spus', () => { const r = I.verifyPresentation(prez(), { ...toate, now: new Date('2026-10-08T00:00:00Z'), statusLists: [LISTA] }); cere(rand(r, /status in/) && rand(r, /status in/).pass === null && /fetch a current one/.test(rand(r, /status in/).detail), JSON.stringify(r.rows.filter((x) => x.pass !== true))); const r2 = I.verifyPresentation(prez(), { ...toate, statusLists: [] }); cere(rand(r2, /status in/).pass === null, 'lipsa trecuta'); }); test('ATAC: o lista care se decomprima peste marimea declarata (bomba gzip) -> refuzata, fara sa se umfle', () => { const l = clon(LISTA); l.statement.encodedList = zlib.gzipSync(Buffer.alloc(64 * 1024 * 1024)).toString('base64url'); l.signature = I.signText('status-list', I.canonical(l.statement), iss); const t0 = Date.now(); const r = I.verifyPresentation(prez(), { ...toate, statusLists: [l] }); cere(picaPe(r, /status in/) && /beyond its declared size/.test(fals(r)) && Date.now() - t0 < 5000, fals(r) || 'trecut'); const l2 = clon(LISTA); l2.statement.size = I.MAX_STATUS_BITS * 8; l2.signature = I.signText('status-list', I.canonical(l2.statement), iss); cere(/may not have/.test(arunca(() => I.statusBit(l2, 1)) || ''), 'marimea uriasa primita'); }); // ---------------------------------------------------------------- delegarea const CID = credential.statement.id; const d1 = I.delegate({ from: hol, to: phone.public, scope: { credentials: [CID], claims: ['age_over_18', 'role'], audiences: '*' }, notAfter: '2026-12-31T00:00:00Z', maxDepth: 1, now: NOW }); const d2 = I.delegate({ from: phone, to: sess.public, parent: d1, scope: { credentials: [CID], claims: ['age_over_18'], audiences: [AUD] }, notAfter: '2026-09-30T06:10:00Z', now: NOW }); const prezD = (lant = [d1, d2], cheie = sess, reveal = ['age_over_18'], o = {}) => I.present({ credential, disclosures, reveal, presenter: cheie, delegations: lant, audience: AUD, nonce: NONCE, now: NOW, ...o }); // o veriga facuta de mana (fara gardurile bibliotecii), semnata de cine o da: asa lucreaza un atacator const verigaDeMana = (from, to, parent, scope, extra = {}) => { const st = { v: 1, kind: 'aere-delegation', id: 'urn:uuid:' + crypto.randomUUID(), from: { id: from.id, keys: from.public }, to: { id: to.id, keys: to.public }, parent: parent ? I.delegationHash(parent) : null, scope, notBefore: '2026-09-30T05:00:00Z', notAfter: '2026-12-31T00:00:00Z', maxDepth: 0, issuedAt: '2026-09-30T05:00:00Z', ...extra }; return { statement: st, signature: I.signText('delegation', I.canonical(st), from) }; }; test('delegare: detinator -> telefon -> cheie de sesiune de 10 minute, arata age_over_18 la magazin: VALID', () => { const r = I.verifyPresentation(prezD(), toate); cere(r.valid && r.notJudged === 1 && rand(r, /revocations/).pass === null && r.claims.age_over_18 === true, fals(r) || JSON.stringify(r.rows.filter((x) => x.pass !== true))); }); test('ATAC: re-delegarea largeste scopul (claims "*" sub ["age_over_18","role"]) -> INVALID; biblioteca refuza sa o scrie', () => { const w = verigaDeMana(phone, sess, d1, { credentials: [CID], claims: '*', audiences: '*' }); const r = I.verifyPresentation(prezD([d1, w], sess, ['name']), toate); cere(picaPe(r, /only narrows/), fals(r) || 'trecut'); cere(/wider than the parent/.test(arunca(() => I.delegate({ from: phone, to: sess.public, parent: d1, scope: { credentials: [CID], claims: '*', audiences: '*' }, notAfter: '2026-10-01T00:00:00Z', now: NOW })) || ''), 'biblioteca a scris-o'); }); test('ATAC: lantul rupt (veriga 2 data de un strain, nu de telefon) -> INVALID', () => { const w = verigaDeMana(strain, sess, d1, d2.statement.scope); const r = I.verifyPresentation(prezD([d1, w]), toate); cere(picaPe(r, /given by the previous delegate/), fals(r) || 'trecut'); }); test('ATAC: prima veriga data de altcineva decat detinatorul credentialului -> INVALID', () => { const w = verigaDeMana(strain, sess, null, { credentials: '*', claims: '*', audiences: '*' }); const r = I.verifyPresentation(prezD([w]), toate); cere(picaPe(r, /given by the holder/), fals(r) || 'trecut'); }); test('ATAC: o veriga in numele telefonului semnata de un strain -> INVALID', () => { const st = clon(d2.statement); const w = { statement: st, signature: I.signText('delegation', I.canonical(st), strain) }; const r = I.verifyPresentation(prezD([d1, w]), toate); cere(picaPe(r, /signed by who gave it/), fals(r) || 'trecut'); }); test('ATAC: veriga-parinte numita gresit (parent = alt hash) -> INVALID', () => { const w = verigaDeMana(phone, sess, null, d2.statement.scope, { parent: '0x' + '11'.repeat(32) }); const r = I.verifyPresentation(prezD([d1, w]), toate); cere(picaPe(r, /names its parent link/), fals(r) || 'trecut'); }); test('ATAC: adancimea: d1 cu maxDepth 0 si inca o veriga dupa ea -> INVALID', () => { const d1z = verigaDeMana(hol, phone, null, d1.statement.scope, { maxDepth: 0 }); const w = verigaDeMana(phone, sess, d1z, d2.statement.scope); const r = I.verifyPresentation(prezD([d1z, w]), toate); cere(picaPe(r, /allows the links after it/), fals(r) || 'trecut'); }); test('ATAC: delegatul arata o afirmatie din afara scopului (role) -> INVALID; alt public -> INVALID; alt credential -> INVALID', () => { const r1 = I.verifyPresentation(prezD([d1, d2], sess, ['role']), toate); cere(picaPe(r1, /covers the disclosed claims/), fals(r1) || 'role trecut'); const r2 = I.verifyPresentation(prezD([d1, d2], sess, ['age_over_18'], { audience: 'https://other.example' }), { ...toate, audience: 'https://other.example' }); cere(picaPe(r2, /covers the audience/), fals(r2) || 'public trecut'); const w = verigaDeMana(hol, sess, null, { credentials: ['urn:uuid:alt'], claims: '*', audiences: '*' }); const r3 = I.verifyPresentation(prezD([w]), toate); cere(picaPe(r3, /covers this credential/), fals(r3) || 'credential trecut'); }); test('ATAC: veriga expirata (sesiunea de 10 minute, la minutul 11) sau inca nevalabila -> INVALID', () => { const t = new Date(NOW.getTime() + 11 * 60000); const r = I.verifyPresentation(prezD([d1, d2], sess, ['age_over_18'], { now: t }), { ...toate, now: t }); cere(picaPe(r, /valid at/), fals(r) || 'expirata trecuta'); const w = verigaDeMana(hol, sess, null, { credentials: '*', claims: '*', audiences: '*' }, { notBefore: '2026-10-01T00:00:00Z' }); const r2 = I.verifyPresentation(prezD([w]), toate); cere(picaPe(r2, /valid at/), fals(r2) || 'inainte trecuta'); }); test('ATAC: detinatorul prezinta singur dar lista lantul (prezentatorul nu e ultimul delegat) -> INVALID', () => { const r = I.verifyPresentation(prezD([d1, d2], hol), toate); cere(picaPe(r, /presented by the last delegate/), fals(r) || 'trecut'); }); test('ATAC: lantul scos sau inversat dupa semnare -> INVALID', () => { const p = prezD(); const p1 = clon(p); p1.delegations = []; const r1 = I.verifyPresentation(p1, toate); cere(picaPe(r1, /exactly this delegation chain/), fals(r1) || 'scos trecut'); const p2 = clon(p); p2.delegations = [p.delegations[1], p.delegations[0]]; const r2 = I.verifyPresentation(p2, toate); cere(picaPe(r2, /exactly this delegation chain/), fals(r2) || 'inversat trecut'); }); test('revocarea: detinatorul revoca veriga telefonului -> INVALID; revocarea unui strain -> ignorata si spusa; una din viitor -> inca nerevocat', () => { const rv = I.revokeDelegation({ by: hol, delegation: d1, now: NOW }); const r = I.verifyPresentation(prezD(), { ...toate, revocations: [rv] }); cere(picaPe(r, /not revoked/), fals(r) || 'trecut'); const rs = I.revokeDelegation({ by: strain, delegation: d1, now: NOW }); const r2 = I.verifyPresentation(prezD(), { ...toate, revocations: [rs] }); cere(r2.valid && r2.rows.some((x) => x.pass === null && /ignored/.test(x.detail)), fals(r2) || 'strainul a revocat'); const rf = I.revokeDelegation({ by: phone, delegation: d2, at: '2026-09-30T07:00:00Z', now: NOW }); const r3 = I.verifyPresentation(prezD(), { ...toate, revocations: [rf] }); cere(r3.valid, 'revocarea din viitor s-a aplicat acum: ' + fals(r3)); const r4 = I.verifyPresentation(prezD(), { ...toate, now: new Date('2026-09-30T07:00:01Z'), revocations: [rf] }); cere(picaPe(r4, /not revoked/), 'dupa momentul ei, revocarea nu s-a aplicat: ' + fals(r4)); }); test('ATAC: o revocare cu semnatura detinatorului dar alt scop (semnatura de delegare) -> ignorata', () => { const rv = I.revokeDelegation({ by: hol, delegation: d1, now: NOW }); rv.signature = I.signText('delegation', I.canonical(rv.statement), hol); const r = I.verifyPresentation(prezD(), { ...toate, revocations: [rv] }); cere(r.valid && r.rows.some((x) => x.pass === null && /ignored/.test(x.detail)), fals(r) || 'aplicata'); }); test('intrari stricate date verificatorului (lista fara chei, revocare cu o valoare necitibila): ignorate si spuse, nu INVALID; o cheie de incredere stricata e o eroare a lui', () => { const l = clon(LISTA); delete l.statement.issuer.keys; const r = I.verifyPresentation(prez(), { ...toate, statusLists: [l] }); cere(r.valid && rand(r, /status in/).pass === null, fals(r) || JSON.stringify(rand(r, /status in/))); const rv = I.revokeDelegation({ by: hol, delegation: d1, now: NOW }); rv.statement.reason = { x: undefined, y: 1 / 0 }; const r2 = I.verifyPresentation(prezD(), { ...toate, revocations: [rv] }); cere(r2.valid && r2.rows.some((x) => x.pass === null && /not readable/.test(x.detail)), fals(r2) || 'aplicata'); cere(/public keys must be/.test(arunca(() => I.verifyPresentation(prez(), { ...toate, trustedIssuers: [{ alg: 'x' }] })) || ''), 'cheia de incredere stricata primita'); }); test('o afirmatie in clar numita in reveal nu cere dezvaluire (se vede oricum)', () => { const r = I.verifyPresentation(I.present({ credential, disclosures, reveal: ['employer', 'age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW }), toate); cere(r.valid && r.claims.employer === 'Example Ltd' && r.claims.age_over_18 === true && !('name' in r.claims), fals(r) || JSON.stringify(r.claims)); }); // ---------------------------------------------------------------- plicurile AIP-23 test('plicurile AIP-23 (identity pentru credential, authorization pentru delegare) verifica la verificatorul de referinta; unul atins nu', () => { if (!fs.existsSync(VERIFY)) { sarite += 1; console.log(` SARIT plicurile AIP-23: verificatorul nu e la ${VERIFY}; AERE_VERIFY_PROOF=`); return 'SARIT'; } const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-id-')); try { const verdict = (o) => { const f = path.join(T, crypto.randomUUID() + '.json'); fs.writeFileSync(f, JSON.stringify(o)); const r = spawnSync(process.execPath, [VERIFY, f, '--json'], { encoding: 'utf8' }); try { return JSON.parse(r.stdout).verdict; } catch { return '?'; } }; const e1 = I.proofOfCredential(credential, buildProof), e2 = I.proofOfDelegation(d1, buildProof); cere(e1.statement.subjectId === hol.id && e2.statement.grantor === hol.id && e2.statement.grantee === phone.id, 'campurile plicurilor'); cere(verdict(e1) === 'VALID' && verdict(e2) === 'VALID', `${verdict(e1)} ${verdict(e2)}`); const rau = clon(e2); rau.statement.grantee = strain.id; cere(verdict(rau) !== 'VALID', 'plicul atins a iesit VALID'); } finally { fs.rmSync(T, { recursive: true, force: true }); } }); // ---------------------------------------------------------------- linia de comanda, cap la cap test('linia de comanda: keygen, pub, issue, status-list, delegate, present, verify (VALID 0, alt public 1, cheie suprascrisa 2)', () => { const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-id-cli-')); const CLI = path.join(AICI, 'identity-cli.mjs'); const run = (...a) => { const r = spawnSync(process.execPath, [CLI, ...a], { cwd: T, encoding: 'utf8' }); return { cod: r.status, out: (r.stdout || '') + (r.stderr || '') }; }; try { for (const n of ['iss', 'hol', 'dev']) cere(run('keygen', '--out', n + '.keys.json').cod === 0, 'keygen ' + n); cere(run('keygen', '--out', 'iss.keys.json').cod === 2, 'keygen a suprascris o cheie'); cere(run('pub', '--keys', 'hol.keys.json', '--out', 'hol.pub.json').cod === 0 && run('pub', '--keys', 'dev.keys.json', '--out', 'dev.pub.json').cod === 0, 'pub'); const iss1 = run('issue', '--issuer-keys', 'iss.keys.json', '--holder-pub', 'hol.pub.json', '--type', 'MemberCredential', '--claim', 'member=true', '--claim', 'tier=gold', '--claim', 'org=Example', '--disclosable', 'member,tier', '--status-list', 'urn:s:1', '--status-index', '5', '--out', 'cred.json'); cere(iss1.cod === 0, iss1.out); cere(run('status-list', '--issuer-keys', 'iss.keys.json', '--id', 'urn:s:1', '--out', 'list.json').cod === 0, 'status-list'); const cid = JSON.parse(fs.readFileSync(path.join(T, 'cred.json'), 'utf8')).credential.statement.id; cere(run('delegate', '--from-keys', 'hol.keys.json', '--to-pub', 'dev.pub.json', '--credentials', cid, '--claims', 'member', '--audiences', 'https://a.example', '--valid-minutes', '5', '--out', 'd.json').cod === 0, 'delegate'); cere(run('present', '--cred', 'cred.json', '--reveal', 'member', '--presenter-keys', 'dev.keys.json', '--delegation', 'd.json', '--audience', 'https://a.example', '--nonce', 'x1', '--out', 'p.json').cod === 0, 'present'); const issId = run('id', '--keys', 'iss.keys.json').out.trim(); const v = run('verify', '--presentation', 'p.json', '--audience', 'https://a.example', '--nonce', 'x1', '--trust-issuer', issId, '--status-list', 'list.json'); cere(v.cod === 0 && /VALID/.test(v.out) && /"member":true/.test(v.out) && !/"tier"/.test(v.out), v.out); const v2 = run('verify', '--presentation', 'p.json', '--audience', 'https://b.example', '--nonce', 'x1', '--trust-issuer', issId, '--status-list', 'list.json'); cere(v2.cod === 1 && /INVALID/.test(v2.out), v2.out); } finally { fs.rmSync(T, { recursive: true, force: true }); } }); console.log(`\naere-identity: ${treceri}/${treceri + esecuri.length} cum trebuia${sarite ? `, ${sarite} NEMASURATE (fara verificatorul AIP-23)` : ''}`); process.exitCode = esecuri.length ? 1 : (sarite ? 2 : 0);