89 lines
8.7 KiB
JavaScript
89 lines
8.7 KiB
JavaScript
// 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;
|