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

103 lines
12 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 = [
// [nume, fisier, tipar, inlocuire, proba (inceputul numelui ei)]
['o dezvaluire nesemnata de emitent primita', L, 'if (!sd.has(dg)) {', 'if (false) {', 'ATAC: o dezvaluire fabricata'],
['aceeasi dezvaluire primita de doua ori', L, 'if (vazuteD.has(dg) || vazuteN.has(d.name)) {', '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)) {', 'if (false) {', 'ATAC: un emitent semneaza in sd o afirmatie'],
['digesturile dublate din sd primite', L, "ok('credential: the digests it signs are distinct', sd.size === S.sd.length,", "ok('credential: the digests it signs are 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,', 'true,', 'ATAC: reluata la alt verificator'],
['nonce-ul verificatorului nu se mai compara', L, 'B.nonce === nonce,', 'true,', 'ATAC: reluata la alt verificator'],
['prospetimea prezentarii nu se mai cere', L, 'Math.abs(acum - t) <= maxAgeS * 1000,', 'true,', 'ATAC: prezentare veche'],
['oricine poate prezenta credentialul detinatorului', L, 'asteptat && B.presenter.id === asteptat.id && canonical(B.presenter.keys) === canonical(asteptat.keys),', '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),', 'true,', 'ATAC: detinatorul prezinta singur'],
['dezvaluirile nu mai sunt legate de semnatura prezentarii', L, 'B.disclosuresHash === hashOf(p.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)),', '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, "ok('credential: the issuer id is the id of its keys', idE && idE === S.issuer.id,", "ok('credential: the issuer id is the id of its keys', 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, "ok('credential: issuer trusted', idsIncredere.includes(S.issuer.id),", "ok('credential: issuer trusted', true,", 'CONTROL: emitent in afara celor de incredere'],
['fereastra credentialului nu se mai cere', L, 'vf <= acum && acum <= vu,', '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,', '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),', '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),', 'true,', 'ATAC: prima veriga data de altcineva'],
['semnatura verigii nu se mai verifica', L, "verifyText('delegation', canonical(D), lant[i].signature, D.from.keys),", '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])),', 'true,', 'ATAC: veriga-parinte numita gresit'],
['adancimea nu se mai cere', L, 'Number.isInteger(D.maxDepth) && D.maxDepth >= lant.length - 1 - i,', 'true,', 'ATAC: adancimea'],
['re-delegarea poate largi', L, "ok(`${et}: only narrows the previous link`, ingust &&", "ok(`${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, 'ok(`${et}: covers the disclosed claims`, !afara.length,', 'ok(`${et}: covers the disclosed claims`, true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['scopul nu mai limiteaza publicul', L, 'permite(sc.audiences, B.audience),', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['scopul nu mai limiteaza credentialul', L, 'permite(sc.credentials, S.id),', 'true,', 'ATAC: delegatul arata o afirmatie din afara scopului'],
['fereastra verigii nu se mai cere', L, 'nb <= acum && acum <= na && nb <= t && t <= na,', '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(`${et}: not revoked`, at > acum,', 'ok(`${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(`${et}: a revocation`, 'ignored: not readable'); continue; }", '', 'intrari stricate date verificatorului'],
['linia de comanda suprascrie o cheie', C, 'if (privat && fs.existsSync(f)) throw new Folosire(', 'if (false) throw new Folosire(', 'linia de comanda'],
['verify din linia de comanda iese 0 si pe INVALID', C, 'return r.valid ? 0 : 1;', '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;