// 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,', 'vf <= acum && acum <= vu,', 'B-30'], ['toleranta si pe sfarsit (validUntil prelungit)', L, 'vf - sk <= acum && acum <= vu,', 'vf - sk <= acum && acum <= vu + sk,', 'B-30'], ['revocarea fara toleranta prudenta (B-30)', L, 'ok(`${et}: not revoked`, at > acum + sk,', 'ok(`${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)) {', '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 - sk <= 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 - sk <= acum && acum <= na && nb - sk <= 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 + sk,', '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;