aere-quantum/pq-secrets/test/proba-secrete.mjs

975 lines
91 KiB
JavaScript

// test/proba-secrete.mjs - proba Aere PQ Secrets, fara framework.
//
// Fluxul intreg cu chei NASCUTE la rulare (un emitent, un admin, un deployer, un cititor, un rotator, un auditor, un telefon delegat, un
// strain, un detinator revocat, unul fara lista de stare): put/get autorizate, fiecare refuz PE NUME (regula serviciului si regulile
// verificatorului de identitate), rotatia cu versiunile vechi citibile numai cu politica, revocarea de versiune, grant/revoke, list,
// valoarea cautata ca SIR (si ca base64) pe disc, in jurnalul sidecar-ului, in jurnalul KMS si in iesirea serverului (cu control pozitiv al
// scanerului), jurnalul intact dupa 100 de operatii, rollback-ul mutatiei al carei rand de audit nu se poate scrie, plicurile VALID pentru
// verificatorul AIP-23 de referinta (AERE_VERIFY_PROOF; fara el, SARIT pe nume si cod 2), serverul HTTP in proces, din linia de comanda si cu
// un KMS la distanta.
//
// Iesire: tabel pe stdout; cod 0 daca toate trec, 1 daca una pica, 2 daca una e SARITA (fara verificator) si niciuna nu pica.
// Cu AERE_SECRETS_PROBA_JSON=<fisier> scrie rezultatul complet ca JSON (il citeste test/control-negativ-secrete.mjs; completed: true
// inseamna ca s-a ajuns la capat).
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import http from 'node:http';
import { spawn, spawnSync, execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { openKms } from '../../pq-kms/kms.mjs';
import { createServer as createKmsServer } from '../../pq-kms/server.mjs';
import { openSecrets as deschideSecrete, kmsClientHttp, SecretsError, MODULE, REGULI, KIND, MAX_VALUE, OPERATII, requestHash, MESAJ_NEAUTORIZAT, MAX_GRANTS, NONCE_PER_ORIGIN, MAX_JSON_DEPTH } from '../secrete.mjs';
import { createServer, configFromEnv, openFromConfig } from '../server.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const SERVER_PATH = path.join(HERE, '..', 'server.mjs');
const { identity: ID, sidecar: SC, proofKinds: PK } = MODULE;
const SIDECAR_PATH = MODULE.cai.sidecar;
const VERIFY = process.env.AERE_VERIFY_PROOF ? path.resolve(process.env.AERE_VERIFY_PROOF) : path.resolve(HERE, '..', '..', '..', 'tools', 'aere-proof-protocol', 'verify.mjs');
const areVerificator = fs.existsSync(VERIFY);
// ---------------------------------------------------------------------------------- harness
const probe = [];
function proba(nume, fn) { if (probe.some((p) => p.nume === nume)) throw new Error('proba dublata: ' + nume); probe.push({ nume, fn }); }
class Esec extends Error {}
class Sarit extends Error {}
const adevarat = (c, ce) => { if (!c) throw new Esec(ce); };
const egal = (a, b, ce) => { if (a !== b) throw new Esec(`${ce}: asteptat ${JSON.stringify(b)}, primit ${JSON.stringify(a)}`); };
/** cere un refuz SecretsError cu codul dat; optional regula serviciului, o regula a identitatii, o regula de conformitate, un fragment de mesaj */
async function refuza(fn, cod, ce, { rule, identityRule, complianceRule, mesaj } = {}) {
let e;
try { await fn(); } catch (x) { e = x; }
if (!e) throw new Esec(`${ce}: trebuia refuzat cu ${cod}, a trecut`);
if (!(e instanceof SecretsError)) throw new Esec(`${ce}: a aruncat altceva decat SecretsError: ${e && e.stack}`);
if (e.code !== cod) throw new Esec(`${ce}: cod asteptat ${cod}, primit ${e.code} (${e.message})`);
if (rule !== undefined && e.rule !== rule) throw new Esec(`${ce}: regula asteptata ${rule}, primita ${e.rule}`);
if (identityRule !== undefined && !(e.identityRules || []).includes(identityRule)) throw new Esec(`${ce}: regula identitatii asteptata ${identityRule}, primite [${(e.identityRules || []).join(', ')}] (${e.message})`);
if (complianceRule !== undefined && !(e.complianceRules || []).includes(complianceRule)) throw new Esec(`${ce}: regula de conformitate asteptata ${complianceRule}, primite [${(e.complianceRules || []).join(', ')}] (${e.message})`);
if (mesaj !== undefined && !String(e.message).includes(mesaj)) throw new Esec(`${ce}: mesajul trebuia sa contina "${mesaj}": ${e.message}`);
return e;
}
const tmpDirs = [];
const tmpDir = (tag = 'd') => { const d = fs.mkdtempSync(path.join(os.tmpdir(), `aeresecrets-proba-${tag}-`)); tmpDirs.push(d); return d; };
const rootHex = () => crypto.randomBytes(32).toString('hex');
const iso = (d) => new Date(d).toISOString();
const ms = (s) => s * 1000;
// ---------------------------------------------------------------------------------- identitatile de proba (nascute la fiecare rulare)
const AUD = 'https://secrets.example';
const ALT_AUD = 'https://other-service.example';
const LID = 'urn:example:secrets-status:1';
const T0 = Date.now();
const issuer = ID.generateKeys(), altIssuer = ID.generateKeys();
const K = { admin: ID.generateKeys(), deployer: ID.generateKeys(), reader: ID.generateKeys(), reader2: ID.generateKeys(), rotator: ID.generateKeys(), auditor: ID.generateKeys(), granter: ID.generateKeys(), phone: ID.generateKeys(), stranger: ID.generateKeys(), revoked: ID.generateKeys(), nostatus: ID.generateKeys() };
function emite(holder, claims, index, { iss = issuer } = {}) {
return ID.issueCredential({ issuer: iss, holder: holder.public, type: 'ServiceAccess', claims, disclosable: Object.keys(claims), validFrom: iso(T0 - ms(3600)), validUntil: iso(T0 + ms(86400 * 365)), status: index === null ? null : { list: LID, index }, now: new Date(T0 - ms(3600)) });
}
const C = {
admin: emite(K.admin, { role: 'secrets-admin', tenant: 'acme' }, 0),
deployer: emite(K.deployer, { role: 'deployer', tenant: 'acme' }, 1),
reader: emite(K.reader, { role: 'reader', tenant: 'acme' }, 2),
reader2: emite(K.reader2, { role: 'reader', tenant: 'acme' }, 5),
rotator: emite(K.rotator, { role: 'rotator', tenant: 'acme' }, 3),
auditor: emite(K.auditor, { role: 'auditor', tenant: 'acme' }, 4),
granter: emite(K.granter, { role: 'granter', tenant: 'acme' }, 6),
revoked: emite(K.revoked, { role: 'deployer', tenant: 'acme' }, 9),
nostatus: emite(K.nostatus, { role: 'deployer', tenant: 'acme' }, null),
// strainul isi emite singur un credential de "secrets-admin" cu cheile LUI de emitent
stranger: emite(K.stranger, { role: 'secrets-admin', tenant: 'acme' }, 0, { iss: altIssuer }),
};
const LISTA = ID.createStatusList({ issuer, id: LID, revoked: [9], validFrom: iso(T0 - ms(3600)), validUntil: iso(T0 + ms(86400)), now: new Date(T0 - ms(3600)) });
const CRED_IDS = [C.deployer.credential.statement.id]; // id-urile credentialelor pe care le acopera delegarea (scope.credentials)
const DELEGARE = ID.delegate({ from: K.deployer, to: K.phone.public, scope: { credentials: CRED_IDS, claims: '*', audiences: [AUD] }, notBefore: iso(T0 - ms(3600)), notAfter: iso(T0 + ms(86400)), maxDepth: 0, now: new Date(T0 - ms(3600)) });
const DELEGARE_INGUSTA = ID.delegate({ from: K.deployer, to: K.phone.public, scope: { credentials: CRED_IDS, claims: ['role'], audiences: [AUD] }, notBefore: iso(T0 - ms(3600)), notAfter: iso(T0 + ms(86400)), maxDepth: 0, now: new Date(T0 - ms(3600)) });
const REVOCARE = ID.revokeDelegation({ by: K.deployer, delegation: DELEGARE, at: iso(T0 - ms(60)) });
const POL = (id, role) => ({ id, trustedIssuers: [issuer.id], require: [{ claim: 'role', equals: role }] });
const ADMIN_POLICY = { id: 'secrets-admin-v1', trustedIssuers: [issuer.id], require: [{ claim: 'role', equals: 'secrets-admin' }, { claim: 'tenant', equals: 'acme' }] };
const GRANTS = () => [
{ id: 'deployers', operations: ['get', 'metadata'], policy: { id: 'deployers-v1', trustedIssuers: [issuer.id], require: [{ claim: 'role', equals: 'deployer' }, { claim: 'tenant', equals: 'acme' }] } },
{ id: 'rotators', operations: ['rotate', 'revoke-version'], policy: POL('rotators-v1', 'rotator') },
{ id: 'auditors', operations: ['get', 'metadata'], versions: 'any', policy: POL('auditors-v1', 'auditor') },
];
const VALOARE = 'S3cr3t-Db-P4ssw0rd-' + crypto.randomBytes(12).toString('hex');
// CLIENTUL DE PROBA, 2026-10-18 (B8): o prezentare se semneaza pentru CEREREA pe care o trimite (digestul ei, requestHash, in legatura).
// prez() intoarce prezentarea fara cerere, cu ce trebuie ca sa fie semnata; serviciul de proba (infasoara) si cererea HTTP (cerere) o
// semneaza, la PRIMA trimitere, pentru exact operatia, secretul si corpul trimise, ca un client cinstit. O prezentare atinsa dupa prez()
// (un atac) pleaca asa cum e; una data cu `pentru: [operatie, secret, corp]` e semnata pe loc pentru acea cerere, si oriunde altundeva e
// depusa e o SUBSTITUTIE (proba ei). A doua trimitere a aceleiasi prezentari e o reluare: pleaca neschimbata.
const INTENTIE = Symbol('cererea pentru care se semneaza la trimitere');
function semneaza(p, op, name, body, valueEncoding = 'bytes') {
const m = p && typeof p === 'object' ? p[INTENTIE] : null;
if (!m || m.folosita) return;
m.folosita = true;
let neatinsa = false; try { neatinsa = ID.canonical(p.binding) === m.semnat; } catch { neatinsa = false; }
if (!neatinsa) return;
let rh; try { rh = requestHash({ audience: m.args.audience, operation: op, secret: name, body, valueEncoding }); } catch { return; }
const q = ID.present({ ...m.args, request: rh });
p.binding = q.binding; p.signature = q.signature;
}
const METODE = { put: 'put', get: 'get', metadata: 'metadata', rotate: 'rotate', revokeVersion: 'revoke-version', grant: 'grant', revoke: 'revoke' };
function infasoara(s) {
for (const [m, op] of Object.entries(METODE)) { const f = s[m].bind(s); s[m] = (name, body) => { semneaza(body && body.presentation, op, name, body); return f(name, body); }; }
const l = s.list.bind(s); s.list = (body) => { semneaza(body && body.presentation, 'list', null, body); return l(body); };
return s;
}
const openSecrets = async (o) => infasoara(await deschideSecrete(o));
async function nouServiciu({ now, cuLista = true, auditLog, trustDir, kms: kmsDat = null } = {}) {
const dir = tmpDir('svc');
const kms = kmsDat || openKms({ dataDir: path.join(dir, 'kms'), rootKey: rootHex() });
const trust = trustDir || path.join(dir, 'trust');
fs.mkdirSync(path.join(trust, 'status-lists'), { recursive: true });
fs.mkdirSync(path.join(trust, 'revocations'), { recursive: true });
if (cuLista) fs.writeFileSync(path.join(trust, 'status-lists', 'lista.json'), JSON.stringify(LISTA));
const s = await openSecrets({ dataDir: path.join(dir, 'data'), kms, audience: AUD, adminPolicy: ADMIN_POLICY, trustDir: trust, auditLog, now });
return { s, dir, kms, trust, data: path.join(dir, 'data') };
}
/** o prezentare pentru serviciu, cu un nonce proaspat al lui (sau unul dat); semnata la trimitere pentru cererea trimisa (vezi sus) */
function prez(s, keys, cred, { reveal = null, nonce = null, audience = AUD, now = new Date(), delegations = [], pentru = null } = {}) {
const r = reveal || Object.keys(Object.fromEntries(cred.disclosures.map((d) => [ID.decodeDisclosure(d).name, 1])));
const args = { credential: cred.credential, disclosures: cred.disclosures, reveal: r, presenter: keys, delegations, audience, nonce: nonce || s.nonce().nonce, now };
if (pentru) { const [op, name, body = {}, enc = 'bytes'] = pentru; return ID.present({ ...args, request: requestHash({ audience, operation: op, secret: name, body, valueEncoding: enc }) }); }
const p = ID.present(args);
Object.defineProperty(p, INTENTIE, { value: { args, semnat: ID.canonical(p.binding), folosita: false }, enumerable: false });
return p;
}
/** ultima declaratie din jurnal */
const ultima = (s) => { const j = jurnal(s.auditLog); return j[j.length - 1].proof.statement; };
/** un serviciu populat: secretul db (valoarea VALOARE, grants: deployers/rotators/auditors) si api-key (fara grants), ambele puse de admin */
async function populat(opts) {
const x = await nouServiciu(opts);
await x.s.put('db', { presentation: prez(x.s, K.admin, C.admin), value: VALOARE, metadata: { owner: 'platform', kind: 'password' }, grants: GRANTS() });
await x.s.put('api-key', { presentation: prez(x.s, K.admin, C.admin), value: 'ak-' + crypto.randomBytes(8).toString('hex') });
return x;
}
/** cauta un sir (si forma lui base64) in toate fisierele de sub un dosar; intoarce fisierele in care apare */
function cauta(dosar, sir) {
const forme = [sir, Buffer.from(sir, 'utf8').toString('base64'), Buffer.from(sir, 'utf8').toString('base64url'), Buffer.from(sir, 'utf8').toString('hex')];
const out = [];
const merge = (d) => { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) merge(p); else if (forme.some((f) => fs.readFileSync(p, 'latin1').includes(f))) out.push(p); } };
if (fs.existsSync(dosar)) merge(dosar);
return out;
}
const jurnal = (p) => fs.readFileSync(p, 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
function cerere(port, method, p, { headers = {}, body } = {}) {
// clientul de proba semneaza prezentarea pentru ruta si corpul pe care le trimite (valoarea, in HTTP, e base64-ul asa cum pleaca)
if (body && typeof body === 'object' && !Buffer.isBuffer(body) && body.presentation) {
const m = /^\/v1\/secrets\/([^/]+)\/([^/?]+)$/.exec(p);
try { if (m) semneaza(body.presentation, m[2], decodeURIComponent(m[1]), body, 'base64'); else if (p === '/v1/list') semneaza(body.presentation, 'list', null, body, 'base64'); } catch { /* o ruta pe care clientul nu o stie: pleaca nesemnata */ }
}
return new Promise((resolve, reject) => {
const data = body === undefined ? null : Buffer.isBuffer(body) ? body : Buffer.from(typeof body === 'string' ? body : JSON.stringify(body));
const h = { ...headers };
if (data) { h['content-length'] = data.length; if (h['content-type'] === undefined) h['content-type'] = 'application/json'; }
const req = http.request({ host: '127.0.0.1', port, method, path: p, headers: h, agent: false }, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); let json = null; try { json = JSON.parse(text); } catch { json = null; } resolve({ status: res.statusCode, json, text }); });
});
req.on('error', reject);
if (data) req.write(data);
req.end();
});
}
const envCurat = () => Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('AERE_KMS_') && !k.startsWith('AERE_SECRETS_')));
// ================================================================================== PROBELE
proba('pornire: creeaza cheia KMS (encrypt) si service.json; redeschiderea citeste aceeasi cheie de MAC; refuzuri numite: fara audienta, admin fara trustedIssuers, admin cu requireStatus false, alta audienta pe acelasi dosar', async () => {
const { s, dir, kms, trust } = await nouServiciu();
const k = kms.getKey('secrets');
egal(k.type, 'encrypt', 'tipul cheii KMS');
adevarat(fs.existsSync(path.join(dir, 'data', 'service.json')), 'service.json scris');
const sj = JSON.parse(fs.readFileSync(path.join(dir, 'data', 'service.json'), 'utf8'));
adevarat(typeof sj.mac_key_wrapped === 'string' && sj.mac_key_wrapped.startsWith('aerekms:v1:'), 'cheia de MAC e invelita de KMS');
await s.put('x', { presentation: prez(s, K.admin, C.admin), value: 'v' });
s.close();
const s2 = await openSecrets({ dataDir: path.join(dir, 'data'), kms, audience: AUD, adminPolicy: ADMIN_POLICY, trustDir: trust });
const r = await s2.get('x', { presentation: prez(s2, K.admin, C.admin) });
egal(r.value.toString(), 'v', 'dupa redeschidere fisierul trece MAC-ul si se citeste');
s2.close();
let e;
try { await openSecrets({ dataDir: tmpDir(), kms, audience: '', adminPolicy: ADMIN_POLICY }); } catch (x) { e = x; }
egal(e && e.code, 'AUDIENCE_INVALID', 'fara audienta');
e = null; try { await openSecrets({ dataDir: tmpDir(), kms, audience: AUD, adminPolicy: { id: 'a', trustedIssuers: [], require: [{ claim: 'role', equals: 'x' }] } }); } catch (x) { e = x; }
egal(e && e.code, 'ADMIN_POLICY_INVALID', 'admin fara trustedIssuers');
e = null; try { await openSecrets({ dataDir: tmpDir(), kms, audience: AUD, adminPolicy: { ...ADMIN_POLICY, requireStatus: false } }); } catch (x) { e = x; }
egal(e && e.code, 'ADMIN_POLICY_INVALID', 'admin cu requireStatus false');
e = null; try { await openSecrets({ dataDir: path.join(dir, 'data'), kms, audience: ALT_AUD, adminPolicy: ADMIN_POLICY }); } catch (x) { e = x; }
egal(e && e.code, 'AUDIENCE_MISMATCH', 'alta audienta pe acelasi dosar');
e = null; try { await openSecrets({ dataDir: tmpDir(), audience: AUD, adminPolicy: ADMIN_POLICY }); } catch (x) { e = x; }
egal(e && e.code, 'KMS_MISSING', 'fara KMS');
kms.close();
});
proba('flux: admin pune secretul cu grants, deployer-ul il citeste: valoarea e exact cea pusa, versiunea 1, metadatele; metadata nu contine valoarea; intrarea de audit a put-ului poarta grantul admin', async () => {
const { s, kms } = await populat();
const r = await s.get('db', { presentation: prez(s, K.deployer, C.deployer) });
egal(r.value.toString('utf8'), VALOARE, 'valoarea citita');
egal(r.version, 1, 'versiunea'); egal(r.current, 1, 'curenta');
egal(r.metadata.owner, 'platform', 'metadata');
const m = await s.metadata('db', { presentation: prez(s, K.deployer, C.deployer) });
egal(m.current, 1, 'metadata: curenta'); egal(m.versions['1'].state, 'enabled', 'metadata: starea');
egal(m.grants.map((g) => g.id).join(','), 'deployers,rotators,auditors', 'metadata: grants');
adevarat(!JSON.stringify(m).includes(VALOARE) && !JSON.stringify(m).includes('sealed') && !JSON.stringify(m).includes('dek_wrapped'), 'metadata nu contine valoarea, nici materialul sigilat');
const j = jurnal(s.auditLog);
egal(j[0].proof.statement.operation, 'put', 'prima intrare: put'); egal(j[0].proof.statement.grant, 'admin', 'grantul admin'); egal(j[0].proof.statement.verdict, 'allowed', 'verdict');
egal(j[0].proof.statement.holder, K.admin.id, 'detinatorul verificat');
s.close(); kms.close();
});
proba('refuz: cerere fara prezentare -> PRESENTATION_MISSING (presentation-form), auditat; cerere care nu e obiect -> INVALID_REQUEST si nume invalid -> INVALID_SECRET_NAME, fara intrare in jurnal', async () => {
const { s, kms } = await populat();
const n0 = jurnal(s.auditLog).length;
await refuza(() => s.get('db', {}), 'PRESENTATION_MISSING', 'fara prezentare', { rule: 'presentation-form' });
await refuza(() => s.get('db', { presentation: 'nu' }), 'PRESENTATION_MISSING', 'prezentare care nu e obiect', { rule: 'presentation-form' });
egal(jurnal(s.auditLog).length, n0 + 2, 'doua intrari de refuz');
const j = jurnal(s.auditLog); const st = j[j.length - 1].proof.statement;
egal(st.verdict, 'refused', 'verdictul'); adevarat(st.rules.includes('presentation-form'), 'regula numita in jurnal: ' + st.rules.join(','));
await refuza(() => s.get('db', null), 'INVALID_REQUEST', 'cerere nula', { rule: 'request-form' });
await refuza(() => s.get('DB NAME', { presentation: prez(s, K.admin, C.admin) }), 'INVALID_SECRET_NAME', 'nume invalid', { rule: 'secret-name' });
await refuza(() => s.get('../x', { presentation: prez(s, K.admin, C.admin) }), 'INVALID_SECRET_NAME', 'nume cu cale', { rule: 'secret-name' });
egal(jurnal(s.auditLog).length, n0 + 2, 'forma cererii si numele nu sunt operatii: fara intrare');
s.close(); kms.close();
});
proba('refuz: nonce refolosit (aceeasi prezentare a doua oara) -> NONCE_INVALID (nonce), si dupa o prima cerere REFUZATA; nonce neemis de serviciu -> NONCE_INVALID "not issued"', async () => {
const { s, kms } = await populat();
const p = prez(s, K.deployer, C.deployer);
egal((await s.get('db', { presentation: p })).version, 1, 'prima citire');
await refuza(() => s.get('db', { presentation: p }), 'NONCE_INVALID', 'a doua oara aceeasi prezentare', { rule: 'nonce', mesaj: 'not issued by this service, or already used' });
const q = prez(s, K.reader, C.reader);
await refuza(() => s.get('db', { presentation: q }), 'UNAUTHORIZED', 'cititorul e refuzat de politica');
await refuza(() => s.get('db', { presentation: q }), 'NONCE_INVALID', 'nonce-ul s-a consumat si la refuz', { rule: 'nonce' });
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer, { nonce: crypto.randomBytes(32).toString('base64url') }) }), 'NONCE_INVALID', 'nonce strain', { rule: 'nonce', mesaj: 'not issued' });
s.close(); kms.close();
});
proba('refuz: nonce expirat (ceasul serviciului trecut de TTL) -> NONCE_INVALID "expired"; inainte de TTL acelasi nonce merge', async () => {
let ceas = Date.now();
const { s, kms } = await populat({ now: () => new Date(ceas) });
const n1 = s.nonce(); ceas += ms(119);
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer, { nonce: n1.nonce, now: new Date(ceas) }) })).version, 1, 'la 119 s nonce-ul e bun');
const n2 = s.nonce(); ceas += ms(121);
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer, { nonce: n2.nonce, now: new Date(ceas) }) }), 'NONCE_INVALID', 'la 121 s', { rule: 'nonce', mesaj: 'expired' });
s.close(); kms.close();
});
proba('refuz: prezentare facuta pentru alt serviciu -> UNAUTHORIZED cu regula identitatii audience', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer, { audience: ALT_AUD }) }), 'UNAUTHORIZED', 'alt public', { rule: 'authorization', identityRule: 'audience' });
s.close(); kms.close();
});
proba('refuz: prezentare veche (facuta acum 10 minute, cu nonce proaspat) -> UNAUTHORIZED cu regula identitatii freshness', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer, { now: new Date(Date.now() - ms(600)) }) }), 'UNAUTHORIZED', 'prezentare veche', { rule: 'authorization', identityRule: 'freshness' });
s.close(); kms.close();
});
proba('refuz: un strain prezinta credentialul deployer-ului cu cheia lui -> UNAUTHORIZED cu regula identitatii presenter', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.stranger, C.deployer) }), 'UNAUTHORIZED', 'strainul cu credentialul altuia', { rule: 'authorization', identityRule: 'presenter' });
s.close(); kms.close();
});
proba('refuz: afirmatiile nu indeplinesc politica: cititorul nu citeste secretul deployer-ilor (authorization, grantul deployers refuza pe rule); credentialul auto-emis al strainului e refuzat (presentation: emitent necrezut)', async () => {
const { s, kms } = await populat();
const e = await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'cititorul', { rule: 'authorization', mesaj: MESAJ_NEAUTORIZAT });
const st = ultima(s);
adevarat(st.rules.includes('rule') && st.judged.some((g) => g.grant === 'deployers' && g.rules.includes('rule')), 'grantul deployers a refuzat pe rule, in jurnal: ' + JSON.stringify(st.judged));
adevarat(e.identityRules === undefined && e.grants === undefined, 'identitatea e valida, politica refuza: raspunsul nu poarta detaliul');
await refuza(() => s.get('db', { presentation: prez(s, K.stranger, C.stranger) }), 'UNAUTHORIZED', 'strainul cu credential auto-emis', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('presentation'), 'strainul: emitent necrezut (presentation), in jurnal: ' + ultima(s).rules.join(','));
await refuza(() => s.put('nou', { presentation: prez(s, K.stranger, C.stranger), value: 'x' }), 'UNAUTHORIZED', 'strainul nu creeaza secrete', { rule: 'authorization' });
s.close(); kms.close();
});
proba('refuz: operatia nu e in grant: deployer-ul (get, metadata) nu poate put, rotate, revoke-version, grant, revoke (authorization)', async () => {
const { s, kms } = await populat();
const d = () => prez(s, K.deployer, C.deployer);
await refuza(() => s.put('db', { presentation: d(), value: 'alta' }), 'UNAUTHORIZED', 'put', { rule: 'authorization' });
await refuza(() => s.rotate('db', { presentation: d() }), 'UNAUTHORIZED', 'rotate', { rule: 'authorization' });
await refuza(() => s.revokeVersion('db', { presentation: d(), version: 1 }), 'UNAUTHORIZED', 'revoke-version', { rule: 'authorization' });
await refuza(() => s.grant('db', { presentation: d(), grant: { id: 'me', operations: ['put'], policy: POL('me', 'deployer') } }), 'UNAUTHORIZED', 'grant', { rule: 'authorization' });
await refuza(() => s.revoke('db', { presentation: d(), grantId: 'rotators' }), 'UNAUTHORIZED', 'revoke', { rule: 'authorization' });
egal((await s.get('db', { presentation: d() })).value.toString(), VALOARE, 'get ramane permis');
s.close(); kms.close();
});
proba('refuz: credential revocat in lista de stare -> UNAUTHORIZED cu regula identitatii credential-revoked; fara lista in dosarul de incredere starea e nejudecata si tot refuzat (status), si pentru admin', async () => {
const { s, kms, trust } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.revoked, C.revoked) }), 'UNAUTHORIZED', 'credential revocat', { rule: 'authorization', identityRule: 'credential-revoked' });
// lista dispare din dosarul de incredere: starea nu mai poate fi judecata si NIMENI nu mai intra, nici adminul (nu "nerevocat fiindca nu stim")
fs.rmSync(path.join(trust, 'status-lists', 'lista.json'));
await refuza(() => s.get('db', { presentation: prez(s, K.revoked, C.revoked) }), 'UNAUTHORIZED', 'fara lista: starea nejudecata', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('status'), 'fara lista: starea nejudecata: regula status in jurnal: ' + ultima(s).rules.join(','));
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer) }), 'UNAUTHORIZED', 'fara lista nici deployer-ul bun nu intra', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('status'), 'deployer-ul: status in jurnal');
await refuza(() => s.put('alt', { presentation: prez(s, K.admin, C.admin), value: 'x' }), 'UNAUTHORIZED', 'fara lista nici adminul', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('status'), 'adminul: status in jurnal');
s.close(); kms.close();
});
proba('refuz: credential fara lista de stare nu deschide nimic (status nejudecat); un grant cu requireStatus false e refuzat la scriere (INVALID_GRANT, grant-form, "waive")', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('db', { presentation: prez(s, K.nostatus, C.nostatus) }), 'UNAUTHORIZED', 'fara lista de stare', { rule: 'authorization' });
adevarat(ultima(s).rules.includes('status'), 'fara lista de stare: trebuia refuzat pe status, in jurnal: ' + ultima(s).rules.join(','));
const g = { id: 'lax', operations: ['get'], policy: { ...POL('lax-v1', 'deployer'), requireStatus: false } };
await refuza(() => s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: g }), 'INVALID_GRANT', 'grant care scuteste starea', { rule: 'grant-form', mesaj: 'waive' });
await refuza(() => s.put('alt', { presentation: prez(s, K.admin, C.admin), value: 'v', grants: [g] }), 'INVALID_GRANT', 'la creare la fel', { rule: 'grant-form', mesaj: 'waive' });
adevarat(!fs.existsSync(path.join(s.dataDir, 'secrets', 'alt.json')), 'secretul refuzat nu a ramas pe disc');
s.close(); kms.close();
});
proba('delegare: telefonul deployer-ului citeste in scopul delegarii; o revocare pusa in dosarul de incredere il opreste (delegation-revoked); o delegare care nu acopera afirmatiile aratate e refuzata (delegation-claims)', async () => {
const { s, kms, trust } = await populat();
const r = await s.get('db', { presentation: prez(s, K.phone, C.deployer, { delegations: [DELEGARE] }) });
egal(r.value.toString(), VALOARE, 'telefonul citeste');
const j = jurnal(s.auditLog); const st = j[j.length - 1].proof.statement;
egal(st.holder, K.deployer.id, 'detinatorul din jurnal'); egal(st.presenter, K.phone.id, 'prezentatorul din jurnal');
await refuza(() => s.get('db', { presentation: prez(s, K.phone, C.deployer, { delegations: [DELEGARE_INGUSTA] }) }), 'UNAUTHORIZED', 'afirmatii in afara scopului', { rule: 'authorization', identityRule: 'delegation-claims' });
fs.writeFileSync(path.join(trust, 'revocations', 'telefon.json'), JSON.stringify(REVOCARE));
await refuza(() => s.get('db', { presentation: prez(s, K.phone, C.deployer, { delegations: [DELEGARE] }) }), 'UNAUTHORIZED', 'delegare revocata', { rule: 'authorization', identityRule: 'delegation-revoked' });
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).version, 1, 'detinatorul insusi citeste in continuare');
s.close(); kms.close();
});
proba('rotatie: rotate face versiunea 2 cu o valoare noua; get fara versiune da 2; versiunea 1 cere versions any: deployer-ul (current) e refuzat (VERSION_NOT_IN_SCOPE), auditorul citeste 1 si 2; versiunea 7 nu exista (VERSION_NOT_FOUND)', async () => {
const { s, kms } = await populat();
const rot = await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator), bytes: 24, encoding: 'hex' });
egal(rot.version, 2, 'versiunea noua'); adevarat(!('value' in rot), 'rotate nu intoarce valoarea');
const r2 = await s.get('db', { presentation: prez(s, K.deployer, C.deployer) });
egal(r2.version, 2, 'curenta e 2'); egal(r2.current, 2, 'current');
adevarat(/^[0-9a-f]{48}$/.test(r2.value.toString()) && r2.value.toString() !== VALOARE, 'valoare noua, 24 de octeti hex');
egal(r2.metadata.rotated, true, 'metadata rotated');
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer), version: 1 }), 'VERSION_NOT_IN_SCOPE', 'deployer la versiunea 1', { rule: 'version-scope' });
egal((await s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 1 })).value.toString(), VALOARE, 'auditorul citeste versiunea 1 (valoarea veche)');
egal((await s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 2 })).value.toString(), r2.value.toString(), 'auditorul citeste versiunea 2');
await refuza(() => s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 7 }), 'VERSION_NOT_FOUND', 'versiunea 7', { rule: 'version-exists' });
await refuza(() => s.rotate('db', { presentation: prez(s, K.rotator, C.rotator), bytes: 8 }), 'INVALID_REQUEST', 'bytes sub 16', { rule: 'request-form' });
s.close(); kms.close();
});
proba('revoke-version: versiunea 1 revocata nu se mai citeste nici cu versions any (VERSION_REVOKED), textul cifrat si cheia invelita dispar de pe disc; a doua revocare refuzata; curenta revocata -> get fara versiune refuzat pana la un put nou', async () => {
const { s, kms } = await populat();
await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) });
const rv = await s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 1 });
egal(rv.state, 'revoked', 'starea');
await refuza(() => s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 1 }), 'VERSION_REVOKED', 'auditorul la versiunea revocata', { rule: 'version-revoked' });
const f = JSON.parse(fs.readFileSync(path.join(s.dataDir, 'secrets', 'db.json'), 'utf8'));
egal(f.versions['1'].sealed, null, 'sealed sters'); egal(f.versions['1'].dek_wrapped, null, 'cheia invelita stearsa'); adevarat(typeof f.versions['2'].sealed === 'string', 'versiunea 2 ramane');
await refuza(() => s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 1 }), 'VERSION_REVOKED', 'a doua revocare', { rule: 'version-revoked' });
await refuza(() => s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 3 }), 'VERSION_NOT_FOUND', 'versiune inexistenta', { rule: 'version-exists' });
await s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 2 });
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer) }), 'VERSION_REVOKED', 'curenta revocata', { rule: 'version-revoked' });
const p3 = await s.put('db', { presentation: prez(s, K.admin, C.admin), value: 'a-treia' });
egal(p3.version, 3, 'put dupa revocari');
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).value.toString(), 'a-treia', 'citeste versiunea 3');
const m = await s.metadata('db', { presentation: prez(s, K.deployer, C.deployer) });
egal([m.versions['1'].state, m.versions['2'].state, m.versions['3'].state].join(','), 'revoked,revoked,enabled', 'starile in metadata');
s.close(); kms.close();
});
proba('grant/revoke: admin da cititorilor un grant get, cititorul citeste, admin il retrage, cititorul e refuzat; grant dublat (GRANT_EXISTS), revoke pe id inexistent (GRANT_NOT_FOUND), grant malformat (INVALID_GRANT); grant cu holders admite numai detinatorul numit', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin);
await s.grant('db', { presentation: a(), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } });
egal((await s.get('db', { presentation: prez(s, K.reader, C.reader) })).value.toString(), VALOARE, 'cititorul citeste dupa grant');
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } }), 'GRANT_EXISTS', 'grant dublat', { rule: 'grant-exists' });
const rv = await s.revoke('db', { presentation: a(), grantId: 'readers' });
adevarat(!rv.grants.includes('readers'), 'grantul retras');
await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'cititorul dupa retragere', { rule: 'authorization' });
await refuza(() => s.revoke('db', { presentation: a(), grantId: 'readers' }), 'GRANT_NOT_FOUND', 'retragere a doua oara', { rule: 'grant-exists' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['fly'], policy: POL('x', 'reader') } }), 'INVALID_GRANT', 'operatie necunoscuta', { rule: 'grant-form' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['get'], policy: { id: 'x', trustedIssuers: [], require: [{ claim: 'role', equals: 'reader' }] } } }), 'INVALID_GRANT', 'politica fara emitenti', { rule: 'grant-form' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['get'], policy: POL('x', 'reader'), holders: ['cineva'] } }), 'INVALID_GRANT', 'holders invalid', { rule: 'grant-form' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['get'], policy: POL('x', 'reader'), versions: 'all' } }), 'INVALID_GRANT', 'versions invalid', { rule: 'grant-form' });
await refuza(() => s.grant('db', { presentation: a(), grant: { id: 'x', operations: ['get'], policy: POL('x', 'reader'), extra: 1 } }), 'INVALID_GRANT', 'camp necunoscut', { rule: 'grant-form' });
await s.grant('db', { presentation: a(), grant: { id: 'one-reader', operations: ['get'], holders: [K.reader.id], policy: POL('one-reader-v1', 'reader') } });
egal((await s.get('db', { presentation: prez(s, K.reader, C.reader) })).version, 1, 'detinatorul numit citeste');
await refuza(() => s.get('db', { presentation: prez(s, K.reader2, C.reader2) }), 'UNAUTHORIZED', 'alt cititor, cu aceleasi afirmatii, nu e numit', { rule: 'authorization' });
adevarat(ultima(s).judged.some((g) => g.grant === 'one-reader' && g.rules.includes('holder')), 'grantul one-reader a refuzat pe holder (jurnal): ' + JSON.stringify(ultima(s).judged));
s.close(); kms.close();
});
proba('list: admin vede toate secretele; deployer-ul vede numai secretele cu un grant care il admite; strainul e refuzat; o cerere fara prezentare e refuzata', async () => {
const { s, kms } = await populat();
egal((await s.list({ presentation: prez(s, K.admin, C.admin) })).secrets.join(','), 'api-key,db', 'admin');
egal((await s.list({ presentation: prez(s, K.deployer, C.deployer) })).secrets.join(','), 'db', 'deployer');
await refuza(() => s.list({ presentation: prez(s, K.stranger, C.stranger) }), 'UNAUTHORIZED', 'strainul', { rule: 'authorization' });
await refuza(() => s.list({}), 'PRESENTATION_MISSING', 'fara prezentare', { rule: 'presentation-form' });
const j = jurnal(s.auditLog); const st = j[j.length - 1].proof.statement;
egal(st.operation, 'list', 'auditat'); egal(st.secret, '*', 'secretul * la list');
s.close(); kms.close();
});
proba('secret inexistent: admin primeste SECRET_NOT_FOUND (secret-exists); deployer-ul primeste UNAUTHORIZED, fara sa afle daca exista', async () => {
const { s, kms } = await populat();
await refuza(() => s.get('nimic', { presentation: prez(s, K.admin, C.admin) }), 'SECRET_NOT_FOUND', 'admin', { rule: 'secret-exists' });
const e1 = await refuza(() => s.get('nimic', { presentation: prez(s, K.deployer, C.deployer) }), 'UNAUTHORIZED', 'deployer pe secret inexistent', { rule: 'authorization' });
const e2 = await refuza(() => s.get('api-key', { presentation: prez(s, K.deployer, C.deployer) }), 'UNAUTHORIZED', 'deployer pe secret existent fara grant', { rule: 'authorization' });
egal(e1.message.replace(/nimic/g, 'N'), e2.message.replace(/api-key/g, 'N'), 'acelasi raspuns, exista sau nu');
s.close(); kms.close();
});
proba('valoare: goala sau peste 64 KB refuzata (value-form); metadate nevalide refuzate; put pe secret existent cu grants refuzat (request-form)', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin);
await refuza(() => s.put('v', { presentation: a(), value: '' }), 'INVALID_VALUE', 'goala', { rule: 'value-form' });
await refuza(() => s.put('v', { presentation: a(), value: Buffer.alloc(MAX_VALUE + 1, 1) }), 'VALUE_TOO_LARGE', 'peste limita', { rule: 'value-form' });
await refuza(() => s.put('v', { presentation: a(), value: 7 }), 'INVALID_VALUE', 'numar', { rule: 'value-form' });
await refuza(() => s.put('v', { presentation: a(), value: 'x', metadata: { 'bad name': 1 } }), 'INVALID_METADATA', 'nume de metadata', { rule: 'value-form' });
await refuza(() => s.put('v', { presentation: a(), value: 'x', metadata: { a: { b: 1 } } }), 'INVALID_METADATA', 'valoare de metadata', { rule: 'value-form' });
await refuza(() => s.put('db', { presentation: a(), value: 'x', grants: [] }), 'INVALID_REQUEST', 'grants pe secret existent', { rule: 'request-form' });
adevarat(!fs.existsSync(path.join(s.dataDir, 'secrets', 'v.json')), 'niciun fisier dupa refuzuri');
egal((await s.put('v', { presentation: a(), value: Buffer.alloc(MAX_VALUE, 2) })).version, 1, 'exact la limita trece');
s.close(); kms.close();
});
proba('stocare: valoarea nu apare pe disc (nici base64/hex), cu controlul pozitiv al scanerului; cheia de date e invelita de KMS; un grant adaugat in fisier cu mana e refuzat (SECRET_FILE_TAMPERED) numai adminului, si auditat; jurnalul KMS nu contine valoarea', async () => {
const { s, kms, dir } = await populat();
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).value.toString(), VALOARE, 'o citire, ca sa existe si decrypt la KMS');
egal(cauta(dir, VALOARE).length, 0, 'valoarea nu e nicaieri pe disc: ' + cauta(dir, VALOARE).join(', '));
const momeala = path.join(dir, 'momeala.txt'); fs.writeFileSync(momeala, 'x' + Buffer.from(VALOARE).toString('base64') + 'y');
egal(cauta(dir, VALOARE).length, 1, 'controlul pozitiv: scanerul gaseste momeala');
fs.rmSync(momeala);
const f = JSON.parse(fs.readFileSync(path.join(dir, 'data', 'secrets', 'db.json'), 'utf8'));
adevarat(f.versions['1'].dek_wrapped.startsWith('aerekms:v1:'), 'cheia de date e un plic KMS');
adevarat(typeof f.mac === 'string' && f.mac.length === 64, 'fisierul poarta MAC');
const kmsLog = fs.readFileSync(path.join(dir, 'kms', 'audit.log'), 'utf8');
adevarat(/"op":"datakey"/.test(kmsLog) && /"op":"decrypt"/.test(kmsLog), 'jurnalul KMS are datakey si decrypt');
// manipulare: grantul deployers primeste put, fara MAC nou
f.grants[0].operations.push('put');
fs.writeFileSync(path.join(dir, 'data', 'secrets', 'db.json'), JSON.stringify(f));
const n0 = jurnal(s.auditLog).length;
await refuza(() => s.put('db', { presentation: prez(s, K.deployer, C.deployer), value: 'x' }), 'UNAUTHORIZED', 'deployer-ul nu afla ca fisierul e stricat', { rule: 'authorization' });
await refuza(() => s.metadata('db', { presentation: prez(s, K.admin, C.admin) }), 'SECRET_FILE_TAMPERED', 'adminul afla', { rule: 'storage' });
const j = jurnal(s.auditLog); egal(j.length, n0 + 2, 'ambele auditate'); adevarat(j[j.length - 1].proof.statement.rules.includes('storage'), 'regula storage in jurnal');
s.close(); kms.close();
});
proba('jurnal: fiecare operatie (reusita sau refuzata) e o intrare secret-access cu serviciul, secretul, versiunea, operatia, verdictul, regulile, grantul, hash-ul legaturii prezentarii; valoarea nu apare in jurnal, cu control pozitiv; lantul INTACT (verify-log cod 0)', async () => {
const { s, kms } = await populat();
const p = prez(s, K.deployer, C.deployer);
await s.get('db', { presentation: p });
const j = jurnal(s.auditLog);
egal(j.length, 3, 'put, put, get');
const st = j[2].proof.statement;
egal(j[2].proof.kind, `aere-proof-of-${KIND}-attestation`, 'felul plicului'); egal(st.kind, `aere-proof-of-${KIND}`, 'felul declaratiei');
egal(st.service, AUD, 'serviciul'); egal(st.secret, 'db', 'secretul'); egal(st.version, 1, 'versiunea'); egal(st.operation, 'get', 'operatia'); egal(st.verdict, 'allowed', 'verdictul');
egal(st.grant, 'deployers', 'grantul'); egal(st.holder, K.deployer.id, 'detinatorul'); egal(st.presenter, K.deployer.id, 'prezentatorul');
egal(st.presentationHash, '0x' + crypto.createHash('sha256').update(ID.canonical(p.binding), 'utf8').digest('hex'), 'hash-ul legaturii semnate');
adevarat(/^0x[0-9a-f]{64}$/.test(st.policyHash), 'hash-ul politicii grantului');
egal(j[2].proof.statementHash, '0x' + crypto.createHash('sha256').update(JSON.stringify(st), 'utf8').digest('hex'), 'statementHash-ul plicului');
egal(j[2].seq, 2, 'seq'); egal(j[2].prev, j[1].hash, 'prev leaga intrarea de dinainte');
await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'refuz de auditat');
const r = jurnal(s.auditLog)[3].proof.statement;
egal(r.verdict, 'refused', 'refuzul'); adevarat(r.rules.includes('authorization') && r.rules.includes('rule'), 'regulile refuzului: ' + r.rules.join(','));
egal(r.holder, K.reader.id, 'detinatorul pretins'); egal(r.grant, null, 'fara grant');
const text = fs.readFileSync(s.auditLog, 'utf8');
adevarat(!text.includes(VALOARE) && !text.includes(Buffer.from(VALOARE).toString('base64')), 'valoarea nu e in jurnal');
adevarat(('x' + Buffer.from(VALOARE).toString('base64')).includes(Buffer.from(VALOARE).toString('base64')), 'controlul pozitiv al cautarii');
const v = SC.verificaJurnal(jurnal(s.auditLog)); adevarat(v.ok, 'lantul intact'); egal(v.count, 4, 'patru intrari');
const cli = execFileSync(process.execPath, [SIDECAR_PATH, 'verify-log', '--log', s.auditLog], { encoding: 'utf8' });
adevarat(cli.includes('log INTACT: 4 entries'), 'verify-log al sidecar-ului: ' + cli.trim());
const cap = s.auditHead(); egal(cap.count, 4, 'capul'); egal(cap.head, jurnal(s.auditLog)[3].hash, 'head-ul e hash-ul ultimei intrari');
s.close(); kms.close();
});
proba('jurnal: dupa 100 de operatii amestecate lantul e intact, are exact numarul de intrari si capul se schimba la fiecare', async () => {
const { s, kms } = await populat();
const n0 = jurnal(s.auditLog).length; const capete = new Set([s.auditHead().head]);
let reusite = 0, refuzuri = 0;
for (let i = 0; i < 100; i++) {
const fel = i % 5;
try {
if (fel === 0) await s.put('db', { presentation: prez(s, K.admin, C.admin), value: 'v' + i });
else if (fel === 1) await s.get('db', { presentation: prez(s, K.deployer, C.deployer) });
else if (fel === 2) await s.get('db', { presentation: prez(s, K.reader, C.reader) });
else if (fel === 3) await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) });
else await s.metadata('db', { presentation: prez(s, K.auditor, C.auditor) });
reusite++;
} catch (e) { if (!(e instanceof SecretsError) || e.code !== 'UNAUTHORIZED') throw e; refuzuri++; }
const h = s.auditHead().head; adevarat(!capete.has(h), 'capul se schimba la operatia ' + i); capete.add(h);
}
egal(reusite, 80, 'reusite'); egal(refuzuri, 20, 'refuzuri');
const j = jurnal(s.auditLog); egal(j.length, n0 + 100, 'exact 100 de intrari noi');
const v = SC.verificaJurnal(j); adevarat(v.ok && v.count === n0 + 100, 'lantul intact dupa 100');
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).version, 41, 'versiunea curenta dupa 20 put + 20 rotate');
s.close(); kms.close();
});
proba('jurnal: o mutatie al carei rand de audit nu se poate scrie NU ramane pe disc (put nou, rotate, grant), iar lantul ramane valid', async () => {
const { s, kms } = await populat();
const log = s.auditLog; const copie = log + '.copie'; fs.copyFileSync(log, copie);
const f = path.join(s.dataDir, 'secrets', 'db.json'); const inainte = fs.readFileSync(f);
fs.rmSync(log); fs.mkdirSync(log); // jurnalul devine un dosar: nimic nu se mai poate adauga
await refuza(() => s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) }), 'AUDIT_WRITE_FAILED', 'rotate fara rand de audit', { rule: 'storage' });
adevarat(fs.readFileSync(f).equals(inainte), 'rotirea nu a ramas pe disc');
await refuza(() => s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'g', operations: ['get'], policy: POL('g', 'reader') } }), 'AUDIT_WRITE_FAILED', 'grant fara rand de audit', { rule: 'storage' });
adevarat(fs.readFileSync(f).equals(inainte), 'grantul nu a ramas pe disc');
await refuza(() => s.put('nou', { presentation: prez(s, K.admin, C.admin), value: 'x' }), 'AUDIT_WRITE_FAILED', 'put nou fara rand de audit', { rule: 'storage' });
adevarat(!fs.existsSync(path.join(s.dataDir, 'secrets', 'nou.json')), 'secretul nou nu a ramas pe disc');
fs.rmSync(log, { recursive: true }); fs.copyFileSync(copie, log);
const v = SC.verificaJurnal(jurnal(log)); adevarat(v.ok, 'lantul de dinainte e valid');
egal((await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) })).version, 2, 'dupa ce jurnalul e la loc, rotirea merge si e versiunea 2');
s.close(); kms.close();
});
proba('plicuri: intrarile de put, get, metadata, rotate, revoke-version, grant, revoke, list si un refuz sunt VALID pentru verificatorul AIP-23 de referinta (SARIT fara verificator)', async () => {
if (!areVerificator) throw new Sarit(`verificatorul AIP-23 nu e la ${VERIFY}; dati AERE_VERIFY_PROOF=<verify-proof.mjs>`);
const { s, kms } = await populat();
await s.get('db', { presentation: prez(s, K.deployer, C.deployer) });
await s.metadata('db', { presentation: prez(s, K.deployer, C.deployer) });
await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) });
await s.revokeVersion('db', { presentation: prez(s, K.rotator, C.rotator), version: 1 });
await s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } });
await s.revoke('db', { presentation: prez(s, K.admin, C.admin), grantId: 'readers' });
await s.list({ presentation: prez(s, K.admin, C.admin) });
await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'refuz');
const j = jurnal(s.auditLog); const ops = j.map((e) => e.proof.statement.operation + ':' + e.proof.statement.verdict);
egal(ops.join(' '), 'put:allowed put:allowed get:allowed metadata:allowed rotate:allowed revoke-version:allowed grant:allowed revoke:allowed list:allowed get:refused', 'operatiile din jurnal');
const d = tmpDir('plic');
for (let i = 0; i < j.length; i++) {
const f = path.join(d, `${i}.json`); fs.writeFileSync(f, JSON.stringify(j[i].proof, null, 1));
let verdict = '?';
try { verdict = JSON.parse(execFileSync(process.execPath, [VERIFY, f, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })).verdict; } catch (e) { try { verdict = JSON.parse(e.stdout || '').verdict; } catch { verdict = '?'; } }
egal(verdict, 'VALID', `plicul ${i} (${ops[i]})`);
}
// control negativ: un plic atins dupa hash e INVALID
const rau = JSON.parse(JSON.stringify(j[2].proof)); rau.statement.verdict = 'refused';
const fr = path.join(d, 'rau.json'); fs.writeFileSync(fr, JSON.stringify(rau));
let vr = '?'; try { vr = JSON.parse(execFileSync(process.execPath, [VERIFY, fr, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })).verdict; } catch (e) { try { vr = JSON.parse(e.stdout || '').verdict; } catch { vr = '?'; } }
egal(vr, 'INVALID', 'plic atins dupa hash');
s.close(); kms.close();
});
proba('server: flux complet prin HTTP (health, nonce, put, get, metadata, rotate, revoke-version, grant, revoke, list, audit head); refuzurile poarta error, rule si identityRules; raspunsurile nu contin valoarea decat la get autorizat; GET pe o operatie -> 405; corp prea mare -> 413', async () => {
const { s, kms } = await nouServiciu();
const server = createServer({ secrets: s, maxBody: 65536 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
const nonce = async () => (await cerere(port, 'POST', '/v1/nonce')).json.nonce;
const P = async (keys, cred, o = {}) => prez(s, keys, cred, { ...o, nonce: await nonce() });
const toate = [];
const post = async (p, body) => { const r = await cerere(port, 'POST', p, { body }); toate.push([p, r.text]); return r; };
try {
const h = await cerere(port, 'GET', '/v1/health'); egal(h.status, 200, 'health'); egal(h.json.audience, AUD, 'audienta in health');
const n = await cerere(port, 'POST', '/v1/nonce'); egal(n.status, 200, 'nonce'); adevarat(typeof n.json.nonce === 'string' && n.json.audience === AUD && typeof n.json.expiresAt === 'string', 'forma nonce-ului');
const b64 = Buffer.from(VALOARE).toString('base64');
const put = await post('/v1/secrets/db/put', { presentation: await P(K.admin, C.admin), value: b64, metadata: { owner: 'platform' }, grants: GRANTS() });
egal(put.status, 200, 'put: ' + put.text); egal(put.json.version, 1, 'put versiunea');
const get = await post('/v1/secrets/db/get', { presentation: await P(K.deployer, C.deployer) });
egal(get.status, 200, 'get: ' + get.text); egal(get.json.value, b64, 'valoarea base64'); egal(get.json.metadata.owner, 'platform', 'metadata');
const bad = await post('/v1/secrets/db/get', { presentation: await P(K.reader, C.reader) });
egal(bad.status, 403, 'cititorul 403'); egal(bad.json.error, 'UNAUTHORIZED', 'error'); egal(bad.json.rule, 'authorization', 'rule'); egal(Object.keys(bad.json).join(','), 'error,rule,message', 'raspunsul generic: numai error, rule, message'); egal(bad.json.message, MESAJ_NEAUTORIZAT, 'mesajul generic');
const alt = await post('/v1/secrets/db/get', { presentation: await P(K.deployer, C.deployer, { audience: ALT_AUD }) });
egal(alt.status, 403, 'alt public 403'); adevarat(alt.json.identityRules.includes('audience'), 'regula audience in raspuns');
const fara = await post('/v1/secrets/db/get', {}); egal(fara.status, 401, 'fara prezentare 401'); egal(fara.json.rule, 'presentation-form', 'regula');
const pr = await P(K.deployer, C.deployer); await post('/v1/secrets/db/get', { presentation: pr });
const replay = await post('/v1/secrets/db/get', { presentation: pr }); egal(replay.status, 401, 'replay 401'); egal(replay.json.rule, 'nonce', 'regula nonce');
const md = await post('/v1/secrets/db/metadata', { presentation: await P(K.deployer, C.deployer) }); egal(md.status, 200, 'metadata'); egal(md.json.current, 1, 'metadata current');
const rot = await post('/v1/secrets/db/rotate', { presentation: await P(K.rotator, C.rotator), bytes: 16 }); egal(rot.status, 200, 'rotate: ' + rot.text); egal(rot.json.version, 2, 'rotate versiunea');
const sc = await post('/v1/secrets/db/get', { presentation: await P(K.deployer, C.deployer), version: 1 }); egal(sc.status, 403, 'versiunea 1 fara any 403'); egal(sc.json.rule, 'version-scope', 'regula');
const rv = await post('/v1/secrets/db/revoke-version', { presentation: await P(K.rotator, C.rotator), version: 1 }); egal(rv.status, 200, 'revoke-version: ' + rv.text);
const gone = await post('/v1/secrets/db/get', { presentation: await P(K.auditor, C.auditor), version: 1 }); egal(gone.status, 410, 'versiunea revocata 410'); egal(gone.json.rule, 'version-revoked', 'regula');
const gr = await post('/v1/secrets/db/grant', { presentation: await P(K.admin, C.admin), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } }); egal(gr.status, 200, 'grant: ' + gr.text);
const ok2 = await post('/v1/secrets/db/get', { presentation: await P(K.reader, C.reader) }); egal(ok2.status, 200, 'cititorul dupa grant');
const dup = await post('/v1/secrets/db/grant', { presentation: await P(K.admin, C.admin), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } }); egal(dup.status, 409, 'grant dublat 409');
const rk = await post('/v1/secrets/db/revoke', { presentation: await P(K.admin, C.admin), grantId: 'readers' }); egal(rk.status, 200, 'revoke: ' + rk.text);
const nf = await post('/v1/secrets/db/revoke', { presentation: await P(K.admin, C.admin), grantId: 'readers' }); egal(nf.status, 404, 'revoke inexistent 404');
const ls = await post('/v1/list', { presentation: await P(K.admin, C.admin) }); egal(ls.status, 200, 'list'); egal(ls.json.secrets.join(','), 'db', 'lista');
const nx = await post('/v1/secrets/nimic/get', { presentation: await P(K.admin, C.admin) }); egal(nx.status, 404, 'inexistent 404'); egal(nx.json.rule, 'secret-exists', 'regula');
const head = await cerere(port, 'GET', '/v1/audit/head'); egal(head.status, 200, 'audit head'); adevarat(head.json.ok && head.json.count >= 14, 'capul: ' + head.text);
egal((await cerere(port, 'GET', '/v1/secrets/db/get')).status, 405, 'GET pe o operatie');
egal((await cerere(port, 'POST', '/v1/secrets/db/fly', { body: {} })).status, 404, 'operatie necunoscuta');
egal((await cerere(port, 'POST', '/v1/secrets/db/put', { body: '{' })).status, 400, 'JSON stricat');
egal((await cerere(port, 'POST', '/v1/secrets/db/put', { body: Buffer.alloc(70000, 0x20) })).status, 413, 'corp prea mare');
egal((await cerere(port, 'POST', '/v1/secrets/db/put', { body: { presentation: await P(K.admin, C.admin), value: 'nu-e-base64!' } })).status, 400, 'valoare care nu e base64');
for (const [p, text] of toate) if (!(p === '/v1/secrets/db/get' && text.includes('"value"') && text.includes(b64) && JSON.parse(text).version === 1)) adevarat(!text.includes(VALOARE) && !text.includes(b64), 'valoarea intr-un raspuns care nu e get autorizat: ' + p + ' ' + text.slice(0, 120));
adevarat(toate.some(([p, text]) => p === '/v1/secrets/db/get' && text.includes(b64)), 'controlul pozitiv: get-ul autorizat o contine');
} finally { await new Promise((r) => server.close(r)); s.close(); kms.close(); }
});
proba('server: pornit din linia de comanda (KMS in proces): nonce, put, get prin HTTP; stdout/stderr nu contin valoarea; refuza fara AERE_SECRETS_AUDIENCE, fara politica de admin, cu ambele KMS-uri, si nu tipareste cheia radacina', async () => {
const dir = tmpDir('cli');
fs.mkdirSync(path.join(dir, 'trust', 'status-lists'), { recursive: true });
fs.writeFileSync(path.join(dir, 'trust', 'status-lists', 'l.json'), JSON.stringify(LISTA));
fs.writeFileSync(path.join(dir, 'admin.json'), JSON.stringify(ADMIN_POLICY));
const root = rootHex();
const env = { ...envCurat(), AERE_SECRETS_AUDIENCE: AUD, AERE_SECRETS_ADMIN_POLICY: path.join(dir, 'admin.json'), AERE_SECRETS_DATA_DIR: path.join(dir, 'data'), AERE_SECRETS_TRUST_DIR: path.join(dir, 'trust'), AERE_KMS_ROOT_KEY: root, AERE_SECRETS_PORT: '0' };
const porneste = (e) => new Promise((resolve) => {
const c = spawn(process.execPath, [SERVER_PATH], { env: e, stdio: ['ignore', 'pipe', 'pipe'] });
let out = '', err = '', gata = false;
const fin = (port) => { if (!gata) { gata = true; resolve({ c, port, out: () => out, err: () => err }); } };
c.stdout.on('data', (d) => { out += d; const m = /listening on 127\.0\.0\.1:(\d+)/.exec(out); if (m) fin(Number(m[1])); });
c.stderr.on('data', (d) => { err += d; });
c.on('close', () => fin(null)); // close, nu exit: la exit stderr-ul poate fi inca pe drum
setTimeout(() => fin(null), 20000);
});
const srv = await porneste(env);
try {
adevarat(srv.port, 'serverul a pornit: ' + srv.err());
// cheia secp... radacina: nu apare in iesire; audienta da
adevarat(!srv.out().includes(root) && !srv.err().includes(root), 'cheia radacina nu e tiparita');
const nonce = async () => (await cerere(srv.port, 'POST', '/v1/nonce')).json.nonce;
const svcFals = { nonce: () => ({ nonce: 'x' }) };
const b64 = Buffer.from(VALOARE).toString('base64');
const put = await cerere(srv.port, 'POST', '/v1/secrets/db/put', { body: { presentation: prez(svcFals, K.admin, C.admin, { nonce: await nonce() }), value: b64, grants: GRANTS() } });
egal(put.status, 200, 'put prin procesul pornit: ' + put.text);
const get = await cerere(srv.port, 'POST', '/v1/secrets/db/get', { body: { presentation: prez(svcFals, K.deployer, C.deployer, { nonce: await nonce() }) } });
egal(get.status, 200, 'get: ' + get.text); egal(get.json.value, b64, 'valoarea');
const rau = await cerere(srv.port, 'POST', '/v1/secrets/db/get', { body: { presentation: prez(svcFals, K.reader, C.reader, { nonce: await nonce() }) } });
egal(rau.status, 403, 'refuz prin procesul pornit');
await new Promise((r) => setTimeout(r, 200));
adevarat(!srv.out().includes(VALOARE) && !srv.out().includes(b64) && !srv.err().includes(VALOARE) && !srv.err().includes(b64), 'valoarea nu e in stdout/stderr');
adevarat(cauta(dir, VALOARE).length === 0, 'valoarea nu e pe discul procesului');
} finally { srv.c.kill(); await new Promise((r) => srv.c.on('exit', r)); }
const refuzaPornirea = async (e, cod) => { const r = await porneste(e); if (r.port) { r.c.kill(); throw new Esec('a pornit desi trebuia refuzat: ' + cod); } adevarat(r.err().includes(cod), `refuz asteptat ${cod}: ${r.err().trim()}`); adevarat(!r.err().includes(root), 'cheia radacina nu e in refuz'); };
await refuzaPornirea({ ...env, AERE_SECRETS_AUDIENCE: '' }, 'AUDIENCE_MISSING');
await refuzaPornirea({ ...env, AERE_SECRETS_ADMIN_POLICY: '' }, 'ADMIN_POLICY_MISSING');
await refuzaPornirea({ ...env, AERE_KMS_URL: 'http://127.0.0.1:1', AERE_KMS_TOKEN: 'x'.repeat(32) }, 'KMS_AMBIGUOUS');
await refuzaPornirea({ ...env, AERE_KMS_ROOT_KEY: '' }, 'KMS_MISSING');
const cfg = configFromEnv(env); egal(cfg.kms.mode, 'in-process', 'configFromEnv: KMS in proces'); egal(cfg.nonceTtlS, 120, 'TTL implicit');
});
proba('server: KMS la distanta (serverul pq-kms pornit in proces, kmsClientHttp): put/get merg, jurnalul KMS are datakey si decrypt fara valori, token gresit -> KMS_INVALID_TOKEN (kms), KMS oprit -> KMS_UNREACHABLE', async () => {
const kdir = tmpDir('kmsd');
const kms = openKms({ dataDir: kdir, rootKey: rootHex() });
const token = crypto.randomBytes(24).toString('base64url') + 'x'.repeat(8);
const ks = createKmsServer({ kms, token, maxBody: 65536 });
await new Promise((r) => ks.listen(0, '127.0.0.1', r));
const url = `http://127.0.0.1:${ks.address().port}`;
const dir = tmpDir('rem');
fs.mkdirSync(path.join(dir, 'trust', 'status-lists'), { recursive: true }); fs.writeFileSync(path.join(dir, 'trust', 'status-lists', 'l.json'), JSON.stringify(LISTA));
try {
const client = kmsClientHttp({ url, token });
const s = await openSecrets({ dataDir: path.join(dir, 'data'), kms: client, audience: AUD, adminPolicy: ADMIN_POLICY, trustDir: path.join(dir, 'trust') });
egal(kms.getKey('secrets').type, 'encrypt', 'cheia creata prin HTTP in KMS');
await s.put('db', { presentation: prez(s, K.admin, C.admin), value: VALOARE, grants: GRANTS() });
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer) })).value.toString(), VALOARE, 'get prin KMS la distanta');
const log = fs.readFileSync(path.join(kdir, 'audit.log'), 'utf8');
adevarat(/"op":"datakey","key":"secrets"/.test(log) && /"op":"decrypt","key":"secrets"/.test(log), 'jurnalul KMS');
adevarat(!log.includes(VALOARE) && cauta(kdir, VALOARE).length === 0, 'valoarea nu e la KMS');
s.close();
let e; try { await openSecrets({ dataDir: path.join(dir, 'data'), kms: kmsClientHttp({ url, token: 'y'.repeat(40) }), audience: AUD, adminPolicy: ADMIN_POLICY }); } catch (x) { e = x; }
egal(e && e.code, 'KMS_INVALID_TOKEN', 'token gresit'); egal(e && e.rule, 'kms', 'regula kms');
let e2; try { kmsClientHttp({ url, token: 'scurt' }); } catch (x) { e2 = x; } egal(e2 && e2.code, 'KMS_TOKEN_INVALID', 'token scurt refuzat la construire');
} finally { await new Promise((r) => ks.close(r)); }
let e3; try { await openSecrets({ dataDir: path.join(dir, 'data'), kms: kmsClientHttp({ url, token }), audience: AUD, adminPolicy: ADMIN_POLICY }); } catch (x) { e3 = x; }
egal(e3 && e3.code, 'KMS_UNREACHABLE', 'KMS oprit');
kms.close();
});
// ================================================================================== revizuirea adversariala din 18 oct (B8): cele 14 puncte
proba('cererea (substitutie): o prezentare semnata pentru "get db" nu deschide revoke pe db, get pe db2, nici get db cu un camp adaugat; una fara cerere e refuzata; regula identitatii request; randul poarta digestul cererii primite', async () => {
const { s, kms } = await populat();
await s.put('db2', { presentation: prez(s, K.admin, C.admin), value: 'valoarea-lui-db2', grants: GRANTS() });
const pentruGet = (k, c) => prez(s, k, c, { pentru: ['get', 'db', {}] });
await refuza(() => s.revoke('db', { presentation: pentruGet(K.admin, C.admin), grantId: 'deployers' }), 'UNAUTHORIZED', 'revoke cu prezentarea adminului pentru get', { rule: 'authorization', identityRule: 'request' });
egal(ultima(s).requestHash, requestHash({ audience: AUD, operation: 'revoke', secret: 'db', body: { grantId: 'deployers' } }), 'randul de refuz poarta digestul cererii PRIMITE');
await refuza(() => s.get('db2', { presentation: pentruGet(K.deployer, C.deployer) }), 'UNAUTHORIZED', 'get pe db2 cu prezentarea pentru db', { rule: 'authorization', identityRule: 'request' });
await refuza(() => s.get('db', { presentation: pentruGet(K.auditor, C.auditor), version: 1 }), 'UNAUTHORIZED', 'get db cu version adaugat', { rule: 'authorization', identityRule: 'request' });
const fara = ID.present({ credential: C.deployer.credential, disclosures: C.deployer.disclosures, reveal: ['role', 'tenant'], presenter: K.deployer, audience: AUD, nonce: s.nonce().nonce });
await refuza(() => s.get('db', { presentation: fara }), 'UNAUTHORIZED', 'o prezentare fara cerere (forma de dinainte)', { rule: 'authorization', identityRule: 'request', mesaj: 'identity rules: request' });
const bun = await s.get('db', { presentation: pentruGet(K.deployer, C.deployer) });
egal(bun.value.toString(), VALOARE, 'prezentarea semnata pentru get db deschide get db');
egal(ultima(s).requestHash, requestHash({ audience: AUD, operation: 'get', secret: 'db', body: {} }), 'randul permis poarta digestul cererii');
egal((await s.metadata('db', { presentation: prez(s, K.admin, C.admin) })).grants.map((g) => g.id).join(','), 'deployers,rotators,auditors', 'grantul deployers nu a fost scos');
s.close(); kms.close();
});
proba('cererea prin HTTP: corpul unui get capturat, depus pe revoke-version cu version 1 adaugat -> 403 (request); un put cu valoarea schimbata in drum -> 403; versiunea 1 ramane', async () => {
const { s, kms } = await populat();
const server = createServer({ secrets: s, maxBody: 65536 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
const nonce = async () => (await cerere(port, 'POST', '/v1/nonce')).json.nonce;
try {
const corp = { presentation: prez(null, K.rotator, C.rotator, { nonce: await nonce(), pentru: ['get', 'db', {}, 'base64'] }) };
const r = await cerere(port, 'POST', '/v1/secrets/db/revoke-version', { body: { ...corp, version: 1 } });
egal(r.status, 403, 'revoke-version cu corpul de get: ' + r.text); adevarat(r.json.identityRules.includes('request'), 'regula request in raspuns: ' + r.text);
const a = Buffer.from('valoarea-adminului').toString('base64'), b = Buffer.from('valoarea-atacatorului').toString('base64');
const pPut = prez(null, K.admin, C.admin, { nonce: await nonce(), pentru: ['put', 'db', { value: a }, 'base64'] });
const r2 = await cerere(port, 'POST', '/v1/secrets/db/put', { body: { presentation: pPut, value: b } });
egal(r2.status, 403, 'put cu valoarea schimbata: ' + r2.text); adevarat(r2.json.identityRules.includes('request'), 'regula request');
const m = await s.metadata('db', { presentation: prez(s, K.admin, C.admin) });
egal(m.versions['1'].state, 'enabled', 'versiunea 1 nu a fost revocata'); egal(m.current, 1, 'nicio versiune noua');
const ok = await cerere(port, 'POST', '/v1/secrets/db/put', { body: { presentation: prez(null, K.admin, C.admin, { nonce: await nonce(), pentru: ['put', 'db', { value: a }, 'base64'] }), value: a } });
egal(ok.status, 200, 'acelasi put, cu valoarea semnata: ' + ok.text); egal(ok.json.version, 2, 'versiunea 2');
} finally { await new Promise((r) => server.close(r)); s.close(); kms.close(); }
});
proba('atenuare: un ne-admin cu grant face numai grant-uri mai inguste (operatii fara grant/revoke, versiuni, emitenti, regulile politicii, varsta, holders, delegates), si retrage numai ce a facut el (grant-scope)', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin), gr = () => prez(s, K.granter, C.granter);
const PG = { id: 'granters-v1', trustedIssuers: [issuer.id], require: [{ claim: 'role', equals: 'granter' }] };
await s.grant('db', { presentation: a(), grant: { id: 'granters', operations: ['get', 'grant', 'metadata', 'revoke'], policy: PG } });
const ingust = (o = {}) => ({ id: 'g-' + crypto.randomBytes(3).toString('hex'), operations: ['get'], holders: [K.granter.id], policy: { ...PG, id: 'ingust', require: [...PG.require, { claim: 'tenant', equals: 'acme' }] }, ...o });
const nu = async (g, ce, mesaj) => refuza(() => s.grant('db', { presentation: gr(), grant: g }), 'GRANT_NOT_ATTENUATED', ce, { rule: 'grant-scope', mesaj });
await nu(ingust({ operations: ['put'] }), 'put, pe care grantul lui nu il are', 'operations');
await nu(ingust({ operations: ['get', 'grant'] }), 'grant mai departe', 'without grant and revoke');
await nu(ingust({ operations: ['revoke'] }), 'revoke mai departe', 'without grant and revoke');
await nu(ingust({ versions: 'any' }), 'versions any dintr-un grant current', 'older versions');
await nu(ingust({ policy: { ...PG, id: 'alt', trustedIssuers: [issuer.id, altIssuer.id] } }), 'un emitent in plus', 'issuers');
await nu(ingust({ policy: { id: 'lax', trustedIssuers: [issuer.id], require: [{ claim: 'tenant', equals: 'acme' }] } }), 'regula role=granter scoasa', 'every rule');
await nu(ingust({ policy: { ...PG, id: 'vechi', maxAgeS: 600 } }), 'o varsta maxima mai mare', 'maximum age');
// auto-acordarea din revizuire (9a) si usa din spate cu emitent strain (9b): refuzate
await nu({ id: 'eu-tot', operations: ['put', 'get', 'metadata', 'rotate', 'revoke-version', 'grant', 'revoke'], versions: 'any', policy: POL('eu-tot', 'granter') }, '9a: toate operatiile', 'operations');
await nu({ id: 'backdoor', operations: ['get'], policy: { id: 'bd', trustedIssuers: [altIssuer.id], require: [{ claim: 'role', present: true }] } }, '9b: emitent strain', 'issuers');
const bun = ingust({ id: 'al-meu' });
egal((await s.grant('db', { presentation: gr(), grant: bun })).grant, 'al-meu', 'un grant mai ingust trece');
const m = await s.metadata('db', { presentation: a() }); const g = m.grants.find((x) => x.id === 'al-meu');
egal(g.createdBy, K.granter.id, 'grantul poarta cine l-a facut'); egal(m.grants.find((x) => x.id === 'deployers').createdBy, K.admin.id, 'grantul de la creare: adminul');
await refuza(() => s.revoke('db', { presentation: gr(), grantId: 'deployers' }), 'GRANT_NOT_OWNED', 'un grant al adminului', { rule: 'grant-scope' });
egal((await s.revoke('db', { presentation: gr(), grantId: 'al-meu' })).revoked, 'al-meu', 'grantul lui il retrage');
// un grant numai cu `grant` nu mai poate da nimic: operatiile lui fara grant si revoke sunt o multime goala
await s.grant('db', { presentation: a(), grant: { id: 'doar-grant', operations: ['grant'], policy: { ...PG, id: 'dg', require: [{ claim: 'role', equals: 'granter' }, { claim: 'tenant', equals: 'acme' }] } } });
// holders si delegates: pe un secret al carui grant de origine ii numeste
await s.put('h', { presentation: a(), value: 'v', grants: [{ id: 'gh', operations: ['get', 'grant'], holders: [K.granter.id], delegates: false, policy: PG }] });
const pe = (g) => s.grant('h', { presentation: gr(), grant: g });
await refuza(() => pe({ id: 'x1', operations: ['get'], policy: PG }), 'GRANT_NOT_ATTENUATED', 'fara holders cand originea ii numeste', { rule: 'grant-scope', mesaj: 'holders' });
await refuza(() => pe({ id: 'x2', operations: ['get'], holders: [K.granter.id], policy: PG }), 'GRANT_NOT_ATTENUATED', 'cu delegati cand originea ii opreste', { rule: 'grant-scope', mesaj: 'delegates' });
egal((await pe({ id: 'x3', operations: ['get'], holders: [K.granter.id], delegates: false, policy: PG })).grant, 'x3', 'holders inclusi si delegatii opriti: trece');
// adminul nu e atenuat: da si grant/revoke
egal((await s.grant('db', { presentation: a(), grant: { id: 'admin-da', operations: ['grant', 'revoke', 'get'], versions: 'any', policy: POL('ad', 'reader') } })).grant, 'admin-da', 'adminul da grant si revoke');
adevarat(jurnal(s.auditLog).filter((e) => e.proof.statement.rules.includes('grant-scope')).length >= 11, 'refuzurile grant-scope sunt in jurnal');
s.close(); kms.close();
});
proba('raspunsul generic: un refuz de autorizare e acelasi, octet cu octet, pentru un secret cu 3 grant-uri, unul cu 0 si unul inexistent (biblioteca si HTTP); detaliul (grant-urile judecate, regulile) e numai in jurnal', async () => {
const { s, kms } = await populat();
// tot ce poarta eroarea: mesajul (propriu, neenumerabil) si fiecare camp enumerabil, cu valoarea lui
const forma = (e) => JSON.stringify({ message: e.message, ...Object.fromEntries(Object.entries(e).sort(([x], [y]) => (x < y ? -1 : 1))) });
const e = []; const judecate = [];
for (const n of ['db', 'api-key', 'nu-exista']) { e.push(await refuza(() => s.get(n, { presentation: prez(s, K.stranger, C.stranger) }), 'UNAUTHORIZED', 'strainul pe ' + n, { rule: 'authorization', mesaj: MESAJ_NEAUTORIZAT })); judecate.push(ultima(s).judged.map((x) => x.grant).join(',')); }
egal(forma(e[1]), forma(e[0]), 'db (3 grant-uri) si api-key (0)'); egal(forma(e[2]), forma(e[0]), 'db si un nume inexistent');
const rot = await refuza(() => s.rotate('db', { presentation: prez(s, K.deployer, C.deployer) }), 'UNAUTHORIZED', 'deployer-ul rotate (grant pe get, nu pe rotate)', { rule: 'authorization' });
const cit = await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'cititorul (grant potrivit pe get, regula picata)', { rule: 'authorization' });
egal(forma(rot), forma(e[0]), 'rotate'); egal(forma(cit), forma(e[0]), 'regula picata');
egal(judecate.join(' | '), 'admin,deployers,auditors | admin | admin', 'jurnalul are grant-urile judecate pentru fiecare');
const server = createServer({ secrets: s, maxBody: 65536 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
try {
const P = async (k, c) => prez(null, k, c, { nonce: (await cerere(port, 'POST', '/v1/nonce')).json.nonce });
const r = [];
for (const n of ['db', 'api-key', 'nu-exista']) r.push(await cerere(port, 'POST', `/v1/secrets/${n}/get`, { body: { presentation: await P(K.stranger, C.stranger) } }));
r.push(await cerere(port, 'POST', '/v1/secrets/db/get', { body: { presentation: await P(K.reader, C.reader) } }));
for (const x of r) { egal(x.status, 403, 'HTTP 403'); egal(x.text, r[0].text, 'acelasi corp, octet cu octet'); }
adevarat(!r[0].text.includes('deployers') && !r[0].text.includes('grants'), 'raspunsul nu numeste grant-uri: ' + r[0].text);
} finally { await new Promise((q) => server.close(q)); s.close(); kms.close(); }
});
proba('jurnalul unui refuz: un id se scrie ca holder/presenter numai VERIFICAT; cele pretinse ca claimedHolder/claimedPresenter numai in forma aere-id; un anonim cu 100.000 de caractere scrie un rand sub 2 KB', async () => {
const { s, kms } = await populat();
const fals = { v: 1, kind: 'aere-presentation', credential: { statement: { holder: { id: 'A'.repeat(100000) } } }, disclosures: [], delegations: [], binding: { nonce: 'nu-conteaza', presenter: { id: K.admin.id } } };
await refuza(() => s.get('db', { presentation: fals }), 'NONCE_INVALID', 'anonimul', { rule: 'nonce' });
const st = ultima(s); const linie = fs.readFileSync(s.auditLog, 'utf8').split('\n').filter(Boolean).pop();
egal(st.holder, null, 'detinatorul neverificat nu e scris ca holder'); egal(st.presenter, null, 'adminul pretins nu e scris ca presenter');
egal(st.claimedHolder, undefined, 'un id pretins de alta forma nu intra'); egal(st.claimedPresenter, K.admin.id, 'id-ul pretins, numit pretins');
adevarat(Buffer.byteLength(linie) < 2048, 'randul are ' + Buffer.byteLength(linie) + ' octeti');
await refuza(() => s.get('db', { presentation: prez(s, K.stranger, C.deployer) }), 'UNAUTHORIZED', 'strainul cu credentialul deployer-ului', { rule: 'authorization', identityRule: 'presenter' });
const s2 = ultima(s);
egal(s2.holder, null, 'semnatura picata: holder nescris'); egal(s2.claimedHolder, K.deployer.id, 'detinatorul pretins'); egal(s2.claimedPresenter, K.stranger.id, 'prezentatorul pretins');
await refuza(() => s.get('db', { presentation: prez(s, K.reader, C.reader) }), 'UNAUTHORIZED', 'cititorul, refuzat de politica', { rule: 'authorization' });
const s3 = ultima(s);
egal(s3.holder, K.reader.id, 'identitatea verificata: holder'); egal(s3.presenter, K.reader.id, 'presenter'); egal(s3.claimedHolder, undefined, 'fara pretinse');
s.close(); kms.close();
});
proba('concurenta cu KMS-ul la distanta: 8 put-uri simultane -> versiunile 2..9, distincte si citibile; grant si rotate simultane -> amandoua pe disc', async () => {
const kdir = tmpDir('kmsc'); const kmsR = openKms({ dataDir: kdir, rootKey: rootHex() });
const token = crypto.randomBytes(24).toString('base64url') + 'x'.repeat(8);
const ks = createKmsServer({ kms: kmsR, token, maxBody: 65536 });
await new Promise((r) => ks.listen(0, '127.0.0.1', r));
try {
const { s } = await nouServiciu({ kms: kmsClientHttp({ url: `http://127.0.0.1:${ks.address().port}`, token }) });
await s.put('db', { presentation: prez(s, K.admin, C.admin), value: VALOARE, grants: GRANTS() });
const rs = await Promise.all(Array.from({ length: 8 }, (_, i) => s.put('db', { presentation: prez(s, K.admin, C.admin), value: 'conc-' + i })));
egal(rs.map((r) => r.version).sort((x, y) => x - y).join(','), '2,3,4,5,6,7,8,9', 'versiunile intoarse');
const valori = new Set();
for (let v = 2; v <= 9; v++) valori.add((await s.get('db', { presentation: prez(s, K.admin, C.admin), version: v })).value.toString());
egal([...valori].sort().join(','), Array.from({ length: 8 }, (_, i) => 'conc-' + i).join(','), 'toate cele 8 valori citibile, fiecare in versiunea ei');
const [g, r] = await Promise.all([s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'readers', operations: ['get'], policy: POL('readers-v1', 'reader') } }), s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) })]);
const m = await s.metadata('db', { presentation: prez(s, K.admin, C.admin) });
egal(r.version, 10, 'rotirea'); egal(m.current, 10, 'curenta pe disc'); adevarat(m.grants.some((x) => x.id === 'readers') && g.grants.includes('readers'), 'grantul a supravietuit rotirii');
s.close();
} finally { await new Promise((r) => ks.close(r)); kmsR.close(); }
});
proba('lacatul dosarului: a doua deschidere a aceluiasi dosar e refuzata (DATA_DIR_LOCKED), din acelasi proces si din altul; dupa close se deschide; lacatul unui proces mort -> DATA_DIR_LOCK_STALE, preluat numai cu staleLock take', async () => {
const { s, dir, kms, trust } = await nouServiciu();
const o = { dataDir: path.join(dir, 'data'), kms, audience: AUD, adminPolicy: ADMIN_POLICY, trustDir: trust };
const lacat = path.join(dir, 'data', 'service.lock');
let e; try { await deschideSecrete(o); } catch (x) { e = x; }
egal(e && e.code, 'DATA_DIR_LOCKED', 'a doua deschidere in acelasi proces');
// alt proces, cu alt KMS: lacatul se judeca inaintea KMS-ului
const copil = path.join(tmpDir('copil'), 'deschide.mjs');
fs.writeFileSync(copil, `import { openSecrets } from ${JSON.stringify(new URL('../secrete.mjs', import.meta.url).href)};\nimport { openKms } from ${JSON.stringify(new URL('../../pq-kms/kms.mjs', import.meta.url).href)};\n`
+ `try { await openSecrets({ dataDir: process.argv[2], kms: openKms({ dataDir: process.argv[3], rootKey: '11'.repeat(32) }), audience: ${JSON.stringify(AUD)}, adminPolicy: ${JSON.stringify(ADMIN_POLICY)} }); console.log('DESCHIS'); } catch (e) { console.log(e.code); }\n`);
const alt = spawnSync(process.execPath, [copil, path.join(dir, 'data'), tmpDir('kmscopil')], { encoding: 'utf8' });
egal(alt.stdout.trim(), 'DATA_DIR_LOCKED', 'un alt proces: ' + alt.stderr);
s.close(); adevarat(!fs.existsSync(lacat), 'close scoate lacatul');
const s2 = await deschideSecrete(o); s2.close();
const mort = spawnSync(process.execPath, ['-e', 'process.stdout.write(String(process.pid))'], { encoding: 'utf8' }); const pid = Number(mort.stdout);
adevarat(Number.isSafeInteger(pid) && pid > 0, 'pid-ul unui proces terminat');
fs.writeFileSync(lacat, JSON.stringify({ pid, host: os.hostname(), since: iso(Date.now() - 60000) }));
e = null; try { await deschideSecrete(o); } catch (x) { e = x; }
egal(e && e.code, 'DATA_DIR_LOCK_STALE', 'lacatul unui proces mort'); adevarat(e.message.includes(String(pid)), 'il numeste: ' + e.message);
const s3 = await deschideSecrete({ ...o, staleLock: 'take' });
egal(s3.tookStaleLock && s3.tookStaleLock.pid, pid, 'preluat si spus'); egal(JSON.parse(fs.readFileSync(lacat, 'utf8')).pid, process.pid, 'lacatul e acum al acestui proces');
s3.close(); kms.close();
});
proba('JSON adanc: o legatura cu 30.000 de liste imbricate (biblioteca si HTTP) si un corp cu 25.000 de valori -> REQUEST_TOO_COMPLEX (request-form), auditat; un camp imbricat de 20 de niveluri trece', async () => {
const { s, kms } = await populat();
let adanc = []; for (let i = 0; i < 30000; i++) adanc = [adanc];
const n0 = jurnal(s.auditLog).length;
await refuza(() => s.get('db', { presentation: { v: 1, kind: 'aere-presentation', credential: {}, disclosures: [], delegations: [], binding: { nonce: s.nonce().nonce, adanc } } }), 'REQUEST_TOO_COMPLEX', 'binding adanc', { rule: 'request-form' });
egal(jurnal(s.auditLog).length, n0 + 1, 'refuzul e in jurnal'); egal(ultima(s).rules.join(','), 'request-form', 'regula numita'); egal(ultima(s).verdict, 'refused', 'refused');
await refuza(() => s.get('db', { presentation: prez(s, K.deployer, C.deployer), extra: Array.from({ length: 25000 }, () => 0) }), 'REQUEST_TOO_COMPLEX', '25.000 de valori', { rule: 'request-form' });
let douazeci = 'x'; for (let i = 0; i < 20; i++) douazeci = { a: douazeci };
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer), extra: douazeci })).value.toString(), VALOARE, 'un camp de 20 de niveluri, semnat in cerere, trece');
const server = createServer({ secrets: s, maxBody: 262144 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
try {
const n1 = jurnal(s.auditLog).length; const n = (await cerere(port, 'POST', '/v1/nonce')).json.nonce;
const corp = `{"presentation":{"v":1,"kind":"aere-presentation","credential":{},"disclosures":[],"delegations":[],"binding":{"nonce":"${n}","adanc":${'['.repeat(30000) + ']'.repeat(30000)}}}}`;
const r = await cerere(port, 'POST', '/v1/secrets/db/get', { body: corp });
egal(r.status, 400, 'HTTP 400: ' + r.text.slice(0, 120)); egal(r.json.error, 'REQUEST_TOO_COMPLEX', 'error'); egal(r.json.rule, 'request-form', 'rule');
egal(jurnal(s.auditLog).length, n1 + 1, 'si prin HTTP refuzul e in jurnal'); egal((await cerere(port, 'GET', '/v1/health')).status, 200, 'serverul viu');
} finally { await new Promise((q) => server.close(q)); s.close(); kms.close(); }
egal(MAX_JSON_DEPTH, 32, 'limita de adancime');
});
proba('nume si chei rezervate: con, nul, aux, prn, com0-9, lpt0-9 refuzate ca nume de secret (secret-name); __proto__, constructor, prototype refuzate ca nume de metadate (value-form)', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin);
for (const n of ['con', 'nul', 'aux', 'prn', 'com1', 'com0', 'lpt9']) await refuza(() => s.put(n, { presentation: a(), value: 'v' }), 'INVALID_SECRET_NAME', n, { rule: 'secret-name', mesaj: 'reserved by Windows' });
egal((await s.put('console', { presentation: a(), value: 'v' })).version, 1, 'console nu e rezervat');
adevarat(!['con', 'nul', 'aux'].some((n) => fs.existsSync(path.join(s.dataDir, 'secrets', n + '.json'))), 'niciun fisier rezervat');
for (const k of ['__proto__', 'constructor', 'prototype']) await refuza(() => s.put('meta', { presentation: a(), value: 'v', metadata: JSON.parse(`{"${k}":"x","a":1}`) }), 'INVALID_METADATA', k, { rule: 'value-form', mesaj: 'reserved' });
adevarat(!fs.existsSync(path.join(s.dataDir, 'secrets', 'meta.json')), 'niciun secret cu metadate rezervate');
s.close(); kms.close();
});
proba('mesajele nu reflecta intrarea neverificata: o versiune de 5000 de caractere -> VERSION_NOT_FOUND cu "(not an integer)", mesaj scurt; grant-ul cu id-ul admin e refuzat (rezervat politicii de admin)', async () => {
const { s, kms } = await populat();
const e = await refuza(() => s.get('db', { presentation: prez(s, K.auditor, C.auditor), version: 'X'.repeat(5000) }), 'VERSION_NOT_FOUND', 'versiunea uriasa', { rule: 'version-exists', mesaj: '(not an integer)' });
adevarat(e.message.length < 120 && !e.message.includes('XXXX'), 'mesajul are ' + e.message.length + ' caractere');
await refuza(() => s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'admin', operations: ['get'], policy: POL('fals-admin', 'reader') } }), 'INVALID_GRANT', 'grant admin', { rule: 'grant-form', mesaj: 'reserved' });
await refuza(() => s.put('x', { presentation: prez(s, K.admin, C.admin), value: 'v', grants: [{ id: 'admin', operations: ['get'], policy: POL('fa', 'reader') }] }), 'INVALID_GRANT', 'grant admin la creare', { rule: 'grant-form' });
s.close(); kms.close();
});
proba('plafonul grant-urilor: 33 la creare -> TOO_MANY_GRANTS (grant-limit), 32 trec si al 33-lea grant e refuzat; un get prin al 32-lea grant verifica prezentarea de doua ori (identitatea, o pereche emitent-varsta), nu de 34', async () => {
const { s, kms } = await populat();
const a = () => prez(s, K.admin, C.admin);
const multe = (n) => Array.from({ length: n }, (_, i) => ({ id: 'g' + i, operations: ['get'], policy: POL('p' + i, 'rol-' + i) }));
await refuza(() => s.put('greu', { presentation: a(), value: 'v', grants: multe(MAX_GRANTS + 1) }), 'TOO_MANY_GRANTS', '33 la creare', { rule: 'grant-limit' });
await s.put('greu', { presentation: a(), value: 'v', grants: [...multe(MAX_GRANTS - 1), { id: 'ultimul', operations: ['get'], policy: POL('ultimul', 'deployer') }] });
await refuza(() => s.grant('greu', { presentation: a(), grant: { id: 'al-33', operations: ['get'], policy: POL('x', 'reader') } }), 'TOO_MANY_GRANTS', 'al 33-lea', { rule: 'grant-limit' });
// numaratoarea verificarilor: fiecare verifyPresentation citeste o data semnatura prezentarii; marimea corpului o mai citeste o data
let citiri = 0; const p = new Proxy(prez(s, K.deployer, C.deployer), { get: (tinta, k) => { if (k === 'signature') citiri++; return tinta[k]; } });
egal((await s.get('greu', { presentation: p })).value.toString(), 'v', 'deployer-ul citeste prin ultimul grant');
egal(citiri, 3, 'citirile semnaturii (1 marimea corpului + 1 identitatea + 1 perechea emitent-varsta a celor 33 de candidati)');
s.close(); kms.close();
});
proba('nonce-uri pe origine: 32 in zbor de la o adresa, al 33-lea NONCE_ORIGIN_FULL (HTTP 429); dupa ce unul e prezentat, inca unul; alta adresa nu e atinsa', async () => {
const { s, kms } = await populat();
const n = []; for (let i = 0; i < NONCE_PER_ORIGIN; i++) n.push(s.nonce({ origin: '10.0.0.1' }).nonce);
let e; try { s.nonce({ origin: '10.0.0.1' }); } catch (x) { e = x; } egal(e && e.code, 'NONCE_ORIGIN_FULL', 'al 33-lea');
adevarat(s.nonce({ origin: '10.0.0.2' }).nonce && s.nonce().nonce, 'alta adresa si fara adresa: nonce');
egal((await s.get('db', { presentation: prez(s, K.deployer, C.deployer, { nonce: n[0] }) })).version, 1, 'unul prezentat');
adevarat(s.nonce({ origin: '10.0.0.1' }).nonce, 'dupa prezentare, inca unul');
const server = createServer({ secrets: s, maxBody: 65536 });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
try {
const st = []; for (let i = 0; i < NONCE_PER_ORIGIN + 1; i++) st.push((await cerere(port, 'POST', '/v1/nonce')).status);
egal(st.filter((x) => x === 200).length, NONCE_PER_ORIGIN, '32 de 200'); egal(st[NONCE_PER_ORIGIN], 429, 'al 33-lea: 429');
const r = await cerere(port, 'POST', '/v1/nonce'); egal(r.json.error, 'NONCE_ORIGIN_FULL', 'refuzul numit');
} finally { await new Promise((q) => server.close(q)); s.close(); kms.close(); }
});
proba('ordinea la mutatie: randul de audit e scris INAINTEA fisierului; o scriere a fisierului care cade dupa rand -> STORAGE_WRITE_FAILED, un rand allowed urmat de unul failed cu acelasi requestHash, fisierul neschimbat', async () => {
const dirK = tmpDir('kmss'); const baza = openKms({ dataDir: dirK, rootKey: rootHex() }); let sabotaj = null;
const kmsS = { getKey: (...x) => baza.getKey(...x), createKey: (...x) => baza.createKey(...x), decrypt: (...x) => baza.decrypt(...x),
datakey: (...x) => { const r = baza.datakey(...x); if (sabotaj) { const f = sabotaj; sabotaj = null; f(); } return r; } };
const { s, data } = await nouServiciu({ kms: kmsS });
try {
await s.put('db', { presentation: prez(s, K.admin, C.admin), value: VALOARE, grants: GRANTS() });
const f = path.join(data, 'secrets', 'db.json'); const inainte = fs.readFileSync(f); const n0 = jurnal(s.auditLog).length;
sabotaj = () => { fs.rmSync(f); fs.mkdirSync(f); }; // fisierul devine un dosar: redenumirea nu mai poate ajunge pe el
await refuza(() => s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) }), 'STORAGE_WRITE_FAILED', 'rotate cu fisierul blocat', { rule: 'storage' });
const j = jurnal(s.auditLog).slice(n0).map((e) => e.proof.statement);
egal(j.map((x) => x.operation + ':' + x.verdict).join(' '), 'rotate:allowed rotate:failed', 'randul inainte, apoi failed');
egal(j[1].requestHash, j[0].requestHash, 'acelasi requestHash'); egal(j[1].rules.join(','), 'storage', 'regula storage');
fs.rmSync(f, { recursive: true }); fs.writeFileSync(f, inainte);
adevarat(!fs.readdirSync(path.join(data, 'secrets')).some((x) => x.includes('.tmp-')), 'niciun temporar ramas');
egal((await s.rotate('db', { presentation: prez(s, K.rotator, C.rotator) })).version, 2, 'rotirea cazuta nu a ramas: urmatoarea e tot versiunea 2');
} finally { s.close(); baza.close(); }
});
proba('delegates: false: un grant care refuza delegatii admite detinatorul, nu si telefonul lui (regula delegates in jurnal)', async () => {
const { s, kms } = await populat();
await s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'numai-el', operations: ['rotate'], holders: [K.deployer.id], delegates: false, policy: POL('ne', 'deployer') } });
await refuza(() => s.rotate('db', { presentation: prez(s, K.phone, C.deployer, { delegations: [DELEGARE] }) }), 'UNAUTHORIZED', 'telefonul', { rule: 'authorization' });
adevarat(ultima(s).judged.some((g) => g.grant === 'numai-el' && g.rules.includes('delegates')), 'delegates in jurnal: ' + JSON.stringify(ultima(s).judged));
egal((await s.rotate('db', { presentation: prez(s, K.deployer, C.deployer) })).version, 2, 'detinatorul insusi roteste');
await refuza(() => s.grant('db', { presentation: prez(s, K.admin, C.admin), grant: { id: 'x', operations: ['get'], delegates: 'no', policy: POL('x', 'reader') } }), 'INVALID_GRANT', 'delegates care nu e boolean', { rule: 'grant-form' });
s.close(); kms.close();
});
proba('vocabularul plicului secret-access: buildProof refuza verdict banana, operatie fly, rules sir, version -5, holder oricine, judged fara forma; operatiile serviciului sunt exact vocabularul felului', async () => {
egal(OPERATII.join(','), PK.SECRET_ACCESS.operations.join(','), 'OPERATII == vocabularul felului');
const baza = { service: AUD, secret: 'db', version: 2, operation: 'get', verdict: 'allowed', rules: [], grant: 'deployers', holder: K.deployer.id, presenter: K.deployer.id, presentation: 'B', createdAt: iso(T0) };
egal(PK.buildProof(KIND, baza).statement.verdict, 'allowed', 'forma buna se construieste');
for (const [camp, v] of [['verdict', 'banana'], ['operation', 'fly'], ['rules', 'nu-e-lista'], ['version', -5], ['holder', 'oricine'], ['grant', 42], ['presenter', { x: 1 }], ['createdAt', 'ieri'], ['judged', [{ grant: 'g' }]], ['claimedHolder', 'aere-id:XYZ']]) {
let m = null; try { PK.buildProof(KIND, { ...baza, [camp]: v }); } catch (e) { m = e.message; }
adevarat(m && m.includes(`field "${camp}"`) && m.includes('vocabulary'), `${camp}=${JSON.stringify(v)}: ${m || 'construit'}`);
}
let m = null; try { PK.buildProof(KIND, { ...baza, requestHash: 'nu-e-hash' }); } catch (e) { m = e.message; }
adevarat(m && m.includes('requestHash'), 'requestHash care nu e un hash: ' + m);
});
proba('reguli: fiecare regula declarata are cel putin un /*PAZA:<regula>*/ in cod, si fiecare PAZA numeste o regula declarata', () => {
const src = fs.readFileSync(path.join(HERE, '..', 'secrete.mjs'), 'utf8');
const marcate = [...src.matchAll(/\/\*PAZA:([a-z-]+)\*\//g)].map((m) => m[1]);
const nedeclarate = marcate.filter((r) => !REGULI.includes(r));
egal(nedeclarate.join(','), '', 'PAZA cu regula nedeclarata');
const faraPaza = REGULI.filter((r) => !marcate.includes(r) && !['kms', 'storage', 'internal', 'secret-name', 'value-form', 'secret-exists'].includes(r));
egal(faraPaza.join(','), '', 'reguli declarate fara PAZA (in afara celor cu un singur loc de refuz, numite prin refuz())');
for (const r of REGULI) adevarat(src.includes(`refuz('${r}'`) || src.includes(`rule: '${r}'`) || marcate.includes(r), 'regula fara niciun refuz: ' + r);
});
// ================================================================================== rulare
async function main() {
const t0 = Date.now();
const rez = [];
for (const p of probe) {
const t = Date.now();
try { await p.fn(); rez.push({ nume: p.nume, ok: true, ms: Date.now() - t }); } catch (e) {
if (e instanceof Sarit) rez.push({ nume: p.nume, ok: true, sarit: true, ms: Date.now() - t, motiv: e.message });
else rez.push({ nume: p.nume, ok: false, ms: Date.now() - t, motiv: String((e && e.stack) || e).slice(0, 700), tip: e instanceof Esec ? 'afirmatie' : 'exceptie' });
}
}
for (const d of tmpDirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* dosar temporar; ramane in %TEMP% daca Windows il tine ocupat */ } }
const picate = rez.filter((r) => !r.ok); const sarite = rez.filter((r) => r.sarit);
for (const r of rez) process.stdout.write(`${r.sarit ? 'SARIT' : r.ok ? 'TRECE' : 'PICA '} ${r.nume}${r.ok && !r.sarit ? '' : `\n -> ${r.motiv}`}\n`);
process.stdout.write(`\nAere PQ Secrets: ${rez.length - picate.length - sarite.length}/${rez.length} cum trebuia, ${picate.length} picate, ${sarite.length} sarite, ${Date.now() - t0} ms\n`);
if (process.env.AERE_SECRETS_PROBA_JSON) {
fs.writeFileSync(process.env.AERE_SECRETS_PROBA_JSON, JSON.stringify({ completed: true, total: rez.length, passed: rez.length - picate.length, failed: picate.map((r) => r.nume), skipped: sarite.map((r) => r.nume), results: rez, node: process.versions.node, openssl: process.versions.openssl }, null, 2));
}
process.exitCode = picate.length ? 1 : sarite.length ? 2 : 0;
}
main().catch((e) => { process.stderr.write(`harnessul probei a cazut: ${(e && e.stack) || e}\n`); process.exitCode = 2; });