Skip to content

Commit

Permalink
fix: validate JWT signed other implemenation (openwallet-foundation#158)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas.J.Han <lukas.j.han@gmail.com>
  • Loading branch information
lukasjhan authored Mar 11, 2024
1 parent d9eaf44 commit 9f28a26
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 17 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/build-test-publish-on-push-cached.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ jobs:
path: coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

lint:
needs: build
Expand All @@ -108,11 +109,11 @@ jobs:
node-version: 20
cache: 'pnpm'
# we are not using the github action for biome, but the package.json script. this makes sure we are using the same versions.
- name: Run Biome
- name: Run Biome
run: pnpm run biome:ci

# Only run this job when the push is on main, next or unstable
publish:
publish:
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' || github.ref == 'refs/heads/unstable')
# needs permissions to write tags to the repository
permissions:
Expand Down
39 changes: 30 additions & 9 deletions packages/core/src/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type JwtData<
header?: Header;
payload?: Payload;
signature?: Base64urlString;
encoded?: string;
};

// This class is used to create and verify JWT
Expand All @@ -20,11 +21,13 @@ export class Jwt<
public header?: Header;
public payload?: Payload;
public signature?: Base64urlString;
private encoded?: string;

constructor(data?: JwtData<Header, Payload>) {
this.header = data?.header;
this.payload = data?.payload;
this.signature = data?.signature;
this.encoded = data?.encoded;
}

public static decodeJWT<
Expand All @@ -48,35 +51,55 @@ export class Jwt<
header,
payload,
signature,
encoded: encodedJwt,
});

return jwt;
}

public setHeader(header: Header): Jwt<Header, Payload> {
this.header = header;
this.encoded = undefined;
return this;
}

public setPayload(payload: Payload): Jwt<Header, Payload> {
this.payload = payload;
this.encoded = undefined;
return this;
}

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

if (this.encoded) {
const parts = this.encoded.split('.');
if (parts.length !== 3) {
throw new SDJWTException(`Invalid JWT format: ${this.encoded}`);
}
const unsignedToken = parts.slice(0, 2).join('.');
return unsignedToken;
}

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

public async sign(signer: Signer) {
const data = this.getUnsignedToken();
this.signature = await signer(data);

return this.encodeJwt();
}

public encodeJwt(): string {
if (this.encoded) {
return this.encoded;
}

if (!this.header || !this.payload || !this.signature) {
throw new SDJWTException('Serialize Error: Invalid JWT');
}
Expand All @@ -85,18 +108,16 @@ export class Jwt<
const payload = Base64urlEncode(JSON.stringify(this.payload));
const signature = this.signature;
const compact = `${header}.${payload}.${signature}`;
this.encoded = compact;

return compact;
}

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

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

const verified = await verifier(data, this.signature);
if (!verified) {
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/kbjwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ export class KBJwt<
throw new SDJWTException('Invalid Key Binding Jwt');
}

const header = Base64urlEncode(JSON.stringify(this.header));
const payload = Base64urlEncode(JSON.stringify(this.payload));
const data = `${header}.${payload}`;
const data = this.getUnsignedToken();
const verified = await values.verifier(
data,
this.signature,
Expand All @@ -63,6 +61,7 @@ export class KBJwt<
header,
payload,
signature,
encoded: encodedJwt,
});

return jwt;
Expand Down
37 changes: 36 additions & 1 deletion packages/core/src/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { SDJwtInstance, type SdJwtPayload } from '../index';
import type { Signer, Verifier, KbVerifier, JwtPayload } from '@sd-jwt/types';
import Crypto, { type KeyLike } from 'node:crypto';
import { describe, expect, test } from 'vitest';
import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
import { digest, generateSalt, ES256 } from '@sd-jwt/crypto-nodejs';
import { importJWK, exportJWK, type JWK } from 'jose';

// Extract the major version as a number
const nodeVersionMajor = Number.parseInt(
process.version.split('.')[0].substring(1),
10,
);

export const createSignerVerifier = () => {
const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
const signer: Signer = async (data: string) => {
Expand Down Expand Up @@ -590,4 +596,33 @@ describe('index', () => {
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, undefined, true);
expect(decode).toBeDefined();
},
);
});
61 changes: 61 additions & 0 deletions packages/core/src/test/jwt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,65 @@ describe('JWT', () => {
expect(e).toBeInstanceOf(SDJWTException);
}
});

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

try {
await jwt.sign(testSigner);
} catch (e: unknown) {
expect(e).toBeInstanceOf(SDJWTException);
}
});

test('wrong encoded field', 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' },
encoded: 'asfasfafaf.dfasfafafasf', // it has to be 3 parts
});

try {
await jwt.sign(testSigner);
} catch (e: unknown) {
expect(e).toBeInstanceOf(SDJWTException);
}
});

test('verify failed no signature', 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' },
});

try {
await jwt.verify(testVerifier);
} catch (e: unknown) {
expect(e).toBeInstanceOf(SDJWTException);
}
});
});
4 changes: 3 additions & 1 deletion packages/core/src/test/sdjwt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,9 @@ describe('SD JWT', () => {

const credential = sdJwt.encodeSDJwt();
const decoded = await SDJwt.decodeSDJwt(credential, hasher);
expect(jwt).toEqual(decoded.jwt);
expect(jwt.header).toEqual(decoded.jwt.header);
expect(jwt.payload).toEqual(decoded.jwt.payload);
expect(jwt.signature).toEqual(decoded.jwt.signature);
expect(decoded.disclosures).toEqual([]);
});
});

0 comments on commit 9f28a26

Please sign in to comment.