aere-quantum/identity/interop/interop-sdjwt-vc.mjs

203 lines
25 KiB
JavaScript

#!/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 <dosar cu node_modules> [--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 <dosar cu node_modules> 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; });