aere-quantum/control-plane/raport-conformitate.mjs

177 lines
13 KiB
JavaScript

// raport-conformitate.mjs - AERE Quantum (roadmap master, punctul 16): RAPORTUL DE CONFORMITATE post-cuantica al unei organizatii,
// construit peste planul de migrare (plan-migrare.mjs, care citeste inventarul crypto-inventory si scanarea readiness-service) si
// legat de un plic AERE Proof Protocol (AIP-23) de fel `compliance`, ca oricine sa poata verifica ce raport a fost emis.
//
// CE SPUNE: pentru fiecare actiune din plan, ce regimuri i se aplica, termenul fiecaruia si cate zile mai sunt la data raportului.
// CE NU SPUNE: raportul NU e o certificare si NU e o opinie juridica; termenele sunt ale documentelor numite, citite la data scrisa
// langa fiecare regim, iar una din surse (CNSA 2.0) e citita dintr-o sursa secundara, spus ca atare (sursa primara NSA refuza
// descarcarea automata, 403, masurat 2026-09-28).
//
// REGIMURILE (fiecare cu sursa si data citirii):
// - NIST IR 8547 ipd (2024-11-12, proiect, nefinalizat): algoritmii cu cheie publica vulnerabili cuantic (RSA, ECDSA, ECDH, DH)
// DEPRECATI dupa 2030 la nivelul de 112 biti, INTERZISI dupa 2035. Se aplica schimbului de cheie si semnaturilor.
// - Foaia de parcurs coordonata a UE (2025-06-23): inventar si planuri pana la 31.12.2026; pentru cazurile cu RISC INALT, tranzitia
// pana la 31.12.2030 ("quantum-vulnerable public key mechanisms shall not be used stand-alone after the end of 2030"); risc mediu
// pana la 31.12.2035. Un mecanism HIBRID (clasic + PQ) nu e "stand-alone", deci e conform la 2030.
// - CNSA 2.0 (NSA, pentru sistemele de securitate nationala; profil OPTIONAL): servere web si servicii cloud: suport si preferinta
// din 2025, exclusiv din 2033; semnarea software/firmware: 2025 / 2030; echipamente de retea: 2026 / 2030; sisteme de operare:
// 2027 / 2033. Categoria unui activ nu se vede din inventar: se da explicit (`cnsaCategories`), altfel se presupune "web-cloud"
// pentru ce vine din scanarea TLS si se SCRIE ca presupunere.
// Raportul e in ENGLEZA (forma 2, 2026-09-29): il citesc clientul si auditorul lui.
//
// import { raportConformitate, verificaRaport } from './raport-conformitate.mjs'
// const r = raportConformitate({ plan, organization, risk: 'high'|'medium', date: '2026-09-28', cnsa: false|true, cnsaCategories })
// -> { report, envelope, evidenceHash } (envelope = AIP-23 compliance, evidenceHash = sha256 al raportului canonic)
import crypto from 'node:crypto';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
// constructorul de plicuri: langa planul de control intr-o copie publica (../proof-kinds), sau in depozitul de dezvoltare (tools/)
const PK = [new URL('../proof-kinds/proof-kinds.mjs', import.meta.url), new URL('../../tools/proof-kinds/proof-kinds.mjs', import.meta.url)].find((u) => fs.existsSync(fileURLToPath(u)));
if (!PK) throw new Error('raport-conformitate: the proof-kinds builder is not beside the control plane (../proof-kinds) nor in tools/');
const { buildProof } = await import(PK.href);
export const VERSIUNE = 'aere-quantum/compliance-report/2 (2026-09-29)';
export const REGIMURI = Object.freeze({
'nist-ir-8547': Object.freeze({
name: 'NIST IR 8547 ipd, Transition to Post-Quantum Cryptography Standards',
source: 'https://nvlpubs.nist.gov/nistpubs/ir/2024/NIST.IR.8547.ipd.pdf', sourceDate: '2024-11-12', read: '2026-09-28', verified: 'primary',
status: 'initial public draft, not final on the date it was read',
deadlines: Object.freeze({ deprecated: '2030-12-31', disallowed: '2035-12-31' }),
}),
'eu-2025': Object.freeze({
name: 'A Coordinated Implementation Roadmap for the Transition to Post-Quantum Cryptography (EU)',
source: 'https://digital-strategy.ec.europa.eu/en/library/coordinated-implementation-roadmap-transition-post-quantum-cryptography',
sourceDate: '2025-06-23', read: '2026-09-28', verified: 'primary (the date) and secondary (the text of the deadlines)',
deadlines: Object.freeze({ inventory: '2026-12-31', highRisk: '2030-12-31', mediumRisk: '2035-12-31' }),
}),
'cnsa-2.0': Object.freeze({
name: 'CNSA 2.0 (NSA), national security systems',
source: 'https://media.defense.gov/2022/Sep/07/2003071836/-1/-1/0/CSI_CNSA_2.0_FAQ_.PDF', sourceDate: '2022-09-07', read: '2026-09-28',
verified: 'secondary: the primary source refuses automated download (403); the same table in two secondary sources',
categories: Object.freeze({
'web-cloud': Object.freeze({ name: 'web servers and cloud services', prefer: '2025-12-31', exclusive: '2033-12-31' }),
'software-signing': Object.freeze({ name: 'software and firmware signing', prefer: '2025-12-31', exclusive: '2030-12-31' }),
'networking': Object.freeze({ name: 'networking equipment (VPNs, routers)', prefer: '2026-12-31', exclusive: '2030-12-31' }),
'operating-systems': Object.freeze({ name: 'operating systems', prefer: '2027-12-31', exclusive: '2033-12-31' }),
}),
}),
});
// natura unei actiuni din plan: schimb de cheie (HNDL), semnatura/autentificare, sau configuratie (nu un algoritm vulnerabil).
// Pentru actiunile din inventar se citeste primitiva scrisa de planificator (campul `primitive`), nu textul problemei.
export function natura(a) {
const ref = String(a.ref || '');
if (ref.startsWith('tls-kex:')) return 'key-exchange';
if (ref.startsWith('tls-cert:')) return 'signature';
if (ref.startsWith('tls-conf:') || ref.startsWith('tls-cert-expiry:') || ref.startsWith('tls-cert-rsa:')) return 'configuration';
if (a.source === 'inventory') {
const p = String(a.primitive || '');
if (p === 'key-agree' || p === 'kem' || p === 'pke') return 'key-exchange';
if (p === 'signature') return 'signature';
// material cu cheie publica vulnerabil a carui FOLOSIRE nu se vede din fisier (ex. o cheie privata RSA: semnatura sau cifrare);
// prins de proba pe iesirea reala a inventarului (28 sept): prima forma il lasa "neclasificat"
return 'key-use-unknown';
}
return 'unclassified';
}
const zile = (de, pana) => Math.floor((Date.parse(pana + 'T23:59:59Z') - Date.parse(de + 'T00:00:00Z')) / 86400e3);
/**
* @param {{plan: object, organization: string, risk?: 'high'|'medium', date: string, cnsa?: boolean, cnsaCategories?: object}} p
*/
export function raportConformitate({ plan, organization, risk, date, cnsa = false, cnsaCategories = {} } = {}) {
if (!plan || !Array.isArray(plan.actions)) throw new Error('compliance report: a plan without actions (the output of the planner is needed)');
if (!organization) throw new Error('compliance report: organization is required');
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date || ''))) throw new Error('compliance report: date is required, YYYY-MM-DD (the report is a dated snapshot)');
// RC2 (2026-09-29): o data de forma buna dar inexistenta (2026-13-45) dadea zile NaN in raport; se cere o zi calendaristica reala
const zi0 = new Date(date + 'T00:00:00Z');
if (Number.isNaN(zi0.getTime()) || zi0.toISOString().slice(0, 10) !== date) throw new Error('compliance report: date is not a real calendar day');
if (risk !== undefined && risk !== 'high' && risk !== 'medium') throw new Error('compliance report: risk = high | medium');
const N = REGIMURI['nist-ir-8547'], U = REGIMURI['eu-2025'], C = REGIMURI['cnsa-2.0'];
const constatari = [];
for (const a of plan.actions) {
const nat = natura(a);
const reg = [];
if (nat === 'key-exchange' || nat === 'signature' || nat === 'key-use-unknown') {
reg.push({ regime: 'nist-ir-8547', deadline: N.deadlines.deprecated, what: 'deprecated after 2030 (112 bits); disallowed after 2035', days: zile(date, N.deadlines.deprecated) });
const tu = risk === 'medium' ? U.deadlines.mediumRisk : U.deadlines.highRisk;
reg.push({ regime: 'eu-2025', deadline: tu, what: risk === 'medium' ? 'medium risk: transition by 2035' : risk === 'high'
? 'high risk: no vulnerable mechanism used stand-alone after 2030 (hybrid is compliant)' : 'risk not declared: the high-risk deadline (2030) applies until it is declared',
days: zile(date, tu) });
if (cnsa) {
// RC3 (2026-09-29): numai chei PROPRII (Object.hasOwn); o categorie ca 'constructor' sau un ref '__proto__' luau valori mostenite din prototip
const data_ = Object.hasOwn(cnsaCategories, a.ref) ? cnsaCategories[a.ref] : null;
const cat = data_ || (String(a.ref).startsWith('tls-') ? 'web-cloud' : null);
const cc = typeof cat === 'string' && Object.hasOwn(C.categories, cat) ? C.categories[cat] : null;
if (cc) reg.push({ regime: 'cnsa-2.0', category: cat, assumed: !data_, deadline: cc.exclusive, prefer: cc.prefer,
what: `${cc.name}: supported and preferred by ${cc.prefer.slice(0, 4)}, exclusive by ${cc.exclusive.slice(0, 4)}`, days: zile(date, cc.exclusive) });
else reg.push({ regime: 'cnsa-2.0', category: null, deadline: null, what: 'the CNSA category of the asset is not given (cnsaCategories); the deadline cannot be set', days: null });
}
}
const termene = reg.filter((r) => r.deadline).map((r) => r.deadline).sort();
constatari.push({
ref: a.ref, asset: a.asset, problem: a.problem, nature: nat, urgency: a.urgency, method: a.method, product: a.product || null,
target: a.target, blocker: a.blocker || null, regimes: reg, nearestDeadline: termene[0] || null,
// HNDL: schimbul de cheie e expus AZI (datele se recolteaza acum); semnatura nu (falsificarea vine odata cu calculatorul cuantic)
// null = nu se stie: o cheie folosita la cifrare/transport e expusa azi, una folosita la semnatura nu
exposedToday: nat === 'key-exchange' ? true : nat === 'key-use-unknown' ? null : false,
});
}
const cuTermen = constatari.filter((c) => c.nearestDeadline);
const rezumat = {
actions: constatari.length,
keyExchange: constatari.filter((c) => c.nature === 'key-exchange').length,
signatures: constatari.filter((c) => c.nature === 'signature').length,
unknownUse: constatari.filter((c) => c.nature === 'key-use-unknown').length,
configuration: constatari.filter((c) => c.nature === 'configuration').length,
unclassified: constatari.filter((c) => c.nature === 'unclassified').length,
exposedToday: constatari.filter((c) => c.exposedToday === true).length,
blockedByEcosystem: constatari.filter((c) => c.method === 'blocked').length,
nearestDeadline: cuTermen.map((c) => c.nearestDeadline).sort()[0] || null,
euInventory2026: date <= U.deadlines.inventory ? 'this report is a dated inventory (the EU requirement for 31.12.2026)' : 'the EU inventory deadline (31.12.2026) has passed',
};
const vulnerabile = rezumat.keyExchange + rezumat.signatures + rezumat.unknownUse;
// RC1 (2026-09-29): actiunile NECLASIFICATE nu sunt judecate; cu ele, raportul nu are voie sa spuna "fara algoritmi vulnerabili"
rezumat.verdict = vulnerabile === 0 && rezumat.unclassified > 0
? `no action classified as vulnerable, but ${rezumat.unclassified} UNCLASSIFIED actions (not judged); the report cannot say that there is no vulnerable algorithm`
: vulnerabile === 0
? 'no quantum-vulnerable public-key algorithm in what was measured'
: `${vulnerabile} quantum-vulnerable assets; nearest deadline ${rezumat.nearestDeadline}${rezumat.exposedToday ? `; ${rezumat.exposedToday} exposed TODAY (harvest now, decrypt later)` : ''}${rezumat.unknownUse ? `; ${rezumat.unknownUse} of unknown use (exposure today not known)` : ''}`;
const raport = {
v: 2, version: VERSIUNE, organization, date, risk: risk || null, cnsa: !!cnsa,
regimes: Object.fromEntries(Object.entries(REGIMURI).filter(([k]) => cnsa || k !== 'cnsa-2.0')),
plan: { version: plan.version || null, summary: plan.summary || null },
summary: rezumat, findings: constatari,
limits: [
'The report covers only what was inventoried and scanned; an asset that was not measured does not appear, and its absence does not mean compliance.',
'It is not a certification and not a legal opinion; the deadlines are those of the documents named, as read on the date written beside each.',
'NIST IR 8547 was a draft when it was read; its deadlines may change in the final version.',
],
};
const evidenta = sha256Canonic(raport);
const plic = buildProof('compliance', {
subject: organization, policy: VERSIUNE, result: rezumat.verdict, evidenceHash: evidenta, createdAt: date + 'T00:00:00Z',
});
return { report: raport, envelope: plic, evidenceHash: evidenta };
}
export { raportConformitate as complianceReport };
// JSON canonic: chei sortate recursiv, fara spatii; amprenta lui e evidenceHash din plic
export function canonicJson(v) {
if (Array.isArray(v)) return '[' + v.map(canonicJson).join(',') + ']';
if (v && typeof v === 'object') return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonicJson(v[k])).join(',') + '}';
return JSON.stringify(v);
}
export const sha256Canonic = (o) => '0x' + crypto.createHash('sha256').update(canonicJson(o), 'utf8').digest('hex');
/** raportul primit e chiar cel legat de plic? (amprenta canonica == evidenceHash; plicul trece separat prin verify-proof.mjs) */
export function verificaRaport(raport, plic) {
const e = plic && plic.statement && plic.statement.evidenceHash;
if (!e) return { ok: false, reason: 'the envelope has no evidenceHash' };
const h = sha256Canonic(raport);
return h === String(e).toLowerCase() ? { ok: true } : { ok: false, reason: 'the report is NOT the one bound to the envelope', fingerprint: h, inEnvelope: e };
}
export { verificaRaport as verifyReport };