// AERE Quantum Security Control Plane - EXECUTORUL migrarii (B1, milestone 4: "migrare automata cu proba si intoarcere").
//
// Ia PLANUL produs de plan-migrare.mjs si, pentru fiecare actiune `auto-aere` la care clientul a CONSIMTIT explicit, executa
// migrarea prin produsul AERE potrivit, masoara starea DUPA, si scrie o inregistrare intr-un lant de hash-uri verificabil:
// gateway (schimb de cheie clasic, HNDL) -> porneste PQ Gateway in fata serviciului si DOVEDESTE cu o strangere de mana TLS 1.3
// care ofera NUMAI X25519MLKEM768; scrie configuratia pentru managerul de servicii al
// clientului; la esec opreste gateway-ul (intoarcere)
// kms (KEM / cifrare cu cheie publica) -> creeaza cheia hibrida (X25519 + ML-KEM-768) in KMS, sau o ROTESTE daca exista;
// versiunile vechi raman decriptabile, deci intoarcerea e "nu folosi versiunea noua"
// pki (semnatura, certificat) -> emite un certificat ML-DSA-65 din CA-ul PQ al clientului si il VERIFICA pe lant pana
// la radacina (verifyChain); fisierele se calculeaza in memorie si se sterg la esec
// PRINCIPII, si fiecare a costat undeva: (1) nimic nu se executa fara consimtamant PE ACTIUNE (`consent` = multime de ref sau 'all');
// (2) modul implicit e USCAT (dry run) - executia se cere cu `execute: true`; (3) verdictul fiecarei actiuni vine dintr-o
// MASURATOARE de dupa, nu din "comanda a iesit cu 0"; (4) o actiune cazuta nu opreste restul si isi face intoarcerea ei; (5) nicio
// valoare secreta nu ajunge in inregistrari sau in erori; (6) inregistrarile sunt un lant sha256(seq|prev|inregistrare) pe care
// consola (consola.mjs) si oricine altcineva il poate re-verifica.
// CE NU FACE: nu schimba DNS-ul, porturile sau firewall-ul clientului, nu emite certificate WebPKI publice, nu sterge chei din KMS.
// Forma inregistrarilor, versiunea 2 (2026-09-29, pentru publicare): chei si valori in engleza (vezi README).
//
// node executa-migrare.mjs --plan plan.json --out
[--consent ref1,ref2|all] [--execute]
// [--gw-cert c.pem --gw-key k.pem --gw-upstream http://h:p [--gw-mode hybrid-only|hybrid-preferred] [--gw-listen 127.0.0.1:8443] [--gw-keep]]
// [--kms-url http://127.0.0.1:8420 --kms-token-file f] [--pki-dir ca --pki-ca issuing --pki-roots ca/root.crt] (AERE_PKI_PASSPHRASE)
// iesire 0 = toate actiunile consimtite au verdict OK (sau nimic de executat); 1 = cel putin una FAILED; 2 = nu s-a putut rula.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import tls from 'node:tls';
import readline from 'node:readline';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const GATEWAY = path.join(AICI, '..', 'pq-gateway', 'pq-gateway.mjs');
export const VERSIUNE = 'aere-control-plane/execution/2 (2026-09-29)';
const GRUP_PQ = 'X25519MLKEM768';
// --- lantul de inregistrari -------------------------------------------------------------------------------------------------
export function canonic(v) {
if (v === null || typeof v !== 'object') return JSON.stringify(v);
if (Array.isArray(v)) return '[' + v.map(canonic).join(',') + ']';
return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonic(v[k])).join(',') + '}';
}
const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
export function lantulExecutiei() {
const inreg = [];
return {
adauga(r) {
const seq = inreg.length; const prev = seq ? inreg[seq - 1].hash : '0'.repeat(64);
const hash = sha(`${seq}|${prev}|${canonic(r)}`);
inreg.push({ seq, prev, record: r, hash });
return hash;
},
lista() { return inreg.slice(); },
};
}
/** Re-verifica un lant de inregistrari (sau fisierul execution.json). Intoarce {ok, seq, reason}. */
export function verificaExecutie(x) {
const lista = Array.isArray(x) ? x : (x && Array.isArray(x.records) ? x.records : null);
if (!lista) return { ok: false, reason: 'no records' };
let prev = '0'.repeat(64);
for (let i = 0; i < lista.length; i++) {
const e = lista[i];
if (!e || typeof e !== 'object') return { ok: false, seq: i, reason: `record ${i} is not an object` };
if (e.seq !== i) return { ok: false, seq: i, reason: `seq ${e.seq} instead of ${i}` };
if (e.prev !== prev) return { ok: false, seq: i, reason: 'prev does not link the previous hash' };
const h = sha(`${i}|${prev}|${canonic(e.record)}`);
if (h !== e.hash) return { ok: false, seq: i, reason: 'the hash of the record does not reproduce (content changed)' };
prev = h;
}
return { ok: true, seq: lista.length, hash: prev };
}
// --- ajutoare fara secrete --------------------------------------------------------------------------------------------------
const taie = (s) => String(s ?? '').replace(/0x[0-9a-fA-F]{20,}/g, '0x…').replace(/[A-Za-z0-9+/=]{48,}/g, '…').slice(0, 300);
// Numele resursei unei actiuni (cheia KMS, fisierele, CN-ul implicit) = un prefix lizibil + 16 hex din sha256(ref). Forma veche (ref-ul
// curatat si taiat la 40) dadea ACELASI nume pentru doua ref-uri reale din acelasi fisier, deci consimtamantul dat unuia ROTEA cheia
// celuilalt (revizuirea din 2026-09-27, reprodusa). Prefixul se taie fara '-' la capete, deci numele e si o eticheta DNS valida (CN)
// si un nume KMS valid (`^[a-z0-9][a-z0-9_-]{0,63}$`): cel mult 45 de caractere.
export function numeMigrare(ref) {
const pref = String(ref).replace(/[^a-z0-9]+/gi, '-').toLowerCase().slice(0, 24).replace(/^-+|-+$/g, '');
return 'mig-' + (pref ? pref + '-' : '') + sha(String(ref)).slice(0, 16);
}
const san = numeMigrare;
export function citesteToken(f) { const t = fs.readFileSync(f, 'utf8').trim(); if (t.length < 32) throw new Error('the KMS token in the file is shorter than 32 characters'); return t; }
// --- gateway -----------------------------------------------------------------------------------------------------------------
function pornesteGateway(env, ms = 10000) {
return new Promise((resolve, reject) => {
const mediu = { ...process.env }; for (const k of Object.keys(mediu)) if (k.startsWith('AERE_PQGW_')) delete mediu[k];
const p = spawn(process.execPath, [GATEWAY], { env: { ...mediu, ...env }, stdio: ['ignore', 'pipe', 'pipe'], detached: env.__keep === '1' });
let err = ''; p.stderr.on('data', (b) => { err += b; });
const t = setTimeout(() => { p.kill(); reject(new Error(`the gateway did not report 'listening' within ${ms} ms: ${taie(err)}`)); }, ms);
readline.createInterface({ input: p.stdout }).on('line', (l) => { let j; try { j = JSON.parse(l); } catch { return; } if (j.event === 'listening') { clearTimeout(t); resolve({ p, port: j.port }); } });
p.on('exit', (cod) => { clearTimeout(t); reject(new Error(`the gateway exited with ${cod}: ${taie(err)}`)); });
});
}
function probaTlsPq(port, { ca, servername, grupuri = GRUP_PQ }) {
return new Promise((resolve) => {
const s = tls.connect({ host: '127.0.0.1', port, servername, ...(ca ? { ca } : {}), minVersion: 'TLSv1.3', maxVersion: 'TLSv1.3', ecdhCurve: grupuri }, () => {
const eki = s.getEphemeralKeyInfo(); const r = { ok: true, protocol: s.getProtocol(), group: eki && eki.name ? eki.name : null, authorized: s.authorized };
s.end(); resolve(r);
});
s.setTimeout(8000, () => s.destroy(new Error('the handshake did not finish within 8 s')));
s.on('error', (e) => resolve({ ok: false, code: e.code, message: taie(e.message) }));
});
}
async function executaGateway(a, o, rec) {
const g = o.gateway || {};
for (const [k, v] of [['cert', g.cert], ['key', g.key], ['upstream', g.upstream]]) if (!v) throw new Error(`gateway: missing ${k}`);
if (!fs.existsSync(g.cert) || !fs.existsSync(g.key)) throw new Error('gateway: the certificate or the key is not on disk');
const mode = g.mode || 'hybrid-preferred'; const listen = g.listen || '127.0.0.1:0';
const env = { AERE_PQGW_LISTEN: listen, AERE_PQGW_CERT: g.cert, AERE_PQGW_KEY: g.key, AERE_PQGW_MODE: mode, AERE_PQGW_UPSTREAM: g.upstream, __keep: g.keep ? '1' : '0' };
const { p, port } = await pornesteGateway(env);
rec.steps.push({ step: 'gateway started', port, mode, upstream: g.upstream });
const ca = g.ca ? fs.readFileSync(g.ca, 'utf8') : (g.selfSigned ? fs.readFileSync(g.cert, 'utf8') : null);
let proba;
try {
proba = await probaTlsPq(port, { ca, servername: g.servername || 'localhost' });
} finally { if (!proba || !proba.ok) { p.kill(); rec.steps.push({ step: 'ROLLBACK: gateway stopped (the TLS test failed)' }); } }
// Ce dovedeste proba, spus exact (revizuirea adversariala 2026-09-27): un CLIENT care ofera numai X25519MLKEM768 a terminat
// strangerea, deci serverul stie schimbul hibrid. Daca serverul REFUZA clasicul o masoara a doua proba, cu un client numai clasic.
rec.steps.push({ step: `TLS 1.3 test: a client offering ONLY ${GRUP_PQ} completed the handshake`, ok: proba.ok, group: proba.group ?? `(structural: the client offered only ${GRUP_PQ})`, protocol: proba.protocol ?? null, ...(proba.ok ? {} : { reason: proba.message || proba.code }) });
if (!proba.ok) throw new Error('the post-quantum TLS test failed: ' + (proba.message || proba.code));
const clasic = await probaTlsPq(port, { ca, servername: g.servername || 'localhost', grupuri: 'X25519:P-256' });
rec.steps.push({ step: 'TLS 1.3 test: a client offering ONLY classical groups (X25519, P-256)', accepted: clasic.ok });
if (mode === 'hybrid-only' && clasic.ok) { p.kill(); rec.steps.push({ step: 'ROLLBACK: gateway stopped (hybrid-only accepted a classical client)' }); throw new Error('hybrid-only accepted a classical-only client: the structural guarantee does not hold'); }
const cfg = path.join(o.out, san(a.ref) + '.gateway.env');
fs.writeFileSync(cfg, Object.entries(env).filter(([k]) => k.startsWith('AERE_PQGW_')).map(([k, v]) => `${k}=${v}`).join('\n') + '\n', { mode: 0o600 });
rec.steps.push({ step: 'gateway configuration written for the service manager', file: cfg });
if (g.keep) { p.unref(); rec.steps.push({ step: 'gateway left running', pid: p.pid, port }); } else { p.kill(); rec.steps.push({ step: 'gateway stopped after the test (keep=false); the operator starts it from the written configuration' }); }
rec.after = { port, mode, pqSupported: true, classicalAccepted: clasic.ok, guarantee: clasic.ok ? 'classical clients still get a classical key exchange (hybrid-preferred); a guarantee that EVERY connection is hybrid needs hybrid-only' : 'every accepted connection used the hybrid key exchange (the classical one was refused, measured)' };
rec.verdict = 'OK';
}
// --- kms -----------------------------------------------------------------------------------------------------------------------
async function kmsCerere(o, metoda, cale, corp) {
const r = await fetch(o.kms.url.replace(/\/$/, '') + cale, { method: metoda, headers: { authorization: 'Bearer ' + o.kms.token, ...(corp ? { 'content-type': 'application/json' } : {}) }, body: corp ? JSON.stringify(corp) : undefined });
let j = null; try { j = await r.json(); } catch { j = null; }
return { status: r.status, j };
}
async function executaKms(a, o, rec) {
if (!o.kms || !o.kms.url || !o.kms.token) throw new Error('kms: missing url or token (file)');
// numele cheii se deriva NUMAI din ref (prefix mig-), nu din plan: un camp din plan putea numi o cheie straina, existenta, care ar fi
// fost ROTITA sub un ref pe care omul l-a consimtit pentru altceva (revizuirea adversariala 2026-09-27)
const nume = san(a.ref); const tip = /sign|semn/i.test(a.target || '') ? 'sign' : 'encrypt';
const inainte = await kmsCerere(o, 'GET', `/v1/keys/${encodeURIComponent(nume)}`);
rec.before = { exists: inainte.status === 200, version: inainte.j?.latest_version ?? null };
let r;
if (inainte.status === 200) { r = await kmsCerere(o, 'POST', `/v1/keys/${encodeURIComponent(nume)}/rotate`); rec.steps.push({ step: 'the key exists: rotated', status: r.status }); }
else {
r = await kmsCerere(o, 'POST', `/v1/keys/${encodeURIComponent(nume)}`, { type: tip });
if (r.status === 404 || r.status === 405) r = await kmsCerere(o, 'POST', '/v1/keys', { name: nume, type: tip });
rec.steps.push({ step: `hybrid key created (${tip})`, status: r.status });
}
if (r.status < 200 || r.status >= 300) throw new Error(`the KMS answered ${r.status}: ${taie(r.j?.error || r.j?.code || '')}`);
const dupa = await kmsCerere(o, 'GET', `/v1/keys/${encodeURIComponent(nume)}`);
if (dupa.status !== 200) throw new Error(`KMS: the key cannot be read afterwards (${dupa.status})`);
const v = dupa.j.latest_version; const ver = dupa.j.versions?.[String(v)];
if (!(v >= 1) || !ver) throw new Error('KMS: no new version after the operation');
if (rec.before.exists && !(v > rec.before.version)) throw new Error('KMS: the rotation did not increase the version');
rec.after = { key: nume, type: tip, latest_version: v, fingerprint: ver.fingerprint ?? null, min_decryption_version: dupa.j.min_decryption_version ?? null };
rec.steps.push({ step: 'rollback: the older versions stay decryptable; nothing is deleted' });
rec.verdict = 'OK';
}
// --- pki -----------------------------------------------------------------------------------------------------------------------
let P = null;
async function pki() { if (!P) P = await import(new URL('../pq-pki/pki.mjs', import.meta.url).href); return P; }
function pem(tip, der) { return `-----BEGIN ${tip}-----\n${Buffer.from(der).toString('base64').match(/.{1,64}/g).join('\n')}\n-----END ${tip}-----\n`; }
async function executaPki(a, o, rec) {
const k = o.pki || {};
if (!k.dir || !k.ca || !k.roots) throw new Error('pki: missing dir, ca or roots');
if (!k.passphrase) throw new Error('pki: missing the CA passphrase (AERE_PKI_PASSPHRASE)');
const Pk = await pki();
const caCert = Pk.unpem(fs.readFileSync(path.join(k.dir, k.ca + '.crt'), 'utf8'))[0];
const caKey = Pk.importPrivateKey(fs.readFileSync(path.join(k.dir, k.ca + '.key'), 'utf8'), k.passphrase);
const roots = Pk.unpem(fs.readFileSync(k.roots, 'utf8'));
// numele algoritmului e cel al lui Node/OpenSSL, cu litere mici (pki.generateKey le cere asa); tinta din plan e scrisa "ML-DSA-65"
const cn = a.cn || (san(a.ref) + '.internal'); const alg = String(k.alg || 'ml-dsa-65').toLowerCase();
if (!/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(cn)) throw new Error(`pki: the cn is not a valid host name (no wildcard): ${String(cn).slice(0, 60)}`);
const kp = Pk.generateKey(alg);
const cert = Pk.issue({ issuer: caCert, signingKey: caKey, subject: { cn }, publicKey: kp.publicKey, days: Number(k.days || 90), dns: [cn] });
rec.steps.push({ step: `${alg} certificate issued by the CA "${k.ca}"`, cn });
const caEsteRadacina = roots.some((r) => Buffer.compare(Buffer.from(r), Buffer.from(caCert)) === 0);
const v = Pk.verifyChain({ leaf: cert, intermediates: caEsteRadacina ? [] : [caCert], roots, host: cn, purpose: 'serverAuth', requireCrl: false, crls: [] });
rec.steps.push({ step: 'the chain verified up to the root', ok: !!v.ok, ...(v.ok ? { chain: v.chain } : { code: v.code, reason: taie(v.reason) }) });
if (!v.ok) throw new Error(`the chain does not verify (${v.code}): ${taie(v.reason)}`);
// ambele iesiri se calculeaza IN MEMORIE inainte de orice scriere (exportPrivateKey poate refuza o parola care a trecut la import);
// abia apoi se scriu, cheia intai, si orice cadere dupa prima scriere sterge ce s-a scris (revizuirea adversariala 2026-09-27)
const baza = path.join(o.out, san(a.ref));
const pemCert = pem('CERTIFICATE', cert); const pemCheie = Pk.exportPrivateKey(kp.privateKey, k.passphrase);
const scrise = [];
try {
fs.writeFileSync(baza + '.key', pemCheie, { mode: 0o600, flag: 'wx' }); scrise.push(baza + '.key');
fs.writeFileSync(baza + '.crt', pemCert, { mode: 0o644, flag: 'wx' }); scrise.push(baza + '.crt');
} catch (e) { for (const f of scrise) { try { fs.rmSync(f, { force: true }); } catch {} } rec.steps.push({ step: 'ROLLBACK: removed the partly written files', removed: scrise.length }); throw e; }
rec.steps.push({ step: 'wrote the certificate and the key SEALED under the CA passphrase', cert: baza + '.crt', key: baza + '.key' });
rec.after = { cn, alg, serial: Pk.parseCert(cert).serial.toString('hex'), chain: v.chain };
rec.verdict = 'OK';
}
// --- executia ------------------------------------------------------------------------------------------------------------------
const EXECUTORI = { gateway: executaGateway, kms: executaKms, pki: executaPki };
export async function executa(plan, o) {
const consimt = o.consent === 'all' ? 'all' : new Set(o.consent || []);
const out = o.out; fs.mkdirSync(out, { recursive: true });
const lant = lantulExecutiei();
const log = o.log || (() => {});
lant.adauga({ type: 'start', version: VERSIUNE, at: new Date().toISOString(), mode: o.execute ? 'executed' : 'dry-run', actionsInPlan: (plan.actions || []).length });
const sumar = { total: 0, ok: 0, failed: 0, dryRun: 0, skipped: 0, notExecutable: 0 };
for (const a of plan.actions || []) {
if (a.method !== 'auto-aere') { sumar.notExecutable++; continue; }
sumar.total++;
const rec = { ref: a.ref, product: a.product, urgency: a.urgency, asset: a.asset, target: a.target, consented: consimt === 'all' || consimt.has(a.ref), mode: o.execute ? 'executed' : 'dry-run', steps: [], verdict: null };
if (!rec.consented) { rec.verdict = 'SKIPPED-no-consent'; sumar.skipped++; lant.adauga(rec); log(` ${a.ref}: skipped (no consent)`); continue; }
rec.effectiveTarget = a.product === 'kms' ? `KMS key ${san(a.ref)}` : a.product === 'pki' ? `certificate for ${a.cn || san(a.ref) + '.internal'}` : a.product === 'gateway' ? `gateway in front of ${(o.gateway && o.gateway.upstream) || '?'}` : '?';
if (!o.execute) { rec.verdict = 'DRY-RUN'; rec.steps.push({ step: `would run through ${a.product} on ${rec.effectiveTarget}: ${a.how || ''}` }); sumar.dryRun++; lant.adauga(rec); log(` ${a.ref}: dry run (${rec.effectiveTarget})`); continue; }
// Object.hasOwn: `constructor`/`toString` sunt functii mostenite din Object.prototype; cu EXECUTORI[a.product] treceau de garda si
// o actiune fara niciun efect iesea OK (revizuirea adversariala 2026-09-27, reprodus)
const ex = Object.hasOwn(EXECUTORI, String(a.product)) ? EXECUTORI[a.product] : null;
if (!ex) { rec.verdict = 'FAILED'; rec.error = `unknown product: ${a.product}`; sumar.failed++; lant.adauga(rec); continue; }
try {
await ex(a, o, rec);
if (rec.verdict !== 'OK') throw new Error('the executor did not write a measured verdict');
sumar.ok++; log(` ${a.ref}: OK`);
}
catch (e) { rec.verdict = 'FAILED'; rec.error = taie(e.message); sumar.failed++; log(` ${a.ref}: FAILED (${rec.error})`); }
lant.adauga(rec);
}
lant.adauga({ type: 'end', at: new Date().toISOString(), summary: sumar });
const dosar = { version: VERSIUNE, records: lant.lista() };
fs.writeFileSync(path.join(out, 'execution.json'), JSON.stringify(dosar, null, 1) + '\n');
return { summary: sumar, records: dosar.records, file: path.join(out, 'execution.json') };
}
// --- CLI -------------------------------------------------------------------------------------------------------------------------
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const arg = (n, d) => { const i = process.argv.indexOf('--' + n); return i > 0 ? process.argv[i + 1] : d; };
const flag = (n) => process.argv.includes('--' + n);
try {
const plan = JSON.parse(fs.readFileSync(arg('plan'), 'utf8'));
const c = arg('consent', ''); const consent = c === 'all' ? 'all' : c.split(',').map((s) => s.trim()).filter(Boolean);
const o = { consent, execute: flag('execute'), out: arg('out', 'execution'), log: (m) => console.log(m) };
if (arg('gw-cert')) o.gateway = { cert: arg('gw-cert'), key: arg('gw-key'), upstream: arg('gw-upstream'), mode: arg('gw-mode'), listen: arg('gw-listen'), keep: flag('gw-keep'), ca: arg('gw-ca'), selfSigned: flag('gw-self-signed'), servername: arg('gw-servername') };
if (arg('kms-url')) o.kms = { url: arg('kms-url'), token: citesteToken(arg('kms-token-file')) };
if (arg('pki-dir')) o.pki = { dir: arg('pki-dir'), ca: arg('pki-ca'), roots: arg('pki-roots'), passphrase: process.env.AERE_PKI_PASSPHRASE || '' };
const r = await executa(plan, o);
console.log(`execution: ${JSON.stringify(r.summary)} -> ${r.file}`);
process.exitCode = r.summary.failed ? 1 : 0;
} catch (e) { console.error('could not run: ' + taie(e.message)); process.exitCode = 2; }
}