UNPKG

@sd-jwt/core

Version:

sd-jwt draft 7 implementation in typescript

975 lines (883 loc) 25.6 kB
import Crypto, { type KeyLike } from 'node:crypto'; import { hasher as digest, ES256, generateSalt } from '@owf/crypto'; import { exportJWK, importJWK, type JWK } from 'jose'; import { describe, expect, test } from 'vitest'; import { SDJwtInstance, type SdJwtPayload } from '../index'; import type { JwtPayload, KbVerifier, Signer, Verifier } from '../types'; // Extract the major version as a number const nodeVersionMajor = Number.parseInt( process.version.split('.')[0].substring(1), 10, ); const createSignerVerifier = () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const signer: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const verifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; return { signer, verifier }; }; describe('index', () => { test('create', async () => { const sdjwt = new SDJwtInstance<SdJwtPayload>(); expect(sdjwt).toBeDefined(); }); test('kbJwt', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, kbSigner: signer, kbSignAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); expect(credential).toBeDefined(); const presentation = await sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '1', iat: 1, nonce: '342', }, }, }, ); expect(presentation).toBeDefined(); }); test('issue', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); expect(credential).toBeDefined(); }); test('issue rejects reserved disclosure frame keys', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, }); await expect( sdjwt.issue( { foo: 'bar', _sd: 'reserved', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ), ).rejects.toThrow('Reserved field name "_sd" is not allowed'); }); test('verify failed', async () => { const { signer } = createSignerVerifier(); const { publicKey } = Crypto.generateKeyPairSync('ed25519'); const failedverifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier: failedverifier, hasher: digest, saltGenerator: generateSalt, }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); try { await sdjwt.verify(credential); } catch (e) { expect(e).toBeDefined(); } }); test('verify failed with kbJwt', async () => { const { signer, verifier } = createSignerVerifier(); const { publicKey } = Crypto.generateKeyPairSync('ed25519'); const failedverifier: Verifier = async (data: string, sig: string) => { return Crypto.verify( null, Buffer.from(data), publicKey, Buffer.from(sig, 'base64url'), ); }; const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, kbSigner: signer, kbVerifier: failedverifier, kbSignAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); const presentation = await sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '', iat: 1, nonce: '342', }, }, }, ); try { await sdjwt.verify(presentation); } catch (e) { expect(e).toBeDefined(); } }); test('verify with kbJwt', async () => { const { signer, verifier } = createSignerVerifier(); const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); //TODO: maybe we can pass a minial class of the jwt to pass the token const kbVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { let publicKey: JsonWebKey; if (payload.cnf) { // use the key from the cnf publicKey = payload.cnf.jwk; } else { throw Error('key binding not supported'); } // get the key of the holder to verify the signature return Crypto.verify( null, Buffer.from(data), (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbSigner = (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, kbSigner: kbSigner, kbVerifier: kbVerifier, kbSignAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iat: Math.floor(Date.now() / 1000), cnf: { jwk: await exportJWK(publicKey), }, }, { _sd: ['foo'], }, ); const presentation = await sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '1', iat: 1, nonce: '342', }, }, }, ); const results = await sdjwt.verify(presentation, { requiredClaimKeys: ['foo'], }); expect(results).toBeDefined(); }); test('verify rejects an expired kbJwt during full verification', async () => { const { signer, verifier } = createSignerVerifier(); const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const kbVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { if (!payload.cnf) throw Error('key binding not supported'); return Crypto.verify( null, Buffer.from(data), (await importJWK(payload.cnf.jwk as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbSigner = (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, kbSigner, kbVerifier, kbSignAlg: 'EdDSA', }); const now = Math.floor(Date.now() / 1000); const credential = await sdjwt.issue( { foo: 'bar', iat: now, cnf: { jwk: await exportJWK(publicKey) }, }, { _sd: ['foo'] }, ); const presentation = await sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '1', iat: now - 7200, nonce: '342', // kb+jwt expired one hour ago exp: now - 3600, } as never, }, }, ); await expect( sdjwt.verify(presentation, { requiredClaimKeys: ['foo'], keyBindingNonce: '342', }), ).rejects.toThrow('Verify Error: JWT is expired'); }); test('verify accepts a kbJwt with a future exp during full verification', async () => { const { signer, verifier } = createSignerVerifier(); const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const kbVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { if (!payload.cnf) throw Error('key binding not supported'); return Crypto.verify( null, Buffer.from(data), (await importJWK(payload.cnf.jwk as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbSigner = (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, kbSigner, kbVerifier, kbSignAlg: 'EdDSA', }); const now = Math.floor(Date.now() / 1000); const credential = await sdjwt.issue( { foo: 'bar', iat: now, cnf: { jwk: await exportJWK(publicKey) }, }, { _sd: ['foo'] }, ); const presentation = await sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '1', iat: now, nonce: '342', exp: now + 3600, } as never, }, }, ); const results = await sdjwt.verify(presentation, { requiredClaimKeys: ['foo'], keyBindingNonce: '342', }); expect(results.kb).toBeDefined(); }); test('Hasher not found', async () => { const sdjwt = new SDJwtInstance<SdJwtPayload>({}); try { const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); expect(credential).toBeDefined(); } catch (e) { expect(e).toBeDefined(); } }); test('decode rejects invalid _sd_alg values', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); const parts = credential.split('.'); const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()); payload._sd_alg = 'SHA-256'; parts[1] = Buffer.from(JSON.stringify(payload)).toString('base64url'); await expect(sdjwt.decode(parts.join('.'))).rejects.toThrow( 'Invalid _sd_alg: SHA-256', ); }); test('SaltGenerator not found', async () => { const sdjwt = new SDJwtInstance<SdJwtPayload>({ hasher: digest, }); try { const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); expect(credential).toBeDefined(); } catch (e) { expect(e).toBeDefined(); } }); test('Signer not found', async () => { const sdjwt = new SDJwtInstance<SdJwtPayload>({ hasher: digest, saltGenerator: generateSalt, }); try { const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); expect(credential).toBeDefined(); } catch (e) { expect(e).toBeDefined(); } }); test('Verifier not found', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, hasher: digest, saltGenerator: generateSalt, kbSigner: signer, kbVerifier: verifier, signAlg: 'EdDSA', kbSignAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); const presentation = await sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '1', iat: 1, nonce: '342', }, }, }, ); try { await sdjwt.verify(presentation, { requiredClaimKeys: ['foo'] }); } catch (e) { expect(e).toBeDefined(); } }); test('kbSigner not found', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, verifier, hasher: digest, saltGenerator: generateSalt, kbVerifier: verifier, signAlg: 'EdDSA', kbSignAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); try { await sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '1', iat: 1, nonce: '342', }, }, }, ); } catch (e) { expect(e).toBeDefined(); } }); test('kbVerifier not found', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, verifier, hasher: digest, saltGenerator: generateSalt, kbSigner: signer, signAlg: 'EdDSA', kbSignAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); const presentation = await sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '1', iat: 1, nonce: '342', }, }, }, ); try { await sdjwt.verify(presentation, { requiredClaimKeys: ['foo'] }); } catch (e) { expect(e).toBeDefined(); } }); test('kbSignAlg not found', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, verifier, hasher: digest, saltGenerator: generateSalt, kbSigner: signer, signAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); const presentation = sdjwt.present( credential, { foo: true }, { kb: { payload: { aud: '1', iat: 1, nonce: '342', }, }, }, ); await expect(presentation).rejects.toThrow( 'Key Binding sign algorithm not specified', ); }); test('hasher is not found', async () => { const { signer } = createSignerVerifier(); const sdjwt_create = new SDJwtInstance<SdJwtPayload>({ signer, hasher: digest, saltGenerator: generateSalt, signAlg: 'EdDSA', }); const credential = await sdjwt_create.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); const sdjwt = new SDJwtInstance<SdJwtPayload>({}); await expect(sdjwt.keys('')).rejects.toThrow('Hasher not found'); await expect(sdjwt.presentableKeys('')).rejects.toThrow('Hasher not found'); await expect(sdjwt.getClaims('')).rejects.toThrow('Hasher not found'); expect(() => sdjwt.decode('')).toThrowError('Hasher not found'); await expect(sdjwt.present(credential, { foo: true })).rejects.toThrow( 'Hasher not found', ); }); test('presentableKeys', async () => { const { signer } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, hasher: digest, saltGenerator: generateSalt, signAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); const keys = await sdjwt.presentableKeys(credential); expect(keys).toBeDefined(); expect(keys).toEqual(['foo']); }); test('present all disclosures with kb jwt', async () => { const { signer } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, kbSigner: signer, hasher: digest, saltGenerator: generateSalt, signAlg: 'EdDSA', kbSignAlg: 'EdDSA', }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), vct: '', }, { _sd: ['foo'], }, ); const presentation = await sdjwt.present(credential, undefined, { kb: { payload: { aud: '1', iat: 1, nonce: '342', }, }, }); const decoded = await sdjwt.decode(presentation); expect(decoded.jwt).toBeDefined(); expect(decoded.disclosures).toBeDefined(); expect(decoded.kbJwt).toBeDefined(); }); (nodeVersionMajor < 20 ? test.skip : test)( 'validate sd-jwt that created in other implemenation', async () => { const publicKeyExampleJwt: JsonWebKey = { kty: 'EC', crv: 'P-256', x: 'b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ', y: 'Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8', }; const kbPubkey: JsonWebKey = { kty: 'EC', crv: 'P-256', x: 'TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc', y: 'ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ', }; const encodedJwt = 'eyJhbGciOiAiRVMyNTYiLCAidHlwIjogInZjK3NkLWp3dCIsICJraWQiOiAiZG9jLXNpZ25lci0wNS0yNS0yMDIyIn0.eyJfc2QiOiBbIjA5dktySk1PbHlUV00wc2pwdV9wZE9CVkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9kYXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9pZUxaaGg3WGJVVHRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNONndxUVhDaXciLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiamRyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5eVZ1bHdRUWxoRmxNXzNKbHpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2Y3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxlLmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.QXgzrePAdq_WZVGCwDxP-l8h0iyckrHBNidxVqGtKJ0LMzObqgaXUD1cgGEf7d9TexPkBcgQYqjuzlfbeCxxuA~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6ICIxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE3MDk5OTYxODUsICJzZF9oYXNoIjogIjc4cFFEazJOblNEM1dKQm5SN015aWpmeUVqcGJ5a01yRnlpb2ZYSjlsN0kifQ.7k4goAlxM4a3tHnvCBCe70j_I-BCwtzhBRXQNk9cWJnQWxxt2kIqCyzcwzzUc0gTwtbGWVQoeWCiL5K6y3a4VQ'; const sdjwt = new SDJwtInstance({ hasher: digest, verifier: await ES256.getVerifier(publicKeyExampleJwt), kbVerifier: await ES256.getVerifier(kbPubkey), }); const decode = await sdjwt.verify(encodedJwt); expect(decode).toBeDefined(); }, ); test('safeVerify - success case', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), }, { _sd: ['foo'], }, ); const result = await sdjwt.safeVerify(credential); expect(result.success).toBe(true); if (result.success) { expect(result.data.payload).toBeDefined(); expect(result.data.payload.foo).toBe('bar'); } }); test('safeVerify - collect multiple errors', async () => { const sdjwt = new SDJwtInstance<SdJwtPayload>({}); const result = await sdjwt.safeVerify('invalid.jwt.token'); expect(result.success).toBe(false); if (!result.success) { // Should have multiple errors: hasher not found, verifier not found expect(result.errors.length).toBeGreaterThanOrEqual(2); const errorCodes = result.errors.map((e) => e.code); expect(errorCodes).toContain('HASHER_NOT_FOUND'); expect(errorCodes).toContain('VERIFIER_NOT_FOUND'); } }); test('safeVerify - invalid signature error', async () => { const { signer } = createSignerVerifier(); const { verifier: wrongVerifier } = createSignerVerifier(); // Different key pair const issuer = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', hasher: digest, saltGenerator: generateSalt, }); const verifierInstance = new SDJwtInstance<SdJwtPayload>({ verifier: wrongVerifier, hasher: digest, }); const credential = await issuer.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), }, { _sd: ['foo'], }, ); const result = await verifierInstance.safeVerify(credential); expect(result.success).toBe(false); if (!result.success) { // The error message from the verifier contains 'signature' which maps to INVALID_JWT_SIGNATURE const hasSignatureError = result.errors.some( (e) => e.code === 'INVALID_JWT_SIGNATURE' || e.message.toLowerCase().includes('signature'), ); expect(hasSignatureError).toBe(true); } }); test('safeVerify - expired JWT error', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago exp: Math.floor(Date.now() / 1000) - 1800, // Expired 30 min ago }, { _sd: ['foo'], }, ); const result = await sdjwt.safeVerify(credential); expect(result.success).toBe(false); if (!result.success) { expect(result.errors.some((e) => e.code === 'JWT_EXPIRED')).toBe(true); } }); test('safeVerify - missing required claims error', async () => { const { signer, verifier } = createSignerVerifier(); const sdjwt = new SDJwtInstance<SdJwtPayload>({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, }); const credential = await sdjwt.issue( { foo: 'bar', iss: 'Issuer', iat: Math.floor(Date.now() / 1000), }, { _sd: ['foo'], }, ); // Present without disclosing 'foo' const presentation = await sdjwt.present(credential, {}); const result = await sdjwt.safeVerify(presentation, { requiredClaimKeys: ['foo', 'missing_claim'], }); expect(result.success).toBe(false); if (!result.success) { expect( result.errors.some((e) => e.code === 'MISSING_REQUIRED_CLAIMS'), ).toBe(true); } }); });