aere-quantum/identity/control-negativ-identity.mjs

113 lines
14 KiB
JavaScript

// Controlul negativ al probei AERE Identity: fiecare paznic se strica intr-o COPIE a dosarului, proba ruleaza pe copie si proba NUMITA
// trebuie sa iasa rosie, cu proba chiar rulata (rezumatul ei exista); pe copia neatinsa, verde. Trei stari: o plantare al carei tipar
// nu apare exact o data, sau o proba care nu ajunge la rezumat, e STRICAT si se numara esec (rosul ei nu masoara nimic).
// node control-negativ-identity.mjs iesire 0 = martorul verde si toate plantarile rosii pe proba lor
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const DEV_VERIFY = path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs');
const VERIFY = process.env.AERE_VERIFY_PROOF || (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : '');
const L = 'identity.mjs', C = 'identity-cli.mjs';
const PLANTARI = [
// B-30: forma publicata in aec0ccb (fara toleranta de ceas pe inceputuri, revocarea numai pe ceasul exact)
['fara toleranta pe validFrom (B-30)', L, 'vf - sk <= acum && acum <= vu /*PAZA:credential-window*/,', 'vf <= acum && acum <= vu,', 'B-30'],
['toleranta si pe sfarsit (validUntil prelungit)', L, 'vf - sk <= acum && acum <= vu /*PAZA:credential-window*/,', 'vf - sk <= acum && acum <= vu + sk,', 'B-30'],
['revocarea fara toleranta prudenta (B-30)', L, "ok('delegation-revoked', `${et}: not revoked`, at > acum + sk /*PAZA:delegation-revoked*/,", "ok('delegation-revoked', `${et}: not revoked`, at > acum,", 'revocarea: detinatorul revoca'],
// [nume, fisier, tipar, inlocuire, proba (inceputul numelui ei)]
['o dezvaluire nesemnata de emitent primita', L, 'if (!(sd.has(dg) /*PAZA:disclosure-signed*/)) {', 'if (false) {', 'ATAC: o dezvaluire fabricata'],
['aceeasi dezvaluire primita de doua ori', L, 'if (!(!(vazuteD.has(dg) || vazuteN.has(d.name)) /*PAZA:disclosure-once*/)) {', 'if (false) {', 'ATAC: aceeasi dezvaluire de doua ori'],
['o dezvaluire care acopera o afirmatie in clar primita', L, 'if (!(!Object.hasOwn(S.claims, d.name) /*PAZA:disclosure-plain*/)) {', 'if (false) {', 'ATAC: un emitent semneaza in sd o afirmatie'],
['digesturile dublate din sd primite', L, 'sd.size === S.sd.length /*PAZA:sd-distinct*/,', 'true,', 'ATAC: un digest de doua ori in sd'],
['numele rezervate (__proto__) primite', L, "return typeof n === 'string' && NUME.test(n) && !REZERVATE.has(n);", "return typeof n === 'string' && NUME.test(n);", 'ATAC: o dezvaluire cu numele __proto__'],
['alta codare base64url a aceleiasi dezvaluiri primita', L, "if (b.toString('base64url') !== enc) throw", 'if (false) throw', 'ATAC: o dezvaluire in alta codare base64url'],
['publicul verificatorului nu se mai compara', L, 'B.audience === audience /*PAZA:audience*/,', 'true,', 'ATAC: reluata la alt verificator'],
['nonce-ul verificatorului nu se mai compara', L, 'B.nonce === nonce /*PAZA:nonce*/,', 'true,', 'ATAC: reluata la alt verificator'],
['prospetimea prezentarii nu se mai cere', L, 'Math.abs(acum - t) <= maxAgeS * 1000 /*PAZA:freshness*/,', 'true,', 'ATAC: prezentare veche'],
['oricine poate prezenta credentialul detinatorului', L, 'asteptat && B.presenter.id === asteptat.id && canonical(B.presenter.keys) === canonical(asteptat.keys) /*PAZA:presenter*/,', 'true,', 'ATAC: un strain prezinta'],
['prezentatorul nu mai trebuie sa fie ultimul delegat', L, 'asteptat && B.presenter.id === asteptat.id && canonical(B.presenter.keys) === canonical(asteptat.keys) /*PAZA:presenter*/,', 'true,', 'ATAC: detinatorul prezinta singur'],
['dezvaluirile nu mai sunt legate de semnatura prezentarii', L, 'B.disclosuresHash === hashOf(p.disclosures) /*PAZA:binding-disclosures*/,', 'true,', 'ATAC: dezvaluiri adaugate dupa semnare'],
['lantul de delegare nu mai e legat de semnatura prezentarii', L, 'canonical(B.delegations) === canonical(p.delegations.map(delegationHash)) /*PAZA:binding-delegations*/,', 'true,', 'ATAC: lantul scos sau inversat'],
['fara separare de domeniu (acelasi mesaj pe toate scopurile)', L, "return Buffer.from(DOMENIU + scop + '\\n' + text, 'utf8');", "return Buffer.from(text, 'utf8');", 'ATAC: semnatura de DELEGARE'],
['una din cele doua semnaturi ajunge', L, "Buffer.from(sig.ed25519, 'base64')) && crypto.verify(", "Buffer.from(sig.ed25519, 'base64')) || crypto.verify(", 'ATAC: partea ML-DSA a semnaturii'],
['algoritmul declarat nu se mai cere', L, "if (!sig || sig.alg !== ALG || typeof sig.ed25519 !== 'string'", "if (!sig || typeof sig.ed25519 !== 'string'", 'ATAC: partea Ed25519 lipsa sau alg retrogradat'],
['id-ul emitentului nu mai e derivat din chei', L, 'idE && idE === S.issuer.id /*PAZA:issuer-id*/,', 'true,', 'ATAC: id-ul emitentului schimbat'],
['tipul cheii publice nu se mai verifica', L, 'if (k.asymmetricKeyType !== tip) throw', 'if (false) throw', 'ATAC: o cheie ML-DSA pusa in campul ed25519'],
['emitentii de incredere nu se mai cer', L, 'idsIncredere.includes(S.issuer.id) /*PAZA:issuer-trusted*/,', 'true,', 'CONTROL: emitent in afara celor de incredere'],
['fereastra credentialului nu se mai cere', L, 'vf - sk <= acum && acum <= vu /*PAZA:credential-window*/,', 'true,', 'CONTROL: emitent in afara celor de incredere'],
['un fisier de chei cu partea publica a altcuiva primit', L, 'if (canonical(keys.public) !== canonical(j.public) || keys.id !== j.id) throw', 'if (false) throw', 'cheile: exportKeys'],
['bitul de revocare ignorat', L, '!rev /*PAZA:credential-revoked*/,', 'true,', 'lista de stare: bitul 42'],
['cu doua liste, ultima castiga (revocarea se ridica)', L, 'rev = statusBit(L, S.status.index) || rev;', 'rev = statusBit(L, S.status.index);', 'lista de stare: bitul 42'],
['bitul 0 numarat de la coada octetului', L, 'biti[i >> 3] |= 0x80 >> (i & 7);', 'biti[i >> 3] |= 1 << (i & 7);', 'lista de stare: bitul 0'],
['o lista a altui emitent crezuta', L, "try { return l.statement.kind === 'aere-status-list'", "try { return true || l.statement.kind === 'aere-status-list'", 'ATAC: o lista cu acelasi id semnata de ALT emitent'],
['o lista expirata tacuta', L, 'else if (!curente.length) nejudecat(', 'else if (false) nejudecat(', 'lista expirata sau lipsa'],
['lista decomprimata fara plafon', L, "{ maxOutputLength: S.size / 8 }", '{}', 'ATAC: o lista care se decomprima peste'],
['marimea declarata a listei nu mai e marginita', L, 'if (!Number.isInteger(S.size) || S.size < 8 || S.size % 8 || S.size > MAX_STATUS_BITS) throw new Error(\'the list declares', 'if (false) throw new Error(\'the list declares', 'ATAC: o lista care se decomprima peste'],
['veriga poate fi data de oricine', L, 'dela && D.from.id === dela.id && canonical(D.from.keys) === canonical(dela.keys) /*PAZA:delegation-from*/,', 'true,', 'ATAC: lantul rupt'],
['prima veriga poate fi data de altcineva decat detinatorul', L, 'dela && D.from.id === dela.id && canonical(D.from.keys) === canonical(dela.keys) /*PAZA:delegation-from*/,', 'true,', 'ATAC: prima veriga data de altcineva'],
['semnatura verigii nu se mai verifica', L, "verifyText('delegation', canonical(D), lant[i].signature, D.from.keys) /*PAZA:delegation-signature*/,", 'true,', 'ATAC: o veriga in numele telefonului semnata de un strain'],
['veriga-parinte nu se mai compara', L, 'D.parent === (i === 0 ? null : delegationHash(lant[i - 1])) /*PAZA:delegation-parent*/,', 'true,', 'ATAC: veriga-parinte numita gresit'],
['adancimea nu se mai cere', L, 'Number.isInteger(D.maxDepth) && D.maxDepth >= lant.length - 1 - i /*PAZA:delegation-depth*/,', 'true,', 'ATAC: adancimea'],
['re-delegarea poate largi', L, "ok('delegation-narrows', `${et}: only narrows the previous link`, ingust &&", "ok('delegation-narrows', `${et}: only narrows the previous link`, true ||", 'ATAC: re-delegarea largeste scopul'],
['biblioteca scrie o re-delegare mai larga', L, "if (!inclus(sc[k], P.scope[k])) throw", 'if (false) throw', 'ATAC: re-delegarea largeste scopul'],
['scopul nu mai limiteaza afirmatiile aratate', L, '!afara.length /*PAZA:delegation-claims*/,', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['scopul nu mai limiteaza publicul', L, 'permite(sc.audiences, B.audience) /*PAZA:delegation-audience*/,', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['scopul nu mai limiteaza credentialul', L, 'permite(sc.credentials, S.id) /*PAZA:delegation-credential*/,', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['fereastra verigii nu se mai cere', L, 'nb - sk <= acum && acum <= na && nb - sk <= t && t <= na /*PAZA:delegation-window*/,', 'true,', 'ATAC: veriga expirata'],
['oricine poate revoca o veriga', L, 'const autor = R.by && (R.by.id === D.from.id || R.by.id === S.holder.id);', 'const autor = true;', 'revocarea: detinatorul revoca'],
['o revocare se aplica inainte de momentul ei', L, "ok('delegation-revoked', `${et}: not revoked`, at > acum + sk /*PAZA:delegation-revoked*/,", "ok('delegation-revoked', `${et}: not revoked`, false,", 'revocarea: detinatorul revoca'],
['revocarea acceptata sub scopul delegarii', L, "verifyText('revocation', canonical(R), r.signature, R.by.keys)", "verifyText('delegation', canonical(R), r.signature, R.by.keys)", 'ATAC: o revocare cu semnatura detinatorului dar alt scop'],
['o lista stricata acuza credentialul', L, "verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch { return false; } });", "verifyText('status-list', canonical(l.statement), l.signature, S.issuer.keys); } catch (e) { throw e; } });", 'intrari stricate date verificatorului'],
['o revocare stricata acuza prezentarea', L, "try { canonical(R); } catch { nejudecat('revocation-ignored', `${et}: a revocation`, 'ignored: not readable'); continue; }", '', 'intrari stricate date verificatorului'],
// 2026-10-05: regulile numite
['cheile emitentului necitibile acuza si detinatorul (forma dinainte de 2026-10-05)', L, "try { idE = idOf(S.issuer.keys); } catch (e) { /* randul de mai jos spune */ }\n try { idH = idOf(S.holder.keys); }", 'try { idE = idOf(S.issuer.keys); idH = idOf(S.holder.keys); }', 'ATAC: emitent cu o cheie Ed25519 in campul mldsa65'],
['rezultatul nu mai numeste regulile care au refuzat', L, "rules: [...new Set(rows.filter((r) => r.pass === false).map((r) => r.rule))].sort(),", 'rules: [],', 'fiecare refuz isi numeste regula'],
['un rand nejudecat fara cauza', L, "rows.push({ name, pass: null, detail, unjudged: cauza });", 'rows.push({ name, pass: null, detail });', 'fiecare refuz isi numeste regula'],
['o lista de stare care nu e lista acuza prezentarea', L, 'if (!Array.isArray(statusLists) || !Array.isArray(revocations)) throw', 'if (false) throw', 'fiecare refuz isi numeste regula'],
['linia de comanda suprascrie o cheie', C, 'if (privat && fs.existsSync(f)) throw new Folosire(', 'if (false) throw new Folosire(', 'linia de comanda'],
// 2026-09-30: tiparul poarta randul de dinainte, fiindca verify-sdjwt are si el `return r.valid ? 0 : 1;`
['verify din linia de comanda iese 0 si pe INVALID', C, "JSON.stringify(r.claims));\n }\n return r.valid ? 0 : 1;", "JSON.stringify(r.claims));\n }\n return 0;", 'linia de comanda'],
];
const FISIERE = [L, C, 'proba-identity.mjs'];
function copie() {
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-id-ctl-'));
fs.mkdirSync(path.join(t, 'aere-identity')); fs.mkdirSync(path.join(t, 'proof-kinds'));
for (const f of FISIERE) fs.copyFileSync(path.join(AICI, f), path.join(t, 'aere-identity', f));
fs.copyFileSync(path.join(AICI, '..', 'proof-kinds', 'proof-kinds.mjs'), path.join(t, 'proof-kinds', 'proof-kinds.mjs'));
return t;
}
function ruleaza(t) {
const env = { ...process.env }; if (VERIFY) env.AERE_VERIFY_PROOF = VERIFY; else delete env.AERE_VERIFY_PROOF;
return new Promise((resolve) => {
const c = spawn(process.execPath, [path.join(t, 'aere-identity', 'proba-identity.mjs')], { env }); let out = '';
const ceas = setTimeout(() => c.kill(), 240000);
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; });
c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /aere-identity: \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => l.startsWith(' RAU ')) }); });
});
}
async function planteaza([nume, fisier, din, inl, tinta]) {
const t = copie();
try {
const f = path.join(t, 'aere-identity', fisier); const src = fs.readFileSync(f, 'utf8');
if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori in ${fisier}`];
fs.writeFileSync(f, src.replace(din, inl));
const r = await ruleaza(t);
if (!r.rulat) return [false, ` STRICAT ${nume}: proba nu a ajuns la rezumat (cod ${r.cod})`];
if (r.rosii.some((l) => l.startsWith(' RAU ' + tinta))) return [true, ` ROSU cum trebuia ${nume} (proba '${tinta}' pica)`];
return [false, ` CONTROL CAZUT ${nume}: proba '${tinta}' a ramas verde (${r.rosii.length} rosii altundeva)`];
} finally { fs.rmSync(t, { recursive: true, force: true }); }
}
const t0 = copie(); const m = await ruleaza(t0); fs.rmSync(t0, { recursive: true, force: true });
let rele = 0;
if (m.rulat && m.cod === 0 && !m.rosii.length) console.log(' OK martorul: copia neatinsa verde');
else if (m.rulat && m.cod === 2 && !m.rosii.length && !VERIFY) console.log(' OK martorul: copia neatinsa verde (plicurile AIP-23 NEMASURATE: fara verificator)');
else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii)`); }
const rez = new Array(PLANTARI.length); let i = 0;
await Promise.all(Array.from({ length: 4 }, async () => { while (i < PLANTARI.length) { const k = i++; rez[k] = await planteaza(PLANTARI[k]); } }));
for (const [bun, linie] of rez) { console.log(linie); if (!bun) rele++; }
console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `DOVEDIT: martorul verde, ${PLANTARI.length} din ${PLANTARI.length} paznici scosi -> proba lor rosie`);
process.exitCode = rele ? 1 : 0;