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

Refactor Credential Signing #401

Merged
merged 13 commits into from
Feb 14, 2024
1,615 changes: 511 additions & 1,104 deletions package-lock.json

Large diffs are not rendered by default.

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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the latest versions right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes -- 0.4.0 for both dids and crypto -- 0.2.3 for common.

"@web5/crypto": "0.4.0",
"@web5/dids": "0.4.0"
},
"devDependencies": {
"@playwright/test": "1.40.1",
Expand Down
64 changes: 26 additions & 38 deletions packages/credentials/src/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import type { PortableDid } from '@web5/dids';
import { BearerDid } from '@web5/dids';
import type {
JwtPayload,
Web5Crypto,
CryptoAlgorithm,
JwtHeaderParams,
JwkParamsEcPrivate,
JwkParamsOkpPrivate,
JwkParamsEcPublic,
JwkParamsOkpPublic,
CryptoAlgorithm,
} 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 { DidDht, DidIon, DidKey, DidJwk, DidWeb, DidResolver, utils as didUtils } from '@web5/dids';

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

Expand All @@ -64,25 +61,23 @@ export type VerifyJwtOptions = {
* 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,
type Signer<T extends CryptoAlgorithm> = {
signer: EcdsaAlgorithm | EdDsaAlgorithm,
options?: T | undefined
alg: string
crv: string
}

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

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

/**
Expand All @@ -92,7 +87,7 @@ const ed25519Signer: Signer<Web5Crypto.EdDsaOptions> = {
*/
export class Jwt {
/** supported cryptographic algorithms. keys are `${alg}:${crv}`. */
static algorithms: { [alg: string]: Signer<Web5Crypto.EcdsaOptions | Web5Crypto.EdDsaOptions> } = {
static algorithms: { [alg: string]: Signer<EcdsaAlgorithm | EdDsaAlgorithm> } = {
'ES256K:' : secp256k1Signer,
'ES256K:secp256k1' : secp256k1Signer,
':secp256k1' : secp256k1Signer,
Expand All @@ -102,7 +97,7 @@ export class Jwt {
/**
* 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 +112,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 +131,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});

nitro-neal marked this conversation as resolved.
Show resolved Hide resolved
const signatureBytes = await signer.sign({ key: privateKeyJwk, data: toSignBytes, algorithm: signatureAlgorithm! });
const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url();

return `${toSign}.${base64UrlEncodedSignature}`;
Expand All @@ -168,13 +157,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 @@ -194,13 +183,12 @@ export class Jwt {
throw new Error(`Verification failed: ${algorithmId} not supported`);
}

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

const isSignatureValid = await signer.verify({
algorithm : signatureAlgorithm!,
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
54 changes: 44 additions & 10 deletions packages/credentials/tests/jwt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { JwtHeaderParams, JwtPayload, PrivateKeyJwk } from '@web5/crypto';

import { expect } from 'chai';
import { Convert } from '@web5/common';
import { Secp256k1 } from '@web5/crypto';
import { DidKeyMethod } from '@web5/dids';
import { Ed25519 } from '@web5/crypto';
import { DidKey, PortableDid } from '@web5/dids';

import { Jwt } from '../src/jwt.js';

Expand Down Expand Up @@ -70,7 +70,7 @@ describe('Jwt', () => {

describe('verify()', () => {
it('throws error if JWT is expired', async () => {
const did = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' });
const did = await DidKey.create({ options: { algorithm: 'secp256k1'} });
const header: JwtHeaderParams = { typ: 'JWT', alg: 'ES256K', kid: did.document.verificationMethod![0].id };
const base64UrlEncodedHeader = Convert.object(header).toBase64Url();

Expand All @@ -85,8 +85,8 @@ describe('Jwt', () => {
}
});
it('throws error if JWT header kid does not dereference a verification method', async () => {
const did = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' });
const header: JwtHeaderParams = { typ: 'JWT', alg: 'ES256K', kid: did.did };
const did = await DidKey.create({ options: { algorithm: 'secp256k1'} });
const header: JwtHeaderParams = { typ: 'JWT', alg: 'ES256K', kid: did.uri };
const base64UrlEncodedHeader = Convert.object(header).toBase64Url();

const payload: JwtPayload = { iat: Math.floor(Date.now() / 1000) };
Expand All @@ -101,7 +101,7 @@ describe('Jwt', () => {
});

it('throws error if alg is not supported', async () => {
const did = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' });
const did = await DidKey.create({ options: { algorithm: 'secp256k1'} });
const header: JwtHeaderParams = { typ: 'JWT', alg: 'RS256', kid: did.document.verificationMethod![0].id };
const base64UrlEncodedHeader = Convert.object(header).toBase64Url();

Expand All @@ -117,8 +117,42 @@ describe('Jwt', () => {
});

it('returns signer DID if verification succeeds', async () => {
const did = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' });
const header: JwtHeaderParams = { typ: 'JWT', alg: 'ES256K', kid: did.document.verificationMethod![0].id };
const portableDid: PortableDid = {
uri : 'did:key:z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN',
document : {
'@context' : 'https://www.w3.org/ns/did/v1',
id : 'did:key:z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN',
verificationMethod : [
{
id : 'did:key:z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN#z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN', // You may need to adjust the ID based on your requirements
type : 'JsonWebKey2020', // Adjust the type according to your needs, assuming JsonWebKey2020
controller : 'did:key:z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN',
publicKeyJwk : {
kty : 'OKP',
crv : 'Ed25519',
x : 'VnSOQ-n7kRcYd0XGW2MNCv7DDY5py5XhNcjM7-Y1HVM',
},
},
],
authentication: [
'did:key:z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN#z6MkkGkByH7rSY3uxDEPTk1CZzPG5hvf564ABFLQzCFwyYNN',
],
// Add other fields like assertionMethod, capabilityInvocation, etc., as needed
},
metadata : {}, // Populate according to DidMetadata interface
privateKeys : [
{
kty : 'OKP',
crv : 'Ed25519',
x : 'VnSOQ-n7kRcYd0XGW2MNCv7DDY5py5XhNcjM7-Y1HVM',
d : 'iTD5DIOKZNkwgzsND-I8CLIXmgTxfQ1HUzl9fpMktAo',
},
],
};

const did = await DidKey.import({ portableDid });

const header: JwtHeaderParams = { typ: 'JWT', alg: 'EdDSA', kid: did.document.verificationMethod![0].id };
const base64UrlEncodedHeader = Convert.object(header).toBase64Url();

const payload: JwtPayload = { iat: Math.floor(Date.now() / 1000) };
Expand All @@ -127,9 +161,9 @@ describe('Jwt', () => {
const toSign = `${base64UrlEncodedHeader}.${base64UrlEncodedPayload}`;
const toSignBytes = Convert.string(toSign).toUint8Array();

const privateKeyJwk = did.keySet.verificationMethodKeys![0].privateKeyJwk;
const privateKeyJwk = portableDid.privateKeys![0];

const signatureBytes = await Secp256k1.sign({ key: privateKeyJwk as PrivateKeyJwk, data: toSignBytes });
const signatureBytes = await Ed25519.sign({ key: privateKeyJwk as PrivateKeyJwk, data: toSignBytes });
const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url();

const jwt = `${toSign}.${base64UrlEncodedSignature}`;
Expand Down
18 changes: 9 additions & 9 deletions packages/credentials/tests/presentation-exchange.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { DidKeyMethod, PortableDid } from '@web5/dids';
import { BearerDid, DidKey } from '@web5/dids';

import type { Validated, PresentationDefinitionV2 } from '../src/presentation-exchange.js';

Expand All @@ -22,18 +22,18 @@ class OtherCredential {

describe('PresentationExchange', () => {
describe('Full Presentation Exchange', () => {
let issuerDid: PortableDid;
let issuerDid: BearerDid;
let btcCredentialJwt: string;
let presentationDefinition: PresentationDefinitionV2;
let groupPresentationDefinition: PresentationDefinitionV2;

before(async () => {
issuerDid = await DidKeyMethod.create();
issuerDid = await DidKey.create();

const vc = await VerifiableCredential.create({
type : 'StreetCred',
issuer : issuerDid.did,
subject : issuerDid.did,
issuer : issuerDid.uri,
subject : issuerDid.uri,
data : new BitcoinCredential('btcAddress123'),
});

Expand All @@ -60,8 +60,8 @@ describe('PresentationExchange', () => {
it('should return the only one verifiable credential', async () => {
const vc = await VerifiableCredential.create({
type : 'StreetCred',
issuer : issuerDid.did,
subject : issuerDid.did,
issuer : issuerDid.uri,
subject : issuerDid.uri,
data : new OtherCredential('otherstuff'),
});

Expand Down Expand Up @@ -146,8 +146,8 @@ describe('PresentationExchange', () => {
it('should fail to create a presentation with vc that does not match presentation definition', async () => {
const vc = await VerifiableCredential.create({
type : 'StreetCred',
issuer : issuerDid.did,
subject : issuerDid.did,
issuer : issuerDid.uri,
subject : issuerDid.uri,
data : new OtherCredential('otherstuff'),
});

Expand Down
Loading
Loading