203 lines
25 KiB
JavaScript
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; });
|