diff --git a/package.json b/package.json index 19f64e9..a6561cc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "lint": "biome lint ./packages", "format": "biome format . --write", "test": "vitest run --coverage.enabled=true --coverage.include=packages/*", + "test:watch": "vitest", "clean": "lerna clean -y", "publish:latest": "lerna publish --no-private --conventional-commits --include-merged-tags --create-release github --yes --dist-tag latest", "publish:next": "lerna publish --no-private --conventional-prerelease --force-publish --canary --no-git-tag-version --include-merged-tags --preid next --pre-dist-tag next --yes", @@ -34,6 +35,7 @@ "@biomejs/biome": "1.5.3", "@types/node": "^20.10.2", "@vitest/coverage-v8": "^1.2.2", + "jose": "^5.2.2", "jsdom": "^24.0.0", "lerna": "^8.1.2", "ts-node": "^10.9.1", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8cdec5a..1fdf44d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,6 +11,7 @@ import { SDJWTConfig, } from '@sd-jwt/types'; import { getSDAlgAndPayload } from '@sd-jwt/decode'; +import { JwtPayload } from '@sd-jwt/types'; export * from './sdjwt'; export * from './kbjwt'; @@ -212,7 +213,13 @@ export class SDJwtInstance { if (!this.userConfig.kbVerifier) { throw new SDJWTException('Key Binding Verifier not found'); } - const kb = await sdjwt.kbJwt.verify(this.userConfig.kbVerifier); + const kb = await sdjwt.kbJwt.verifyKB({ + verifier: this.userConfig.kbVerifier, + payload: payload as JwtPayload, + }); + if (!kb) { + throw new Error('signature is not valid'); + } const sdHashfromKb = kb.payload.sd_hash; const sdjwtWithoutKb = new SDJwt({ jwt: sdjwt.jwt, diff --git a/packages/core/src/kbjwt.ts b/packages/core/src/kbjwt.ts index 3ca1d36..6230eac 100644 --- a/packages/core/src/kbjwt.ts +++ b/packages/core/src/kbjwt.ts @@ -1,13 +1,14 @@ -import { SDJWTException } from '@sd-jwt/utils'; +import { Base64urlEncode, SDJWTException } from '@sd-jwt/utils'; import { Jwt } from './jwt'; -import { Verifier, kbHeader, kbPayload } from '@sd-jwt/types'; +import { JwtPayload, kbHeader, kbPayload, KbVerifier } from '@sd-jwt/types'; export class KBJwt< Header extends kbHeader = kbHeader, Payload extends kbPayload = kbPayload, > extends Jwt { // Checking the validity of the key binding jwt - public async verify(verifier: Verifier) { + // the type unknown is not good, but we don't know at this point how to get the public key of the signer, this is defined in the kbVerifier + public async verifyKB(values: { verifier: KbVerifier; payload: JwtPayload }) { if ( !this.header?.alg || !this.header.typ || @@ -22,7 +23,22 @@ export class KBJwt< ) { throw new SDJWTException('Invalid Key Binding Jwt'); } - return await super.verify(verifier); + if (!this.header || !this.payload || !this.signature) { + throw new SDJWTException('Verify Error: Invalid JWT'); + } + + const header = Base64urlEncode(JSON.stringify(this.header)); + const payload = Base64urlEncode(JSON.stringify(this.payload)); + const data = `${header}.${payload}`; + const verified = await values.verifier( + data, + this.signature, + values.payload, + ); + if (!verified) { + throw new SDJWTException('Verify Error: Invalid JWT Signature'); + } + return { payload: this.payload, header: this.header }; } // This function is for creating KBJwt object for verify properly diff --git a/packages/core/src/test/index.spec.ts b/packages/core/src/test/index.spec.ts index d82d5e3..16d1ab6 100644 --- a/packages/core/src/test/index.spec.ts +++ b/packages/core/src/test/index.spec.ts @@ -1,8 +1,10 @@ import { SDJwtInstance, SdJwtPayload } from '../index'; import { Signer, Verifier } from '@sd-jwt/types'; -import Crypto from 'node:crypto'; +import Crypto, { KeyLike } from 'node:crypto'; import { describe, expect, test } from 'vitest'; import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; +import { KbVerifier, JwtPayload } from '@sd-jwt/types'; +import { importJWK, exportJWK, JWK } from 'jose'; export const createSignerVerifier = () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); @@ -181,23 +183,53 @@ describe('index', () => { 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({ signer, signAlg: 'EdDSA', verifier, hasher: digest, saltGenerator: generateSalt, - kbSigner: signer, - kbVerifier: verifier, + kbSigner: kbSigner, + kbVerifier: kbVerifier, kbSignAlg: 'EdDSA', }); - const credential = await sdjwt.issue( { foo: 'bar', - iss: 'Issuer', iat: new Date().getTime(), - vct: '', + cnf: { + jwk: await exportJWK(publicKey), + }, }, { _sd: ['foo'], diff --git a/packages/core/src/test/kbjwt.spec.ts b/packages/core/src/test/kbjwt.spec.ts index 4b079bc..ec23b8e 100644 --- a/packages/core/src/test/kbjwt.spec.ts +++ b/packages/core/src/test/kbjwt.spec.ts @@ -1,8 +1,15 @@ import { SDJWTException } from '@sd-jwt/utils'; import { KBJwt } from '../kbjwt'; -import { KB_JWT_TYP, Signer, Verifier } from '@sd-jwt/types'; -import Crypto from 'node:crypto'; +import { + JwtPayload, + KB_JWT_TYP, + KbVerifier, + Signer, + Verifier, +} from '@sd-jwt/types'; +import Crypto, { KeyLike } from 'node:crypto'; import { describe, expect, test } from 'vitest'; +import { JWK, exportJWK, importJWK } from 'jose'; describe('KB JWT', () => { test('create', async () => { @@ -69,11 +76,27 @@ describe('KB JWT', () => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; - const testVerifier: Verifier = async (data: string, sig: string) => { + + 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), - publicKey, + (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; @@ -91,7 +114,10 @@ describe('KB JWT', () => { }); const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); - const verified = await decoded.verify(testVerifier); + const verified = await decoded.verifyKB({ + verifier: testVerifier, + payload, + }); expect(verified).toStrictEqual({ header: { typ: KB_JWT_TYP, @@ -112,11 +138,26 @@ describe('KB JWT', () => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; - const testVerifier: Verifier = async (data: string, sig: string) => { + + 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), - publicKey, + (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; @@ -136,26 +177,34 @@ describe('KB JWT', () => { const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); try { - await decoded.verify(testVerifier); + await decoded.verifyKB({ verifier: testVerifier, payload }); } catch (e: unknown) { const error = e as SDJWTException; expect(error.message).toBe('Invalid Key Binding Jwt'); } }); - test('verify with custom Verifier', async () => { + 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 testVerifier: Verifier = async (data: string, sig: string) => { - return Crypto.verify( - null, - Buffer.from(data), - publicKey, - Buffer.from(sig, '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({ @@ -170,37 +219,37 @@ describe('KB JWT', () => { sd_hash: 'hash', }, }); - const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); - const verified = await decoded.verify(testVerifier); - expect(verified).toStrictEqual({ - header: { - typ: KB_JWT_TYP, - alg: 'EdDSA', - }, - payload: { - iat: 1, - aud: 'aud', - nonce: 'nonce', - sd_hash: 'hash', - }, - }); + try { + await decoded.verifyKB({ verifier: testVerifier, payload }); + } catch (e: unknown) { + const error = e as SDJWTException; + expect(error.message).toBe('Verify Error: Invalid JWT Signature'); + } }); - test('verify failed with custom Verifier', async () => { + 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 testVerifier: Verifier = async (data: string, sig: string) => { - return Crypto.verify( - null, - Buffer.from(data), - publicKey, - Buffer.from(sig, '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({ @@ -212,17 +261,17 @@ describe('KB JWT', () => { iat: 1, aud: 'aud', nonce: 'nonce', - sd_hash: '', + sd_hash: 'hash', }, }); - const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); + decoded.signature = undefined; try { - await decoded.verify(testVerifier); + await decoded.verifyKB({ verifier: testVerifier, payload }); } catch (e: unknown) { const error = e as SDJWTException; - expect(error.message).toBe('Invalid Key Binding Jwt'); + expect(error.message).toBe('Verify Error: Invalid JWT'); } }); @@ -232,11 +281,25 @@ describe('KB JWT', () => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); return Buffer.from(sig).toString('base64url'); }; - const testVerifier: Verifier = async (data: string, sig: string) => { + 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), - publicKey, + (await importJWK(publicKey as JWK, 'EdDSA')) as KeyLike, Buffer.from(sig, 'base64url'), ); }; @@ -258,7 +321,10 @@ describe('KB JWT', () => { const encodedKbJwt = await kbJwt.sign(testSigner); const decoded = KBJwt.fromKBEncode(encodedKbJwt); - const verified = await decoded.verify(testVerifier); + const verified = await decoded.verifyKB({ + verifier: testVerifier, + payload, + }); expect(verified).toStrictEqual({ header: { typ: KB_JWT_TYP, diff --git a/packages/node-crypto/src/crypto.ts b/packages/node-crypto/src/crypto.ts index d260512..b18f539 100644 --- a/packages/node-crypto/src/crypto.ts +++ b/packages/node-crypto/src/crypto.ts @@ -1,4 +1,4 @@ -import { createHash, randomBytes } from 'crypto'; +import { createHash, randomBytes, subtle } from 'crypto'; export const generateSalt = (length: number): string => { if (length <= 0) { @@ -24,7 +24,6 @@ export const ES256 = { alg: 'ES256', async generateKeyPair() { - const { subtle } = globalThis.crypto; const keyPair = await subtle.generateKey( { name: 'ECDSA', @@ -42,7 +41,6 @@ export const ES256 = { }, async getSigner(privateKeyJWK: object) { - const { subtle } = globalThis.crypto; const privateKey = await subtle.importKey( 'jwk', privateKeyJWK, @@ -73,7 +71,6 @@ export const ES256 = { }, async getVerifier(publicKeyJWK: object) { - const { subtle } = globalThis.crypto; const publicKey = await subtle.importKey( 'jwk', publicKeyJWK, diff --git a/packages/types/src/type.ts b/packages/types/src/type.ts index 1f38f02..b5f3918 100644 --- a/packages/types/src/type.ts +++ b/packages/types/src/type.ts @@ -19,7 +19,7 @@ export type SDJWTConfig = { verifier?: Verifier; kbSigner?: Signer; kbSignAlg?: string; - kbVerifier?: Verifier; + kbVerifier?: KbVerifier; }; export type kbHeader = { typ: 'kb+jwt'; alg: string }; @@ -34,10 +34,22 @@ export type KBOptions = { payload: Omit; }; +export interface JwtPayload { + cnf?: { + jwk: JsonWebKey; + }; + [key: string]: unknown; +} + export type OrPromise = T | Promise; export type Signer = (data: string) => OrPromise; export type Verifier = (data: string, sig: string) => OrPromise; +export type KbVerifier = ( + data: string, + sig: string, + payload: JwtPayload, +) => OrPromise; export type Hasher = (data: string, alg: string) => OrPromise; export type SaltGenerator = (length: number) => OrPromise; export type HasherAndAlg = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5335220..1812cef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@vitest/coverage-v8': specifier: ^1.2.2 version: 1.3.1(vitest@1.3.1) + jose: + specifier: ^5.2.2 + version: 5.2.2 jsdom: specifier: ^24.0.0 version: 24.0.0 @@ -3171,6 +3174,10 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /jose@5.2.2: + resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==} + dev: true + /joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'}