aere-quantum/identity/proba-sdjwt.mjs
Aere Network 4ffec8d7e3 identity: standard SD-JWT (IETF RFC 9901) from the same keys - issue, present with key binding, verify
sdjwt.mjs: an issuer-signed JWT with the digests of the disclosable claims (_sd, list elements as {"...": digest}, _sd_alg sha-256),
the holder's key in cnf.jwk and exp, followed by the disclosures; the holder keeps the ones it picks and adds a Key Binding JWT
(kb+jwt, aud, nonce, iat, sd_hash); the verifier follows RFC 9901 sections 7.1 and 7.3 with the issuer key it chose, and requires its
audience and nonce, exp, and an explicitly typed token. Algorithms: ES256, EdDSA (Ed25519, the key every AERE identity has) and
ML-DSA-65 (JWK type AKP, names from the IETF draft draft-ietf-cose-dilithium, not yet a published standard).

Checked against the standard's own vectors (fixturi-rfc9901.json, from RFC 9901 Section 5 and Appendix A.5): the ten example
disclosure digests, the example SD-JWT signed by someone else with the A.5 key, and the example presentation with key binding, which
gives exactly the processed payload printed in the RFC. Not checked against another SD-JWT library.

An adversarial review before publication found ten defects, all fixed with a test and a planted negative control each (among them:
the issuer signed SD-JWT structure coming in claim values; the holder revealed an element with the same value from any list; audience
and nonce were not required). Tests: SD-JWT 22/22, negative control 28/28; identity 44/44.
2026-09-30 12:37:17 +03:00

198 lines
19 KiB
JavaScript

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