Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JWT and DID dereference() #339

Merged
merged 28 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
44942de
add jwt verification and move separate jwt functionality
mistermoe Dec 7, 2023
e98735c
remove `did-jwt` dependency
mistermoe Dec 7, 2023
bcfedce
fix typo
mistermoe Dec 7, 2023
3ddd22a
Fix typo
mistermoe Dec 7, 2023
82a29ed
add `CompactJwt` tests
mistermoe Dec 7, 2023
89cdd92
fix tests
mistermoe Dec 7, 2023
fe11f93
remove unused function
mistermoe Dec 7, 2023
60874c6
add TSDoc for `DidResolver.dereference`
mistermoe Dec 7, 2023
684d5b0
change decode to parse and add link to jwt rfc
mistermoe Dec 8, 2023
a99e05e
remove unused type
mistermoe Dec 8, 2023
c5ce5f3
align DidResolver.dereference implementation with did-core spec and a…
mistermoe Dec 8, 2023
eaefc0d
fix tests
mistermoe Dec 8, 2023
483f5ee
have `VerifiableCredential.verify` validate `vc` property
mistermoe Dec 8, 2023
3cc312f
use existing variable
mistermoe Dec 8, 2023
a3d4111
Minor formatting and import statement changes for consistency
frankhinek Dec 10, 2023
f14943c
Improve @web5/crypto utils.randomUuid performance
frankhinek Dec 10, 2023
ce7acf5
Minor test formatting and imports
frankhinek Dec 10, 2023
050286c
Use @web5 UUID function
frankhinek Dec 10, 2023
d09b165
Minor nit
frankhinek Dec 10, 2023
805d550
Allow arbitrary JWS & JWE alg values
frankhinek Dec 10, 2023
627d5d5
Consistent async for create/verify and sync for parse
frankhinek Dec 11, 2023
d864ec4
Consistent TSDoc comments
frankhinek Dec 11, 2023
3e9daf5
Add JWT Header and Payload types
frankhinek Dec 11, 2023
107ceb2
Rename to Jwt, use repo design patterns, and Use JWT types from crypt…
frankhinek Dec 11, 2023
d545b48
Consistently use options objects in method signatures
frankhinek Dec 11, 2023
e03ae3f
Bump credentials and crypto package versions
frankhinek Dec 11, 2023
11f7f90
Fix issue with web5-spec
frankhinek Dec 11, 2023
679e74c
Remove unused jwt-decode dependency
frankhinek Dec 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .web5-spec/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function credentialIssue(req: Request, res: Response) {

const ownDid = await DidKeyMethod.create();

const vc: VerifiableCredential = VerifiableCredential.create({
const vc: VerifiableCredential = await VerifiableCredential.create({
type: body.credential.type[body.credential.type.length - 1],
issuer: body.credential.issuer,
subject: body.credential.credentialSubject["id"] as string,
Expand Down
1,012 changes: 467 additions & 545 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions packages/credentials/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@web5/credentials",
"version": "0.3.2",
"version": "0.4.0",
"description": "Verifiable Credentials",
"type": "module",
"main": "./dist/cjs/index.js",
Expand Down Expand Up @@ -75,9 +75,9 @@
},
"dependencies": {
"@sphereon/pex": "2.1.0",
"did-jwt": "^7.2.6",
"jwt-decode": "4.0.0",
"uuid": "^9.0.0"
"@web5/common": "0.2.2",
"@web5/crypto": "0.2.4",
"@web5/dids": "0.2.3"
},
"devDependencies": {
"@playwright/test": "1.40.1",
Expand All @@ -88,9 +88,6 @@
"@typescript-eslint/parser": "6.4.0",
"@web/test-runner": "0.18.0",
"@web/test-runner-playwright": "0.11.0",
"@web5/crypto": "0.2.3",
"@web5/common": "0.2.2",
"@web5/dids": "0.2.3",
"c8": "8.0.1",
"chai": "4.3.10",
"esbuild": "0.19.8",
Expand Down
3 changes: 2 additions & 1 deletion packages/credentials/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './verifiable-credential.js';
export * from './jwt.js';
export * from './presentation-exchange.js';
export * from './verifiable-credential.js';
export * as utils from './utils.js';
263 changes: 263 additions & 0 deletions packages/credentials/src/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import type { PortableDid } from '@web5/dids';
import type {
JwtPayload,
Web5Crypto,
CryptoAlgorithm,
JwtHeaderParams,
JwkParamsEcPrivate,
JwkParamsOkpPrivate,
JwkParamsEcPublic,
JwkParamsOkpPublic,
} from '@web5/crypto';

import { Convert } from '@web5/common';
import { EdDsaAlgorithm, EcdsaAlgorithm } from '@web5/crypto';
import { DidDhtMethod, DidIonMethod, DidKeyMethod, DidResolver, utils as didUtils } from '@web5/dids';

/**
* Result of parsing a JWT.
*/
export type JwtParseResult = {
decoded: JwtVerifyResult
encoded: {
header: string
payload: string
signature: string
}
}

/**
* Result of verifying a JWT.
*/
export interface JwtVerifyResult {
/** JWT Protected Header */
header: JwtHeaderParams;

/** JWT Claims Set */
payload: JwtPayload;
}

/**
* Parameters for parsing a JWT.
* used in {@link Jwt.parse}
*/
export type ParseJwtOptions = {
jwt: string
}

/**
* Parameters for signing a JWT.
*/
export type SignJwtOptions = {
signerDid: PortableDid
payload: JwtPayload
}

/**
* Parameters for verifying a JWT.
*/
export type VerifyJwtOptions = {
jwt: string
}

/**
* 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.
* More information on JWTs can be found [here](https://datatracker.ietf.org/doc/html/rfc7519)
*/
export class Jwt {
/** 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 signed JWT.
*
* @example
* ```ts
* const jwt = await Jwt.sign({ signerDid: myDid, payload: myPayload });
* ```
*
* @param options - Parameters for JWT creation including signer DID and payload.
* @returns The compact JWT as a string.
*/
static async sign(options: SignJwtOptions): Promise<string> {
const { signerDid, payload } = options;
const privateKeyJwk = signerDid.keySet.verificationMethodKeys![0].privateKeyJwk! as JwkParamsEcPrivate | JwkParamsOkpPrivate;

let vmId = signerDid.document.verificationMethod![0].id;
if (vmId.charAt(0) === '#') {
vmId = `${signerDid.did}${vmId}`;
}

const header: JwtHeaderParams = {
typ : 'JWT',
alg : privateKeyJwk.alg!,
kid : vmId
};

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 Jwt.algorithms)) {
throw new Error(`Signing failed: ${algorithmId} not supported`);
}

Check warning on line 142 in packages/credentials/src/jwt.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/jwt.ts#L141-L142

Added lines #L141 - L142 were not covered by tests

const { signer, options: signatureAlgorithm } = Jwt.algorithms[algorithmId];

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

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

/**
* Verifies a JWT.
*
* @example
* ```ts
* const verifiedJwt = await Jwt.verify({ jwt: myJwt });
* ```
*
* @param options - Parameters for JWT verification
* @returns Verified JWT information including signer DID, header, and payload.
*/
static async verify(options: VerifyJwtOptions): Promise<JwtVerifyResult> {
const { decoded: decodedJwt, encoded: encodedJwt } = Jwt.parse({ jwt: options.jwt });

// TODO: should really be looking for verificationMethod with authentication verification relationship
const dereferenceResult = await Jwt.didResolver.dereference({ didUrl: decodedJwt.header.kid! });
if (dereferenceResult.dereferencingMetadata.error) {
throw new Error(`Failed to resolve ${decodedJwt.header.kid}`);
}

Check warning on line 170 in packages/credentials/src/jwt.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/jwt.ts#L169-L170

Added lines #L169 - L170 were not covered by tests

const verificationMethod = dereferenceResult.contentStream;
if (!verificationMethod || !didUtils.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 JwkParamsEcPublic | JwkParamsOkpPublic;
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');
}

Check warning on line 181 in packages/credentials/src/jwt.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/jwt.ts#L180-L181

Added lines #L180 - L181 were not covered by tests

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 Jwt.algorithms)) {
throw new Error(`Verification failed: ${algorithmId} not supported`);
}

const { signer, options: signatureAlgorithm } = Jwt.algorithms[algorithmId];

const isSignatureValid = await signer.verify({
algorithm : signatureAlgorithm!,
key : publicKeyJwk,
data : signedDataBytes,
signature : signatureBytes
});

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

Check warning on line 204 in packages/credentials/src/jwt.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/jwt.ts#L203-L204

Added lines #L203 - L204 were not covered by tests

return decodedJwt;
}

/**
* Parses a JWT without verifying its signature.
*
* @example
* ```ts
* const { encoded: encodedJwt, decoded: decodedJwt } = Jwt.parse({ jwt: myJwt });
* ```
*
* @param options - Parameters for JWT decoding, including the JWT string.
* @returns both encoded and decoded JWT parts
*/
static parse(options: ParseJwtOptions): JwtParseResult {
const splitJwt = options.jwt.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: JwtHeaderParams;
let jwtPayload: JwtPayload;

try {
jwtHeader = Convert.base64Url(base64urlEncodedJwtHeader).toObject() as JwtHeaderParams;
} 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() as JwtPayload;
} catch(e) {
throw new Error('Verification failed: Malformed JWT. Invalid base64url encoding for JWT payload');
}

return {
decoded: {
header : jwtHeader,
payload : jwtPayload,
},
encoded: {
header : base64urlEncodedJwtHeader,
payload : base64urlEncodedJwtPayload,
signature : base64urlEncodedSignature
}
};
}
}
Loading