// Controlul negativ al probei PKI: fiecare paznic din pki.mjs / der.mjs se strica pe rand, la RULARE (conditie falsa, ca sa compileze), // intr-o COPIE a modulului (pki.mjs, der.mjs, cli.mjs), iar proba, chemata cu PKI_MODULE pe copie, trebuie sa iasa ROSIE pe EXACT proba // numita. O ancora care nu apare o singura data e un ESEC al controlului, nu o trecere (ancorele stau pe partea STABILA a randului). // Martorul (proba pe original) ruleaza INTAI: din el se deriva numarul de probe pe care fiecare plantare trebuie sa il ruleze intreg // (nu e scris fix: o constanta de ieri minte in ziua in care suita creste). La sfarsit originalul trebuie sa fie identic (sha256). // Plantarile ruleaza cate PARALEL deodata (fiecare proba costa ~10 deschideri scrypt de ~0,7 s). // node test/control-negativ.mjs import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; import { spawn, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const SRC = path.join(AICI, '..'); const PROBA = path.join(AICI, 'proba.mjs'); const FISIERE = ['pki.mjs', 'der.mjs', 'cli.mjs']; const PARALEL = Math.max(1, Math.min(4, Number(process.env.PKI_PARALEL) || 4)); const sha = (f) => crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex'); const inainte = Object.fromEntries(FISIERE.map((n) => [n, sha(path.join(SRC, n))])); // [nume, fisier, ancora (exact o data), inlocuitor, prefixul probei care trebuie sa cada] const PLANTARI = [ ['semnatura oricum valida', 'pki.mjs', ' return crypto.verify(null, tbs, key, signature);', " return crypto.verify(null, tbs, key, signature) || Boolean(Number('1'));", 'un octet schimbat'], ['pathLen ignorat', 'pki.mjs', 'if (p.pathLen !== undefined && casBelow > p.pathLen)', "if (p.pathLen !== undefined && casBelow > p.pathLen && Boolean(Number('0')))", 'pathLen'], ['revocarea ignorata', 'pki.mjs', "if (list.revoked.has(c.serial.toString('hex')))", "if (list.revoked.has(c.serial.toString('hex')) && Boolean(Number('0')))", 'revocarea'], ['emitentul fara basicConstraints CA', 'pki.mjs', " if (!p.isCA) return fail('NOT_CA'", " if (!p.isCA && Boolean(Number('0'))) return fail('NOT_CA'", 'un certificat de entitate folosit ca emitent'], ['numele gazdei ignorat', 'pki.mjs', " if (!ok) return fail('HOSTNAME'", " if (!ok && Boolean(Number('0'))) return fail('HOSTNAME'", 'frunza pentru alt nume'], ['expirarea ignorata', 'pki.mjs', " if (at > c.notAfter) return fail('EXPIRED'", " if (at > c.notAfter && Boolean(Number('0'))) return fail('EXPIRED'", 'timpul'], ['parametri acceptati pe ML-DSA', 'pki.mjs', " if (f.length !== 1) throw new PkiError('ALG_PARAMS'", " if (f.length !== 1 && Boolean(Number('0'))) throw new PkiError('ALG_PARAMS'", 'forma'], ['extensia critica necunoscuta ignorata', 'pki.mjs', ' for (const [id, x] of exts) if (x.critical && !EXT_STIUTE.has(id))', " for (const [id, x] of exts) if (x.critical && !EXT_STIUTE.has(id) && Boolean(Number('0')))", 'extensie critica necunoscuta'], ['scopul (EKU) pe frunza ignorat', 'pki.mjs', ' if (L.eku && !L.eku.has(OID[purpose]))', " if (L.eku && !L.eku.has(OID[purpose]) && Boolean(Number('0')))", 'scopul'], ['cheia privata scrisa in clar (PKCS#8)', 'pki.mjs', ' return pem(KEY_LABEL, D.seq(header, D.octets(tag), D.octets(ct)));', " return key.export({ type: 'pkcs8', format: 'pem' });", 'linia de comanda'], ['DER necanonic acceptat', 'der.mjs', " if (l < 0x80) throw new Error('DER: lungime scurta scrisa in forma lunga (necanonica)');", " if (l < 0x80 && Boolean(Number('0'))) throw new Error('DER: lungime scurta scrisa in forma lunga (necanonica)');", 'forma'], // revizuirea din 2026-09-25 ['[A1] scrypt coborat la N=2^10', 'pki.mjs', 'N: 2 ** 17,', 'N: 2 ** 10,', 'sigilarea cheii'], ['[A1] politica de parola redusa la 12 caractere (accepta aaaaaaaaaaaa)', 'pki.mjs', 'export function checkPassphrase(p) {', "export function checkPassphrase(p) { if (typeof p === 'string' && p.length >= 12 && Boolean(Number('1'))) return p;", 'politica de parola'], ['[A1] parolele evidente acceptate', 'pki.mjs', " if (PAROLE_EVIDENTE.has(nucleu) || PAROLE_EVIDENTE.has(nucleu.replace(/[0-9]+$/, '')))", " if ((PAROLE_EVIDENTE.has(nucleu) || PAROLE_EVIDENTE.has(nucleu.replace(/[0-9]+$/, ''))) && Boolean(Number('0')))", 'politica de parola'], ['[A2] prima lista din intrare in loc de cea mai noua', 'pki.mjs', ' curente.sort((a, b) => (b.thisUpdate - a.thisUpdate) || cmpBig(b.crlNumber, a.crlNumber));', ' curente.sort(() => 0);', 'cea mai noua lista'], ['[A3] extensiile listei ignorate', 'pki.mjs', ' const rea = verificate.find((l) => l.refusedExt);', " const rea = verificate.find((l) => l.refusedExt && Boolean(Number('0')));", 'extensiile listei'], ['[A5] lungimea cheii publice neverificata', 'pki.mjs', ' if (pkBits.unused !== 0 || pkBits.bytes.length !== PK_LEN[pkAlg])', " if ((pkBits.unused !== 0 || pkBits.bytes.length !== PK_LEN[pkAlg]) && Boolean(Number('0')))", 'cheie publica malformata'], ['[A6] EKU pe autoritati ignorat', 'pki.mjs', ' if (purpose && p.eku && !p.eku.has(OID[purpose]))', " if (purpose && p.eku && !p.eku.has(OID[purpose]) && Boolean(Number('0')))", 'EKU pe autoritati'], ['[A7] numai prima ancora candidata', 'pki.mjs', ' const ancore = certs.roots.filter((r) => emite(r, cur));', ' const ancore = certs.roots.filter((r) => emite(r, cur)).slice(0, 1);', 'reinnoirea radacinii'], ['[A7] numai primul intermediar candidat', 'pki.mjs', 'certs.inter.filter((c) => c !== cur && !stiva.includes(c) && emite(c, cur)) : [];', 'certs.inter.filter((c) => c !== cur && !stiva.includes(c) && emite(c, cur)).slice(0, 1) : [];', 'reinnoirea radacinii'], ['[A10] primul subidentificator OID scris pe un singur octet', 'der.mjs', ' const out = base128(p[0] * 40n + p[1]);', ' const out = [Number((p[0] * 40n + p[1]) & 0x7fn)];', 'OID'], ['[A10] arcul OID dedus fara regula de 80', 'der.mjs', ' const arc = first < 80n ? first / 40n : 2n;', ' const arc = first / 40n;', 'OID'], ]; // 1. martorul: originalul trebuie sa fie verde, si din el se ia numarul de probe const martorJson = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pki-martor-')), 'rez.json'); const martor = spawnSync(process.execPath, [PROBA], { encoding: 'utf8', env: { ...process.env, PKI_JSON: martorJson }, timeout: 600000 }); if (!fs.existsSync(martorJson)) { console.log('STRICAT: martorul nu a scris rezultatul (cod ' + martor.status + ')\n' + (martor.stdout || '').slice(-600)); process.exit(2); } const rezMartor = JSON.parse(fs.readFileSync(martorJson, 'utf8')); const N = rezMartor.length, verde = martor.status === 0 && rezMartor.every((x) => x.ok); console.log(`martorul: ${rezMartor.filter((x) => x.ok).length}/${N} probe trecute pe original${verde ? '' : ' (NU e verde: ' + rezMartor.filter((x) => !x.ok).map((x) => x.nume.slice(0, 40)).join(' | ') + ')'}`); for (const [, , , , tinta] of PLANTARI) if (!rezMartor.some((x) => x.nume.startsWith(tinta))) console.log(` ESEC tinta "${tinta}..." nu exista printre probele martorului`); fs.rmSync(path.dirname(martorJson), { recursive: true, force: true }); // 2. plantarile, cate PARALEL deodata function planteaza([nume, fis, vechi, nou, tinta]) { return new Promise((gata) => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pki-plantat-')); const sfarsit = (linie, ok) => { fs.rmSync(dir, { recursive: true, force: true }); gata({ nume, linie, ok }); }; for (const n of FISIERE) fs.copyFileSync(path.join(SRC, n), path.join(dir, n)); const t = fs.readFileSync(path.join(dir, fis), 'utf8'); const ap = t.split(vechi).length - 1; if (ap !== 1) return sfarsit(` ESEC ${nume}: ancora apare de ${ap} ori (cerut 1), plantarea nu s-a pus`, false); fs.writeFileSync(path.join(dir, fis), t.replace(vechi, nou)); const out = path.join(dir, 'rez.json'); const p = spawn(process.execPath, [PROBA], { env: { ...process.env, PKI_MODULE: path.join(dir, 'pki.mjs'), PKI_JSON: out }, stdio: ['ignore', 'pipe', 'pipe'] }); let log = ''; p.stdout.on('data', (d) => { log += d; }); p.stderr.on('data', (d) => { log += d; }); const timer = setTimeout(() => p.kill(), 600000); p.on('close', (cod) => { clearTimeout(timer); if (!fs.existsSync(out)) return sfarsit(` STRICAT ${nume}: proba nu a scris rezultatul (cod ${cod}) ${log.slice(-200).replace(/\s+/g, ' ')}`, false); const rez = JSON.parse(fs.readFileSync(out, 'utf8')); const rosii = rez.filter((x) => !x.ok).map((x) => x.nume); const prins = rosii.some((n) => n.startsWith(tinta)); if (rez.length !== N) return sfarsit(` STRICAT ${nume}: au rulat ${rez.length} probe din ${N}`, false); if (prins) return sfarsit(` PRINS ${nume}: ${rosii.length} rosii, intre ele "${tinta}..."`, true); return sfarsit(` NEPRINS ${nume}: proba "${tinta}..." a ramas verde (rosii: ${rosii.map((r) => r.slice(0, 40)).join(' | ').slice(0, 200)})`, false); }); }); } const rezultate = new Array(PLANTARI.length); let urm = 0; async function lucrator() { while (urm < PLANTARI.length) { const k = urm++; rezultate[k] = await planteaza(PLANTARI[k]); process.stdout.write(`\r ${rezultate.filter(Boolean).length}/${PLANTARI.length} plantari rulate`); } } await Promise.all(Array.from({ length: PARALEL }, lucrator)); process.stdout.write('\r'); for (const r of rezultate) console.log(r.linie); const bune = rezultate.filter((r) => r.ok).length, rele = rezultate.length - bune; // 3. originalul e neatins const dupa = Object.fromEntries(FISIERE.map((n) => [n, sha(path.join(SRC, n))])); const neatins = JSON.stringify(inainte) === JSON.stringify(dupa); const tinteOk = PLANTARI.every(([, , , , tinta]) => rezMartor.some((x) => x.nume.startsWith(tinta))); console.log(`\ncontrol negativ: ${bune}/${PLANTARI.length} plantari prinse; originalul ${neatins ? 'identic' : 'SCHIMBAT'}; martorul ${verde ? `verde ${N}/${N}` : 'NU e verde'}`); process.exitCode = bune === PLANTARI.length && rele === 0 && neatins && verde && tinteOk ? 0 : 1;