// AERE Identity, SD-JWT (IETF RFC 9901, noiembrie 2025): acelasi emitent si acelasi detinator ca in identity.mjs, in formatul standardului // (roadmap master punctul 12, "interoperare SD-JWT"; pista B, 2026-09-30). Judecat pe vectorii standardului (RFC 9901 s.5 si A.5) si, din // 2026-10-18, contra @sd-jwt/core in ambele sensuri (interop/interop-sdjwt.mjs; fixturile lor, judecate offline, in proba-sdjwt.mjs). // // CE FACE: // issueSdJwt emitentul face un SD-JWT in serializarea compacta: JWT semnat de emitent, cu digesturile afirmatiilor dezvaluibile in `_sd` // (si `...` pentru elemente de lista), `_sd_alg` sha-256, cheia detinatorului in `cnf.jwk`, `exp`, plus dezvaluirile, unite prin `~` // presentSdJwt detinatorul alege dezvaluirile (afirmatii de sus dupa nume, elemente dupa lista LOR si valoare) si adauga un Key Binding // JWT (typ kb+jwt, aud, nonce, iat, sd_hash) semnat cu cheia lui // verifySdJwt verificatorul face pasii din RFC 9901 s.7.1 si s.7.3 cu cheia EMITENTULUI data de el (niciodata din jeton), cu publicul si // nonce-ul lui CERUTE cand exista (sau se cere) Key Binding, cu `exp` cerut, cu tipul explicit al jetonului verificat // // ALGORITMII (JWS): ES256 (P-256, cel din exemplele RFC 9901), EdDSA / Ed25519 (RFC 8037) si ML-DSA-65 (post-cuantic; numele si forma // cheii JWK "AKP" din RFC 9964, mai 2026). Un emitent AERE semneaza cu cheia lui Ed25519 (algoritm cunoscut tuturor bibliotecilor JOSE) // sau cu cheia ML-DSA-65 (masurat 2026-10-18: jose 6.2.12 pe Node 24 o semneaza si o verifica); o semnatura hibrida in acelasi jeton ar // cere serializarea JSON generala a JWS si nu e facuta aici. // // Revizuirea adversariala din 2026-09-30 (inainte de publicare) a gasit si s-au reparat: emitentul semna structura SD-JWT venita in valorile // afirmatiilor (`_sd`, `{"...": d}`), deci detinatorul putea dezvalui afirmatii pe care emitentul nu le-a vazut; prezentarea arata un element // cu aceeasi valoare din ORICE lista; publicul si nonce-ul nu erau cerute implicit; `exp` nu se putea cere; tipul jetonului nu se verifica // implicit (un KB-JWT trecea drept jeton de emitent); o cheie publica Node nu era primita; exp/nbf ale KB, `crit` stricat, base64url // necanonic si `_sd_alg` imbricat. Fiecare are proba lui in proba-sdjwt.mjs si plantarea lui in control-negativ-sdjwt.mjs. // // REGULILE NUMITE (2026-10-18, pentru suita de conformitate tools/aere-conformance): fiecare refuz al lui verifySdJwt numeste regula lui // (`rule`), din lista REGULI; fiecare verificare a unei reguli poarta marcajul /*PAZA:*/ pe conditia ei BUNA, pe care controlul // negativ al suitei o face adevarata intr-o copie. Verificatorul se opreste la primul refuz (un SD-JWT invalid nu se mai citeste mai departe). // // CE NU FACE: nu e SD-JWT VC (profilul e sdjwt-vc.mjs); nu citeste liste de stare in jeton (sdjwt-vc.mjs citeste Token Status List); nu // dezvaluie recursiv la emitere (verificarea stie dezvaluiri recursive). import crypto from 'node:crypto'; const b64u = (b) => Buffer.from(b).toString('base64url'); export const ALGS = ['ES256', 'EdDSA', 'Ed25519', 'ML-DSA-65']; export const TYP = 'aere+sd-jwt'; const AERE_ALG = 'ed25519+ml-dsa-65'; /** regulile pe care le poate numi un refuz al lui verifySdJwt (ordinea e cea a verificarii; `form` si pentru ceasul verificatorului) */ export const REGULI = ['form', 'alg', 'typ', 'crit', 'key', 'signature', 'issuer', 'sd-alg', 'disclosure-form', 'disclosure-twice', 'digest-twice', 'disclosure-name', 'claim-conflict', 'unreferenced', 'time', 'kb-context', 'kb-required', 'kb-cnf', 'kb-claims', 'kb-audience', 'kb-nonce', 'kb-freshness', 'kb-sd-hash']; class Refuz extends Error { constructor(rule, message) { super(message); this.rule = rule; } } const nu = (rule, msg) => { if (!REGULI.includes(rule)) throw new Error('sd-jwt: undeclared rule ' + rule); return new Refuz(rule, 'sd-jwt: ' + msg); }; // base64url CANONIC: decodat si recodat trebuie sa dea acelasi text (altfel aceeasi semnatura are mai multe scrieri, si un depozit de // reluari care tine sirul jetonului e ocolit schimbatnd bitii nefolositi ai ultimului caracter) const dinB64u = (s, ce) => { if (!(typeof s === 'string' && /^[A-Za-z0-9_-]*$/.test(s) /*PAZA:form*/)) throw nu('form', `${ce} is not base64url`); const b = Buffer.from(s, 'base64url'); if (!(b.toString('base64url') === s /*PAZA:form*/)) throw nu('form', `${ce} is not canonical base64url`); return b; }; const jsonB64u = (s, ce) => { let o; try { o = JSON.parse(dinB64u(s, ce).toString('utf8')); } catch (e) { throw e instanceof Refuz ? e : nu('form', `${ce} is not base64url JSON`); } return o; }; const sha256b64u = (s) => crypto.createHash('sha256').update(Buffer.from(s, 'ascii')).digest('base64url'); const obiect = (x) => x !== null && typeof x === 'object' && !Array.isArray(x); // ---------------------------------------------------------------- chei JWK si semnaturi JWS /** * Cheia PUBLICA ca JWK, din: o pereche de chei AERE (identity.mjs; `privat`), cheile publice ale unei identitati AERE ({alg, ed25519, * mldsa65}), un KeyObject public sau privat, sau un JWK public. `alg` alege cheia AERE: 'EdDSA' (Ed25519) sau 'ML-DSA-65'. */ export function publicJwk(k, alg = 'EdDSA') { const ml = alg === 'ML-DSA-65'; if (k && k.privat) k = ml ? k.privat.mldsa65 : k.privat.ed25519; else if (obiect(k) && k.alg === AERE_ALG && typeof k.ed25519 === 'string') k = crypto.createPublicKey({ key: Buffer.from(ml ? k.mldsa65 : k.ed25519, 'base64'), format: 'der', type: 'spki' }); if (k instanceof crypto.KeyObject) return (k.type === 'public' ? k : crypto.createPublicKey(k)).export({ format: 'jwk' }); if (obiect(k) && k.kty) return k; throw nu('key', 'not a key (a JWK, a Node key object, or AERE identity keys)'); } function cheiePublica(jwk) { if (!(obiect(jwk) && ['EC', 'OKP', 'AKP'].includes(jwk.kty) /*PAZA:key*/)) throw nu('key', 'the key is not an EC, OKP or AKP JWK'); if (!(!('d' in jwk) && !('priv' in jwk) /*PAZA:key*/)) throw nu('key', 'a private JWK was given where a public key belongs'); try { return crypto.createPublicKey({ key: jwk, format: 'jwk' }); } catch (e) { throw nu('key', 'the key cannot be read: ' + String(e.message || e).slice(0, 120)); } } // algoritmul trebuie sa fie al cheii: un jeton nu alege el cu ce se verifica (atacul de confuzie a algoritmului, RFC 8725 s.2.1) function potriveste(alg, key) { const t = key.asymmetricKeyType; if (alg === 'ES256') return t === 'ec' && key.asymmetricKeyDetails && key.asymmetricKeyDetails.namedCurve === 'prime256v1'; if (alg === 'EdDSA' || alg === 'Ed25519') return t === 'ed25519'; if (alg === 'ML-DSA-65') return t === 'ml-dsa-65'; return false; } // tipul: un sir exact; sau `undefined` = orice tip explicit de SD-JWT ("...+sd-jwt" sau "sd-jwt"); sau null = orice, in afara de kb+jwt. // RFC 7515 s.4.1.9: un typ fara "/" se citeste ca si cum ar avea prefixul "application/", deci "application/dc+sd-jwt" si "dc+sd-jwt" sunt // acelasi tip (2026-10-18, la interoperare: bibliotecile JOSE scriu oricare din cele doua forme) const faraPrefix = (t) => (typeof t === 'string' && t.startsWith('application/') && !t.slice(12).includes('/') ? t.slice(12) : t); function tipBun(typ, cerut) { const t = faraPrefix(typ); if (cerut === undefined) return typeof t === 'string' && /(^|\+)sd-jwt$/.test(t); if (cerut === null) return t !== 'kb+jwt'; return t === faraPrefix(cerut); } function verificaJws(jws, jwk, { typ } = {}) { const parti = String(jws).split('.'); if (!(parti.length === 3 /*PAZA:form*/)) throw nu('form', 'a JWS has three parts'); const antet = jsonB64u(parti[0], 'the JWS header'), corp = jsonB64u(parti[1], 'the JWS payload'); if (!(obiect(antet) && obiect(corp) /*PAZA:form*/)) throw nu('form', 'the JWS header and payload must be objects'); if (!(ALGS.includes(antet.alg) /*PAZA:alg*/)) throw nu('alg', `alg ${JSON.stringify(antet.alg)} is not accepted (accepted: ${ALGS.join(', ')}; never none)`); if (!(tipBun(antet.typ, typ) /*PAZA:typ*/)) throw nu('typ', `typ ${JSON.stringify(antet.typ)} is not the type expected (${typ === undefined ? 'an explicit ...+sd-jwt' : typ === null ? 'anything but kb+jwt' : typ})`); // `crit` numeste parametri pe care verificatorul TREBUIE sa-i inteleaga; nu intelegem niciunul, deci orice `crit` (bun sau stricat) e refuz if (!(!Object.hasOwn(antet, 'crit') /*PAZA:crit*/)) throw nu('crit', 'critical header parameters (crit) are not understood'); const key = cheiePublica(jwk); if (!(potriveste(antet.alg, key) /*PAZA:key*/)) throw nu('key', `alg ${antet.alg} does not match the ${key.asymmetricKeyType} key given`); const date = Buffer.from(parti[0] + '.' + parti[1], 'ascii'), sig = dinB64u(parti[2], 'the JWS signature'); let ok = false; try { ok = antet.alg === 'ES256' ? sig.length === 64 && crypto.verify('sha256', date, { key, dsaEncoding: 'ieee-p1363' }, sig) : crypto.verify(null, date, key, sig); } catch { ok = false; } if (!(ok /*PAZA:signature*/)) throw nu('signature', 'the signature does not verify with the key given'); return { antet, corp }; } function semneazaJws(antet, corp, priv) { const cap = b64u(JSON.stringify(antet)) + '.' + b64u(JSON.stringify(corp)); const date = Buffer.from(cap, 'ascii'); const sig = antet.alg === 'ES256' ? crypto.sign('sha256', date, { key: priv, dsaEncoding: 'ieee-p1363' }) : crypto.sign(null, date, priv); return cap + '.' + b64u(sig); } const cheiePrivata = (k, alg) => (k && k.privat ? (alg === 'ML-DSA-65' ? k.privat.mldsa65 : k.privat.ed25519) : k); // pentru profilurile de deasupra (sdjwt-vc.mjs: Status List Token), ca semnatura JWS sa se faca intr-un singur loc export { verificaJws as verifyJws, semneazaJws as signJws, cheiePrivata as privateKeyOf }; // structura SD-JWT intr-o valoare: o cheie `_sd`/`_sd_alg` intr-un obiect sau un element {"...": x} intr-o lista, oriunde in adancime function areStructuraSd(v) { const stiva = [v]; while (stiva.length) { const x = stiva.pop(); if (Array.isArray(x)) { for (const e of x) { if (obiect(e) && Object.hasOwn(e, '...')) return true; stiva.push(e); } } else if (obiect(x)) { if (Object.hasOwn(x, '_sd') || Object.hasOwn(x, '_sd_alg') || Object.hasOwn(x, '__proto__')) return true; for (const k of Object.keys(x)) stiva.push(x[k]); } } return false; } // ---------------------------------------------------------------- emitere si prezentare /** * @param {{issuer, claims:object, disclosable?:string[], arrayDisclosable?:string[], holder, iss:string, iat?:number, exp?:number|null, nbf?:number, * alg?:'EdDSA'|'ML-DSA-65'|'ES256', holderAlg?:'EdDSA'|'ML-DSA-65'|'ES256', decoys?:number, typ?:string}} o * issuer: chei AERE sau KeyObject privat. holder: cheia PUBLICA a detinatorului (sau cheile lui AERE). disclosable: numele afirmatiilor de * sus dezvaluibile; arrayDisclosable: listele de sus ale caror ELEMENTE sunt dezvaluibile una cate una. exp implicit: un an de la iat * (null: fara exp, pe care verifySdJwt il refuza implicit). typ: tipul explicit al jetonului (implicit TYP; un profil ca SD-JWT VC isi * pune al lui, "dc+sd-jwt"; RFC 9901 s.9.11). Intoarce { sdJwt, disclosures:[{name, value, disclosure}] }. */ export function issueSdJwt({ issuer, claims, disclosable = [], arrayDisclosable = [], holder, iss, iat = Math.floor(Date.now() / 1000), exp, nbf, alg = 'EdDSA', holderAlg = 'EdDSA', decoys = 0, typ = TYP }) { if (!obiect(claims)) throw new Error('sd-jwt: claims must be an object'); if (typeof iss !== 'string' || !iss) throw new Error('sd-jwt: iss is required'); if (!ALGS.includes(alg) || !ALGS.includes(holderAlg)) throw new Error('sd-jwt: unknown alg'); if (!tipBun(typ, undefined)) throw new Error('sd-jwt: typ must be an explicit SD-JWT type ("...+sd-jwt", RFC 9901 s.9.11)'); // RFC 9901 s.4.1 (regula 7 a emitentului): emitentul nu semneaza digesturi pe care nu le-a facut el; o valoare care poarta deja `_sd`, // `_sd_alg` sau un element {"...": d} ar lasa detinatorul sa dezvaluie afirmatii nevazute de emitent if (areStructuraSd(claims)) throw new Error('sd-jwt: a claim value carries SD-JWT structure (_sd, _sd_alg or a {"...": digest} element) or a __proto__ key; the issuer signs only digests it made'); for (const n of [...disclosable, ...arrayDisclosable]) if (!Object.hasOwn(claims, n) || ['_sd', '...', '_sd_alg', 'cnf', 'iss', 'iat', 'exp', 'nbf'].includes(n)) throw new Error(`sd-jwt: cannot make ${n} disclosable`); for (const n of arrayDisclosable) if (!Array.isArray(claims[n])) throw new Error(`sd-jwt: ${n} is not a list`); const sare = () => crypto.randomBytes(16).toString('base64url'); const dez = [], corp = {}, sd = []; for (const [n, v] of Object.entries(claims)) { if (disclosable.includes(n)) { const d = b64u(JSON.stringify([sare(), n, v])); dez.push({ name: n, value: v, disclosure: d }); sd.push(sha256b64u(d)); } else if (arrayDisclosable.includes(n)) corp[n] = v.map((e) => { const d = b64u(JSON.stringify([sare(), e])); dez.push({ name: n + '[]', value: e, disclosure: d }); return { '...': sha256b64u(d) }; }); else corp[n] = v; } for (let i = 0; i < decoys; i++) sd.push(sha256b64u(b64u(crypto.randomBytes(32)))); if (sd.length) corp._sd = sd.sort(); corp._sd_alg = 'sha-256'; corp.iss = iss; corp.iat = iat; const e = exp === undefined ? iat + 365 * 86400 : exp; if (e != null) corp.exp = e; if (nbf != null) corp.nbf = nbf; corp.cnf = { jwk: publicJwk(holder, holderAlg) }; const jwt = semneazaJws({ alg, typ }, corp, cheiePrivata(issuer, alg)); return { sdJwt: jwt + '~' + dez.map((x) => x.disclosure + '~').join(''), disclosures: dez }; } /** * Detinatorul pastreaza numai dezvaluirile alese si leaga prezentarea de verificator. reveal: un sir = afirmatia de SUS cu acel nume; * { element, in } = elementul cu acea valoare din lista de SUS `in` (numai din ea). O alegere care nu gaseste nimic e o eroare, nu o tacere. */ export function presentSdJwt({ sdJwt, reveal = [], holder, holderAlg = 'EdDSA', audience, nonce, iat = Math.floor(Date.now() / 1000) }) { if (typeof audience !== 'string' || !audience || typeof nonce !== 'string' || !nonce) throw new Error('sd-jwt: a presentation needs the verifier\'s audience and nonce'); const parti = String(sdJwt).split('~'); if (parti[parti.length - 1] !== '') throw new Error('sd-jwt: this SD-JWT already ends with a key binding JWT'); const jwt = parti[0], dez = parti.slice(1, -1); const corp = jsonB64u(jwt.split('.')[1] || '', 'the issuer-signed JWT payload'); // unde sta fiecare digest in JWT-ul emitentului: proprietate de sus, sau element al unei liste de sus (cu numele listei) const loc = new Map(); for (const dg of Array.isArray(corp._sd) ? corp._sd : []) loc.set(dg, { tip: 'prop' }); for (const [n, v] of Object.entries(corp)) if (Array.isArray(v)) for (const e of v) if (obiect(e) && typeof e['...'] === 'string') loc.set(e['...'], { tip: 'elem', lista: n }); const gasite = new Set(); const alese = dez.filter((d) => { const l = loc.get(sha256b64u(d)); if (!l) return false; const c = JSON.parse(Buffer.from(d, 'base64url').toString('utf8')); for (let i = 0; i < reveal.length; i++) { const r = reveal[i]; const bun = l.tip === 'prop' ? typeof r === 'string' && c.length === 3 && c[1] === r : obiect(r) && r.in === l.lista && c.length === 2 && JSON.stringify(r.element) === JSON.stringify(c[1]); if (bun) { gasite.add(i); return true; } } return false; }); const lipsa = reveal.filter((_, i) => !gasite.has(i)); if (lipsa.length) throw new Error('sd-jwt: nothing to reveal for ' + JSON.stringify(lipsa) + ' (a name reveals a top-level claim; {element, in} an element of the top-level list named)'); const baza = jwt + '~' + alese.map((d) => d + '~').join(''); const kb = semneazaJws({ alg: holderAlg, typ: 'kb+jwt' }, { iat, aud: audience, nonce, sd_hash: sha256b64u(baza) }, cheiePrivata(holder, holderAlg)); return baza + kb; } // ---------------------------------------------------------------- verificare (RFC 9901 s.7.1, s.7.3) /** * @param {string} sdJwt SD-JWT sau SD-JWT+KB, serializarea compacta * @param {{issuerKey, issuerAlg?, expectedIssuer?:string, audience?:string, nonce?:string, requireKeyBinding?:boolean, requireExp?:boolean, * now?:Date, maxAgeS?:number, clockSkewS?:number, expectedTyp?:string|null}} o * issuerKey: cheia EMITENTULUI data de verificator (JWK, KeyObject, cheile AERE ale emitentului); niciodata din jeton. audience si nonce * sunt ale verificatorului si se CER cand exista (sau se cere) Key Binding. expectedTyp: implicit orice tip explicit "...+sd-jwt". * Intoarce { valid, rule, reason, payload, disclosed:[nume], issuer, issuerChecked, keyBinding:{aud, nonce, iat, alg}|null }; `rule` e * regula (din REGULI) care a refuzat, null cand e valid. */ export function verifySdJwt(sdJwt, { issuerKey, issuerAlg = 'EdDSA', expectedIssuer = null, audience = null, nonce = null, requireKeyBinding = true, requireExp = true, now = new Date(), maxAgeS = 300, clockSkewS = 60, expectedTyp } = {}) { const refuz = (rule, reason) => ({ valid: false, rule, reason, payload: null, disclosed: [], keyBinding: null }); const motiv = (e) => String((e && e.message) || e).replace(/^sd-jwt: /, ''); const regula = (e) => (e instanceof Refuz ? e.rule : 'form'); try { if (!(typeof sdJwt === 'string' && sdJwt.includes('~') /*PAZA:form*/)) return refuz('form', 'not an SD-JWT (no ~)'); // ceasul verificatorului: un `now` care nu e un timp ar lasa toate comparatiile de timp sa treaca (NaN nu e mai mare decat nimic) const acum = Math.floor(new Date(now).getTime() / 1000); if (!(Number.isFinite(acum) /*PAZA:form*/)) return refuz('form', 'the verifier clock (now) is not a time'); const parti = sdJwt.split('~'); const jwt = parti[0], kb = parti[parti.length - 1], dez = parti.slice(1, -1); if (!(!dez.some((d) => d === '') /*PAZA:form*/)) return refuz('form', 'an empty disclosure between two ~'); // RFC 9901 s.7.3 pasul 5: publicul si nonce-ul verificatorului se compara; fara ele, o prezentare facuta pentru altul ar trece const context = typeof audience === 'string' && !!audience && typeof nonce === 'string' && !!nonce; if (!(!(requireKeyBinding || kb !== '') || context /*PAZA:kb-context*/)) return refuz('kb-context', 'the verifier must give its audience and nonce to judge a key binding (RFC 9901 s.7.3)'); // 2. JWT-ul emitentului const { antet, corp } = verificaJws(jwt, publicJwk(issuerKey, issuerAlg), { typ: expectedTyp }); if (!(expectedIssuer == null || corp.iss === expectedIssuer /*PAZA:issuer*/)) return refuz('issuer', `issued by ${JSON.stringify(corp.iss)}, not ${expectedIssuer}`); const alg = corp._sd_alg === undefined ? 'sha-256' : corp._sd_alg; if (!(alg === 'sha-256' /*PAZA:sd-alg*/)) return refuz('sd-alg', `_sd_alg ${JSON.stringify(corp._sd_alg)} is not understood (only sha-256)`); // 3. dezvaluirile: digest -> continut const dupaDigest = new Map(); for (const d of dez) { const dg = sha256b64u(d); if (!(!dupaDigest.has(dg) /*PAZA:disclosure-twice*/)) return refuz('disclosure-twice', 'the same disclosure is given twice'); let c; try { c = JSON.parse(dinB64u(d, 'a disclosure').toString('utf8')); } catch { if (!(false /*PAZA:disclosure-form*/)) return refuz('disclosure-form', 'a disclosure is not base64url JSON'); continue; } if (!(Array.isArray(c) && (c.length === 2 || c.length === 3) && typeof c[0] === 'string' /*PAZA:disclosure-form*/)) return refuz('disclosure-form', 'a disclosure is not [salt, name, value] or [salt, value]'); dupaDigest.set(dg, c); } const vazute = new Set(), folosite = new Set(); const vezi = (dg) => { if (!(!vazute.has(dg) /*PAZA:digest-twice*/)) throw nu('digest-twice', 'the digest ' + dg.slice(0, 12) + '.. appears more than once'); vazute.add(dg); }; const proceseaza = (x, sus = false) => { if (Array.isArray(x)) { const out = []; for (const e of x) { if (obiect(e) && Object.keys(e).length === 1 && typeof e['...'] === 'string') { vezi(e['...']); const c = dupaDigest.get(e['...']); if (!c) continue; // nedezvaluit: elementul se scoate if (!(c.length === 2 /*PAZA:disclosure-form*/)) throw nu('disclosure-form', 'an array element disclosure must be [salt, value]'); folosite.add(e['...']); out.push(proceseaza(c[1])); } else out.push(proceseaza(e)); } return out; } if (!obiect(x)) return x; // JSON.parse face din "__proto__" o proprietate proprie, iar `out[k] = v` pe un obiect obisnuit i-ar schimba PROTOTIPUL (afirmatii // mostenite, nevazute de Object.keys); o afirmatie cu numele asta, in clar sau intr-o valoare dezvaluita, se refuza if (!(!Object.hasOwn(x, '__proto__') /*PAZA:disclosure-name*/)) throw nu('disclosure-name', 'a claim may not be named __proto__'); const out = {}; // `_sd_alg` se scoate de sus (RFC 9901 s.7.1 pasul 3.f). Mai jos NU are voie sa existe: "It MUST NOT be used in any object nested // within the payload" (s.4.1.1), deci un jeton care il poarta imbricat (in clar sau intr-o valoare dezvaluita) nu e un SD-JWT valid. // Pana pe 2026-10-18 era pastrat ca o afirmatie oarecare; la interoperare, @sd-jwt/core 0.23.0 il refuza, si textul s.4.1.1 ii da dreptate. for (const [k, v] of Object.entries(x)) { if (k === '_sd') continue; if (k === '_sd_alg') { if (!(sus /*PAZA:sd-alg*/)) throw nu('sd-alg', '_sd_alg below the top level of the payload (RFC 9901 s.4.1.1)'); continue; } out[k] = proceseaza(v); } if (x._sd !== undefined) { if (!(Array.isArray(x._sd) && x._sd.every((s) => typeof s === 'string') /*PAZA:disclosure-form*/)) throw nu('disclosure-form', '_sd is not a list of strings'); for (const dg of x._sd) { vezi(dg); const c = dupaDigest.get(dg); if (!c) continue; if (!(c.length === 3 && typeof c[1] === 'string' /*PAZA:disclosure-form*/)) throw nu('disclosure-form', 'an object property disclosure must be [salt, name, value]'); // `_sd_alg` dezvaluit: s.7.1 l-ar insera (3.c.ii.4) si apoi l-ar scoate (3.f); refuzam in loc sa il scoatem in tacere, ca lista // `disclosed` sa nu numeasca o afirmatie pe care payload-ul procesat nu o poarta (la fel face si @sd-jwt/core 0.23.0) if (!(c[1] !== '_sd' && c[1] !== '...' && c[1] !== '_sd_alg' && c[1] !== '__proto__' /*PAZA:disclosure-name*/)) throw nu('disclosure-name', `a disclosure may not name ${c[1]}`); if (!(!Object.hasOwn(out, c[1]) /*PAZA:claim-conflict*/)) throw nu('claim-conflict', `the disclosed claim ${c[1]} already exists at its level`); folosite.add(dg); out[c[1]] = proceseaza(c[2]); } } return out; }; let payload; try { payload = proceseaza(corp, true); } catch (e) { return refuz(regula(e), motiv(e)); } // 5. o dezvaluire nelegata de niciun digest = refuz if (!(folosite.size === dupaDigest.size /*PAZA:unreferenced*/)) return refuz('unreferenced', 'a disclosure is not referenced by any digest of the issuer-signed JWT'); const disclosed = [...dupaDigest.entries()].map(([, c]) => (c.length === 3 ? c[1] : '[]')); // 6. valabilitatea pe ceasul verificatorului (exp cerut si fara toleranta; nbf si iat cu toleranta numai pe inceput, ca identity.mjs) for (const k of ['exp', 'nbf', 'iat']) if (!(payload[k] === undefined || Number.isFinite(payload[k]) /*PAZA:time*/)) return refuz('time', `${k} is not a number`); if (!(!requireExp || payload.exp !== undefined /*PAZA:time*/)) return refuz('time', 'exp is required and the processed payload has none (RFC 9901 s.9.7)'); if (!(payload.exp === undefined || acum < payload.exp /*PAZA:time*/)) return refuz('time', `expired at ${payload.exp}`); if (!(payload.nbf === undefined || payload.nbf - clockSkewS <= acum /*PAZA:time*/)) return refuz('time', `not valid before ${payload.nbf}`); if (!(payload.iat === undefined || payload.iat - clockSkewS <= acum /*PAZA:time*/)) return refuz('time', `issued in the future (${payload.iat})`); const rez = { valid: true, rule: null, reason: '', payload, disclosed, issuer: corp.iss, issuerChecked: expectedIssuer != null, alg: antet.alg }; // s.7.3: Key Binding JWT if (kb === '') { if (!(!requireKeyBinding /*PAZA:kb-required*/)) return refuz('kb-required', 'key binding is required and the SD-JWT has none (it ends with ~)'); return { ...rez, keyBinding: null }; } if (!(obiect(payload.cnf) && obiect(payload.cnf.jwk) /*PAZA:kb-cnf*/)) return refuz('kb-cnf', 'a key binding JWT is given but the SD-JWT names no holder key (cnf.jwk)'); let k; try { k = verificaJws(kb, payload.cnf.jwk, { typ: 'kb+jwt' }); } catch (e) { return refuz(regula(e), 'key binding: ' + motiv(e)); } const K = k.corp; if (!(typeof K.nonce === 'string' && typeof K.aud === 'string' && Number.isFinite(K.iat) && typeof K.sd_hash === 'string' /*PAZA:kb-claims*/)) return refuz('kb-claims', 'key binding: aud, nonce, iat and sd_hash are required'); if (!(K.aud === audience /*PAZA:kb-audience*/)) return refuz('kb-audience', `key binding: made for ${K.aud}, not ${audience}`); if (!(K.nonce === nonce /*PAZA:kb-nonce*/)) return refuz('kb-nonce', 'key binding: another nonce'); if (!(Math.abs(acum - K.iat) <= maxAgeS /*PAZA:kb-freshness*/)) return refuz('kb-freshness', `key binding: made at ${K.iat}, outside ${maxAgeS} s of this clock`); // RFC 9901 s.7.3 pasul 5.h: si exp/nbf ale KB-JWT, daca le poarta (RFC 7519) if (!(K.exp === undefined || (Number.isFinite(K.exp) && acum < K.exp) /*PAZA:kb-freshness*/)) return refuz('kb-freshness', `key binding: expired at ${K.exp}`); if (!(K.nbf === undefined || (Number.isFinite(K.nbf) && K.nbf - clockSkewS <= acum) /*PAZA:kb-freshness*/)) return refuz('kb-freshness', `key binding: not valid before ${K.nbf}`); const baza = sdJwt.slice(0, sdJwt.length - kb.length); if (!(sha256b64u(baza) === K.sd_hash /*PAZA:kb-sd-hash*/)) return refuz('kb-sd-hash', 'key binding: sd_hash is not the hash of what was presented'); return { ...rez, keyBinding: { aud: K.aud, nonce: K.nonce, iat: K.iat, alg: k.antet.alg } }; } catch (e) { return refuz(regula(e), motiv(e)); } }