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 10 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
52 changes: 9 additions & 43 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions packages/credentials/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,8 @@
},
"dependencies": {
"@sphereon/pex": "2.1.0",
"did-jwt": "^7.2.6",
"jwt-decode": "4.0.0",
"uuid": "^9.0.0"
"uuid": "9.0.0"
},
"devDependencies": {
"@playwright/test": "1.40.1",
Expand Down
222 changes: 222 additions & 0 deletions packages/credentials/src/compact-jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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 parsing a JWT.
* used in {@link CompactJwt.parse}
*/
export type ParseJwtParams = {
compactJwt: 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)
*/
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
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;

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

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

Check warning on line 109 in packages/credentials/src/compact-jwt.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/compact-jwt.ts#L108-L109

Added lines #L108 - L109 were not covered by tests

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.parse({ compactJwt: params.compactJwt });
// TODO: should really be looking for verificationMethod with authentication verification relationship
const verificationMethod = await CompactJwt.didResolver.dereference({ 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');
}

Check warning on line 140 in packages/credentials/src/compact-jwt.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/compact-jwt.ts#L139-L140

Added lines #L139 - L140 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 CompactJwt.algorithms)) {
throw new Error(`Verification failed: ${algorithmId} not supported`);
}

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

const isLegit = await signer.verify({
frankhinek marked this conversation as resolved.
Show resolved Hide resolved
algorithm : options!,
key : publicKeyJwk as PublicKeyJwk,
data : signedDataBytes,
signature : signatureBytes
});

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

Check warning on line 163 in packages/credentials/src/compact-jwt.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/compact-jwt.ts#L162-L163

Added lines #L162 - L163 were not covered by tests

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

/**
* Parses a JWT without verifying its signature.
* @param params - Parameters for JWT decoding, including the JWT string.
* @returns both encoded and decoded JWT parts
* @example
* const { encoded: encodedJwt, decoded: decodedJwt } = CompactJwt.parse({ compactJwt: myJwt });
*/
static parse(params: ParseJwtParams) {
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
}
};
}
}
3 changes: 3 additions & 0 deletions packages/credentials/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export type * from 'jwt-decode';

export * from './verifiable-credential.js';
export * from './presentation-exchange.js';
export * from './compact-jwt.js';
export * as utils from './utils.js';
Loading