#!/usr/bin/env node // interop-sdjwt-vc.mjs: interoperarea SD-JWT VC (draft-ietf-oauth-sd-jwt-vc-19) si a listei de stare (Token Status List, // draft-ietf-oauth-status-list-21) MASURATA in ambele sensuri intre sdjwt-vc.mjs (al nostru) si @sd-jwt/sd-jwt-vc (openwallet-foundation-labs), // cu @owf/token-status-list pentru lista LOR de stare si jose ca motor JWS al partii straine (pista B, B9, 2026-10-18). // node interop-sdjwt-vc.mjs --lab [--scrie] // --scrie rescrie fixturi-interop-sdjwt-vc.json (credentialele si listele de stare ale LOR, chei publice de proba, versiuni) si // rezultate-interop-sdjwt-vc.json, ca proba-sdjwt-vc.mjs sa le judece OFFLINE // Iesire: 0 = fiecare rand a dat EXACT verdictele asteptate de la amandoua partile; 1 = divergent; 2 = NEMASURAT (bibliotecile lipsesc). // Ceas FIX (CEAS), nicio cheie privata nu se tipareste sau se scrie (chei de proba nascute la rulare, AERE-SINTETIC). import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { createRequire } from 'node:module'; import { pathToFileURL, fileURLToPath } from 'node:url'; import * as I from '../identity.mjs'; import * as J from '../sdjwt.mjs'; import * as V from '../sdjwt-vc.mjs'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const arg = (n) => { const i = process.argv.indexOf(n); return i === -1 ? null : (process.argv[i + 1] ?? null); }; const LAB = arg('--lab') ?? process.env.AERE_SDJWT_LAB ?? null; const SCRIE = process.argv.includes('--scrie'); const CEAS = 1792281600, ACUM = CEAS + 60, LA = new Date(ACUM * 1000); const AUD = 'https://verifier.example', NONCE = 'interop-vc-' + crypto.randomBytes(6).toString('hex'); const VCT = 'urn:eudi:pid:1', URI = 'https://issuer.example/status/pid/1', IDX = 5, N = 16; const b64u = (b) => Buffer.from(b).toString('base64url'); const dec = (s) => JSON.parse(Buffer.from(s, 'base64url').toString('utf8')); 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 taie = (s, n = 110) => (s == null ? '' : String(s).replace(/\s+/g, ' ').slice(0, n)); async function main() { if (!LAB || !fs.existsSync(path.join(LAB, 'node_modules'))) { console.log('NEMASURAT: laboratorul cu bibliotecile straine lipseste (--lab sau AERE_SDJWT_LAB)'); return 2; } const req = createRequire(path.join(LAB, 'package.json')); const modul = async (nume) => { let cale; try { cale = req.resolve(nume); } catch { return null; } const m = await import(pathToFileURL(cale).href); return m.digest || m.SDJwtVcInstance || m.compactVerify || m.StatusList ? m : (m.default ?? m); }; const versiune = (nume) => { try { return JSON.parse(fs.readFileSync(path.join(LAB, 'node_modules', ...nume.split('/'), 'package.json'), 'utf8')).version; } catch { return null; } }; const vc = await modul('@sd-jwt/sd-jwt-vc'), cn = await modul('@sd-jwt/crypto-nodejs'), jose = await modul('jose'), tsl = await modul('@owf/token-status-list'); const lipsa = [['@sd-jwt/sd-jwt-vc', vc], ['@sd-jwt/crypto-nodejs', cn], ['jose', jose], ['@owf/token-status-list', tsl]].filter(([, m]) => !m).map(([n]) => n); if (lipsa.length) { console.log('NEMASURAT: lipsesc din laborator: ' + lipsa.join(', ')); return 2; } const biblioteci = { '@sd-jwt/sd-jwt-vc': versiune('@sd-jwt/sd-jwt-vc'), '@sd-jwt/core': versiune('@sd-jwt/core'), '@owf/token-status-list': versiune('@owf/token-status-list'), '@sd-jwt/crypto-nodejs': versiune('@sd-jwt/crypto-nodejs'), jose: versiune('jose'), node: process.version }; console.log('biblioteci: ' + JSON.stringify(biblioteci)); // ---------------------------------------------------------------- partea LOR const subtleParams = { EdDSA: { name: 'Ed25519' }, ES256: { name: 'ECDSA', hash: 'SHA-256' }, 'ML-DSA-65': { name: 'ML-DSA-65' } }; async function cheiLor(alg) { const { publicKey, privateKey } = await jose.generateKeyPair(alg, { extractable: true }); const jwk = await jose.exportJWK(publicKey); if (alg === 'ML-DSA-65') jwk.alg = jwk.alg ?? 'ML-DSA-65'; return { publicKey, privateKey, jwk, alg }; } const semnatorLor = (k) => async (data) => b64u(Buffer.from(await crypto.subtle.sign(subtleParams[k.alg], k.privateKey, Buffer.from(data, 'ascii')))); let ultimaJose = ''; async function verificatorJose(jwk, alg) { const key = await jose.importJWK(jwk, alg); return async (data, sig) => { try { await jose.compactVerify(data + '.' + sig, key, { algorithms: [alg] }); return true; } catch (e) { ultimaJose = e.message; return false; } }; } const kbVerificatorJose = async (data, sig, payload) => { try { const h = dec(data.split('.')[0]); const key = await jose.importJWK(payload.cnf.jwk, payload.cnf.jwk.alg ?? h.alg); await jose.compactVerify(data + '.' + sig, key, { algorithms: [h.alg] }); return true; } catch (e) { ultimaJose = e.message; return false; } }; const lor = (cfg) => new vc.SDJwtVcInstance({ hasher: cn.digest, saltGenerator: cn.generateSalt, hashAlg: 'sha-256', kbVerifier: kbVerificatorJose, ...cfg }); async function lorVerifica(sd, { issuerJwk, alg, statusListJwt = null, optiuni = {} }) { const inst = lor({ verifier: await verificatorJose(issuerJwk, alg), ...(statusListJwt != null ? { statusListFetcher: async () => statusListJwt } : {}) }); try { const r = await inst.verify(sd, { currentDate: ACUM, keyBindingNonce: NONCE, expectedKeyBindingAudience: AUD, expectedVct: VCT, ...(statusListJwt == null ? { disableStatusVerification: true } : {}), ...optiuni }); return { valid: true, payload: r.payload, header: r.header, kb: r.kb ? r.kb.payload : null }; } catch (e) { return { valid: false, reason: e.message + (e.cause && e.cause.message ? ' <- ' + e.cause.message : '') + (/Signature/.test(e.message) && ultimaJose ? ' [jose: ' + ultimaJose + ']' : ''), code: e.code ?? null }; } } // lista LOR de stare: @owf/token-status-list + jose async function listaLor(chei, statuses, bits = 1, { uri = URI, exp = CEAS + 3600 } = {}) { const { header, payload } = tsl.createHeaderAndPayload(new tsl.StatusList(statuses, bits), { sub: uri, iat: CEAS, exp }, { alg: chei.alg, typ: 'statuslist+jwt' }); return new jose.SignJWT(payload).setProtectedHeader(header).sign(chei.privateKey); } // ---------------------------------------------------------------- partea NOASTRA function cheiNoi(alg) { if (alg === 'ES256') { const k = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); return { priv: k.privateKey, pub: k.publicKey, jwk: J.publicJwk(k.publicKey), alg }; } const k = I.generateKeys(); return { priv: k, pub: k.public, jwk: J.publicJwk(k, alg), alg }; } const CL = { given_name: 'Ana', family_name: 'Pop', birthdate: '1990-01-01', age_over_18: true, nationalities: ['RO', 'DE'], address: { locality: 'Cluj', country: 'RO' }, issuing_authority: 'Example Authority', issuing_country: 'RO' }; // status: null = credential FARA status (un `undefined` ar lua valoarea implicita a parametrului) const noiEmite = ({ iss, hol, alg, status = { status_list: { idx: IDX, uri: URI } }, extra = {} }) => V.issueSdJwtVc({ issuer: iss.priv, holder: hol.pub, vct: VCT, claims: CL, disclosable: ['given_name', 'family_name', 'birthdate', 'age_over_18', 'address'], arrayDisclosable: ['nationalities'], iss: 'https://issuer.aere.example', iat: CEAS, nbf: CEAS, exp: CEAS + 365 * 86400, alg, holderAlg: hol.alg, decoys: 2, ...(status !== null ? { status } : {}), ...extra }); const noiPrezinta = (sd, reveal, hol, extra = {}) => V.presentSdJwtVc({ sdJwt: sd, reveal, holder: hol.priv, holderAlg: hol.alg, audience: AUD, nonce: NONCE, iat: CEAS, ...extra }); const noiVerifica = (sd, { issuerKey, alg, statusListJwt = null, optiuni = {} }) => V.verifySdJwtVc(sd, { issuerKey, issuerAlg: alg, audience: AUD, nonce: NONCE, now: LA, expectedVct: VCT, ...(statusListJwt != null ? { statusListJwt } : {}), ...optiuni }); const listaNoi = (iss, statuses, bits = 1, extra = {}) => V.issueStatusListJwt({ issuer: iss.priv, alg: iss.alg, uri: URI, statuses, bits, iat: CEAS, exp: CEAS + 3600, ...extra }); const toateValide = new Array(N).fill(0); const revocat = toateValide.map((_, i) => (i === IDX ? 1 : 0)); const suspendat = toateValide.map((_, i) => (i === IDX ? 2 : 0)); // ---------------------------------------------------------------- tabelul const randuri = [], fixturi = []; let divergente = 0; const fixtura = (f) => fixturi.push(f); function rand({ sens, caz, noi, lor: l, asteptat, nota = '' }) { const vN = noi.valid ? 'VALID' : 'REFUZ', vL = l.valid ? 'VALID' : 'REFUZ'; const probleme = []; if (vN !== asteptat.noi) probleme.push(`noi: ${vN} in loc de ${asteptat.noi} (${taie(noi.reason)})`); if (vL !== asteptat.lor) probleme.push(`lor: ${vL} in loc de ${asteptat.lor} (${taie(l.reason)})`); if (asteptat.motivNoi && !noi.valid && !asteptat.motivNoi.test(noi.reason || '')) probleme.push(`motivul nostru nu e cel asteptat: ${taie(noi.reason)}`); if (asteptat.motivLor && !l.valid && !asteptat.motivLor.test(l.reason || '')) probleme.push(`motivul lor nu e cel asteptat: ${taie(l.reason)}`); if (asteptat.continut && noi.valid && l.valid) { const m = asteptat.continut(noi, l); if (m) probleme.push(m); } const ok = probleme.length === 0; if (!ok) divergente++; randuri.push({ sens, caz, asteptat: `${asteptat.noi}/${asteptat.lor}`, noi: vN + (noi.valid ? '' : ': ' + taie(noi.reason)), lor: vL + (l.valid ? '' : ': ' + taie(l.reason)), ok, probleme, nota }); console.log(` ${ok ? 'OK ' : 'DIVERGENT'} [${sens}] ${caz}\n noi: ${vN}${noi.valid ? '' : ' (' + taie(noi.reason, 130) + ')'}\n lor: ${vL}${l.valid ? '' : ' (' + taie(l.reason, 130) + ')'}${nota ? '\n nota: ' + nota : ''}${ok ? '' : '\n PROBLEME: ' + probleme.join('; ')}`); } const continutPid = (pn, pl) => { const p = pn.payload; if (p.vct !== VCT || p.birthdate !== '1990-01-01' || p.age_over_18 !== true || 'given_name' in p || !egal(p.nationalities, ['RO']) || !p.cnf) return 'payload-ul nostru nu e cel asteptat: ' + taie(JSON.stringify(p), 200); if (!egal(pn.payload, pl.payload)) return 'payload-urile difera: noi ' + taie(JSON.stringify(sortat(pn.payload)), 160) + ' lor ' + taie(JSON.stringify(sortat(pl.payload)), 160); return null; }; // ================================================================ NOI -> LOR const nl = {}; for (const alg of ['EdDSA', 'ES256', 'ML-DSA-65']) { const iss = cheiNoi(alg), hol = cheiNoi(alg); const E = noiEmite({ iss, hol, alg }); const p = noiPrezinta(E.sdJwt, ['birthdate', 'age_over_18', { element: 'RO', in: 'nationalities' }], hol); const lista = listaNoi(iss, toateValide); const noi = noiVerifica(p, { issuerKey: iss.jwk, alg, statusListJwt: lista }), l = await lorVerifica(p, { issuerJwk: iss.jwk, alg, statusListJwt: lista }); rand({ sens: 'noi->lor', caz: `${alg}: SD-JWT VC (typ dc+sd-jwt, vct ${VCT}, status_list idx ${IDX}) + lista noastra de stare (toate 0), KB(aud, nonce)`, noi, lor: l, asteptat: { noi: 'VALID', lor: 'VALID', continut: (a, b) => continutPid(a, b) || (a.status && a.status.judged && a.status.value === 0 ? null : 'starea nu e judecata la noi: ' + JSON.stringify(a.status)) || (b.header && b.header.typ === 'dc+sd-jwt' ? null : 'typ-ul vazut de ei nu e dc+sd-jwt') } }); nl[alg] = { iss, hol, E, p, lista }; } { const { iss, hol, E, p } = nl.EdDSA; const ambii = async (jeton, listaNoastra, optLor = {}, optNoi = {}) => [noiVerifica(jeton, { issuerKey: iss.jwk, alg: 'EdDSA', statusListJwt: listaNoastra, optiuni: optNoi }), await lorVerifica(jeton, { issuerJwk: iss.jwk, alg: 'EdDSA', statusListJwt: listaNoastra, optiuni: optLor })]; let [n, l] = await ambii(p, listaNoi(iss, revocat)); rand({ sens: 'noi->lor', caz: `NEGATIV: lista noastra cu indexul ${IDX} = 1 (INVALID, revocat)`, noi: n, lor: l, asteptat: { noi: 'REFUZ', lor: 'REFUZ', motivNoi: /status 1 \(INVALID\)/, motivLor: /Status is not valid/ } }); [n, l] = await ambii(p, listaNoi(iss, suspendat, 2)); rand({ sens: 'noi->lor', caz: `NEGATIV: lista noastra pe 2 biti cu indexul ${IDX} = 2 (SUSPENDED)`, noi: n, lor: l, asteptat: { noi: 'REFUZ', lor: 'REFUZ', motivNoi: /status 2 \(SUSPENDED\)/, motivLor: /Status is not valid/ } }); [n, l] = await ambii(p, listaNoi(iss, toateValide, 8)); rand({ sens: 'noi->lor', caz: 'lista noastra pe 8 biti, toate 0 (impachetarea pe mai multi biti citita de ei)', noi: n, lor: l, asteptat: { noi: 'VALID', lor: 'VALID' } }); [n, l] = await ambii(p, listaNoi(iss, toateValide, 1, { uri: URI + '-alta' })); rand({ sens: 'noi->lor', caz: 'NEGATIV: lista noastra publicata pentru ALT uri (sub != uri-ul din jeton)', noi: n, lor: l, asteptat: { noi: 'REFUZ', lor: 'REFUZ', motivNoi: /sub .* is not the uri/, motivLor: /sub|uri|Status List/i } }); [n, l] = await ambii(p, null, {}, { requireStatus: false }); rand({ sens: 'noi->lor', caz: 'fara lista: noi cu requireStatus:false (stare nejudecata, spusa), ei cu disableStatusVerification', noi: n, lor: l, asteptat: { noi: 'VALID', lor: 'VALID', continut: (a) => (a.status && a.status.present && !a.status.judged ? null : 'starea trebuia raportata prezenta si nejudecata: ' + JSON.stringify(a.status)) }, nota: 'la noi, implicit (requireStatus:true) acelasi credential e REFUZAT: ' + taie(noiVerifica(p, { issuerKey: iss.jwk, alg: 'EdDSA' }).reason, 90) }); [n, l] = await ambii(p, listaNoi(iss, toateValide), { expectedVct: 'urn:eudi:pid:2' }, { expectedVct: 'urn:eudi:pid:2' }); rand({ sens: 'noi->lor', caz: 'NEGATIV: verificatorul asteapta alt vct (urn:eudi:pid:2)', noi: n, lor: l, asteptat: { noi: 'REFUZ', lor: 'REFUZ', motivNoi: /not the type expected/, motivLor: /Invalid VCT/ } }); const altAud = noiPrezinta(E.sdJwt, ['birthdate'], hol, { audience: 'https://other.example' }); [n, l] = await ambii(altAud, listaNoi(iss, toateValide)); rand({ sens: 'noi->lor', caz: 'NEGATIV: KB pentru alt aud', noi: n, lor: l, asteptat: { noi: 'REFUZ', lor: 'REFUZ', motivNoi: /made for/, motivLor: /Invalid Key Binding audience/ } }); // un credential fara status: valid la amandoi, fara lista const faraStatus = noiEmite({ iss, hol, alg: 'EdDSA', status: null }); [n, l] = await ambii(noiPrezinta(faraStatus.sdJwt, ['birthdate'], hol), null); rand({ sens: 'noi->lor', caz: 'credential FARA status (nu numeste nicio lista): valid fara lista', noi: n, lor: l, asteptat: { noi: 'VALID', lor: 'VALID', continut: (a) => (a.status && !a.status.present ? null : 'status trebuia absent') } }); // emitentul nostru refuza sa faca vct dezvaluibil (s.2.2.2.3); al lor la fel ("Cannot disclose protected field") let mNoi = null; try { V.issueSdJwtVc({ issuer: iss.priv, holder: hol.pub, vct: VCT, iss: 'x', iat: CEAS, claims: { a: 1 }, disclosable: ['vct'] }); } catch (e) { mNoi = e.message; } let mLor = null; try { await lor({ signer: semnatorLor(await cheiLor('EdDSA')), signAlg: 'EdDSA' }).issue({ vct: VCT, iss: 'x', iat: CEAS, a: 1 }, { _sd: ['vct'] }); } catch (e) { mLor = e.message; } rand({ sens: 'noi->lor', caz: 'EMITERE: vct cerut dezvaluibil, refuzat de amandoi emitentii (s.2.2.2.3)', noi: { valid: !mNoi, reason: mNoi }, lor: { valid: !mLor, reason: mLor }, asteptat: { noi: 'REFUZ', lor: 'REFUZ', motivNoi: /may not be selectively disclosed/, motivLor: /Cannot disclose protected field/ } }); } // ================================================================ LOR -> NOI const claimsLor = { vct: VCT, iss: 'https://issuer.lor.example', iat: CEAS, nbf: CEAS, exp: CEAS + 365 * 86400, status: { status_list: { idx: IDX, uri: URI } }, given_name: 'Ana', family_name: 'Pop', birthdate: '1990-01-01', age_over_18: true, nationalities: ['RO', 'DE'], address: { locality: 'Cluj', country: 'RO' }, issuing_authority: 'Example Authority', issuing_country: 'RO' }; const frameLor = { _sd: ['given_name', 'family_name', 'birthdate', 'age_over_18', 'address'], nationalities: { _sd: [0, 1] }, _sd_decoy: 2 }; const prezFrame = { birthdate: true, age_over_18: true, nationalities: { 0: true } }; const lorEmitePrezinta = async (alg, claims = claimsLor) => { const iss = await cheiLor(alg), hol = await cheiLor(alg); const inst = lor({ signer: semnatorLor(iss), signAlg: alg, kbSigner: semnatorLor(hol), kbSignAlg: alg }); const sd = await inst.issue({ ...claims, cnf: { jwk: hol.jwk } }, frameLor); const p = await inst.present(sd, prezFrame, { kb: { payload: { aud: AUD, nonce: NONCE, iat: CEAS } } }); return { iss, hol, sd, p }; }; const ln = {}; for (const alg of ['EdDSA', 'ES256', 'ML-DSA-65']) { const r = await lorEmitePrezinta(alg); const lista = await listaLor(r.iss, toateValide); const noi = noiVerifica(r.p, { issuerKey: r.iss.jwk, alg, statusListJwt: lista }), l = await lorVerifica(r.p, { issuerJwk: r.iss.jwk, alg, statusListJwt: lista }); const antet = dec(r.sd.split('.')[0]); rand({ sens: 'lor->noi', caz: `${alg}: SD-JWT VC al LOR (typ ${antet.typ}, status_list idx ${IDX}) + lista LOR de stare (@owf/token-status-list, toate 0), KB(aud, nonce); tipul implicit al nostru`, noi, lor: l, asteptat: { noi: 'VALID', lor: 'VALID', continut: (a, b) => continutPid(a, b) || (a.status && a.status.judged && a.status.value === 0 ? null : 'starea nu e judecata la noi: ' + JSON.stringify(a.status)) || (antet.typ === 'dc+sd-jwt' ? null : 'typ-ul lor nu e dc+sd-jwt: ' + antet.typ) } }); ln[alg] = { ...r, lista }; if (noi.valid) fixtura({ nume: `${alg}: SD-JWT VC @sd-jwt/sd-jwt-vc cu lista de stare @owf/token-status-list (toate 0)`, alg, issuerJwk: r.iss.jwk, sdJwt: r.sd, prezentare: r.p, statusListJwt: lista, expectedVct: VCT, optiuni: {}, asteptat: { valid: true, payload: noi.payload, disclosed: [...noi.disclosed].sort(), status: noi.status } }); } { const { iss, hol, sd, p } = ln.EdDSA; const neg = async (nume, { jeton = p, lista, motivNoi, motivLor, optNoi = {}, optLor = {}, asteptatNoi = 'REFUZ', asteptatLor = 'REFUZ', nota, fix = true }) => { const n = noiVerifica(jeton, { issuerKey: iss.jwk, alg: 'EdDSA', statusListJwt: lista, optiuni: optNoi }), l = await lorVerifica(jeton, { issuerJwk: iss.jwk, alg: 'EdDSA', statusListJwt: lista, optiuni: optLor }); rand({ sens: 'lor->noi', caz: nume, noi: n, lor: l, asteptat: { noi: asteptatNoi, lor: asteptatLor, motivNoi, motivLor }, nota }); if (fix && (n.valid ? asteptatNoi === 'VALID' : asteptatNoi === 'REFUZ' && (!motivNoi || motivNoi.test(n.reason)))) fixtura({ nume, alg: 'EdDSA', issuerJwk: iss.jwk, prezentare: jeton, ...(lista != null ? { statusListJwt: lista } : {}), expectedVct: optNoi.expectedVct ?? VCT, optiuni: Object.fromEntries(Object.entries(optNoi).filter(([k]) => k !== 'expectedVct')), asteptat: n.valid ? { valid: true, payload: n.payload, disclosed: [...n.disclosed].sort(), status: n.status } : { valid: false, motiv: motivNoi.source } }); }; await neg(`NEGATIV: lista LOR cu indexul ${IDX} = 1 (INVALID)`, { lista: await listaLor(iss, revocat), motivNoi: /status 1 \(INVALID\)/, motivLor: /Status is not valid/ }); await neg(`NEGATIV: lista LOR pe 2 biti cu indexul ${IDX} = 2 (SUSPENDED)`, { lista: await listaLor(iss, suspendat, 2), motivNoi: /status 2 \(SUSPENDED\)/, motivLor: /Status is not valid/ }); await neg('lista LOR pe 8 biti, toate 0 (impachetarea lor pe mai multi biti citita de noi)', { lista: await listaLor(iss, toateValide, 8), asteptatNoi: 'VALID', asteptatLor: 'VALID' }); // controlul pozitiv al impachetarii: o lista LOR cu stari diferite pe fiecare index, citita de noi index cu index const amestec = [0, 1, 2, 3, 0, 1, 2, 3, 3, 2, 1, 0]; const lAmestec = await listaLor(iss, amestec, 2); const citit = V.readStatusListJwt(lAmestec, { key: iss.jwk, alg: 'EdDSA', uri: URI, now: LA }); const citite = citit.ok ? amestec.map((_, i) => citit.statusAt(i)) : null; rand({ sens: 'lor->noi', caz: 'IMPACHETARE: lista LOR pe 2 biti cu 12 stari amestecate, citite de noi index cu index', noi: { valid: citit.ok && egal(citite, amestec), reason: citit.ok ? 'citit ' + JSON.stringify(citite) : citit.reason }, lor: { valid: true }, asteptat: { noi: 'VALID', lor: 'VALID' }, nota: 'citit: ' + JSON.stringify(citite) + ' = scris: ' + JSON.stringify(amestec) }); const lNoiAmestec = V.issueStatusListJwt({ issuer: cheiNoi('EdDSA').priv, uri: URI, statuses: amestec, bits: 2, iat: CEAS }); const lorCitit = amestec.map((_, i) => tsl.getListFromStatusListJWT(lNoiAmestec).getStatus(i)); rand({ sens: 'noi->lor', caz: 'IMPACHETARE: lista NOASTRA pe 2 biti cu 12 stari amestecate, citite de ei index cu index', noi: { valid: true }, lor: { valid: egal(lorCitit, amestec), reason: 'citit ' + JSON.stringify(lorCitit) }, asteptat: { noi: 'VALID', lor: 'VALID' }, nota: 'citit: ' + JSON.stringify(lorCitit) + ' = scris: ' + JSON.stringify(amestec) }); await neg('NEGATIV: lista LOR pentru ALT uri (sub != uri-ul din jeton)', { lista: await listaLor(iss, toateValide, 1, { uri: URI + '-alta' }), motivNoi: /sub .* is not the uri/, motivLor: /sub|uri|Status List/i }); await neg('NEGATIV: lista LOR expirata', { lista: await listaLor(iss, toateValide, 1, { exp: CEAS + 30 }), motivNoi: /status list: expired/, motivLor: /expired|exp/i }); await neg('NEGATIV: verificatorul asteapta alt vct (urn:eudi:pid:2)', { lista: await listaLor(iss, toateValide), optNoi: { expectedVct: 'urn:eudi:pid:2' }, optLor: { expectedVct: 'urn:eudi:pid:2' }, motivNoi: /not the type expected/, motivLor: /Invalid VCT/ }); await neg('NEGATIV: fara lista, la noi implicit (requireStatus:true): refuz numit; la ei disableStatusVerification: valid', { lista: null, motivNoi: /was not judged/, asteptatLor: 'VALID', nota: 'politica: noi nu dam VALID pe o stare nejudecata fara ca verificatorul sa o ceara explicit (requireStatus:false); ei lasa decizia pe disableStatusVerification' }); await neg('fara lista, la noi cu requireStatus:false: valid, cu starea raportata nejudecata', { lista: null, optNoi: { requireStatus: false }, asteptatNoi: 'VALID', asteptatLor: 'VALID' }); const altAud = await lor({ kbSigner: semnatorLor(hol), kbSignAlg: 'EdDSA' }).present(sd, prezFrame, { kb: { payload: { aud: 'https://other.example', nonce: NONCE, iat: CEAS } } }); await neg('NEGATIV: KB pentru alt aud', { jeton: altAud, lista: await listaLor(iss, toateValide), motivNoi: /made for/, motivLor: /Invalid Key Binding audience/ }); // un credential LOR fara vct: la noi refuzat (vct REQUIRED, s.2.2.2.3); la ei cu expectedVct refuzat const { vct: _v, ...faraVct } = claimsLor; const fv = await lorEmitePrezinta('EdDSA', faraVct); const nFv = noiVerifica(fv.p, { issuerKey: fv.iss.jwk, alg: 'EdDSA', statusListJwt: await listaLor(fv.iss, toateValide) }), lFv = await lorVerifica(fv.p, { issuerJwk: fv.iss.jwk, alg: 'EdDSA', statusListJwt: await listaLor(fv.iss, toateValide) }); rand({ sens: 'lor->noi', caz: 'NEGATIV: SD-JWT VC al LOR FARA vct (emitentul lor il lasa afara)', noi: nFv, lor: lFv, asteptat: { noi: 'REFUZ', lor: 'REFUZ', motivNoi: /vct is required/, motivLor: /Invalid VCT/ }, nota: 'al lor: SDJwtVcInstance.issue nu cere vct; verificatorul lor il refuza numai daca i se da expectedVct' }); const lFvFaraAsteptare = await lorVerifica(fv.p, { issuerJwk: fv.iss.jwk, alg: 'EdDSA', statusListJwt: await listaLor(fv.iss, toateValide), optiuni: { expectedVct: undefined } }); rand({ sens: 'lor->noi', caz: 'DIFERENTA: acelasi SD-JWT VC fara vct, verificatorul LOR fara expectedVct', noi: nFv, lor: lFvFaraAsteptare, asteptat: { noi: 'REFUZ', lor: 'VALID' }, nota: 'al lor: fara expectedVct nu cere vct deloc; la noi expectedVct e obligatoriu si vct e cerut in jeton (s.2.2.2.3 REQUIRED)' }); } const ok = randuri.filter((r) => r.ok).length; console.log(`\ninterop-sdjwt-vc: ${ok}/${randuri.length} randuri cu verdictele asteptate de amandoua partile${divergente ? `, ${divergente} DIVERGENTE` : ''}`); if (SCRIE) { const generat = new Date().toISOString(); fs.writeFileSync(path.join(AICI, 'fixturi-interop-sdjwt-vc.json'), JSON.stringify({ generat, unealta: 'tools/aere-identity/interop/interop-sdjwt-vc.mjs --scrie', nota: 'AERE-SINTETIC: credentiale emise de @sd-jwt/sd-jwt-vc si liste de stare de @owf/token-status-list (semnate prin WebCrypto/jose) cu chei de PROBA nascute la generare; numai cheile publice sunt aici. Judecate offline de proba-sdjwt-vc.mjs la ceasVerificare.', biblioteci, ceas: CEAS, ceasVerificare: ACUM, audience: AUD, nonce: NONCE, vct: VCT, cazuri: fixturi }, null, 1) + '\n'); fs.writeFileSync(path.join(AICI, 'rezultate-interop-sdjwt-vc.json'), JSON.stringify({ generat, biblioteci, ceas: CEAS, randuri: randuri.map(({ sens, caz, asteptat, noi, lor, ok, nota }) => ({ sens, caz, asteptat, noi, lor, ok, nota })) }, null, 1) + '\n'); console.log(`scrise: fixturi-interop-sdjwt-vc.json (${fixturi.length} cazuri), rezultate-interop-sdjwt-vc.json (${randuri.length} randuri)`); } return divergente ? 1 : 0; } main().then((c) => { process.exitCode = c; }, (e) => { console.log('STRICAT: ' + (e.stack || e)); process.exitCode = 2; });