Skip to content

Commit

Permalink
Refactor Credential Signing (#401)
Browse files Browse the repository at this point in the history
* refactor cred signing

* updates

* fixing jwt test

* updates

* updates to latest did and crypto package

* update package lock

* spacing

* update package lock

* package lock builds

* Simplify JWT verify by using CryptoApi from @web5/crypto

Signed-off-by: Frank Hinek <[email protected]>

* Update packages/credentials/src/jwt.ts

Co-authored-by: Frank Hinek <[email protected]>

* updates to alg checking

---------

Signed-off-by: Frank Hinek <[email protected]>
Co-authored-by: Frank Hinek <[email protected]>
  • Loading branch information
nitro-neal and frankhinek authored Feb 14, 2024
1 parent d97cb9a commit 69e5380
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 345 deletions.
139 changes: 3 additions & 136 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions packages/credentials/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ Sign a `VerifiableCredential` with a DID:
First create a `Did` object as follows:

```javascript
import { DidKeyMethod } from '@web5/dids';
const issuer = await DidKeyMethod.create();
import { DidKey } from '@web5/dids';
const issuer: BearerDid = await DidKey.create();
```

Then sign the VC using the `did` object
Expand Down
6 changes: 3 additions & 3 deletions packages/credentials/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@
},
"dependencies": {
"@sphereon/pex": "2.1.0",
"@web5/common": "0.2.2",
"@web5/crypto": "0.2.4",
"@web5/dids": "0.2.4"
"@web5/common": "0.2.3",
"@web5/crypto": "0.4.0",
"@web5/dids": "0.4.0"
},
"devDependencies": {
"@playwright/test": "1.40.1",
Expand Down
87 changes: 21 additions & 66 deletions packages/credentials/src/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import type { PortableDid } from '@web5/dids';
import { BearerDid } 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';
import { LocalKeyManager as CryptoApi } from '@web5/crypto';
import { DidDht, DidIon, DidKey, DidJwk, DidWeb, DidResolver, utils as didUtils } from '@web5/dids';

const crypto = new CryptoApi();

/**
* Result of parsing a JWT.
Expand Down Expand Up @@ -49,7 +47,7 @@ export type ParseJwtOptions = {
* Parameters for signing a JWT.
*/
export type SignJwtOptions = {
signerDid: PortableDid
signerDid: BearerDid
payload: JwtPayload
}

Expand All @@ -60,49 +58,16 @@ 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] });
static didResolver: DidResolver = new DidResolver({ didResolvers: [DidDht, DidIon, DidKey, DidJwk, DidWeb] });

/**
* Creates a signed JWT.
Expand All @@ -117,17 +82,17 @@ export class Jwt {
*/
static async sign(options: SignJwtOptions): Promise<string> {
const { signerDid, payload } = options;
const privateKeyJwk = signerDid.keySet.verificationMethodKeys![0].privateKeyJwk! as JwkParamsEcPrivate | JwkParamsOkpPrivate;
const signer = await signerDid.getSigner();

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

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

const base64UrlEncodedHeader = Convert.object(header).toBase64Url();
Expand All @@ -136,14 +101,8 @@ export class Jwt {
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`);
}

const { signer, options: signatureAlgorithm } = Jwt.algorithms[algorithmId];
const signatureBytes = await signer.sign({ data: toSignBytes });

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

return `${toSign}.${base64UrlEncodedSignature}`;
Expand All @@ -168,13 +127,13 @@ export class Jwt {
}

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

const verificationMethod = dereferenceResult.contentStream;
if (!verificationMethod || !didUtils.isVerificationMethod(verificationMethod)) { // ensure that appropriate verification method was found
if (!verificationMethod || !didUtils.isDidVerificationMethod(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');
}

Expand All @@ -184,23 +143,19 @@ export class Jwt {
throw new Error('Verification failed: Expected kid in JWT header to dereference to a DID Document Verification Method with publicKeyJwk');
}

if(publicKeyJwk.alg && (publicKeyJwk.alg !== decodedJwt.header.alg)) {
throw new Error('Verification failed: Expected alg in JWT header to match DID Document Verification Method alg');
}

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!,
const isSignatureValid = await crypto.verify({
key : publicKeyJwk,
signature : signatureBytes,
data : signedDataBytes,
signature : signatureBytes
});

if (!isSignatureValid) {
Expand Down
4 changes: 2 additions & 2 deletions packages/credentials/src/verifiable-credential.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PortableDid } from '@web5/dids';
import type { BearerDid } from '@web5/dids';
import type { ICredential, ICredentialSubject} from '@sphereon/ssi-types';

import { utils as cryptoUtils } from '@web5/crypto';
Expand Down Expand Up @@ -41,7 +41,7 @@ export type VerifiableCredentialCreateOptions = {
* @param did - The issuer DID of the credential, represented as a PortableDid.
*/
export type VerifiableCredentialSignOptions = {
did: PortableDid;
did: BearerDid;
};

type CredentialSubject = ICredentialSubject;
Expand Down
Loading

0 comments on commit 69e5380

Please sign in to comment.