aere-quantum/identity/proba-identity.mjs
Aere Network 0b56d8e5df identity: 60 s of clock allowance on starts (validFrom, notBefore) and, the careful way, on revocations; none on ends
A credential issued on a machine whose clock was one second ahead was "not yet valid" at a verifier synchronized by NTP: measured on
2026-09-30 through the Aere Cloud identity route, the first time a credential was issued on one machine and judged on another.
Starts (a credential's and a status list's validFrom, a delegation's notBefore) are now accepted up to 60 s in the verifier's future
(verifyPresentation clockSkewS, 0..600); a revocation dated up to 60 s ahead already applies; ends (validUntil, notAfter) get no
allowance, since that would extend a validity.

Tests: identity 44/44, negative control 49/49.
2026-09-30 11:36:02 +03:00

327 lines
31 KiB
JavaScript

// 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=<verify-proof.mjs>, 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');
});
// B-30 (2026-09-30): masurat prin API-ul viu, un credential emis pe un laptop cu ceasul inainte cu ~1 s era 'not yet valid' la rpc2 (NTP exact)
test('B-30: toleranta de ceas pe INCEPUTURI (60 s): validFrom al credentialului si al listei de stare cu 30 s in viitor -> VALID; cu 90 s -> INVALID; sfarsiturile fara toleranta', () => {
const in30 = new Date(NOW.getTime() + 30000), in90 = new Date(NOW.getTime() + 90000);
const c30 = I.issueCredential({ issuer: iss, holder: hol.public, type: 'EmployeeCredential', claims: CLAIMS, disclosable: ['age_over_18'], validUntil: '2027-09-30T00:00:00Z', status: { list: 'urn:example:status:30', index: 1 }, now: in30 });
const L30 = I.createStatusList({ issuer: iss, id: 'urn:example:status:30', revoked: [], validUntil: '2026-10-07T00:00:00Z', now: in30 });
const p30 = I.present({ ...c30, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW });
const r = I.verifyPresentation(p30, { ...toate, statusLists: [L30] }); cere(r.valid && r.notJudged === 0, 'emis cu 30 s in viitor: ' + (fals(r) || 'nejudecate ' + r.notJudged));
const c90 = I.issueCredential({ issuer: iss, holder: hol.public, type: 'EmployeeCredential', claims: CLAIMS, disclosable: ['age_over_18'], validUntil: '2027-09-30T00:00:00Z', now: in90 });
const p90 = I.present({ ...c90, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW });
cere(picaPe(I.verifyPresentation(p90, toate), /valid at/), 'emis cu 90 s in viitor a trecut');
const cExp = I.issueCredential({ issuer: iss, holder: hol.public, type: 'EmployeeCredential', claims: CLAIMS, disclosable: ['age_over_18'], validFrom: '2026-09-01T00:00:00Z', validUntil: new Date(NOW.getTime() - 30000).toISOString(), now: new Date('2026-09-01T00:00:00Z') });
const pExp = I.present({ ...cExp, reveal: ['age_over_18'], presenter: hol, audience: AUD, nonce: NONCE, now: NOW });
cere(picaPe(I.verifyPresentation(pExp, toate), /valid at/), 'expirat de 30 s a trecut: sfarsitul a primit toleranta');
});
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));
// B-30: o revocare cu momentul la 30 s in viitor (ceasul celui care revoca inainte) se aplica deja: toleranta merge in sensul prudent
const rp30 = I.revokeDelegation({ by: hol, delegation: d1, at: new Date(NOW.getTime() + 30000).toISOString(), now: NOW });
cere(picaPe(I.verifyPresentation(prezD(), { ...toate, revocations: [rp30] }), /not revoked/), 'B-30: revocarea de peste 30 s nu s-a aplicat');
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=<verify-proof.mjs>`); 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);