aere-quantum/identity/proba-conformitate.mjs

121 lines
12 KiB
JavaScript

// Proba conformitatii fara supraveghere (conformitate.mjs): politica, judecata pe prezentari reale (identity.mjs), si inregistrarea
// fara date personale. Fiecare afirmatie cu perechea ei negativa. Offline. Plicul AIP-23 se judeca si cu verificatorul de referinta
// (AERE_VERIFY_PROOF sau, in depozitul de dezvoltare, ../aere-proof-protocol/verify.mjs); fara el, acea proba iese NEMASURATA (cod 2).
// node proba-conformitate.mjs iesire 0 = toate cum trebuia
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import * as I from './identity.mjs';
import { definePolicy, checkCompliance, complianceEnvelope } from './conformitate.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 = [];
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 NOW = new Date('2026-09-30T08:00:00Z'), AUD = 'https://exchange.example', NONCE = 'c-1';
const iss = I.generateKeys(), hol = I.generateKeys(), alt = I.generateKeys();
const CLAIMS = { name: 'Ana Pop', birthdate: '1990-01-01', age_over_18: true, jurisdiction: 'RO', kyc_level: 2 };
const emite = (emitent = iss, index = 3) => I.issueCredential({ issuer: emitent, holder: hol.public, type: 'KycCredential', claims: CLAIMS,
disclosable: ['name', 'birthdate', 'age_over_18', 'jurisdiction', 'kyc_level'], validUntil: '2027-09-30T00:00:00Z', status: { list: 'urn:s:kyc', index }, now: NOW });
const { credential, disclosures } = emite();
const LISTA = I.createStatusList({ issuer: iss, id: 'urn:s:kyc', revoked: [9], validUntil: '2026-10-07T00:00:00Z', now: NOW });
const POL = { id: 'exchange-onboarding-v1', trustedIssuers: [iss.id], require: [{ claim: 'age_over_18', equals: true }, { claim: 'jurisdiction', notIn: ['KP', 'IR'] }, { claim: 'kyc_level', atLeast: 2 }] };
const prez = (reveal = ['age_over_18', 'jurisdiction', 'kyc_level'], o = {}) => I.present({ credential: o.credential || credential, disclosures: o.disclosures || disclosures, reveal, presenter: hol, audience: o.audience || AUD, nonce: NONCE, now: NOW });
const judeca = (p, pol = POL, o = {}) => checkCompliance(p, pol, { audience: AUD, nonce: NONCE, now: NOW, statusLists: [LISTA], ...o });
test('politica: forma normala; aceeasi politica cu regulile si emitentii in alta ordine are acelasi hash; alta regula, alt hash', () => {
const a = definePolicy(POL), b = definePolicy({ ...POL, require: [...POL.require].reverse(), trustedIssuers: [iss.public] });
cere(a.policyHash === b.policyHash && /^0x[0-9a-f]{64}$/.test(a.policyHash), `${a.policyHash} ${b.policyHash}`);
cere(definePolicy({ ...POL, require: [{ claim: 'kyc_level', atLeast: 1 }] }).policyHash !== a.policyHash, 'o politica mai laxa are acelasi hash');
});
test('CONTROL: politica refuza campuri necunoscute, lista de emitenti goala, o regula cu doi operatori sau fara niciunul', () => {
const m = [arunca(() => definePolicy({ ...POL, allowAll: true })), arunca(() => definePolicy({ ...POL, trustedIssuers: [] })),
arunca(() => definePolicy({ ...POL, require: [{ claim: 'x', equals: 1, in: [1] }] })), arunca(() => definePolicy({ ...POL, require: [{ claim: 'x' }] }))];
cere(/unknown field/.test(m[0]) && /trustedIssuers is required/.test(m[1]) && /a rule is/.test(m[2]) && /a rule is/.test(m[3]), m.join(' | '));
});
test('conform: varsta, jurisdictia in afara celor interzise, nivelul de verificare; starea judecata', () => {
const r = judeca(prez()); cere(r.compliant && !r.reasons.length && r.notJudged === 0, JSON.stringify(r.reasons));
});
test('neconform: o afirmatie ceruta nearatata (kyc_level) -> motivul numeste regula', () => {
const r = judeca(prez(['age_over_18', 'jurisdiction'])); cere(!r.compliant && r.reasons.some((x) => /kyc_level/.test(x)), JSON.stringify(r.reasons));
});
test('neconform: jurisdictie interzisa (notIn); nivel sub prag (atLeast); valoare falsa (equals)', () => {
const c2 = I.issueCredential({ issuer: iss, holder: hol.public, type: 'KycCredential', claims: { ...CLAIMS, jurisdiction: 'KP', kyc_level: 1, age_over_18: false },
disclosable: ['age_over_18', 'jurisdiction', 'kyc_level'], validUntil: '2027-09-30T00:00:00Z', status: { list: 'urn:s:kyc', index: 4 }, now: NOW });
const r = judeca(prez(undefined, c2));
cere(!r.compliant && ['jurisdiction', 'kyc_level', 'age_over_18'].every((n) => r.reasons.some((x) => x.includes(n))), JSON.stringify(r.reasons));
});
test('neconform: emitent in afara celor ai politicii', () => {
const c3 = emite(alt); const r = judeca(prez(undefined, c3)); cere(!r.compliant && r.reasons.some((x) => /issuer trusted/.test(x)), JSON.stringify(r.reasons));
});
test('neconform: starea ceruta si lista nedata (nejudecat NU e "nerevocat"); fara cerinta de stare, conform cu randul nejudecat numarat', () => {
const r = judeca(prez(), POL, { statusLists: [] }); cere(!r.compliant && r.reasons.some((x) => /status to be judged/.test(x)), JSON.stringify(r.reasons));
const r2 = judeca(prez(), { ...POL, requireStatus: false }, { statusLists: [] }); cere(r2.compliant && r2.notJudged === 1, JSON.stringify(r2));
});
test('neconform: credentialul revocat de emitent', () => {
const c4 = emite(iss, 9); const r = judeca(prez(undefined, c4)); cere(!r.compliant && r.reasons.some((x) => /revoked/.test(x)), JSON.stringify(r.reasons));
});
test('neconform: prezentare facuta pentru alt verificator (reluare); fara public si nonce, judecata refuza sa inceapa', () => {
const r = judeca(prez(undefined, { audience: 'https://other.example' })); cere(!r.compliant && r.reasons.some((x) => /made for/.test(x)), JSON.stringify(r.reasons));
cere(/audience and nonce are required/.test(arunca(() => checkCompliance(prez(), POL, { now: NOW, statusLists: [LISTA] })) || ''), 'a judecat fara public');
});
test('rezultatul nu poarta afirmatiile detinatorului decat la cerere (keepClaims), si numai pentru o judecata conforma', () => {
const r = judeca(prez()); cere(!('claims' in r) && !JSON.stringify(r).includes('"RO"'), JSON.stringify(r));
const k = judeca(prez(), POL, { keepClaims: true }); cere(k.claims && k.claims.jurisdiction === 'RO', JSON.stringify(k.claims));
const kn = judeca(prez(['age_over_18']), POL, { keepClaims: true }); cere(!kn.compliant && !('claims' in kn), 'afirmatiile intoarse pentru o judecata neconforma');
});
test('inregistrarea (plic AIP-23 compliance) NU poarta date personale: nici valori, nici id-ul detinatorului', () => {
const e = complianceEnvelope(judeca(prez()), { audience: AUD, createdAt: NOW.toISOString(), buildProof });
const s = JSON.stringify(e);
for (const x of ['Ana Pop', '1990-01-01', '"RO"', hol.id, hol.public.mldsa65.slice(0, 40)]) cere(!s.includes(x), 'plicul contine ' + x.slice(0, 30));
cere(e.statement.result === 'compliant' && /^pseudonym:[0-9a-f]{40}$/.test(e.statement.subject) && e.statement.policy === definePolicy(POL).policyHash, JSON.stringify(e.statement));
});
test('pseudonimul: alt verificator (alt public) vede alt pseudonim; cu o cheie a verificatorului, altul decat forma publica refacuta de oricine', () => {
const r = judeca(prez());
const a = complianceEnvelope(r, { audience: AUD, buildProof }).statement.subject, b = complianceEnvelope(r, { audience: 'https://bank.example', buildProof }).statement.subject;
const k = complianceEnvelope(r, { audience: AUD, buildProof, pseudonymKey: crypto.randomBytes(32) }).statement.subject;
const refacut = 'pseudonym:' + crypto.createHash('sha256').update(`aere-compliance-subject|${hol.id}|${AUD}`).digest('hex').slice(0, 40);
cere(a !== b && a === refacut && k !== refacut, `${a} ${b} ${k}`);
});
test('plicul verifica la verificatorul AIP-23 de referinta; unul cu rezultatul rescris nu', () => {
if (!fs.existsSync(VERIFY)) { sarite++; console.log(` SARIT plicul AIP-23: verificatorul nu e la ${VERIFY}; AERE_VERIFY_PROOF=<verify-proof.mjs>`); return 'SARIT'; }
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-conf-'));
try {
const v = (o) => { const f = path.join(T, crypto.randomUUID() + '.json'); fs.writeFileSync(f, JSON.stringify(o)); try { return JSON.parse(spawnSync(process.execPath, [VERIFY, f, '--json'], { encoding: 'utf8' }).stdout).verdict; } catch { return '?'; } };
const e = complianceEnvelope(judeca(prez(['age_over_18'])), { audience: AUD, buildProof });
const rescris = JSON.parse(JSON.stringify(e)); rescris.statement.result = 'compliant';
cere(e.statement.result === 'not-compliant' && v(e) === 'VALID' && v(rescris) !== 'VALID', `${v(e)} ${v(rescris)}`);
} finally { fs.rmSync(T, { recursive: true, force: true }); }
});
test('linia de comanda: comply -> COMPLIANT (0) si inregistrarea scrisa fara date personale; o prezentare care nu arata destul -> NOT COMPLIANT (1)', () => {
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-conf-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']) cere(run('keygen', '--out', n + '.keys.json').cod === 0, 'keygen');
cere(run('pub', '--keys', 'hol.keys.json', '--out', 'hol.pub.json').cod === 0, 'pub');
cere(run('issue', '--issuer-keys', 'iss.keys.json', '--holder-pub', 'hol.pub.json', '--type', 'KycCredential', '--claim', 'age_over_18=true', '--claim', 'jurisdiction=RO',
'--claim', 'name=Ana Pop', '--all-disclosable', '--status-list', 'urn:s:1', '--status-index', '2', '--out', 'cred.json').cod === 0, 'issue');
cere(run('status-list', '--issuer-keys', 'iss.keys.json', '--id', 'urn:s:1', '--out', 'list.json').cod === 0, 'status-list');
const issId = run('id', '--keys', 'iss.keys.json').out.trim();
fs.writeFileSync(path.join(T, 'pol.json'), JSON.stringify({ id: 'p1', trustedIssuers: [issId], require: [{ claim: 'age_over_18', equals: true }, { claim: 'jurisdiction', notIn: ['KP'] }] }));
cere(run('present', '--cred', 'cred.json', '--reveal', 'age_over_18,jurisdiction', '--presenter-keys', 'hol.keys.json', '--audience', AUD, '--nonce', 'n1', '--out', 'p.json').cod === 0, 'present');
const a = run('comply', '--presentation', 'p.json', '--policy', 'pol.json', '--audience', AUD, '--nonce', 'n1', '--status-list', 'list.json', '--record', 'rec.json');
const rec = fs.readFileSync(path.join(T, 'rec.json'), 'utf8');
cere(a.cod === 0 && /COMPLIANT/.test(a.out) && /"result": "compliant"/.test(rec) && !/Ana Pop|"RO"|aere-id:/.test(rec), a.out + rec);
cere(run('present', '--cred', 'cred.json', '--reveal', 'jurisdiction', '--presenter-keys', 'hol.keys.json', '--audience', AUD, '--nonce', 'n2', '--out', 'p2.json').cod === 0, 'present 2');
const b = run('comply', '--presentation', 'p2.json', '--policy', 'pol.json', '--audience', AUD, '--nonce', 'n2', '--status-list', 'list.json');
cere(b.cod === 1 && /NOT COMPLIANT/.test(b.out) && /age_over_18/.test(b.out), b.out);
} finally { fs.rmSync(T, { recursive: true, force: true }); }
});
console.log(`\naere-compliance: ${treceri}/${treceri + esecuri.length} cum trebuia${sarite ? `, ${sarite} NEMASURATE (fara verificatorul AIP-23)` : ''}`);
process.exitCode = esecuri.length ? 1 : (sarite ? 2 : 0);