Skip to content

Commit

Permalink
Support 07 version of standard and new features for v2.0 (openwallet-…
Browse files Browse the repository at this point in the history
…foundation#46)

Signed-off-by: Lukas <[email protected]>
  • Loading branch information
lukasjhan authored Feb 1, 2024
1 parent 6aa790c commit b43afa7
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 49 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This is the reference implmentation of [IETF SD-JWT specification](https://data

Hopae, a founding member of OpenWallet Foundation, is building wallet module in Typesript and need this project as a core component.

Currently compliant with: **draft-ietf-oauth-selective-disclosure-jwt-06**
Currently compliant with: **draft-ietf-oauth-selective-disclosure-jwt-07**

## **Background**

Expand Down
2 changes: 1 addition & 1 deletion examples/kb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const createKeyPair = () => {
aud: 'https://example.com',
nonce: '1234',
custom: 'data',
_sd_hash: '1234',
sd_hash: '1234',
};

const encodedSdjwt = await sdjwt.issue(claims, privateKey, disclosureFrame, {
Expand Down
38 changes: 18 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { generateSalt, digest, getHasher } from './crypto';
import { SDJWTException } from './error';
import { Jwt } from './jwt';
import { KBJwt } from './kbjwt';
import { SDJwt, pack } from './sdjwt';
Expand All @@ -25,6 +26,8 @@ export const defaultConfig: Required<SDJWTConfig> = {
omitTyp: false,
hasher: digest,
saltGenerator: generateSalt,
signer: null,
verifier: null,
};

export class SDJwtInstance {
Expand Down Expand Up @@ -135,48 +138,43 @@ export class SDJwtInstance {
publicKey: Uint8Array | KeyLike;
};
},
): Promise<boolean> {
) {
const sdjwt = SDJwt.fromEncode(encodedSDJwt);
if (!sdjwt.jwt) {
return false;
}
const validated = await this.validate(encodedSDJwt, publicKey);
if (!validated) {
return false;
throw new SDJWTException('Invalid SD JWT');
}
const { payload, header } = await this.validate(encodedSDJwt, publicKey);

if (requiredClaimKeys) {
const keys = await sdjwt.keys();
const missingKeys = requiredClaimKeys.filter((k) => !keys.includes(k));
if (missingKeys.length > 0) {
return false;
throw new SDJWTException(
'Missing required claim keys: ' + missingKeys.join(', '),
);
}
}

if (options?.kb) {
if (!sdjwt.kbJwt) {
return false;
}
const kbVerified = await sdjwt.kbJwt.verify(options.kb.publicKey);
if (!kbVerified) {
return false;
throw new SDJWTException('Key Binding JWT not exist');
}
const kb = await sdjwt.kbJwt.verify(options.kb.publicKey);
return { payload, header, kb };
}

return true;
return { payload, header };
}

public async validate(
encodedSDJwt: string,
publicKey: Uint8Array | KeyLike,
): Promise<boolean> {
public async validate(encodedSDJwt: string, publicKey: Uint8Array | KeyLike) {
const sdjwt = SDJwt.fromEncode(encodedSDJwt);
if (!sdjwt.jwt) {
return false;
throw new SDJWTException('Invalid SD JWT');
}

const verified = await sdjwt.jwt.verify(publicKey);
return verified;
const verifiedPayloads = await sdjwt.jwt.verify(publicKey);
const claims = await sdjwt.getClaims();
return { payload: claims, header: verifiedPayloads.header };
}

public config(newConfig: SDJWTConfig) {
Expand Down
35 changes: 32 additions & 3 deletions src/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Base64Url } from './base64url';
import { SDJWTException } from './error';
import * as jose from 'jose';
import { Base64urlString } from './type';
import { Base64urlString, Signer, Verifier } from './type';

export type JwtData<
Header extends Record<string, any>,
Expand Down Expand Up @@ -86,6 +86,19 @@ export class Jwt<
return encodedJwt;
}

public async signWithSigner(signer: Signer) {
if (!this.header || !this.payload) {
throw new SDJWTException('Sign Error: Invalid JWT');
}

const header = Base64Url.encode(JSON.stringify(this.header));
const payload = Base64Url.encode(JSON.stringify(this.payload));
const data = `${header}.${payload}`;
this.signature = await signer(data);

return this.encodeJwt();
}

public encodeJwt(): string {
if (!this.header || !this.payload || !this.signature) {
throw new SDJWTException('Serialize Error: Invalid JWT');
Expand All @@ -108,8 +121,24 @@ export class Jwt<
try {
await jose.jwtVerify(jwt, publicKey);
} catch (e) {
return false;
throw new SDJWTException('Verify Error: Invalid JWT Signature');
}
return { payload: this.payload, header: this.header };
}

public async verifyWithVerifier(verifier: Verifier) {
if (!this.header || !this.payload || !this.signature) {
throw new SDJWTException('Verify Error: Invalid JWT');
}

const header = Base64Url.encode(JSON.stringify(this.header));
const payload = Base64Url.encode(JSON.stringify(this.payload));
const data = `${header}.${payload}`;

const verified = verifier(data, this.signature);
if (!verified) {
throw new SDJWTException('Verify Error: Invalid JWT Signature');
}
return true;
return { payload: this.payload, header: this.header };
}
}
20 changes: 18 additions & 2 deletions src/kbjwt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { KeyLike } from 'jose';
import { SDJWTException } from './error';
import { Jwt } from './jwt';
import { kbHeader, kbPayload } from './type';
import { Verifier, kbHeader, kbPayload } from './type';

export class KBJwt<
Header extends kbHeader = kbHeader,
Expand All @@ -14,13 +14,29 @@ export class KBJwt<
!this.payload?.iat ||
!this.payload?.aud ||
!this.payload?.nonce ||
!this.payload?._sd_hash
// this is for backward compatibility with version 06
!(this.payload?.sd_hash || (this.payload as any)?._sd_hash)
) {
throw new SDJWTException('Invalid Key Binding Jwt');
}
return await super.verify(publicKey);
}

public async verifyWithVerifier(verifier: Verifier) {
if (
!this.header?.alg ||
!this.header.typ ||
!this.payload?.iat ||
!this.payload?.aud ||
!this.payload?.nonce ||
// this is for backward compatibility with version 06
!(this.payload?.sd_hash || (this.payload as any)?._sd_hash)
) {
throw new SDJWTException('Invalid Key Binding Jwt');
}
return await super.verifyWithVerifier(verifier);
}

public static fromKBEncode<
Header extends kbHeader = kbHeader,
Payload extends kbPayload = kbPayload,
Expand Down
4 changes: 2 additions & 2 deletions src/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('index', () => {
kb: {
alg: 'EdDSA',
payload: {
_sd_hash: 'sha-256',
sd_hash: 'sha-256',
aud: '1',
iat: 1,
nonce: '342',
Expand All @@ -50,7 +50,7 @@ describe('index', () => {
kb: {
alg: 'EdDSA',
payload: {
_sd_hash: 'sha-256',
sd_hash: 'sha-256',
aud: '1',
iat: 1,
nonce: '342',
Expand Down
64 changes: 59 additions & 5 deletions src/test/jwt.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SDJWTException } from '../error';
import { Jwt } from '../jwt';
import Crypto from 'node:crypto';
import { Signer, Verifier } from '../type';

describe('JWT', () => {
test('create', async () => {
Expand Down Expand Up @@ -43,11 +44,15 @@ describe('JWT', () => {
const encodedJwt = await jwt.sign(privateKey);
const newJwt = Jwt.fromEncode(encodedJwt);
const verified = await newJwt.verify(publicKey);
expect(verified).toBe(true);
const notVerified = await newJwt.verify(
Crypto.generateKeyPairSync('ed25519').privateKey,
);
expect(notVerified).toBe(false);
expect(verified).toStrictEqual({
header: { alg: 'EdDSA' },
payload: { foo: 'bar' },
});
try {
await newJwt.verify(Crypto.generateKeyPairSync('ed25519').privateKey);
} catch (e: unknown) {
expect(e).toBeInstanceOf(SDJWTException);
}
});

test('encode', async () => {
Expand Down Expand Up @@ -104,4 +109,53 @@ describe('JWT', () => {
expect(e).toBeInstanceOf(SDJWTException);
}
});

test('custom signer', 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 jwt = new Jwt({
header: { alg: 'EdDSA' },
payload: { foo: 'bar' },
});

const encodedJwt = await jwt.signWithSigner(testSigner);
const encodedJwt2 = await jwt.sign(privateKey);
expect(encodedJwt).toEqual(encodedJwt2);

const newJwt = Jwt.fromEncode(encodedJwt);
const verified = await newJwt.verify(publicKey);
expect(verified).toStrictEqual({
header: { alg: 'EdDSA' },
payload: { foo: 'bar' },
});
});

test('custom verifier', async () => {
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
const testVerifier: Verifier = async (data: string, sig: string) => {
return Crypto.verify(
null,
Buffer.from(data),
publicKey,
Buffer.from(sig, 'base64url'),
);
};

const jwt = new Jwt({
header: { alg: 'EdDSA' },
payload: { foo: 'bar' },
});

const encodedJwt = await jwt.sign(privateKey);
const newJwt = Jwt.fromEncode(encodedJwt);
const verified = await newJwt.verifyWithVerifier(testVerifier);
expect(verified).toStrictEqual({
header: { alg: 'EdDSA' },
payload: { foo: 'bar' },
});
});
});
Loading

0 comments on commit b43afa7

Please sign in to comment.