UNPKG

@sd-jwt/core

Version:

sd-jwt draft 7 implementation in typescript

566 lines (522 loc) 14.5 kB
import Crypto, { type KeyLike } from 'node:crypto'; import { exportJWK, importJWK, type JWK } from 'jose'; import { describe, expect, test } from 'vitest'; import { KBJwt } from '../kbjwt'; import { type JwtPayload, KB_JWT_TYP, type KbVerifier, type kbPayload, type Signer, } from '../types'; import type { SDJWTException } from '../utils'; describe('KB JWT', () => { test('create', async () => { const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }, }); expect(kbJwt.header).toEqual({ typ: KB_JWT_TYP, alg: 'EdDSA', }); expect(kbJwt.payload).toEqual({ iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }); }); test('decode', async () => { const { privateKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); expect(decoded.header).toEqual({ typ: KB_JWT_TYP, alg: 'EdDSA', }); expect(decoded.payload).toEqual({ iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }); }); test('verify', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { expect(payload).toStrictEqual(payload); expect(payload.cnf?.jwk).toBeDefined(); const publicKey = payload.cnf?.jwk; return Crypto.verify( null, Buffer.from(data), (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); const verified = await decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', }); expect(verified).toStrictEqual({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }, }); }); test('verify failed', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { expect(payload).toStrictEqual(payload); expect(payload.cnf?.jwk).toBeDefined(); const publicKey = payload.cnf?.jwk; return Crypto.verify( null, Buffer.from(data), (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: '', }, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); try { await decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', }); } catch (e: unknown) { const error = e as SDJWTException; expect(error.message).toBe('Invalid Key Binding Jwt'); } }); test('verify failed with verifier return false', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( _data: string, _sig: string, payload: JwtPayload, ) => { expect(payload).toStrictEqual(payload); expect(payload.cnf?.jwk).toBeDefined(); return false; }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); try { await decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', }); } catch (e: unknown) { const error = e as SDJWTException; expect(error.message).toBe('Verify Error: Invalid JWT Signature'); } }); test('verify failed with invalid jwt', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( _data: string, _sig: string, payload: JwtPayload, ) => { expect(payload).toStrictEqual(payload); expect(payload.cnf?.jwk).toBeDefined(); return false; }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); decoded.signature = undefined; try { await decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', }); } catch (e: unknown) { const error = e as SDJWTException; expect(error.message).toBe('Verify Error: Invalid JWT'); } }); test('verify failed with expired exp claim', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { const publicKey = payload.cnf?.jwk; return Crypto.verify( null, Buffer.from(data), (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', // expired one hour before the current date used below exp: 1000, } as kbPayload, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); await expect( decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', options: { currentDate: 5000 }, }), ).rejects.toThrow('Verify Error: JWT is expired'); }); test('verify failed with iat in the future', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { const publicKey = payload.cnf?.jwk; return Crypto.verify( null, Buffer.from(data), (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 5000, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); await expect( decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', // iat (5000) is after the current date (1000) options: { currentDate: 1000 }, }), ).rejects.toThrow('Verify Error: JWT is not yet valid'); }); test('verify failed with nbf in the future', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { const publicKey = payload.cnf?.jwk; return Crypto.verify( null, Buffer.from(data), (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', nbf: 5000, } as kbPayload, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); await expect( decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', options: { currentDate: 1000 }, }), ).rejects.toThrow('Verify Error: JWT is not yet valid'); }); test('verify succeeds for expired exp within the allowed skew', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { const publicKey = payload.cnf?.jwk; return Crypto.verify( null, Buffer.from(data), (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', exp: 1000, } as kbPayload, }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); // exp (1000) is before the current date (1100) but within the 200s skew const verified = await decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', options: { currentDate: 1100, skewSeconds: 200 }, }); expect(verified.payload.exp).toBe(1000); }); test('compatibility test for version 06', async () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const testSigner: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; const payload = { cnf: { jwk: await exportJWK(publicKey), }, }; const testVerifier: KbVerifier = async ( data: string, sig: string, payload: JwtPayload, ) => { expect(payload).toStrictEqual(payload); expect(payload.cnf?.jwk).toBeDefined(); const publicKey = payload.cnf?.jwk; return Crypto.verify( null, Buffer.from(data), (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; const kbJwt = new KBJwt({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', sd_hash: 'hash', }, }); (kbJwt.payload as Record<string, unknown>)._sd_hash = 'hash'; (kbJwt.payload as Record<string, unknown>).sd_hash = undefined; const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); const verified = await decoded.verifyKB({ verifier: testVerifier, payload, nonce: 'nonce', }); expect(verified).toStrictEqual({ header: { typ: KB_JWT_TYP, alg: 'EdDSA', }, payload: { iat: 1, aud: 'aud', nonce: 'nonce', _sd_hash: 'hash', }, }); }); });