Skip to content

Commit

Permalink
add jwt verification and move separate jwt functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
mistermoe committed Dec 7, 2023
1 parent 2a38d1c commit 44942de
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 145 deletions.
224 changes: 224 additions & 0 deletions packages/credentials/src/compact-jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import type { CryptoAlgorithm, JwkParamsEcPrivate, JwkParamsOkpPrivate, PublicKeyJwk, Web5Crypto } from '@web5/crypto';
import type { PortableDid } from '@web5/dids';

import { PrivateKeyJwk, EdDsaAlgorithm, EcdsaAlgorithm } from '@web5/crypto';
import { DidDhtMethod, DidIonMethod, DidKeyMethod, DidResolver, utils } from '@web5/dids';
import { JwtHeader, JwtPayload } from 'jwt-decode';
import { Convert } from '@web5/common';

/**
* Parameters for creating a JWT.
*/
export type CreateJwtParams = {
signerDid: PortableDid
payload: JwtPayload & Record<string, any>
}

/**
* Parameters for verifying a JWT.
*/
export type VerifyJwtParams = {
compactJwt: string
}

/**
* Parameters for decoding a JWT.
*/
export type DecodeJwtParams = {
compactJwt: string
}

/**
* Represents a verified JWT with signer information, header, and payload.
*/
export type VerifiedJwt = {
signerDid: string
header: JwtHeader
payload: JwtPayload & Record<string, any>
}

/**
* Represents a signer with a specific cryptographic algorithm and options.
* @template T - The type of cryptographic options.
*/
type Signer<T extends Web5Crypto.Algorithm> = {
signer: CryptoAlgorithm,
options?: T | undefined
alg: string
crv: string
}

const secp256k1Signer: Signer<Web5Crypto.EcdsaOptions> = {
signer : new EcdsaAlgorithm(),
options : { name: 'ES256K'},
alg : 'ES256K',
crv : 'secp256k1'
};

const ed25519Signer: Signer<Web5Crypto.EdDsaOptions> = {
signer : new EdDsaAlgorithm(),
options : { name: 'EdDSA' },
alg : 'EdDSA',
crv : 'Ed25519'
};

/**
* Class for handling Compact JSON Web Tokens (JWTs).
* This class provides methods to create, verify, and decode JWTs using various cryptographic algorithms.
*/
export class CompactJwt {
/** supported cryptographic algorithms. keys are `${alg}:${crv}`. */
static algorithms: { [alg: string]: Signer<Web5Crypto.EcdsaOptions | Web5Crypto.EdDsaOptions> } = {
'ES256K:' : secp256k1Signer,
'ES256K:secp256k1' : secp256k1Signer,
':secp256k1' : secp256k1Signer,
'EdDSA:Ed25519' : ed25519Signer
};

/**
* DID Resolver instance for resolving decentralized identifiers.
*/
static didResolver: DidResolver = new DidResolver({ didResolvers: [DidIonMethod, DidKeyMethod, DidDhtMethod] });

/**
* Creates a JWT.
* @param params - Parameters for JWT creation including signer DID and payload.
* @returns The compact JWT as a string.
* @example
* ```
* const jwt = await CompactJwt.create({ signerDid: myDid, payload: myPayload });
* ```
*/
static async create(params: CreateJwtParams) {
const { signerDid, payload } = params;
const privateKeyJwk = signerDid.keySet.verificationMethodKeys![0].privateKeyJwk! as JwkParamsEcPrivate | JwkParamsOkpPrivate;

const header: JwtHeader = {
typ : 'JWT',
alg : privateKeyJwk.alg,
kid : signerDid.document.verificationMethod![0].id
};

const base64UrlEncodedHeader = Convert.object(header).toBase64Url();
const base64UrlEncodedPayload = Convert.object(payload).toBase64Url();

const toSign = `${base64UrlEncodedHeader}.${base64UrlEncodedPayload}`;
const toSignBytes = Convert.string(toSign).toUint8Array();

const algorithmId = `${header.alg}:${privateKeyJwk['crv'] || ''}`;
if (!(algorithmId in CompactJwt.algorithms)) {
throw new Error(`Signing failed: ${algorithmId} not supported`);
}

const { signer, options } = CompactJwt.algorithms[algorithmId];

const signatureBytes = await signer.sign({ key: privateKeyJwk as PrivateKeyJwk, data: toSignBytes, algorithm: options! });
const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url();

return `${toSign}.${base64UrlEncodedSignature}`;
}

/**
* Verifies a JWT.
* @param params - Parameters for JWT verification
* @returns Verified JWT information including signer DID, header, and payload.
* @example
* ```
* const verifiedJwt = await CompactJwt.verify({ compactJwt: myJwt });
* ```
*/
static async verify(params: VerifyJwtParams) {
const { decoded: decodedJwt, encoded: encodedJwt } = CompactJwt.decode({ compactJwt: params.compactJwt });
// TODO: should really be looking for verificationMethod with authentication verification relationship
const verificationMethod = await CompactJwt.didResolver.deference({ didUrl: decodedJwt.header.kid! });
if (!utils.isVerificationMethod(verificationMethod)) { // ensure that appropriate verification method was found
throw new Error('Verification failed: Expected kid in JWT header to dereference a DID Document Verification Method');
}

// will be used to verify signature
const publicKeyJwk = verificationMethod.publicKeyJwk as JwkParamsEcPrivate | JwkParamsOkpPrivate;
if (!publicKeyJwk) { // ensure that Verification Method includes public key as a JWK.
throw new Error('Verification failed: Expected kid in JWT header to dereference to a DID Document Verification Method with publicKeyJwk');
}

const signedData = `${encodedJwt.header}.${encodedJwt.payload}`;
const signedDataBytes = Convert.string(signedData).toUint8Array();

const signatureBytes = Convert.base64Url(encodedJwt.signature).toUint8Array();

const algorithmId = `${decodedJwt.header.alg}:${publicKeyJwk['crv'] || ''}`;
if (!(algorithmId in CompactJwt.algorithms)) {
throw new Error(`Verification failed: ${algorithmId} not supported`);
}

const { signer, options } = CompactJwt.algorithms[algorithmId];

const isLegit = await signer.verify({
algorithm : options!,
key : publicKeyJwk as PublicKeyJwk,
data : signedDataBytes,
signature : signatureBytes
});

if (!isLegit) {
throw new Error('Signature verification failed: Integrity mismatch');
}

return {
signerDid : verificationMethod.controller,
header : encodedJwt.header,
payload : encodedJwt.payload,
};
}

/**
* Decodes a JWT without verifying its signature.
* @param params - Parameters for JWT decoding, including the JWT string.
* @returns Decoded JWT parts, including header and payload.
* @example
* const decodedJwt = CompactJwt.decode({ compactJwt: myJwt });
*/
static decode(params: DecodeJwtParams) {
const splitJwt = params.compactJwt.split('.');
if (splitJwt.length !== 3) {
throw new Error(`Verification failed: Malformed JWT. expected 3 parts. got ${splitJwt.length}`);
}

const [base64urlEncodedJwtHeader, base64urlEncodedJwtPayload, base64urlEncodedSignature] = splitJwt;
let jwtHeader: JwtHeader;
let jwtPayload: JwtPayload;

try {
jwtHeader = Convert.base64Url(base64urlEncodedJwtHeader).toObject();
} catch(e) {
throw new Error('Verification failed: Malformed JWT. Invalid base64url encoding for JWT header');
}

if (!jwtHeader.typ || jwtHeader.typ !== 'JWT') {
throw new Error('Verification failed: Expected JWT header to contain typ property set to JWT');
}

if (!jwtHeader.alg || !jwtHeader.kid) { // ensure that JWT header has required properties
throw new Error('Verification failed: Expected JWT header to contain alg and kid');
}

// TODO: validate optional payload fields: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
try {
jwtPayload = Convert.base64Url(base64urlEncodedJwtPayload).toObject();
} catch(e) {
throw new Error('Verification failed: Malformed JWT. Invalid base64url encoding for JWT payload');
}

return {
decoded: {
header : jwtHeader,
payload : jwtPayload as JwtPayload & Record<string, any>,
},
encoded: {
header : base64urlEncodedJwtHeader,
payload : base64urlEncodedJwtPayload,
signature : base64urlEncodedSignature
}
};
}
}
Loading

0 comments on commit 44942de

Please sign in to comment.