aere-quantum/identity/proba-sdjwt.mjs
Aere Network 5f1e60b8d2 identity: verify-sdjwt on the command line (an SD-JWT+KB checked with the issuer key you give; Aere Cloud runs it behind POST /v1/identity/sd-jwt/verify)
The issuer key (a public JWK, or the public keys of an AERE identity) is checked first: one that cannot be read, is private, or is not
a key for the algorithm asked exits 2 with the reason, instead of reporting the token INVALID. --json prints compact JSON (indented
output grew with the square of the claims' nesting depth). --at judges at a given time, as for verify. Test on the RFC 9901 example
presentation: valid at its own time, invalid now, a private key refused. Tests: SD-JWT 23/23, negative control 28/28; identity 44/44,
negative control 49/49.
2026-09-30 13:17:07 +03:00

212 lines
20 KiB
JavaScript

// Proba SD-JWT (sdjwt.mjs): intai pe vectorii STANDARDULUI (RFC 9901 s.5 si A.5, extrasi in fixturi-rfc9901.json: facuti de altii, cu
// cheia si semnaturile lor), apoi pe jetoane emise cu chei AERE (Ed25519 si ML-DSA-65), apoi fiecare atac ca proba numita. Offline.
// node proba-sdjwt.mjs iesire 0 = toate cum trebuia
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import os from 'node:os';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import * as I from './identity.mjs';
import * as J from './sdjwt.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const F = JSON.parse(fs.readFileSync(path.join(AICI, 'fixturi-rfc9901.json'), 'utf8'));
let treceri = 0; const esecuri = [];
function test(nume, fn) { try { fn(); treceri++; console.log(' OK ' + nume); } catch (e) { esecuri.push(nume); console.log(' RAU ' + nume + ' -- ' + (e.message || e)); } }
const cere = (c, m) => { if (!c) throw new Error(m); };
const sortat = (o) => (Array.isArray(o) ? o.map(sortat) : o && typeof o === 'object' ? Object.fromEntries(Object.keys(o).sort().map((k) => [k, sortat(o[k])])) : o);
const egal = (a, b) => JSON.stringify(sortat(a)) === JSON.stringify(sortat(b));
const b64u = (x) => Buffer.from(x).toString('base64url');
const KB = F.keyBindingPayload, LA_KB = new Date(KB.iat * 1000);
const refuz = (r, re) => !r.valid && re.test(r.reason);
// ---------------------------------------------------------------- vectorii RFC 9901
test('RFC 9901 s.5.1: fiecare din cele 10 dezvaluiri are digestul SHA-256 base64url scris in standard', () => {
for (const d of F.disclosures) cere(crypto.createHash('sha256').update(Buffer.from(d.disclosure, 'ascii')).digest('base64url') === d.digest, d.digest);
cere(F.disclosures.length === 10, String(F.disclosures.length));
});
test('RFC 9901 s.5.1: SD-JWT-ul standardului (ES256, cheia din A.5) verifica si se reface in afirmatiile de intrare, toate', () => {
const r = J.verifySdJwt(F.sdJwt, { issuerKey: F.issuerJwk, requireKeyBinding: false, now: new Date('2026-09-30T00:00:00Z') });
cere(r.valid, r.reason);
for (const [k, v] of Object.entries(F.inputClaims)) cere(egal(r.payload[k], v), 'afirmatia ' + k);
cere(r.payload.iss === 'https://issuer.example.com' && r.alg === 'ES256' && !('_sd' in r.payload) && !('_sd_alg' in r.payload), JSON.stringify(Object.keys(r.payload)));
});
test('RFC 9901 s.5.2: prezentarea standardului cu Key Binding JWT verifica (aud, nonce, iat, sd_hash, cheia din cnf) si da exact payload-ul procesat din standard', () => {
const r = J.verifySdJwt(F.presentation, { issuerKey: F.issuerJwk, audience: KB.aud, nonce: KB.nonce, now: LA_KB });
cere(r.valid, r.reason);
cere(egal(r.payload, F.processedPayload), JSON.stringify(r.payload));
cere(r.keyBinding && r.keyBinding.nonce === '1234567890' && r.keyBinding.aud === 'https://verifier.example.org' && r.keyBinding.alg === 'ES256', JSON.stringify(r.keyBinding));
});
test('RFC 9901: aceeasi prezentare pentru ALT verificator, cu ALT nonce, sau judecata la o ora dupa iat -> refuzata', () => {
cere(refuz(J.verifySdJwt(F.presentation, { issuerKey: F.issuerJwk, audience: 'https://other.example', nonce: KB.nonce, now: LA_KB }), /made for/), 'alt aud');
cere(refuz(J.verifySdJwt(F.presentation, { issuerKey: F.issuerJwk, audience: KB.aud, nonce: 'x', now: LA_KB }), /nonce/), 'alt nonce');
cere(refuz(J.verifySdJwt(F.presentation, { issuerKey: F.issuerJwk, audience: KB.aud, nonce: KB.nonce, now: new Date((KB.iat + 3600) * 1000) }), /outside 300 s/), 'veche');
});
test('RFC 9901: SD-JWT-ul standardului cu ALTA cheie de emitent (P-256 a noastra) -> refuzat; fara Key Binding cand e cerut -> refuzat', () => {
const alta = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).publicKey.export({ format: 'jwk' });
cere(refuz(J.verifySdJwt(F.presentation, { issuerKey: alta, audience: KB.aud, nonce: KB.nonce, now: LA_KB }), /does not verify/), 'alta cheie');
cere(refuz(J.verifySdJwt(F.sdJwt, { issuerKey: F.issuerJwk, audience: KB.aud, nonce: KB.nonce, now: LA_KB }), /key binding is required/), 'fara KB');
});
test('RFC 9901: o dezvaluire scoasa din prezentare dupa semnarea KB (sd_hash) sau una adaugata -> refuzata', () => {
const p = F.presentation.split('~'); const kb = p.pop();
const scoasa = [...p.slice(0, 2), ...p.slice(3), kb].join('~');
cere(refuz(J.verifySdJwt(scoasa, { issuerKey: F.issuerJwk, audience: KB.aud, nonce: KB.nonce, now: LA_KB }), /sd_hash/), 'scoasa: ' + J.verifySdJwt(scoasa, { issuerKey: F.issuerJwk, now: LA_KB }).reason);
const adaugata = [...p, F.disclosures[2].disclosure, kb].join('~');
cere(refuz(J.verifySdJwt(adaugata, { issuerKey: F.issuerJwk, audience: KB.aud, nonce: KB.nonce, now: LA_KB }), /sd_hash/), 'adaugata');
});
// ---------------------------------------------------------------- jetoane emise cu chei AERE
const NOW = Math.floor(Date.parse('2026-09-30T08:00:00Z') / 1000), LA = new Date(NOW * 1000);
const iss = I.generateKeys(), hol = I.generateKeys(), strain = I.generateKeys();
const CL = { given_name: 'Ana', family_name: 'Pop', age_over_18: true, nationalities: ['RO', 'DE'], employer: 'Example Ltd' };
const emite = (o = {}) => J.issueSdJwt({ issuer: iss, holder: hol, claims: CL, disclosable: ['given_name', 'family_name', 'age_over_18'], arrayDisclosable: ['nationalities'], iss: 'https://issuer.aere.example', iat: NOW, exp: NOW + 86400, decoys: 2, ...o });
const AUD = 'https://verifier.example', NONCE = 'n-' + crypto.randomBytes(4).toString('hex');
const arata = (sd, reveal, o = {}) => J.presentSdJwt({ sdJwt: sd, reveal, holder: hol, audience: AUD, nonce: NONCE, iat: NOW, ...o });
const judeca = (p, o = {}) => J.verifySdJwt(p, { issuerKey: iss, audience: AUD, nonce: NONCE, now: LA, ...o });
const E = emite();
test('AERE -> SD-JWT (EdDSA, cheia Ed25519 a emitentului AERE): detinatorul arata age_over_18 si RO, verificatorul vede numai atat plus afirmatiile in clar', () => {
const r = judeca(arata(E.sdJwt, ['age_over_18', { element: 'RO', in: 'nationalities' }]));
cere(r.valid, r.reason);
cere(r.payload.age_over_18 === true && !('given_name' in r.payload) && egal(r.payload.nationalities, ['RO']) && r.payload.employer === 'Example Ltd' && r.alg === 'EdDSA', JSON.stringify(r.payload));
});
test('AERE -> SD-JWT in forma standardului: typ explicit, _sd sortat (cu momeli), _sd_alg sha-256, cnf.jwk = cheia Ed25519 a detinatorului, dezvaluiri [sare, nume, valoare]', () => {
const [h, p] = E.sdJwt.split('~')[0].split('.').slice(0, 2).map((x) => JSON.parse(Buffer.from(x, 'base64url').toString('utf8')));
cere(h.alg === 'EdDSA' && h.typ === J.TYP, JSON.stringify(h));
cere(Array.isArray(p._sd) && p._sd.length === 5 && egal([...p._sd].sort(), p._sd) && p._sd_alg === 'sha-256', JSON.stringify(p._sd));
cere(p.cnf.jwk.kty === 'OKP' && p.cnf.jwk.crv === 'Ed25519' && p.cnf.jwk.x === J.publicJwk(hol).x && !('d' in p.cnf.jwk), JSON.stringify(p.cnf));
const c = JSON.parse(Buffer.from(E.disclosures[0].disclosure, 'base64url').toString('utf8'));
cere(c.length === 3 && typeof c[0] === 'string' && c[0].length >= 22 && c[1] === 'given_name', JSON.stringify(c));
});
test('AERE -> SD-JWT post-cuantic (ML-DSA-65, cheia AKP): verifica cu cheia ML-DSA-65 a emitentului, nu cu cea Ed25519; detinatorul poate semna KB tot cu ML-DSA-65', () => {
const M = emite({ alg: 'ML-DSA-65', holderAlg: 'ML-DSA-65' });
const p = arata(M.sdJwt, ['age_over_18'], { holderAlg: 'ML-DSA-65' });
const r = judeca(p, { issuerAlg: 'ML-DSA-65' }); cere(r.valid && r.alg === 'ML-DSA-65' && r.keyBinding.alg === 'ML-DSA-65', r.reason);
cere(refuz(judeca(p, { issuerAlg: 'EdDSA' }), /does not match/), 'cheia Ed25519 a primit un jeton ML-DSA-65');
});
// ---------------------------------------------------------------- atacuri
const jwtDin = (antet, corp, cheie) => { const cap = b64u(JSON.stringify(antet)) + '.' + b64u(JSON.stringify(corp)); return cap + '.' + b64u(crypto.sign(null, Buffer.from(cap), cheie)); };
const corpDe = (sd) => JSON.parse(Buffer.from(sd.split('~')[0].split('.')[1], 'base64url').toString('utf8'));
test('ATAC: alg none si alg ES256 cu cheie Ed25519 (confuzia algoritmului) -> refuzate', () => {
const corp = corpDe(E.sdJwt);
const none = b64u(JSON.stringify({ alg: 'none', typ: J.TYP })) + '.' + b64u(JSON.stringify(corp)) + '.';
cere(refuz(J.verifySdJwt(none + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /not accepted/), 'none a trecut');
const conf = jwtDin({ alg: 'ES256', typ: J.TYP }, corp, iss.privat.ed25519);
cere(refuz(J.verifySdJwt(conf + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /does not match/), 'confuzia a trecut');
});
test('ATAC: o dezvaluire cu valoarea schimbata (alt digest, nelegata) -> refuzata; aceeasi dezvaluire de doua ori -> refuzata', () => {
const p = arata(E.sdJwt, ['given_name']).split('~');
const c = JSON.parse(Buffer.from(p[1], 'base64url').toString('utf8')); c[2] = 'Maria';
const schimbat = [p[0], b64u(JSON.stringify(c)), ''].join('~');
cere(refuz(J.verifySdJwt(schimbat, { issuerKey: iss, requireKeyBinding: false, now: LA }), /not referenced/), 'valoarea schimbata a trecut');
const dublu = [p[0], p[1], p[1], ''].join('~');
cere(refuz(J.verifySdJwt(dublu, { issuerKey: iss, requireKeyBinding: false, now: LA }), /twice/), 'dublura a trecut');
});
test('ATAC: un emitent care pune in _sd o dezvaluire cu numele "_sd", una care acopera o afirmatie in clar, sau acelasi digest de doua ori -> refuzat', () => {
const faci = (dezvaluiri, extra = {}) => { const ds = dezvaluiri.map((x) => b64u(JSON.stringify(x))); const sd = ds.map((d) => crypto.createHash('sha256').update(d).digest('base64url'));
return jwtDin({ alg: 'EdDSA', typ: J.TYP }, { _sd: extra.dublu ? [...sd, sd[0]] : sd, _sd_alg: 'sha-256', iss: 'x', ...(extra.clar || {}) }, iss.privat.ed25519) + '~' + ds.map((d) => d + '~').join(''); };
cere(refuz(J.verifySdJwt(faci([['s1', '_sd', ['x']]]), { issuerKey: iss, requireKeyBinding: false, now: LA }), /may not name _sd/), '_sd a trecut');
cere(refuz(J.verifySdJwt(faci([['s1', 'role', 'admin']], { clar: { role: 'user' } }), { issuerKey: iss, requireKeyBinding: false, now: LA }), /already exists/), 'coliziunea a trecut');
cere(refuz(J.verifySdJwt(faci([['s1', 'a', 1]], { dublu: true }), { issuerKey: iss, requireKeyBinding: false, now: LA }), /more than once/), 'digestul dublu a trecut');
cere(refuz(J.verifySdJwt(faci([['s1', '__proto__', { admin: true }]]), { issuerKey: iss, requireKeyBinding: false, now: LA }), /may not name __proto__/), '__proto__ a trecut');
});
test('ATAC: o afirmatie numita __proto__ in clar sau intr-o valoare dezvaluita (prototipul payload-ului schimbat) -> refuzata', () => {
const cap = (corp) => jwtDin({ alg: 'EdDSA', typ: J.TYP }, corp, iss.privat.ed25519);
const clar = JSON.parse('{"iss":"x","_sd_alg":"sha-256","__proto__":{"admin":true}}');
cere(refuz(J.verifySdJwt(cap(clar) + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /__proto__/), 'in clar a trecut');
const d = b64u('["s1","profil",' + '{"__proto__":{"admin":true}}' + ']');
const sd = crypto.createHash('sha256').update(d).digest('base64url');
cere(refuz(J.verifySdJwt(cap({ _sd: [sd], _sd_alg: 'sha-256', iss: 'x' }) + '~' + d + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /__proto__/), 'in valoarea dezvaluita a trecut');
});
test('ATAC: KB semnat de alta cheie decat cnf.jwk, KB cu typ gresit, KB fara nonce -> refuzate', () => {
const alt = J.presentSdJwt({ sdJwt: E.sdJwt, reveal: ['age_over_18'], holder: strain, audience: AUD, nonce: NONCE, iat: NOW });
cere(refuz(judeca(alt), /key binding: the signature does not verify/), 'cheia straina a trecut');
const baza = arata(E.sdJwt, ['age_over_18']).split('~').slice(0, -1).join('~') + '~';
const sh = crypto.createHash('sha256').update(Buffer.from(baza, 'ascii')).digest('base64url');
const tip = jwtDin({ alg: 'EdDSA', typ: 'JWT' }, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sh }, hol.privat.ed25519);
cere(refuz(judeca(baza + tip), /typ/), 'typ gresit a trecut');
const faraNonce = jwtDin({ alg: 'EdDSA', typ: 'kb+jwt' }, { iat: NOW, aud: AUD, sd_hash: sh }, hol.privat.ed25519);
cere(refuz(judeca(baza + faraNonce), /required/), 'fara nonce a trecut');
});
test('timpul: exp trecut -> refuzat (fara toleranta); nbf peste 30 s -> admis (toleranta de ceas), peste 90 s -> refuzat', () => {
cere(refuz(judeca(arata(emite({ exp: NOW - 1 }).sdJwt, ['age_over_18'])), /expired/), 'expirat a trecut');
cere(judeca(arata(emite({ nbf: NOW + 30 }).sdJwt, ['age_over_18'])).valid, 'nbf +30 s refuzat');
cere(refuz(judeca(arata(emite({ nbf: NOW + 90 }).sdJwt, ['age_over_18'])), /not valid before/), 'nbf +90 s a trecut');
});
// revizuirea din 30 sept: fara public si nonce, o prezentare facuta pentru altul trecea (numai un steag spunea ca nu s-au comparat)
test('REVIZUIRE: fara publicul si nonce-ul verificatorului, o prezentare cu Key Binding NU se judeca (RFC 9901 s.7.3)', () => {
cere(refuz(J.verifySdJwt(arata(E.sdJwt, ['age_over_18']), { issuerKey: iss, now: LA }), /must give its audience and nonce/), 'fara aud/nonce a trecut');
cere(refuz(J.verifySdJwt(arata(E.sdJwt, ['age_over_18']), { issuerKey: iss, audience: AUD, now: LA }), /must give its audience and nonce/), 'fara nonce a trecut');
});
test('REVIZUIRE: emitentul refuza structura SD-JWT venita in valori (_sd imbricat, _sd de sus, element {"...": d}) - altfel detinatorul dezvaluie ce emitentul n-a vazut', () => {
const h = crypto.createHash('sha256').update(b64u(JSON.stringify(['s', 'country', 'US']))).digest('base64url');
const incearca = (claims) => { try { J.issueSdJwt({ issuer: iss, holder: hol, claims, iss: 'x', iat: NOW }); return null; } catch (e) { return e.message; } };
cere(/SD-JWT structure/.test(incearca({ address: { street: 'a', _sd: [h] } }) || ''), '_sd imbricat emis');
cere(/SD-JWT structure/.test(incearca({ name: 'x', _sd: [h] }) || ''), '_sd de sus emis');
cere(/SD-JWT structure/.test(incearca({ roles: ['user', { '...': h }] }) || ''), 'element ... emis');
cere(incearca({ roles: ['user', { note: '...' }] }) === null, 'o valoare obisnuita refuzata');
});
test('REVIZUIRE: prezentarea arata elementul NUMAI din lista numita (acelasi RO in doua liste), iar o alegere fara corespondent e o eroare', () => {
const D = J.issueSdJwt({ issuer: iss, holder: hol, claims: { nationalities: ['RO', 'DE'], criminal_record_countries: ['RO', 'FR'] }, arrayDisclosable: ['nationalities', 'criminal_record_countries'], iss: 'x', iat: NOW });
const r = judeca(arata(D.sdJwt, [{ element: 'RO', in: 'nationalities' }]));
cere(r.valid && egal(r.payload.nationalities, ['RO']) && egal(r.payload.criminal_record_countries, []), JSON.stringify(r.payload));
let m = null; try { arata(D.sdJwt, [{ element: 'IT', in: 'nationalities' }]); } catch (e) { m = e.message; } cere(/nothing to reveal/.test(m || ''), 'alegerea fara corespondent a tacut');
});
test('REVIZUIRE: cheia emitentului data ca KeyObject public sau ca identitate AERE publica (keys.public) e primita; detinatorul dat ca cheie publica', () => {
const pubKO = crypto.createPublicKey(iss.privat.ed25519);
const D = J.issueSdJwt({ issuer: iss, holder: crypto.createPublicKey(hol.privat.ed25519), claims: { age_over_18: true }, disclosable: ['age_over_18'], iss: 'x', iat: NOW });
const p = arata(D.sdJwt, ['age_over_18']);
cere(J.verifySdJwt(p, { issuerKey: pubKO, audience: AUD, nonce: NONCE, now: LA }).valid, 'KeyObject public refuzat');
cere(J.verifySdJwt(p, { issuerKey: iss.public, audience: AUD, nonce: NONCE, now: LA }).valid, 'identitatea AERE publica refuzata');
});
test('REVIZUIRE: exp cerut implicit (un exp dezvaluibil si retinut de detinator nu mai lasa jetonul fara expirare); requireExp:false il lasa', () => {
const dExp = b64u(JSON.stringify(['s', 'exp', NOW - 10]));
const corp = { _sd: [crypto.createHash('sha256').update(dExp).digest('base64url')], _sd_alg: 'sha-256', iss: 'x' };
const jwt = jwtDin({ alg: 'EdDSA', typ: J.TYP }, corp, iss.privat.ed25519);
cere(refuz(J.verifySdJwt(jwt + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /exp is required/), 'fara exp a trecut');
cere(refuz(J.verifySdJwt(jwt + '~' + dExp + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /expired/), 'exp dezvaluit si trecut a trecut');
cere(J.verifySdJwt(jwt + '~', { issuerKey: iss, requireKeyBinding: false, requireExp: false, now: LA }).valid, 'requireExp:false a refuzat');
});
test('REVIZUIRE: tipul jetonului: un KB-JWT sau un JWT cu typ JWT dat drept jeton de emitent -> refuzat implicit; expectedIssuer altul -> refuzat', () => {
const corp = { iss: 'https://issuer.aere.example', exp: NOW + 100, _sd_alg: 'sha-256' };
cere(refuz(J.verifySdJwt(jwtDin({ alg: 'EdDSA', typ: 'kb+jwt' }, corp, iss.privat.ed25519) + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /typ/), 'kb+jwt drept emitent a trecut');
const simplu = jwtDin({ alg: 'EdDSA', typ: 'JWT' }, corp, iss.privat.ed25519) + '~';
cere(refuz(J.verifySdJwt(simplu, { issuerKey: iss, requireKeyBinding: false, now: LA }), /typ/), 'typ JWT a trecut implicit');
cere(J.verifySdJwt(simplu, { issuerKey: iss, requireKeyBinding: false, expectedTyp: null, now: LA }).valid, 'expectedTyp:null a refuzat un JWT simplu');
const bun = J.verifySdJwt(arata(E.sdJwt, ['age_over_18']), { issuerKey: iss, audience: AUD, nonce: NONCE, now: LA, expectedIssuer: 'https://issuer.aere.example' });
cere(bun.valid && bun.issuerChecked, 'emitentul asteptat refuzat');
cere(refuz(J.verifySdJwt(arata(E.sdJwt, ['age_over_18']), { issuerKey: iss, audience: AUD, nonce: NONCE, now: LA, expectedIssuer: 'https://altul.example' }), /issued by/), 'alt emitent a trecut');
});
test('REVIZUIRE: crit (orice forma), base64url necanonic al semnaturii, KB expirat -> refuzate; _sd_alg imbricat ramane afirmatie', () => {
const corp = { iss: 'x', exp: NOW + 100, _sd_alg: 'sha-256', meta: { _sd_alg: 'x', note: 1 } };
for (const crit of [[], 'b64', ['b64']]) cere(refuz(J.verifySdJwt(jwtDin({ alg: 'EdDSA', typ: J.TYP, crit, b64: false }, corp, iss.privat.ed25519) + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /crit/), 'crit ' + JSON.stringify(crit));
const j = jwtDin({ alg: 'EdDSA', typ: J.TYP }, corp, iss.privat.ed25519);
const r = J.verifySdJwt(j + '~', { issuerKey: iss, requireKeyBinding: false, now: LA });
cere(r.valid && r.payload.meta._sd_alg === 'x' && !('_sd_alg' in r.payload), '_sd_alg imbricat: ' + JSON.stringify(r.payload));
const ALF = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
const u = j[j.length - 1], alt = ALF[ALF.indexOf(u) ^ 1];
const nec = j.slice(0, -1) + alt;
cere(Buffer.from(nec.split('.')[2], 'base64url').equals(Buffer.from(j.split('.')[2], 'base64url')), 'proba: scrierea alternativa trebuie sa dea aceiasi octeti');
cere(refuz(J.verifySdJwt(nec + '~', { issuerKey: iss, requireKeyBinding: false, now: LA }), /canonical/), 'semnatura necanonica a trecut');
const baza = arata(E.sdJwt, ['age_over_18']).split('~').slice(0, -1).join('~') + '~';
const sh = crypto.createHash('sha256').update(Buffer.from(baza, 'ascii')).digest('base64url');
const kbExp = jwtDin({ alg: 'EdDSA', typ: 'kb+jwt' }, { iat: NOW, aud: AUD, nonce: NONCE, sd_hash: sh, exp: NOW - 100 }, hol.privat.ed25519);
cere(refuz(judeca(baza + kbExp), /key binding: expired/), 'KB expirat a trecut');
});
test('linia de comanda: verify-sdjwt pe prezentarea RFC 9901 s.5.2 -> 0 VALID la momentul ei (--at), 1 INVALID acum (KB vechi); o cheie privata -> 2', () => {
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-sdjwt-cli-'));
try {
fs.writeFileSync(path.join(T, 'p.txt'), F.presentation); fs.writeFileSync(path.join(T, 'k.json'), JSON.stringify(F.issuerJwk));
const run = (...a) => spawnSync(process.execPath, [path.join(AICI, 'identity-cli.mjs'), 'verify-sdjwt', '--sd-jwt', 'p.txt', '--issuer-key', 'k.json', '--issuer-alg', 'ES256', '--audience', KB.aud, '--nonce', KB.nonce, ...a], { cwd: T, encoding: 'utf8' });
const a = run('--at', new Date(KB.iat * 1000).toISOString(), '--json'); cere(a.status === 0 && JSON.parse(a.stdout).valid === true, 'la iat: ' + a.status + ' ' + a.stderr);
const b = run(); cere(b.status === 1 && /outside 300 s/.test(b.stdout), 'acum: ' + b.status + ' ' + b.stdout);
fs.writeFileSync(path.join(T, 'k.json'), JSON.stringify(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey.export({ format: 'jwk' })));
const c = run(); cere(c.status === 2 && /private JWK/.test(c.stderr), 'cheia privata: ' + c.status + ' ' + c.stderr);
} finally { fs.rmSync(T, { recursive: true, force: true }); }
});
console.log(`\naere-sd-jwt: ${treceri}/${treceri + esecuri.length} cum trebuia`);
process.exitCode = esecuri.length ? 1 : 0;