// test/proba-hsm.mjs - proba radacinii KMS sigilate de un HSM (hsm-radacina.mjs), pe un HSM PKCS#11 REAL (SoftHSM2), nu pe o imitatie // a uneltei: token nou intr-un dosar temporar, cheie AES-256 generata IN token (sensitive, never extractable), apoi sigilare, // deschidere, KMS-ul pornit din radacina deschisa si o runda hibrida (ML-KEM-768 + X25519) prin el. Fiecare refuz se judeca dupa // MOTIV (codul), nu dupa faptul ca a aruncat ceva. // Cere: Node 24 (kms.mjs), softhsm2-util si pkcs11-tool (OpenSC) in PATH; libsofthsm2.so. Ruleaza in WSL / Linux: // /root/node24/bin/node test/proba-hsm.mjs -> 0 toate cum trebuia, 1 altfel, 2 NEMASURAT (HSM-ul de proba nu porneste) import crypto from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { sigileazaRadacina, deschideRadacina, radacinaDinHsm, FORMAT_HSM } from '../hsm-radacina.mjs'; import { configFromEnv } from '../server.mjs'; import { openKms } from '../kms.mjs'; const MODUL = process.env.AERE_PKCS11_MODUL || '/usr/lib/softhsm/libsofthsm2.so'; let rele = 0, n = 0; const cer = (nume, ok, extra = '') => { n++; console.log(` [${ok ? 'OK ' : 'RAU '}] ${nume}${extra ? ' (' + extra + ')' : ''}`); if (!ok) rele++; }; const codul = (fn) => { try { fn(); return 'FARA REFUZ'; } catch (e) { return e.code || String(e.message).slice(0, 60); } }; const nemasurat = (m) => { console.log('NEMASURAT: ' + m); process.exit(2); }; if (!fs.existsSync(MODUL)) nemasurat('lipseste modulul PKCS#11 ' + MODUL); const T = fs.mkdtempSync(path.join(os.tmpdir(), 'proba-hsm-')); // 2026-09-29 (H1): deschiderea cere biblioteca din configuratia procesului, nu din fisierul sigilat const env = { ...process.env, SOFTHSM2_CONF: path.join(T, 'softhsm2.conf'), AERE_HSM_PIN: '483920', AERE_HSM_MODULE: MODUL }; fs.mkdirSync(path.join(T, 'tokens')); fs.writeFileSync(env.SOFTHSM2_CONF, `directories.tokendir = ${path.join(T, 'tokens')}\nobjectstore.backend = file\nlog.level = ERROR\n`); const ruleaza = (c, a) => spawnSync(c, a, { env, encoding: 'utf8' }); try { let r = ruleaza('softhsm2-util', ['--init-token', '--free', '--label', 'aere-kms', '--pin', env.AERE_HSM_PIN, '--so-pin', '771122']); if (r.status !== 0) nemasurat('softhsm2-util nu initializeaza tokenul: ' + (r.stderr || r.error)); r = ruleaza('pkcs11-tool', ['--module', MODUL, '--token-label', 'aere-kms', '--login', '--pin', 'env:AERE_HSM_PIN', '--keygen', '--key-type', 'AES:32', '--label', 'kms-root', '--id', '0a', '--sensitive']); if (r.status !== 0) nemasurat('cheia AES nu se genereaza in token: ' + (r.stderr || r.error)); cer('cheia AES e generata IN token: sensitive, never extractable', /never extractable/.test(r.stdout) && /sensitive/.test(r.stdout)); r = ruleaza('pkcs11-tool', ['--module', MODUL, '--token-label', 'aere-kms', '--login', '--pin', 'env:AERE_HSM_PIN', '--read-object', '--type', 'secrkey', '--id', '0a', '--output-file', path.join(T, 'k')]); cer('cheia NU poate fi citita afara din HSM', r.status !== 0 && (!fs.existsSync(path.join(T, 'k')) || fs.statSync(path.join(T, 'k')).size === 0)); const radacina = crypto.randomBytes(32); const sig = sigileazaRadacina({ modul: MODUL, token: 'aere-kms', idCheie: '0a', radacina: Buffer.from(radacina), env }); cer('sigilarea: formatul, 64 de octeti, si radacina NU apare in ea', sig.format === FORMAT_HSM && sig.ct.length === 128 && !JSON.stringify(sig).includes(radacina.toString('hex'))); const d = deschideRadacina(sig, { env }); cer('deschiderea cu HSM-ul si PIN-ul da exact radacina sigilata', d.equals(radacina)); const s2 = sigileazaRadacina({ modul: MODUL, token: 'aere-kms', idCheie: '0a', radacina: Buffer.from(radacina), env }); cer('doua sigilari ale aceleiasi radacini difera (iv aleator), amandoua se deschid', s2.ct !== sig.ct && deschideRadacina(s2, { env }).equals(radacina)); // refuzuri, fiecare cu motivul lui cer('PIN gresit -> HSM_LOGIN_REFUSED', codul(() => deschideRadacina(sig, { env: { ...env, AERE_HSM_PIN: '000000' } })) === 'HSM_LOGIN_REFUSED'); cer('PIN lipsa -> HSM_PIN_MISSING (nu se incearca fara)', codul(() => deschideRadacina(sig, { env: { ...env, AERE_HSM_PIN: '' } })) === 'HSM_PIN_MISSING'); cer('alt token -> HSM_TOKEN_NOT_FOUND', codul(() => deschideRadacina({ ...sig, token: 'alt-token' }, { env })) === 'HSM_TOKEN_NOT_FOUND'); const cti = Buffer.from(sig.ct, 'hex'); cti[3] ^= 0x01; // primul bloc: CBC descifreaza, dar in altceva cer('sigilare atinsa in primul bloc -> HSM_ROOT_TAMPERED (amprenta), nu o radacina gresita', codul(() => deschideRadacina({ ...sig, ct: cti.toString('hex') }, { env })) === 'HSM_ROOT_TAMPERED'); const ctu = Buffer.from(sig.ct, 'hex'); ctu[63] ^= 0x80; // ultimul bloc: umplutura strica cer('sigilare atinsa in ultimul bloc -> HSM_ROOT_TAMPERED', codul(() => deschideRadacina({ ...sig, ct: ctu.toString('hex') }, { env })) === 'HSM_ROOT_TAMPERED'); // H2 (2026-09-29): umplutura stricata (ultimul bloc) si amprenta gresita (primul bloc) dau ACELASI mesaj: altfel jurnalul pornirii e un oracol de umplutura const mesaj = (s) => { try { deschideRadacina(s, { env }); return 'FARA REFUZ'; } catch (e) { return e.code + ': ' + e.message; } }; const m1 = mesaj({ ...sig, ct: cti.toString('hex') }), m2 = mesaj({ ...sig, ct: ctu.toString('hex') }); cer('H2: umplutura stricata si amprenta gresita nu se deosebesc prin mesaj', m1 === m2 && m1.startsWith('HSM_ROOT_TAMPERED'), m1 === m2 ? 'identice' : m1 + ' | ' + m2); // CONTROLUL NECESITATII amprentei: fara ea, CBC ar fi dat 48 de octeti de gunoi, deci amprenta e cea care prinde atingerea const ctf = path.join(T, 'c'), ptf = path.join(T, 'p'); fs.writeFileSync(ctf, cti); r = ruleaza('pkcs11-tool', ['--module', MODUL, '--token-label', 'aere-kms', '--login', '--pin', 'env:AERE_HSM_PIN', '--id', '0a', '--mechanism', 'AES-CBC-PAD', '--decrypt', '--iv', sig.iv, '--input-file', ctf, '--output-file', ptf]); cer('CONTROL: HSM-ul singur descifreaza sigilarea atinsa fara sa se planga (48 de octeti) - de aceea amprenta', r.status === 0 && fs.statSync(ptf).size === 48); r = ruleaza('pkcs11-tool', ['--module', MODUL, '--token-label', 'aere-kms', '--login', '--pin', 'env:AERE_HSM_PIN', '--keygen', '--key-type', 'AES:32', '--label', 'alta', '--id', '0b', '--sensitive']); cer('alta cheie din acelasi token -> HSM_ROOT_TAMPERED (nu se deschide cu cheia gresita)', r.status === 0 && codul(() => deschideRadacina({ ...sig, idCheie: '0b' }, { env })) === 'HSM_ROOT_TAMPERED'); // KMS-ul pornit din radacina deschisa de HSM, capat la capat const fsig = path.join(T, 'root-hsm.json'); fs.writeFileSync(fsig, JSON.stringify(sig)); const tok = crypto.randomBytes(24).toString('hex'); const cfg = configFromEnv({ ...env, AERE_KMS_ROOT_HSM: fsig, AERE_KMS_TOKEN: tok, AERE_KMS_DATA_DIR: path.join(T, 'date') }); cer('serverul ia radacina din HSM (rootSource=hsm) si e chiar cea sigilata', cfg.rootSource === 'hsm' && cfg.rootKey === radacina.toString('hex')); const kms = openKms({ dataDir: cfg.dataDir, rootKey: cfg.rootKey }); kms.createKey('client', { type: 'encrypt' }); const c = kms.encrypt('client', 'date de pastrat', 'ctx').ciphertext; cer('KMS pornit din radacina HSM: runda hibrida X25519 + ML-KEM-768', kms.decrypt('client', c, 'ctx').plaintext.toString() === 'date de pastrat'); kms.close(); const cfg2 = configFromEnv({ ...env, AERE_KMS_ROOT_HSM: fsig, AERE_KMS_TOKEN: tok, AERE_KMS_DATA_DIR: path.join(T, 'date') }); const kms2 = openKms({ dataDir: cfg2.dataDir, rootKey: cfg2.rootKey }); cer('redeschis din HSM, KMS-ul descifreaza ce a cifrat inainte (aceeasi radacina)', kms2.decrypt('client', c, 'ctx').plaintext.toString() === 'date de pastrat'); kms2.close(); cer('amandoua sursele de radacina deodata -> ROOT_KEY_AMBIGUOUS', codul(() => configFromEnv({ ...env, AERE_KMS_ROOT_HSM: fsig, AERE_KMS_ROOT_KEY: radacina.toString('hex'), AERE_KMS_TOKEN: tok })) === 'ROOT_KEY_AMBIGUOUS'); cer('sigilarea lipsa -> HSM_SEAL_INVALID', codul(() => radacinaDinHsm({ ...env, AERE_KMS_ROOT_HSM: path.join(T, 'nu-exista.json') })) === 'HSM_SEAL_INVALID'); cer('cu PIN gresit serverul REFUZA sa porneasca (nu cade pe alta radacina)', codul(() => configFromEnv({ ...env, AERE_HSM_PIN: '999999', AERE_KMS_ROOT_HSM: fsig, AERE_KMS_TOKEN: tok })) === 'HSM_LOGIN_REFUSED'); // /dev/shm curat dupa operatii: fisierele temporare cu radacina nu raman const ramase = fs.existsSync('/dev/shm') ? fs.readdirSync('/dev/shm').filter((x) => x.startsWith('aerekms-hsm-')) : []; cer('nicio urma a radacinii in /dev/shm dupa operatii', ramase.length === 0, ramase.join(',')); } finally { fs.rmSync(T, { recursive: true, force: true }); } console.log(`\nradacina KMS sigilata de HSM (SoftHSM2 + OpenSC): ${n - rele}/${n} cum trebuia`); process.exitCode = rele ? 1 : 0;