aere-quantum/pq-gateway/proba-pq-gateway.mjs

966 lines
62 KiB
JavaScript

// Probele gateway-ului PQ (pq-gateway.mjs). Totul pe 127.0.0.1, pe porturi alese de sistem; nicio retea reala.
// node aerenew/cloud-gateway/pq-gateway/proba-pq-gateway.mjs
//
// Ce porneste: un certificat auto-semnat facut cu openssl CLI intr-un dosar temporar, un upstream HTTP local care
// ecouieste metoda, calea, antetele si sha256-ul corpului, si gateway-ul ca PROCES COPIL (node pq-gateway.mjs cu
// mediul lui), exact cum l-ar rula un client. Singura exceptie e (l), oprirea curata, care cheama functia exportata in
// proces: pe Windows un semnal trimis unui copil il omoara fara handler, deci semnalul adevarat nu se poate livra aici.
//
// Grupul negociat se citeste in trei feluri, fiindca fiecare singur ar fi putut minti:
// 1. DE PE FIR: un robinet TCP intre client si gateway retine octetii, iar ServerHello (in clar in TLS 1.3) se
// desface pana la extensia key_share (0x0033); 0x11ec = X25519MLKEM768, 0x001d = X25519.
// 2. openssl s_client, un client independent de codul nostru si de Node ("Negotiated TLS1.3 group").
// 3. getEphemeralKeyInfo() din clientul Node, TIPARIT exact. Masurat 2026-09-25 pe Node 24.14.1 + OpenSSL 3.5.5:
// intoarce {} pentru grupul hibrid (si {type:'ECDH',name:'X25519'} pentru cel clasic), deci API-ul NU numeste
// hibridul; proba cere doar ca el sa nu pretinda un grup clasic, iar numele il ia de pe fir.
//
// Drumul gateway -> upstream https se citeste la fel, cu un al doilea robinet in fata unui upstream https local.
//
// Iesire: un rand " OK ", " ESEC " sau " -- " (nemasurat) pe proba, apoi "N treceri, M esecuri". Cod 1 la esec,
// 3 cand pregatirea a cazut (STRICAT: nicio proba nu a rulat).
import http from 'node:http';
import https from 'node:https';
import tls from 'node:tls';
import net from 'node:net';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import readline from 'node:readline';
import { EventEmitter } from 'node:events';
import { spawn, spawnSync, execFileSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const GATEWAY = path.join(AICI, 'pq-gateway.mjs');
const CALE_STARE = '/.well-known/aere-pq-gateway';
const GAZDA = '127.0.0.1';
const HIBRID = 0x11ec, X25519 = 0x001d;
const NUME_GRUP = { 0x11ec: 'X25519MLKEM768', 0x11eb: 'SecP256r1MLKEM768', 0x11ed: 'SecP384r1MLKEM1024', 0x001d: 'X25519', 0x001e: 'X448', 0x0017: 'P-256', 0x0018: 'P-384', 0x0019: 'P-521' };
const numeGrup = (g) => (g === null || g === undefined ? 'nimic' : NUME_GRUP[g] || '0x' + g.toString(16).padStart(4, '0'));
const HRR = Buffer.from('cf21ad74e59a6111be1d8c021e65b891c2a211167abb8c5e079e09e2c8a8339c', 'hex');
const dormi = (ms) => new Promise((r) => setTimeout(r, ms));
let treceri = 0, esecuri = 0, nemasurate = 0;
const NEMASURAT = Symbol('nemasurat');
async function test(nume, fn) {
try {
const r = await fn();
if (r && r[NEMASURAT]) { console.log(` -- ${nume}: NEMASURAT, ${r.motiv}`); nemasurate++; return; }
console.log(` OK ${nume}${r ? ` [${r}]` : ''}`);
treceri++;
} catch (e) {
console.log(` ESEC ${nume}: ${String(e && e.message ? e.message : e).replace(/\s+/g, ' ').slice(0, 400)}`);
esecuri++;
}
}
function cere(cond, mesaj) { if (!cond) throw new Error(mesaj); }
// ---------------------------------------------------------------- pregatirea
const copii = [];
const deInchis = [];
let TMP = null;
function curata() {
for (const p of copii) { try { p.kill(); } catch { /* deja mort */ } }
for (const s of socluriTacute) s.destroy();
for (const s of deInchis) { try { s.close(); } catch { /* deja inchis */ } }
if (TMP) { try { fs.rmSync(TMP, { recursive: true, force: true }); } catch { /* ramane in temp */ } }
}
process.on('exit', curata);
// Paznic: o proba agatata nu are voie sa tina suita la nesfarsit. Dupa ultimul rand, un rest agatat (un socket
// ramas deschis) nu mai schimba verdictul, doar il spune.
let suitaTerminata = false;
const paznic = setTimeout(() => {
if (suitaTerminata) { console.log('(nota: bucla nu s-a golit singura in 240 s; iesire fortata cu verdictul de mai sus)'); process.exit(process.exitCode || 0); }
console.log('STRICAT: suita a depasit 240 s');
curata();
process.exit(3);
}, 240000);
paznic.unref();
function gasesteOpenssl() {
const candidati = [process.env.AERE_OPENSSL, 'openssl', 'C:\\Program Files\\Git\\mingw64\\bin\\openssl.exe', '/usr/bin/openssl'].filter(Boolean);
for (const bin of candidati) {
try {
const v = execFileSync(bin, ['version'], { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
const m = v.match(/OpenSSL (\d+)\.(\d+)/);
if (m && (Number(m[1]) > 3 || (Number(m[1]) === 3 && Number(m[2]) >= 5))) return { bin, versiune: v };
} catch { /* urmatorul */ }
}
return null;
}
// Robinet TCP: tine octetii fiecarei conexiuni in ambele sensuri, ca grupul sa se citeasca de pe fir.
function robinet(portTinta) {
const conexiuni = [];
const srv = net.createServer((cl) => {
const rec = { c2s: [], s2c: [] };
conexiuni.push(rec);
const sv = net.connect(portTinta, GAZDA);
cl.on('data', (b) => rec.c2s.push(b));
sv.on('data', (b) => rec.s2c.push(b));
cl.pipe(sv);
sv.pipe(cl);
const gata = () => { cl.destroy(); sv.destroy(); };
cl.on('error', gata); sv.on('error', gata); cl.on('close', gata); sv.on('close', gata);
});
deInchis.push(srv);
return new Promise((r) => srv.listen(0, GAZDA, () => r({ port: srv.address().port, conexiuni })));
}
// Mesajele de handshake in clar (inregistrari de tip 22 dinaintea primei inregistrari criptate, tip 23).
function mesajeHandshake(bucati) {
const b = Buffer.concat(bucati);
const flux = [];
let i = 0;
while (i + 5 <= b.length) {
const tip = b[i], lung = b.readUInt16BE(i + 3);
if (i + 5 + lung > b.length || tip === 23) break;
if (tip === 22) flux.push(b.subarray(i + 5, i + 5 + lung));
i += 5 + lung;
}
const h = Buffer.concat(flux);
const mesaje = [];
let p = 0;
while (p + 4 <= h.length) {
const lung = h.readUIntBE(p + 1, 3);
if (p + 4 + lung > h.length) break;
mesaje.push({ tip: h[p], corp: h.subarray(p + 4, p + 4 + lung) });
p += 4 + lung;
}
return mesaje;
}
function extensii(c, p, sfarsit) {
const m = new Map();
while (p + 4 <= sfarsit) { const t = c.readUInt16BE(p), l = c.readUInt16BE(p + 2); m.set(t, c.subarray(p + 4, p + 4 + l)); p += 4 + l; }
return m;
}
function clientHello(c) {
let p = 2 + 32;
p += 1 + c[p];
p += 2 + c.readUInt16BE(p);
p += 1 + c[p];
const lung = c.readUInt16BE(p); p += 2;
const ext = extensii(c, p, p + lung);
const grupuri = [], cote = [], versiuni = [];
const sg = ext.get(0x000a);
if (sg) for (let q = 2; q + 2 <= 2 + sg.readUInt16BE(0); q += 2) grupuri.push(sg.readUInt16BE(q));
const ks = ext.get(0x0033);
if (ks) { let q = 2; const sf = 2 + ks.readUInt16BE(0); while (q + 4 <= sf) { cote.push(ks.readUInt16BE(q)); q += 4 + ks.readUInt16BE(q + 2); } }
const sv = ext.get(0x002b);
if (sv) for (let q = 1; q + 2 <= 1 + sv[0]; q += 2) versiuni.push(sv.readUInt16BE(q));
else versiuni.push(c.readUInt16BE(0));
return { grupuri, cote, versiuni };
}
function serverHello(c) {
let p = 2;
const aleator = c.subarray(p, p + 32); p += 32;
p += 1 + c[p];
p += 3;
const lung = c.readUInt16BE(p); p += 2;
const ext = extensii(c, p, p + lung);
const ks = ext.get(0x0033), sv = ext.get(0x002b);
return { hrr: aleator.equals(HRR), grup: ks ? ks.readUInt16BE(0) : null, versiune: sv ? sv.readUInt16BE(0) : c.readUInt16BE(0) };
}
function citesteFir(rec) {
return {
ch: mesajeHandshake(rec.c2s).filter((m) => m.tip === 1).map((m) => clientHello(m.corp)),
sh: mesajeHandshake(rec.s2c).filter((m) => m.tip === 2).map((m) => serverHello(m.corp)),
};
}
const descrieSH = (sh) => sh.map((s) => (s.hrr ? `HRR->${numeGrup(s.grup)}` : `SH ${numeGrup(s.grup)}`)).join(', ') || 'niciun ServerHello';
let CERT_PEM, CHEIE_PEM, CALE_CERT, CALE_CHEIE, OPENSSL, AMPRENTA;
function cerere({ port, cale = '/', metoda = 'GET', anteturi = {}, corp = null, curbe = 'X25519MLKEM768', maxVersion, timeoutMs = 8000, ca = CERT_PEM }) {
return new Promise((resolve, reject) => {
let eki = null, protocol = null;
const r = https.request({
host: GAZDA, port, path: cale, method: metoda, headers: anteturi, ca, servername: 'localhost', agent: false,
...(curbe ? { ecdhCurve: curbe } : {}), ...(maxVersion ? { maxVersion } : {}),
}, (res) => {
const b = [];
res.on('data', (x) => b.push(x));
res.on('end', () => resolve({ status: res.statusCode, anteturi: res.headers, corp: Buffer.concat(b), eki, protocol }));
res.on('error', reject);
});
r.on('socket', (s) => s.once('secureConnect', () => { eki = s.getEphemeralKeyInfo(); protocol = s.getProtocol(); }));
r.setTimeout(timeoutMs, () => r.destroy(new Error(`clientul a asteptat ${timeoutMs} ms fara raspuns`)));
r.on('error', reject);
r.end(corp || undefined);
});
}
const json = (r) => { try { return JSON.parse(r.corp.toString('utf8')); } catch { throw new Error(`corpul nu e JSON (status ${r.status}): ${r.corp.toString('utf8').slice(0, 120)}`); } };
// Numai strangerea de mana; intoarce ok sau codul erorii din client.
function strangere({ port, curbe, maxVersion, alpn }) {
return new Promise((resolve) => {
const s = tls.connect({
host: GAZDA, port, ca: CERT_PEM, servername: 'localhost',
...(curbe ? { ecdhCurve: curbe } : {}), ...(maxVersion ? { maxVersion } : {}), ...(alpn ? { ALPNProtocols: alpn } : {}),
}, () => {
const r = { ok: true, eki: s.getEphemeralKeyInfo(), protocol: s.getProtocol(), alpn: s.alpnProtocol };
s.end();
resolve(r);
});
s.setTimeout(8000, () => s.destroy(new Error('strangerea nu s-a terminat in 8 s')));
s.on('error', (e) => resolve({ ok: false, cod: e.code, mesaj: e.message }));
});
}
// O cerere de upgrade scrisa de mana; intoarce tot ce a venit pana la inchiderea conexiunii.
function upgradeBrut(port, cale, ms = 6000) {
return new Promise((resolve, reject) => {
const cheie = crypto.randomBytes(16).toString('base64');
const s = tls.connect({ host: GAZDA, port, ca: CERT_PEM, servername: 'localhost', ecdhCurve: 'X25519MLKEM768' }, () => {
s.write(`GET ${cale} HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${cheie}\r\nSec-WebSocket-Version: 13\r\n\r\n`);
});
let acc = Buffer.alloc(0);
const t = setTimeout(() => { s.destroy(); reject(new Error(`conexiunea nu s-a inchis in ${ms} ms; primit: '${acc.toString('latin1').split('\r\n')[0] || 'nimic'}'`)); }, ms);
s.on('data', (b) => { acc = Buffer.concat([acc, b]); });
s.on('error', () => {});
s.on('close', () => {
clearTimeout(t);
const txt = acc.toString('latin1');
const k = txt.indexOf('\r\n\r\n');
resolve({ antet: k === -1 ? txt : txt.slice(0, k), corp: k === -1 ? '' : txt.slice(k + 4) });
});
});
}
async function stare(port, ca) { const r = await cerere({ port, cale: CALE_STARE, ...(ca ? { ca } : {}) }); cere(r.status === 200, `starea a raspuns ${r.status}`); return json(r); }
async function asteaptaStare(port, pred, ms = 4000, ca) {
const t0 = Date.now();
let s;
while (Date.now() - t0 < ms) { s = await stare(port, ca); if (pred(s)) return s; await dormi(100); }
return s;
}
function sClient(argumente, intrare = '', ms = 20000) {
return new Promise((resolve) => {
const p = spawn(OPENSSL, ['s_client', ...argumente], { stdio: ['pipe', 'pipe', 'pipe'] });
let o = '', e = '';
const t = setTimeout(() => p.kill(), ms);
p.stdout.on('data', (b) => { o += b; });
p.stderr.on('data', (b) => { e += b; });
p.on('close', (cod) => { clearTimeout(t); resolve({ cod, o, e }); });
p.on('error', (err) => { clearTimeout(t); resolve({ cod: -1, o, e: e + String(err.message) }); });
p.stdin.on('error', () => {});
p.stdin.end(intrare);
});
}
function mediuGateway(extra) {
const env = { ...process.env };
for (const k of Object.keys(env)) if (k.startsWith('AERE_PQGW_')) delete env[k];
return { ...env, ...extra };
}
function pornesteGw(nume, extra) {
return new Promise((resolve, reject) => {
const p = spawn(process.execPath, [GATEWAY], { env: mediuGateway(extra), stdio: ['ignore', 'pipe', 'pipe'] });
copii.push(p);
let err = '';
const jurnal = [];
p.stderr.on('data', (b) => { err += b; });
const t = setTimeout(() => reject(new Error(`${nume}: nu a raportat 'listening' in 10 s; stderr: ${err.slice(0, 300)}`)), 10000);
readline.createInterface({ input: p.stdout }).on('line', (l) => {
let j = null;
try { j = JSON.parse(l); } catch { return; }
jurnal.push(j);
if (j.event === 'listening') { clearTimeout(t); resolve({ p, port: j.port, nume, jurnal, pornire: j }); }
});
p.on('exit', (cod) => { clearTimeout(t); reject(new Error(`${nume} a iesit cu ${cod}: ${err.slice(0, 300)}`)); });
});
}
// Upstream-ul de proba: ecou pentru orice cerere, 101 + salut + ecou pentru upgrade (sau 403 pe /ws-refuz).
const jurnalUpstream = [];
const evUp = new EventEmitter();
// Raspunde INAINTE sa fi primit tot corpul, apoi curge inca ~2,8 s dupa sfarsitul cererii (ca un flux SSE).
function devreme(req, res) {
res.writeHead(200, { 'content-type': 'text/plain' });
res.write('inceput\n');
req.resume();
req.on('end', () => {
let n = 0;
const t = setInterval(() => {
if (res.destroyed) { clearInterval(t); return; }
n++;
if (n <= 6) res.write(`bucata ${n}\n`);
else { clearInterval(t); res.end('gata\n'); }
}, 400);
});
}
function ecou(req, res) {
if (req.url === '/devreme') return devreme(req, res);
const h = crypto.createHash('sha256');
let n = 0;
const id = req.headers['x-proba-id'];
req.on('data', (b) => { if (n === 0 && id) evUp.emit(`primul:${id}`); n += b.length; h.update(b); });
req.on('end', async () => {
const u = new URL(req.url, 'http://upstream.invalid');
jurnalUpstream.push({ method: req.method, url: req.url });
if (u.pathname === '/slow') await dormi(Number(u.searchParams.get('ms')) || 1000);
if (res.destroyed) return;
const corp = JSON.stringify({ method: req.method, url: req.url, rawHeaders: req.rawHeaders, headers: req.headers, bodySha256: h.digest('hex'), bodyBytes: n });
res.writeHead(200, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(corp) });
res.end(corp);
});
}
function laUpgrade(req, sock, head) {
jurnalUpstream.push({ method: req.method, url: req.url, upgrade: req.headers.upgrade, headers: req.headers, tls: Boolean(sock.encrypted) });
sock.on('error', () => {});
if (req.url === '/ws-refuz') { sock.end('HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nConnection: close\r\n\r\nnu-ws'); return; }
if (req.url === '/ws-lent') { setTimeout(() => sock.destroy(), 2500); return; }
const accept = crypto.createHash('sha1').update(String(req.headers['sec-websocket-key']) + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
sock.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
sock.write('SALUT-DE-LA-UPSTREAM\n');
if (head && head.length) sock.write(head);
sock.on('data', (d) => sock.write(d));
}
function pornesteUpstreamHttps() {
const srv = https.createServer({ key: CHEIE_PEM, cert: CERT_PEM }, ecou);
srv.on('upgrade', laUpgrade);
deInchis.push(srv);
return new Promise((r) => srv.listen(0, GAZDA, () => r(srv.address().port)));
}
// Accepta TCP si nu spune nimic: o strangere TLS catre el nu se termina niciodata.
const socluriTacute = new Set();
function pornesteTacut() {
const srv = net.createServer((s) => { socluriTacute.add(s); s.on('error', () => {}); s.on('close', () => socluriTacute.delete(s)); });
deInchis.push(srv);
return new Promise((r) => srv.listen(0, GAZDA, () => r(srv.address().port)));
}
function pornesteUpstream() {
const srv = http.createServer(ecou);
srv.on('upgrade', laUpgrade);
deInchis.push(srv);
return new Promise((r) => srv.listen(0, GAZDA, () => r(srv.address().port)));
}
async function portLiber() {
const s = net.createServer();
await new Promise((r) => s.listen(0, GAZDA, r));
const p = s.address().port;
await new Promise((r) => s.close(r));
return p;
}
let GH, GP, GM, GS, GSX, GT, TH, TP, TS, PORT_UP;
try {
const o = gasesteOpenssl();
if (!o) throw new Error('nu am gasit openssl >= 3.5 (AERE_OPENSSL, PATH sau Git for Windows)');
OPENSSL = o.bin;
TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-pqgw-proba-'));
CALE_CERT = path.join(TMP, 'cert.pem');
CALE_CHEIE = path.join(TMP, 'cheie.pem');
// execFileSync cu cai native (Node pe Windows da cai Windows), deci nicio conversie MSYS pe drum.
execFileSync(OPENSSL, ['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', '-nodes', '-keyout', CALE_CHEIE, '-out', CALE_CERT,
'-days', '2', '-subj', '/CN=localhost', '-addext', 'subjectAltName=DNS:localhost,IP:127.0.0.1'], { stdio: ['ignore', 'pipe', 'pipe'] });
CERT_PEM = fs.readFileSync(CALE_CERT);
CHEIE_PEM = fs.readFileSync(CALE_CHEIE, 'utf8');
AMPRENTA = new crypto.X509Certificate(CERT_PEM).fingerprint256;
PORT_UP = await pornesteUpstream();
const portUpHttps = await pornesteUpstreamHttps();
const portTacut = await pornesteTacut();
TS = await robinet(portUpHttps);
const baza = { AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: CALE_CERT, AERE_PQGW_KEY: CALE_CHEIE, AERE_PQGW_MODE: 'hybrid-only' };
[GH, GP, GM, GS, GSX, GT] = await Promise.all([
pornesteGw('gateway hybrid-only', { ...baza, AERE_PQGW_UPSTREAM: `http://${GAZDA}:${PORT_UP}`, AERE_PQGW_REQUEST_TIMEOUT_MS: '1500' }),
pornesteGw('gateway hybrid-preferred', { ...baza, AERE_PQGW_MODE: 'hybrid-preferred', AERE_PQGW_UPSTREAM: `http://${GAZDA}:${PORT_UP}` }),
pornesteGw('gateway catre upstream oprit', { ...baza, AERE_PQGW_UPSTREAM: `http://${GAZDA}:${await portLiber()}` }),
pornesteGw('gateway catre upstream https (cu CA)', { ...baza, AERE_PQGW_UPSTREAM: `https://${GAZDA}:${TS.port}`, AERE_PQGW_UPSTREAM_CA: CALE_CERT }),
pornesteGw('gateway catre upstream https (fara CA)', { ...baza, AERE_PQGW_UPSTREAM: `https://${GAZDA}:${portUpHttps}` }),
pornesteGw('gateway catre upstream tacut', { ...baza, AERE_PQGW_UPSTREAM: `https://${GAZDA}:${portTacut}`, AERE_PQGW_UPSTREAM_CA: CALE_CERT, AERE_PQGW_CONNECT_TIMEOUT_MS: '1000' }),
]);
[TH, TP] = await Promise.all([robinet(GH.port), robinet(GP.port)]);
console.log(`pregatire: ${o.versiune}; Node ${process.version} + OpenSSL ${process.versions.openssl}; certificat ${AMPRENTA.slice(0, 23)}...`);
} catch (e) {
console.log(`STRICAT: pregatirea a cazut, nicio proba nu a rulat: ${e.message}`);
process.exitCode = 3;
curata();
process.exit(3);
}
// ---------------------------------------------------------------- pozitive
await test('(a) hybrid-only + client Node X25519MLKEM768: cererea trece, grupul de pe fir e X25519MLKEM768', async () => {
const inainte = TH.conexiuni.length;
const r = await cerere({ port: TH.port, cale: '/a?x=1', curbe: 'X25519MLKEM768' });
cere(r.status === 200, `status ${r.status}`);
cere(json(r).url === '/a?x=1', `upstream-ul a vazut ${json(r).url}`);
cere(r.protocol === 'TLSv1.3', `protocol ${r.protocol}`);
const rec = TH.conexiuni[inainte];
cere(rec, 'robinetul nu a vazut conexiunea');
const f = citesteFir(rec);
cere(f.ch.length >= 1 && f.ch[0].cote.includes(HIBRID), `ClientHello nu poarta o cota X25519MLKEM768 (cote: ${f.ch[0] ? f.ch[0].cote.map(numeGrup) : '-'})`);
const final = f.sh.filter((s) => !s.hrr).pop();
cere(final && final.grup === HIBRID, `ServerHello de pe fir: ${descrieSH(f.sh)}`);
const ekiText = JSON.stringify(r.eki);
cere(!r.eki || !r.eki.name || /MLKEM/i.test(r.eki.name), `getEphemeralKeyInfo pretinde un grup clasic: ${ekiText}`);
return `fir: ${descrieSH(f.sh)}; getEphemeralKeyInfo() = ${ekiText}${r.eki && r.eki.name ? '' : ' (Node nu numeste grupul hibrid)'}`;
});
await test('(b) openssl s_client -groups X25519MLKEM768 (client independent): strangere reusita, grupul negociat X25519MLKEM768', async () => {
const r = await sClient(['-connect', `${GAZDA}:${GH.port}`, '-groups', 'X25519MLKEM768', '-servername', 'localhost', '-CAfile', CALE_CERT,
'-verify_return_error', '-ign_eof'], 'GET /b-openssl HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n');
const rand = (r.o.split(/\r?\n/).find((l) => /^Negotiated TLS1\.3 group:/.test(l)) || '').trim();
cere(r.cod === 0, `s_client a iesit cu ${r.cod}: ${r.e.slice(0, 200)}`);
cere(rand.includes('X25519MLKEM768'), `randul grupului: '${rand || 'lipseste'}'`);
cere(/Verify return code: 0 \(ok\)/.test(r.o), 'certificatul nu a fost verificat de s_client');
cere(/HTTP\/1\.1 200/.test(r.o) && r.o.includes('"url":"/b-openssl"'), 'raspunsul HTTP prin s_client lipseste sau nu vine de la upstream');
return rand;
});
await test('(c) POST de 1 MiB ajunge intreg la upstream (sha256 egal)', async () => {
const corp = crypto.randomBytes(1024 * 1024);
const r = await cerere({ port: GH.port, cale: '/c', metoda: 'POST', corp, anteturi: { 'content-type': 'application/octet-stream', 'content-length': corp.length } });
cere(r.status === 200, `status ${r.status}`);
const j = json(r);
const asteptat = crypto.createHash('sha256').update(corp).digest('hex');
cere(j.bodyBytes === corp.length, `upstream-ul a primit ${j.bodyBytes} octeti din ${corp.length}`);
cere(j.bodySha256 === asteptat, 'sha256 diferit la upstream');
return `${j.bodyBytes} octeti, sha256 ${asteptat.slice(0, 16)}...`;
});
await test('(k) corpul curge catre upstream fara bufferare intreaga (chunked, cererea ramane deschisa)', async () => {
const id = `curge-${crypto.randomUUID()}`;
const primul = new Promise((r) => evUp.once(`primul:${id}`, () => r(true)));
const p1 = crypto.randomBytes(64 * 1024), p2 = crypto.randomBytes(64 * 1024);
let aVazut = null;
const r = await new Promise((resolve, reject) => {
const q = https.request({ host: GAZDA, port: GH.port, path: '/curge', method: 'POST', headers: { 'x-proba-id': id, 'content-type': 'application/octet-stream' },
ca: CERT_PEM, servername: 'localhost', ecdhCurve: 'X25519MLKEM768', agent: false }, (res) => {
const b = [];
res.on('data', (x) => b.push(x));
res.on('end', () => resolve({ status: res.statusCode, corp: Buffer.concat(b) }));
});
q.setTimeout(15000, () => q.destroy(new Error('fara raspuns in 15 s')));
q.on('error', reject);
q.write(p1);
Promise.race([primul, dormi(5000).then(() => false)]).then((v) => { aVazut = v; q.end(p2); });
});
cere(aVazut === true, 'upstream-ul nu a primit NIMIC din corp cat timp clientul tinea cererea deschisa (5 s): corpul e bufferat');
cere(r.status === 200, `status ${r.status}`);
const j = json(r);
cere(j.bodySha256 === crypto.createHash('sha256').update(Buffer.concat([p1, p2])).digest('hex') && j.bodyBytes === p1.length + p2.length, `corp diferit la upstream (${j.bodyBytes} octeti)`);
cere(!j.headers['transfer-encoding'] || j.headers['transfer-encoding'] === 'chunked', `transfer-encoding la upstream: ${j.headers['transfer-encoding']}`);
return `primii octeti la upstream inainte de sfarsitul cererii; ${j.bodyBytes} octeti intregi`;
});
// Un tunel WebSocket prin gateway-ul de pe `port`: 101, salutul upstream-ului, apoi ecoul unei incarcaturi aleatoare.
async function verificaTunel(port, cale) {
const cheie = crypto.randomBytes(16).toString('base64');
const asteptatAccept = crypto.createHash('sha1').update(cheie + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
const incarcatura = crypto.randomBytes(32 * 1024);
const salut = 'SALUT-DE-LA-UPSTREAM\n';
const rez = await new Promise((resolve, reject) => {
const s = tls.connect({ host: GAZDA, port, ca: CERT_PEM, servername: 'localhost', ecdhCurve: 'X25519MLKEM768' }, () => {
s.write(`GET ${cale} HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${cheie}\r\nSec-WebSocket-Version: 13\r\n\r\n`);
});
let acc = Buffer.alloc(0), antet = null, trimis = false;
const t = setTimeout(() => { s.destroy(); reject(new Error(`tunelul nu a intors tot in 8 s (antet: ${antet ? antet.split('\r\n')[0] : 'niciunul'}, octeti dupa antet: ${antet ? acc.length : 0})`)); }, 8000);
s.on('data', (b) => {
acc = Buffer.concat([acc, b]);
if (antet === null) {
const k = acc.indexOf('\r\n\r\n');
if (k === -1) return;
antet = acc.subarray(0, k).toString('latin1');
acc = acc.subarray(k + 4);
}
if (!trimis && acc.length >= salut.length) { trimis = true; s.write(incarcatura); }
if (acc.length >= salut.length + incarcatura.length) { clearTimeout(t); s.end(); resolve({ antet, acc }); }
});
s.on('error', (e) => { clearTimeout(t); reject(e); });
});
cere(/^HTTP\/1\.1 101/.test(rez.antet), `raspuns: ${rez.antet.split('\r\n')[0]}`);
cere(rez.antet.toLowerCase().includes(`sec-websocket-accept: ${asteptatAccept.toLowerCase()}`), 'Sec-WebSocket-Accept lipsa sau gresit');
cere(rez.acc.subarray(0, salut.length).toString() === salut, 'salutul upstream -> client nu a trecut');
cere(rez.acc.subarray(salut.length, salut.length + incarcatura.length).equals(incarcatura), 'ecoul client -> upstream -> client difera');
const u = jurnalUpstream.filter((x) => x.upgrade && x.url === cale).pop();
cere(u && u.upgrade === 'websocket' && u.headers['x-aere-pq-gateway'] === 'hybrid-only' && u.headers['x-forwarded-proto'] === 'https', 'upstream-ul nu a vazut cererea de upgrade cu antetele gateway-ului');
return { octeti: incarcatura.length, u };
}
await test('(k2) raspuns inceput inainte de sfarsitul cererii si care curge mai mult decat REQUEST_TIMEOUT: nu e taiat', async () => {
const t0 = Date.now();
let msLaSfarsitCerere = null;
const r = await new Promise((resolve, reject) => {
const q = https.request({ host: GAZDA, port: GH.port, path: '/devreme', method: 'POST', headers: { 'content-type': 'application/octet-stream' },
ca: CERT_PEM, servername: 'localhost', ecdhCurve: 'X25519MLKEM768', agent: false }, (res) => {
let corp = '';
res.setEncoding('utf8');
res.on('data', (x) => { corp += x; });
res.on('end', () => resolve({ status: res.statusCode, corp, complet: res.complete }));
res.on('close', () => { if (!res.complete) resolve({ status: res.statusCode, corp, complet: false }); });
res.on('error', () => {});
// cererea se incheie abia DUPA ce raspunsul a inceput
msLaSfarsitCerere = Date.now() - t0;
q.end('sfarsitul-cererii');
});
q.setTimeout(10000, () => q.destroy(new Error('fara raspuns complet in 10 s')));
q.on('error', reject);
q.write('inceputul-cererii');
});
const ms = Date.now() - t0;
cere(r.status === 200, `status ${r.status}`);
cere(r.complet && r.corp.endsWith('gata\n') && (r.corp.match(/bucata/g) || []).length === 6, `raspuns taiat la ${ms} ms (${ms - msLaSfarsitCerere} ms dupa sfarsitul cererii): ${JSON.stringify(r.corp.slice(-30))}`);
cere(ms - msLaSfarsitCerere > 2000, `fixtura: raspunsul a curs doar ${ms - msLaSfarsitCerere} ms dupa sfarsitul cererii, sub REQUEST_TIMEOUT+marja, deci nu masoara nimic`);
return `raspuns complet, ${ms - msLaSfarsitCerere} ms dupa sfarsitul cererii (REQUEST_TIMEOUT 1500)`;
});
await test('(d) upgrade WebSocket: 101, salutul upstream-ului si ecoul trec prin tunel in ambele sensuri', async () => {
const r = await verificaTunel(GH.port, '/ws');
cere(r.u.tls === false, 'fixtura: upstream-ul acestui gateway trebuia sa fie http');
return `${r.octeti} octeti ecou identici, salut primit`;
});
await test('(d3) upgrade WebSocket catre upstream https (UPSTREAM_CA): tunelul merge, iar drumul catre upstream prefera hibridul', async () => {
const inainte = TS.conexiuni.length;
const r = await verificaTunel(GS.port, '/ws-tls');
cere(r.u.tls === true, 'upstream-ul nu a primit upgrade-ul peste TLS');
const rec = TS.conexiuni.slice(inainte).find((x) => citesteFir(x).sh.length > 0);
cere(rec, 'robinetul din fata upstream-ului https nu a vazut strangerea de mana a tunelului');
const f = citesteFir(rec);
const final = f.sh.filter((s) => !s.hrr).pop();
cere(final && final.grup === HIBRID, `drumul tunelului catre upstream: ${descrieSH(f.sh)}`);
return `${r.octeti} octeti ecou identici peste TLS; drum catre upstream: ${descrieSH(f.sh)}`;
});
await test('(d2) upstream-ul refuza upgrade-ul: raspunsul lui (403) ajunge la client cu Connection: close, apoi conexiunea se inchide', async () => {
const r = await upgradeBrut(GH.port, '/ws-refuz');
cere(/^HTTP\/1\.1 403/.test(r.antet), `raspuns: '${r.antet.split('\r\n')[0]}'`);
cere(/\r\nconnection: close(\r\n|$)/i.test(r.antet), `fara Connection: close in raspuns (${r.antet.replace(/\r\n/g, ' | ').slice(0, 200)})`);
cere(r.corp === 'nu-ws', `corp '${r.corp.slice(0, 40)}'`);
return '403 + corpul upstream-ului, conexiune inchisa';
});
await test('(e) antetele: X-Forwarded-* puse de gateway, x-aere-pq-gateway prezent, hop-by-hop si numele din Connection NU ajung', async () => {
const r = await cerere({ port: GH.port, cale: '/e', anteturi: {
Connection: 'keep-alive, X-Secret-Hop', 'X-Secret-Hop': 'nu-trebuie-sa-ajunga', 'Keep-Alive': 'timeout=5', 'Proxy-Authorization': 'Basic bnU=',
TE: 'trailers', 'X-Forwarded-For': '203.0.113.9', 'x-aere-pq-gateway': 'falsificat', 'X-End-To-End': 'da', Host: 'aplicatia.example',
} });
cere(r.status === 200, `status ${r.status}`);
const h = json(r).headers;
// controlul pozitiv al fixturii: un antet obisnuit TREBUIE sa treaca, altfel lipsa celorlalte nu inseamna nimic
cere(h['x-end-to-end'] === 'da', 'nici antetul obisnuit nu a ajuns: fixtura nu masoara nimic');
cere(h['x-forwarded-proto'] === 'https', `X-Forwarded-Proto: ${h['x-forwarded-proto']}`);
cere(h['x-aere-pq-gateway'] === 'hybrid-only', `x-aere-pq-gateway: ${h['x-aere-pq-gateway']}`);
cere(h['x-forwarded-host'] === 'aplicatia.example', `X-Forwarded-Host: ${h['x-forwarded-host']}`);
cere(h['x-forwarded-for'] === '127.0.0.1', `X-Forwarded-For: ${h['x-forwarded-for']} (valoarea clientului nu are voie sa treaca)`);
const scapate = ['x-secret-hop', 'keep-alive', 'proxy-authorization', 'te'].filter((n) => n in h);
cere(scapate.length === 0, `au ajuns la upstream: ${scapate.join(', ')}`);
cere(!String(h.connection || '').toLowerCase().includes('x-secret-hop'), `Connection la upstream: ${h.connection}`);
return 'x-secret-hop, keep-alive, proxy-authorization, te oprite; x-end-to-end trecut';
});
await test('(e2) X-Forwarded-For pe un ascultator dual-stack: ::ffff:a.b.c.d devine a.b.c.d, restul ramane neatins (functia, fara socket)', async () => {
// Masurat pe functie, nu pe un socket: un ascultator dual-stack ar insemna sa ascultam pe toate interfetele.
const mod = await import(pathToFileURL(GATEWAY).href);
const cazuri = [['::ffff:127.0.0.1', '127.0.0.1'], ['::FFFF:198.51.100.7', '198.51.100.7'], ['::1', '::1'], ['2001:db8::5', '2001:db8::5'], ['127.0.0.1', '127.0.0.1'], ['::ffff:nu-e-ip', '::ffff:nu-e-ip'], [undefined, '']];
const rele = cazuri.filter(([i, o]) => mod.adresaClient(i) !== o).map(([i, o]) => `${i} -> ${mod.adresaClient(i)} (asteptat ${o})`);
cere(rele.length === 0, rele.join('; '));
return `${cazuri.length} cazuri`;
});
await test('(f) punctul de stare: mode, contoare, amprenta certificatului, FARA cheia privata, servit de gateway (nu de upstream)', async () => {
const inainte = jurnalUpstream.length;
const r = await cerere({ port: GH.port, cale: CALE_STARE });
cere(r.status === 200, `status ${r.status}`);
cere(String(r.anteturi['content-type']).startsWith('application/json'), `content-type ${r.anteturi['content-type']}`);
const text = r.corp.toString('utf8');
const s = JSON.parse(text);
cere(s.mode === 'hybrid-only', `mode ${s.mode}`);
cere(JSON.stringify(s.groupsOffered) === '["X25519MLKEM768"]', `groupsOffered ${JSON.stringify(s.groupsOffered)}`);
cere(s.minTlsVersion === 'TLSv1.3', `minTlsVersion ${s.minTlsVersion}`);
cere(s.guarantee && s.guarantee.hybridKeyExchangeOnEveryConnection === true, 'hybrid-only nu isi declara garantia structurala');
cere(s.certificate && s.certificate.sha256 === AMPRENTA, `amprenta ${s.certificate && s.certificate.sha256} != ${AMPRENTA}`);
const k = s.counters || {};
for (const n of ['connectionsAccepted', 'requestsForwarded', 'responses502', 'responses504']) cere(Number.isInteger(k[n]), `contorul ${n} lipseste`);
cere(k.handshakesRefused && ['no shared group', 'unsupported protocol', 'other'].every((m) => Number.isInteger(k.handshakesRefused[m])), 'contoarele de refuz pe motiv lipsesc');
cere(k.connectionsAccepted >= 1 && k.requestsForwarded >= 1, 'contoarele nu numara nimic dupa probele de dinainte');
const baza64 = CHEIE_PEM.split(/\r?\n/).filter((l) => l && !l.startsWith('-----')).join('');
const fragmente = [baza64.slice(8, 48), baza64.slice(-40, -4), 'PRIVATE KEY', CALE_CHEIE, path.basename(CALE_CHEIE)];
const gasite = fragmente.filter((x) => x && text.includes(x));
cere(gasite.length === 0, `starea contine material sau cale a cheii (${gasite.length} fragmente)`);
cere(jurnalUpstream.slice(inainte).every((x) => !x.url.startsWith(CALE_STARE)), 'cererea de stare a fost trimisa la upstream');
return `acceptate ${k.connectionsAccepted}, trimise ${k.requestsForwarded}; ${fragmente.length} fragmente ale cheii cautate, 0 gasite`;
});
// ---------------------------------------------------------------- controale negative, fiecare cu MOTIVUL
await test('(g) hybrid-only + client numai X25519: refuzat la strangerea de mana, motivul numarat e "no shared group"', async () => {
const s0 = await stare(GH.port);
const inainte = TH.conexiuni.length;
const r = await strangere({ port: TH.port, curbe: 'X25519' });
const f = citesteFir(TH.conexiuni[inainte] || { c2s: [], s2c: [] });
// fixtura trebuie sa poata EXPRIMA atacul: clientul chiar nu a oferit hibridul
cere(f.ch.length === 1 && f.ch[0].grupuri.length === 1 && f.ch[0].grupuri[0] === X25519, `clientul de proba a oferit ${f.ch[0] ? f.ch[0].grupuri.map(numeGrup) : '-'}, nu numai X25519`);
cere(!r.ok, `strangerea de mana a REUSIT fara hibrid (${descrieSH(f.sh)}, getEphemeralKeyInfo ${JSON.stringify(r.eki)})`);
cere(f.sh.length === 0, `serverul a trimis ${descrieSH(f.sh)}`);
cere(r.cod === 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE', `clientul a primit alt refuz: ${r.cod}`);
const s1 = await asteaptaStare(GH.port, (s) => s.counters.handshakesRefused['no shared group'] > s0.counters.handshakesRefused['no shared group']);
const d = (m) => s1.counters.handshakesRefused[m] - s0.counters.handshakesRefused[m];
cere(d('no shared group') === 1, `contorul 'no shared group' a crescut cu ${d('no shared group')} (unsupported protocol +${d('unsupported protocol')}, other +${d('other')})`);
cere(d('unsupported protocol') === 0 && d('other') === 0, `au crescut si alte motive: unsupported protocol +${d('unsupported protocol')}, other +${d('other')}`);
return `client: ${r.cod}; server: 'no shared group' +1 (${JSON.stringify(s1.counters.handshakesRefusedByCode)})`;
});
await test('(g2) hybrid-only + openssl s_client -groups X25519 (client independent): refuzat cu alerta handshake failure', async () => {
const s0 = await stare(GH.port);
const r = await sClient(['-connect', `${GAZDA}:${GH.port}`, '-groups', 'X25519', '-servername', 'localhost', '-CAfile', CALE_CERT], '');
const tot = r.o + r.e;
// Masurat 2026-09-25: la refuz s_client tipareste totusi randul grupului, cu '<NULL>', si 'Cipher is (NONE)'.
const rand = (r.o.split(/\r?\n/).find((l) => /^Negotiated TLS1\.3 group:/.test(l)) || '').trim();
cere(r.cod !== 0, 's_client a iesit cu 0: strangerea de mana a reusit fara hibrid');
cere(/alert handshake failure/i.test(tot) && /SSL alert number 40/.test(tot), `s_client nu a raportat alerta handshake failure (40): ${tot.replace(/\s+/g, ' ').slice(0, 200)}`);
cere(rand === '' || /<NULL>$/.test(rand), `s_client raporteaza un grup negociat: '${rand}'`);
cere(/Cipher is \(NONE\)/.test(r.o), 's_client raporteaza o suita negociata');
const s1 = await asteaptaStare(GH.port, (s) => s.counters.handshakesRefused['no shared group'] > s0.counters.handshakesRefused['no shared group']);
cere(s1.counters.handshakesRefused['no shared group'] === s0.counters.handshakesRefused['no shared group'] + 1, "contorul 'no shared group' nu a crescut cu 1");
return (tot.match(/[^\n]*alert handshake failure[^\n]*/i) || [''])[0].trim().slice(0, 120);
});
await test('(h) client TLS 1.2 (maxVersion TLSv1.2): refuzat in AMBELE moduri, motivul numarat e "unsupported protocol"', async () => {
// Amandoua gateway-urile se masoara inainte de verdict, ca un esec pe primul sa nu ascunda ce spune al doilea.
const note = [], probleme = [];
for (const [g, t] of [[GH, TH], [GP, TP]]) {
try {
const s0 = await stare(g.port);
const inainte = t.conexiuni.length;
const r = await strangere({ port: t.port, maxVersion: 'TLSv1.2' });
const f = citesteFir(t.conexiuni[inainte] || { c2s: [], s2c: [] });
cere(f.ch.length === 1 && !f.ch[0].versiuni.includes(0x0304), `clientul de proba a oferit TLS 1.3 (${f.ch[0] ? f.ch[0].versiuni.map((v) => v.toString(16)) : '-'})`);
cere(!r.ok, `un client TLS 1.2 a fost ACCEPTAT (${r.protocol})`);
const s1 = await asteaptaStare(g.port, (s) => s.counters.handshakesRefused['unsupported protocol'] > s0.counters.handshakesRefused['unsupported protocol'], 2000);
const d = (m) => s1.counters.handshakesRefused[m] - s0.counters.handshakesRefused[m];
cere(r.cod === 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', `clientul a primit alt refuz: ${r.cod} (server: 'no shared cipher' +${d('no shared cipher')}, other +${d('other')})`);
cere(d('unsupported protocol') === 1, `'unsupported protocol' +${d('unsupported protocol')}, 'no shared cipher' +${d('no shared cipher')}, other +${d('other')}`);
note.push(`${s1.mode}: ${r.cod}, 'unsupported protocol' +1`);
} catch (e) { probleme.push(`${g.nume}: ${e.message}`); }
}
cere(probleme.length === 0, probleme.join(' | '));
return note.join('; ');
});
await test('(i) upstream oprit: 502 cu corp JSON, in cateva secunde, numarat', async () => {
const s0 = await stare(GM.port);
const t0 = Date.now();
const r = await cerere({ port: GM.port, cale: '/i', timeoutMs: 9000 });
const ms = Date.now() - t0;
cere(r.status === 502, `status ${r.status}`);
const j = json(r);
cere(j.error === 'bad_gateway', `corp ${JSON.stringify(j)}`);
cere(ms < 7000, `a durat ${ms} ms`);
const s1 = await stare(GM.port);
cere(s1.counters.responses502 === s0.counters.responses502 + 1, `responses502 ${s0.counters.responses502} -> ${s1.counters.responses502}`);
return `502 in ${ms} ms: ${JSON.stringify(j)}`;
});
await test('(i2) upstream care nu raspunde la timp: 504 cu corp JSON dupa REQUEST_TIMEOUT, numarat', async () => {
const s0 = await stare(GH.port);
const t0 = Date.now();
const r = await cerere({ port: GH.port, cale: '/slow?ms=6000', timeoutMs: 9000 });
const ms = Date.now() - t0;
cere(r.status === 504, `status ${r.status} dupa ${ms} ms`);
const j = json(r);
cere(j.error === 'gateway_timeout', `corp ${JSON.stringify(j)}`);
cere(ms >= 1400 && ms < 4500, `a durat ${ms} ms (timeout configurat 1500)`);
const s1 = await stare(GH.port);
cere(s1.counters.responses504 === s0.counters.responses504 + 1, `responses504 ${s0.counters.responses504} -> ${s1.counters.responses504}`);
return `504 in ${ms} ms`;
});
await test('(i3) upgrade catre upstream oprit: 502 cu corp JSON pe socket, conexiune inchisa, numarat', async () => {
const s0 = await stare(GM.port);
const r = await upgradeBrut(GM.port, '/ws');
cere(/^HTTP\/1\.1 502/.test(r.antet), `raspuns: '${r.antet.split('\r\n')[0]}'`);
let j;
try { j = JSON.parse(r.corp); } catch { throw new Error(`corpul nu e JSON: '${r.corp.slice(0, 80)}'`); }
cere(j.error === 'bad_gateway', `corp ${r.corp.trim()}`);
const s1 = await stare(GM.port);
cere(s1.counters.responses502 === s0.counters.responses502 + 1, `responses502 ${s0.counters.responses502} -> ${s1.counters.responses502}`);
return r.corp.trim();
});
await test('(s) clientul pleaca inainte de raspuns (cerere si upgrade): nu se numara ca 502/504 si nu apare ca eroare de upstream', async () => {
const s0 = await stare(GP.port);
const jurnal0 = GP.jurnal.length;
const plecat = await new Promise((resolve) => {
const q = https.request({ host: GAZDA, port: GP.port, path: '/slow?ms=2500', ca: CERT_PEM, servername: 'localhost', agent: false });
q.on('error', () => {});
q.end();
setTimeout(() => { q.destroy(); resolve(true); }, 400);
});
const sus = await new Promise((resolve) => {
const s = tls.connect({ host: GAZDA, port: GP.port, ca: CERT_PEM, servername: 'localhost' }, () => {
s.write('GET /ws-lent HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGVzdGVzdGVzdGVzdGVzdA==\r\nSec-WebSocket-Version: 13\r\n\r\n');
setTimeout(() => { s.destroy(); resolve(true); }, 400);
});
s.on('error', () => {});
});
cere(plecat && sus, 'fixtura nu a putut pleca');
await dormi(700);
const s1 = await stare(GP.port);
const d502 = s1.counters.responses502 - s0.counters.responses502, d504 = s1.counters.responses504 - s0.counters.responses504;
const erori = GP.jurnal.slice(jurnal0).filter((j) => j.event === 'upstream_error');
cere(s1.counters.requestsForwarded >= s0.counters.requestsForwarded + 2, `fixtura: cererile nu au ajuns la upstream (trimise +${s1.counters.requestsForwarded - s0.counters.requestsForwarded})`);
cere(d502 === 0 && d504 === 0, `plecarea clientului numarata ca eroare de upstream: responses502 +${d502}, responses504 +${d504}`);
cere(erori.length === 0, `jurnalul gateway-ului scrie upstream_error pentru o plecare a clientului: ${JSON.stringify(erori.map((j) => j.code))}`);
return 'responses502 +0, responses504 +0, niciun upstream_error';
});
await test('(q) upstream https cu AERE_PQGW_UPSTREAM_CA: cererea trece, iar drumul gateway -> upstream prefera hibridul (citit de pe fir)', async () => {
const inainte = TS.conexiuni.length;
const r = await cerere({ port: GS.port, cale: '/q' });
cere(r.status === 200 && json(r).url === '/q', `status ${r.status}`);
cere(json(r).headers['x-aere-pq-gateway'] === 'hybrid-only', 'upstream-ul https nu a vazut antetul gateway-ului');
const rec = TS.conexiuni[inainte];
cere(rec, 'robinetul din fata upstream-ului https nu a vazut conexiunea');
const f = citesteFir(rec);
const final = f.sh.filter((s) => !s.hrr).pop();
cere(final && final.grup === HIBRID, `drumul catre upstream: ${descrieSH(f.sh)}`);
const s = await stare(GS.port);
cere(s.upstream && s.upstream.scheme === 'https' && s.upstream.encrypted === true, `starea: ${JSON.stringify(s.upstream)}`);
cere(JSON.stringify(Object.keys(s.upstream)) === '["scheme","encrypted"]', `starea spune despre upstream mai mult decat schema: ${JSON.stringify(s.upstream)}`);
return `drum catre upstream: ${descrieSH(f.sh)}`;
});
await test('(q2) upstream https cu certificat pe care gateway-ul nu il poate verifica (fara UPSTREAM_CA): 502, nicio incredere oarba', async () => {
const inainte = jurnalUpstream.length;
const r = await cerere({ port: GSX.port, cale: '/q2' });
cere(r.status === 502, `status ${r.status}: gateway-ul a trimis cererea unui upstream neverificat`);
cere(json(r).error === 'bad_gateway', `corp ${r.corp.toString().trim()}`);
cere(jurnalUpstream.slice(inainte).every((x) => x.url !== '/q2'), 'cererea a ajuns la upstream');
const t0 = Date.now();
let rand;
while (!(rand = GSX.jurnal.filter((j) => j.event === 'upstream_error').pop()) && Date.now() - t0 < 2000) await dormi(50);
cere(rand && /CERT|SELF_SIGNED|VERIFY/i.test(String(rand.code)), `motivul din jurnalul gateway-ului: ${rand ? rand.code : 'lipseste'}`);
return `502, motiv in jurnal: ${rand.code}`;
});
await test('(r) upstream care accepta TCP dar nu termina strangerea TLS: 504 "upstream connect timeout" dupa CONNECT_TIMEOUT, numarat', async () => {
const s0 = await stare(GT.port);
const t0 = Date.now();
const r = await cerere({ port: GT.port, cale: '/r', timeoutMs: 9000 });
const ms = Date.now() - t0;
cere(r.status === 504, `status ${r.status} dupa ${ms} ms`);
const j = json(r);
cere(j.detail === 'upstream connect timeout', `corp ${JSON.stringify(j)}`);
cere(ms >= 900 && ms < 3500, `a durat ${ms} ms (timeout de conectare configurat 1000)`);
const s1 = await stare(GT.port);
cere(s1.counters.responses504 === s0.counters.responses504 + 1, `responses504 ${s0.counters.responses504} -> ${s1.counters.responses504}`);
return `504 in ${ms} ms: ${JSON.stringify(j)}`;
});
await test('(j) hybrid-preferred + client numai X25519: ACCEPTAT pe X25519, iar starea NU pretinde hibrid', async () => {
const s0 = await stare(GP.port);
const inainte = TP.conexiuni.length;
const r = await cerere({ port: TP.port, cale: '/j', curbe: 'X25519' });
cere(r.status === 200, `status ${r.status}`);
const f = citesteFir(TP.conexiuni[inainte]);
cere(f.ch[0] && f.ch[0].grupuri.join() === String(X25519), `clientul de proba a oferit ${f.ch[0] ? f.ch[0].grupuri.map(numeGrup) : '-'}`);
const final = f.sh.filter((s) => !s.hrr).pop();
cere(final && final.grup === X25519, `fir: ${descrieSH(f.sh)}`);
cere(r.eki && r.eki.name === 'X25519', `getEphemeralKeyInfo ${JSON.stringify(r.eki)}`);
const s1 = await stare(GP.port);
cere(s1.mode === 'hybrid-preferred', `mode ${s1.mode}`);
cere(s1.guarantee && s1.guarantee.hybridKeyExchangeOnEveryConnection === false, 'hybrid-preferred isi declara hibridul garantat pe fiecare conexiune');
cere(typeof s1.negotiatedGroupPerConnection === 'string' && /not reported/i.test(s1.negotiatedGroupPerConnection), 'starea nu spune ca grupul per conexiune nu e raportat');
const pretentii = [];
(function umbla(o, cale) {
for (const [k, v] of Object.entries(o || {})) {
if (v && typeof v === 'object') umbla(v, `${cale}.${k}`);
else if (/hybrid|mlkem|quantum|pq/i.test(k) && (v === true || (typeof v === 'number' && v > 0))) pretentii.push(`${cale}.${k}=${v}`);
}
})(s1, '');
cere(pretentii.length === 0, `starea pretinde hibrid: ${pretentii.join(', ')}`);
cere(s1.counters.connectionsAccepted > s0.counters.connectionsAccepted, 'conexiunea clasica nu a fost numarata ca acceptata');
return `fir: ${descrieSH(f.sh)}; getEphemeralKeyInfo() = ${JSON.stringify(r.eki)}; nicio pretentie de hibrid in stare`;
});
await test('(j2) hybrid-preferred prefera CU ADEVARAT: client "X25519:X25519MLKEM768" (cota numai X25519) ajunge pe hibrid prin HelloRetryRequest', async () => {
const inainte = TP.conexiuni.length;
const r = await cerere({ port: TP.port, cale: '/j2', curbe: 'X25519:X25519MLKEM768' });
cere(r.status === 200, `status ${r.status}`);
const f = citesteFir(TP.conexiuni[inainte]);
cere(f.ch[0] && f.ch[0].cote.join() === String(X25519) && f.ch[0].grupuri.includes(HIBRID), `fixtura: primul ClientHello are cotele ${f.ch[0] ? f.ch[0].cote.map(numeGrup) : '-'} si grupurile ${f.ch[0] ? f.ch[0].grupuri.map(numeGrup) : '-'}`);
const final = f.sh.filter((s) => !s.hrr).pop();
cere(final && final.grup === HIBRID, `fir: ${descrieSH(f.sh)} (serverul nu a cerut hibridul)`);
return `fir: ${descrieSH(f.sh)}`;
});
await test('(m) antet peste limita: 431, nimic trimis la upstream', async () => {
const inainte = jurnalUpstream.length;
let r;
try { r = await cerere({ port: GH.port, cale: '/m-mare', anteturi: { 'x-mare': 'a'.repeat(20000) } }); } catch (e) { throw new Error(`fara raspuns HTTP: ${e.message}`); }
cere(r.status === 431, `status ${r.status}`);
cere(jurnalUpstream.slice(inainte).every((x) => x.url !== '/m-mare'), 'cererea a ajuns la upstream');
return '431';
});
await test('(n) ALPN: un client care ofera h2 primeste http/1.1 (gateway-ul nu face HTTP/2)', async () => {
const r = await strangere({ port: GH.port, curbe: 'X25519MLKEM768', alpn: ['h2', 'http/1.1'] });
cere(r.ok, `strangere esuata: ${r.cod}`);
cere(r.alpn === 'http/1.1', `ALPN ${r.alpn}`);
return `ALPN ${r.alpn}`;
});
await test('(o) configuratie din fisier JSON (AERE_PQGW_CONFIG), iar mediul are prioritate', async () => {
const fis = path.join(TMP, 'config.json');
fs.writeFileSync(fis, JSON.stringify({ listen: `${GAZDA}:0`, cert: CALE_CERT, key: CALE_CHEIE, upstream: `http://${GAZDA}:${PORT_UP}`, mode: 'hybrid-preferred', connectTimeoutMs: 3000 }));
const g = await pornesteGw('gateway din fisier', { AERE_PQGW_CONFIG: fis, AERE_PQGW_MODE: 'hybrid-only' });
try {
const s = await stare(g.port);
cere(s.mode === 'hybrid-only', `mode ${s.mode} (mediul trebuia sa castige fata de fisier)`);
const r = await cerere({ port: g.port, cale: '/o' });
cere(r.status === 200 && json(r).url === '/o', `cererea prin gateway-ul din fisier: ${r.status}`);
} finally { g.p.kill(); }
// exemplul publicat trebuie sa fie o configuratie pe care gateway-ul chiar o accepta
const mod = await import(pathToFileURL(GATEWAY).href);
const ex = mod.citesteConfig({ AERE_PQGW_CONFIG: path.join(AICI, 'pq-gateway.example.json') });
cere(ex.mode === 'hybrid-preferred' && ex.port === 8443, `exemplul citit: ${JSON.stringify({ mode: ex.mode, port: ex.port })}`);
return 'fisier citit, MODE din mediu aplicat; pq-gateway.example.json acceptat';
});
await test('(p) configuratie gresita: refuza sa porneasca, cu motivul numit, si nu asculta', async () => {
const altaCheie = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey.export({ type: 'pkcs8', format: 'pem' });
const caleAlta = path.join(TMP, 'alta-cheie.pem');
fs.writeFileSync(caleAlta, altaCheie);
const fisRau = path.join(TMP, 'config-rau.json');
fs.writeFileSync(fisRau, JSON.stringify({ upstrem: 'http://x.invalid' }));
const baza = { AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: CALE_CERT, AERE_PQGW_KEY: CALE_CHEIE, AERE_PQGW_UPSTREAM: `http://${GAZDA}:${PORT_UP}` };
const cazuri = [
['fara MODE', { ...baza }, 2, /MODE is required/],
['MODE necunoscut', { ...baza, AERE_PQGW_MODE: 'classic' }, 2, /MODE must be/],
['cheie in fisier gresita', { ...baza, AERE_PQGW_MODE: 'hybrid-only', AERE_PQGW_CONFIG: fisRau }, 2, /unknown key in config file: upstrem/],
['cheia nu se potriveste cu certificatul', { ...baza, AERE_PQGW_MODE: 'hybrid-only', AERE_PQGW_KEY: caleAlta }, 1, /KEY does not match/],
];
const note = [];
for (const [nume, env, codAsteptat, motiv] of cazuri) {
const r = spawnSync(process.execPath, [GATEWAY], { env: mediuGateway(env), timeout: 10000, encoding: 'utf8' });
cere(r.status === codAsteptat, `${nume}: cod ${r.status}, asteptat ${codAsteptat}; stderr ${String(r.stderr).slice(0, 160)}`);
cere(motiv.test(r.stderr), `${nume}: motivul lipseste din stderr: ${String(r.stderr).slice(0, 160)}`);
cere(!/"event":"listening"/.test(r.stdout), `${nume}: a ascultat totusi`);
cere(!String(r.stderr).includes(caleAlta) && !String(r.stderr).includes(CALE_CHEIE), `${nume}: calea cheii apare in mesaj`);
note.push(`${nume}: ${r.status}`);
}
return note.join('; ');
});
await test('(l) oprire curata: cererea in curs se termina, conexiunile noi sunt refuzate, oprirea se incheie singura', async () => {
const mod = await import(pathToFileURL(GATEWAY).href);
const cfg = mod.citesteConfig({ AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: CALE_CERT, AERE_PQGW_KEY: CALE_CHEIE, AERE_PQGW_UPSTREAM: `http://${GAZDA}:${PORT_UP}`, AERE_PQGW_MODE: 'hybrid-only' });
const gw = await mod.pornesteGateway(cfg, { jurnal: () => {} });
const port = gw.adresa.port;
const inCurs = cerere({ port, cale: '/slow?ms=1200', timeoutMs: 9000 }).then((r) => r, (e) => ({ eroare: e }));
await dormi(400);
const t0 = Date.now();
const oprit = gw.opreste(5000).then(() => Date.now() - t0);
await dormi(100);
const nou = await strangere({ port, curbe: 'X25519MLKEM768' });
const r = await inCurs;
const msOprire = await Promise.race([oprit, dormi(8000).then(() => null)]);
cere(!r.eroare, `cererea in curs a fost taiata: ${r.eroare && r.eroare.message}`);
cere(r.status === 200 && json(r).url === '/slow?ms=1200', `cererea in curs: status ${r.status}`);
cere(String(r.anteturi.connection).toLowerCase() === 'close', `raspunsul din timpul opririi are Connection: ${r.anteturi.connection}`);
cere(!nou.ok && nou.cod === 'ECONNREFUSED', `o conexiune noua dupa oprire: ${nou.ok ? 'ACCEPTATA' : nou.cod}`);
cere(msOprire !== null && msOprire < 4000, `oprirea nu s-a incheiat (${msOprire} ms)`);
return `cererea in curs 200, conexiune noua ${nou.cod}, oprire in ${msOprire} ms`;
});
// ---------------------------------------------------------------- autentificarea post-cuantica (2026-09-25)
function pornireRefuzata(extra) {
const r = spawnSync(process.execPath, [GATEWAY], { env: mediuGateway(extra), encoding: 'utf8', timeout: 15000 });
return { cod: r.status, stderr: r.stderr || '', stdout: r.stdout || '' };
}
await test('(s) lant ML-DSA emis de aere-pq-pki + REQUIRE_PQ_AUTH: porneste, starea spune post-quantum, openssl vede mldsa65 + X25519MLKEM768 si verifica lantul', async () => {
// AERE_PQ_PKI: controlul negativ ruleaza proba dintr-o copie a dosarului, deci calea autoritatii vine din mediu
const P = await import(pathToFileURL(process.env.AERE_PQ_PKI || path.join(AICI, '..', 'pq-pki', 'pki.mjs')).href);
const kr = P.generateKey('ml-dsa-87'), ki = P.generateKey('ml-dsa-65'), kl = P.generateKey('ml-dsa-65');
const root = P.issue({ issuer: null, signingKey: kr.privateKey, subject: { cn: 'Gateway Proba Root' }, publicKey: kr.publicKey, ca: true, pathLen: 1, days: 30 });
const inter = P.issue({ issuer: root, signingKey: kr.privateKey, subject: { cn: 'Gateway Proba Issuing' }, publicKey: ki.publicKey, ca: true, pathLen: 0, days: 30 });
const leaf = P.issue({ issuer: inter, signingKey: ki.privateKey, subject: { cn: 'localhost' }, publicKey: kl.publicKey, days: 5, dns: ['localhost'], ips: ['127.0.0.1'], eku: ['serverAuth'] });
const cRoot = path.join(TMP, 'pq-root.pem'), cLant = path.join(TMP, 'pq-lant.pem'), cCheie = path.join(TMP, 'pq-cheie.pem');
fs.writeFileSync(cRoot, P.pem('CERTIFICATE', root));
fs.writeFileSync(cLant, P.pem('CERTIFICATE', leaf) + P.pem('CERTIFICATE', inter));
fs.writeFileSync(cCheie, kl.privateKey.export({ type: 'pkcs8', format: 'pem' }));
const g = await pornesteGw('gateway cu lant ML-DSA', { AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: cLant, AERE_PQGW_KEY: cCheie, AERE_PQGW_MODE: 'hybrid-only',
AERE_PQGW_UPSTREAM: `http://${GAZDA}:${PORT_UP}`, AERE_PQGW_REQUIRE_PQ_AUTH: '1' });
const radacina = fs.readFileSync(cRoot);
const s = await stare(g.port, radacina);
cere(s.certificate.authentication === 'post-quantum', `autentificarea din stare: ${s.certificate.authentication}`);
cere(s.certificate.chain.length === 2 && s.certificate.chain.every((c) => c.publicKeyAlgorithm === 'ML-DSA-65'), `lantul din stare: ${JSON.stringify(s.certificate.chain)}`);
cere(s.certificate.chain[1].signatureAlgorithm === 'ML-DSA-87', `semnatura radacinii pe intermediar: ${s.certificate.chain[1].signatureAlgorithm}`);
const o = await sClient(['-connect', `${GAZDA}:${g.port}`, '-servername', 'localhost', '-verify_hostname', 'localhost', '-CAfile', cRoot, '-verify_return_error', '-groups', 'X25519MLKEM768'], 'Q\n');
const t = o.o + o.e;
cere(/Verify return code: 0 \(ok\)/.test(t), `openssl verificarea: ${t.slice(0, 300)}`);
cere(/Peer signature type: mldsa65/.test(t), 'semnatura serverului nu e mldsa65');
cere(/Negotiated TLS1.3 group: X25519MLKEM768/.test(t), 'grupul nu e X25519MLKEM768');
const r = await cerere({ port: g.port, cale: '/ecou', ca: radacina });
cere(r.status === 200, `cererea prin gateway: ${r.status}`);
return 'post-quantum, mldsa65 + X25519MLKEM768, lantul verificat de openssl';
});
await test('(s2) REQUIRE_PQ_AUTH cu certificatul clasic (EC P-256): refuza sa porneasca, motivul numeste lantul classical; fara optiune starea spune classical', async () => {
const r = pornireRefuzata({ AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: CALE_CERT, AERE_PQGW_KEY: CALE_CHEIE, AERE_PQGW_MODE: 'hybrid-only',
AERE_PQGW_UPSTREAM: `http://${GAZDA}:${PORT_UP}`, AERE_PQGW_REQUIRE_PQ_AUTH: 'true' });
cere(r.cod === 1, `cod ${r.cod}`);
cere(/REQUIRE_PQ_AUTH is set but the served chain is classical/.test(r.stderr), `motivul: ${r.stderr.slice(0, 200)}`);
cere(!/"event":"listening"/.test(r.stdout), 'a ascultat totusi');
const s = await stare(GH.port);
cere(s.certificate.authentication === 'classical', `starea gateway-ului clasic: ${s.certificate.authentication}`);
return 'refuzat la pornire; starea clasicului: classical';
});
await test('(s3) lant MIXT (frunza ML-DSA semnata de o autoritate EC, facut de openssl): REQUIRE_PQ_AUTH refuza, cu "mixed" si semnatura numita', async () => {
const k = (n) => path.join(TMP, n);
execFileSync(OPENSSL, ['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', '-nodes', '-keyout', k('ecca.key'), '-out', k('ecca.pem'), '-days', '2',
'-subj', '/CN=EC CA', '-addext', 'basicConstraints=critical,CA:TRUE', '-addext', 'keyUsage=critical,keyCertSign,cRLSign'], { stdio: ['ignore', 'pipe', 'pipe'] });
execFileSync(OPENSSL, ['req', '-new', '-newkey', 'mldsa65', '-nodes', '-keyout', k('mix.key'), '-out', k('mix.csr'), '-subj', '/CN=localhost'], { stdio: ['ignore', 'pipe', 'pipe'] });
execFileSync(OPENSSL, ['x509', '-req', '-in', k('mix.csr'), '-CA', k('ecca.pem'), '-CAkey', k('ecca.key'), '-out', k('mix.pem'), '-days', '2', '-set_serial', '9'], { stdio: ['ignore', 'pipe', 'pipe'] });
const r = pornireRefuzata({ AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: k('mix.pem'), AERE_PQGW_KEY: k('mix.key'), AERE_PQGW_MODE: 'hybrid-only',
AERE_PQGW_UPSTREAM: `http://${GAZDA}:${PORT_UP}`, AERE_PQGW_REQUIRE_PQ_AUTH: '1' });
cere(r.cod === 1, `cod ${r.cod}; ${r.stderr.slice(0, 200)}`);
cere(/the served chain is mixed/.test(r.stderr) && /ML-DSA-65 key signed with classical/.test(r.stderr), `motivul: ${r.stderr.slice(0, 260)}`);
return 'mixed: cheie ML-DSA-65, semnatura clasica';
});
// ---------------------------------------------------------------- revizuirea adversariala din 2026-09-25 (A8, A9)
await test('(e3) antetele de identitate: X-Real-IP e al gateway-ului (nu al clientului), X-Client-* si X-SSL-* ale clientului NU ajung', async () => {
const r = await cerere({ port: GH.port, cale: '/e3', anteturi: {
'X-Real-IP': '1.2.3.4', 'X-Client-Cert': 'fals', 'X-SSL-Client-S-DN': 'CN=fals', 'X-Client-Verify': 'SUCCESS', 'X-End-To-End': 'da',
} });
cere(r.status === 200, `status ${r.status}`);
const h = json(r).headers;
cere(h['x-end-to-end'] === 'da', 'nici antetul obisnuit nu a ajuns: fixtura nu masoara nimic');
cere(h['x-real-ip'] === '127.0.0.1', `X-Real-IP la upstream: ${h['x-real-ip']} (trebuia adresa din socket, nu a clientului)`);
const scapate = ['x-client-cert', 'x-ssl-client-s-dn', 'x-client-verify'].filter((n) => n in h);
cere(scapate.length === 0, `antete de identitate ale clientului ajunse la upstream: ${scapate.join(', ')}`);
return 'X-Real-IP din socket; x-client-cert, x-ssl-client-s-dn, x-client-verify oprite';
});
await test('(g3) lant ML-DSA + client fara algoritm de semnatura PQ (numai ECDSA/RSA-PSS, ca browserele de azi): refuzat si numarat "no shared signature algorithm"', async () => {
const P = await import(pathToFileURL(process.env.AERE_PQ_PKI || path.join(AICI, '..', 'pq-pki', 'pki.mjs')).href);
const kr = P.generateKey('ml-dsa-65'), kl = P.generateKey('ml-dsa-65');
const root = P.issue({ issuer: null, signingKey: kr.privateKey, subject: { cn: 'G3 Root' }, publicKey: kr.publicKey, ca: true, pathLen: 0, days: 5 });
const leaf = P.issue({ issuer: root, signingKey: kr.privateKey, subject: { cn: 'localhost' }, publicKey: kl.publicKey, days: 5, dns: ['localhost'], eku: ['serverAuth'] });
const cLant = path.join(TMP, 'g3-lant.pem'), cCheie = path.join(TMP, 'g3-cheie.pem'), cRoot = path.join(TMP, 'g3-root.pem');
fs.writeFileSync(cLant, P.pem('CERTIFICATE', leaf)); fs.writeFileSync(cRoot, P.pem('CERTIFICATE', root));
fs.writeFileSync(cCheie, kl.privateKey.export({ type: 'pkcs8', format: 'pem' }));
const g = await pornesteGw('gateway ML-DSA pentru (g3)', { AERE_PQGW_LISTEN: `${GAZDA}:0`, AERE_PQGW_CERT: cLant, AERE_PQGW_KEY: cCheie, AERE_PQGW_MODE: 'hybrid-only',
AERE_PQGW_UPSTREAM: `http://${GAZDA}:${PORT_UP}`, AERE_PQGW_REQUIRE_PQ_AUTH: '1' });
const radacina = fs.readFileSync(cRoot);
// controlul pozitiv: cu algoritmii impliciti (care includ mldsa65) strangerea reuseste
// s_client tipareste "Verify return code: 0 (ok)" SI cand strangerea a cazut inainte de orice verificare (masurat 2026-09-25:
// alerta 40, 7 octeti cititi, si tot "0 (ok)"); succesul se citeste din "Peer signature type", refuzul din alerta.
const bun = await sClient(['-connect', `${GAZDA}:${g.port}`, '-servername', 'localhost', '-CAfile', cRoot, '-verify_return_error', '-groups', 'X25519MLKEM768'], 'Q\n');
cere(/Peer signature type: mldsa65/.test(bun.o + bun.e) && /Verify return code: 0 \(ok\)/.test(bun.o + bun.e), 'controlul pozitiv (client cu mldsa65) nu a reusit');
const rau = await sClient(['-connect', `${GAZDA}:${g.port}`, '-servername', 'localhost', '-CAfile', cRoot, '-groups', 'X25519MLKEM768', '-sigalgs', 'ECDSA+SHA256:RSA-PSS+SHA256'], 'Q\n');
const tr = rau.o + rau.e;
cere(/alert handshake failure|SSL alert number 40/.test(tr) && !/Peer signature type:/.test(tr), `clientul fara algoritm PQ de semnatura nu a fost refuzat cu alerta: ${tr.split('\n').filter((l) => /alert|Peer signature|Negotiated/.test(l)).join(' | ').slice(0, 200)}`);
const s = await asteaptaStare(g.port, (x) => (x.counters.handshakesRefused['no shared signature algorithm'] || 0) >= 1, 4000, radacina);
cere(s && s.counters.handshakesRefused['no shared signature algorithm'] >= 1, `refuzul nu e numarat sub motivul lui: ${JSON.stringify(s && s.counters.handshakesRefused)}`);
return `numarat: no shared signature algorithm = ${s.counters.handshakesRefused['no shared signature algorithm']}`;
});
console.log(`PROBA PQ-GATEWAY: ${treceri} treceri, ${esecuri} esecuri${nemasurate ? `, ${nemasurate} nemasurate` : ''}`);
process.exitCode = esecuri ? 1 : 0;
suitaTerminata = true;
curata();