100 lines
9.9 KiB
JavaScript
100 lines
9.9 KiB
JavaScript
// 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;
|