diff --git a/packages/credentials/src/verifiable-credential.ts b/packages/credentials/src/verifiable-credential.ts index 4b8ef4a94..434642087 100644 --- a/packages/credentials/src/verifiable-credential.ts +++ b/packages/credentials/src/verifiable-credential.ts @@ -10,6 +10,8 @@ import { Convert } from '@web5/common'; import { verifyJWT } from 'did-jwt'; import { DidDhtMethod, DidIonMethod, DidKeyMethod, DidResolver } from '@web5/dids'; import { SsiValidator } from './validators.js'; +import { PortableDid } from '@web5/dids'; +import { Jose, Ed25519 } from '@web5/crypto'; export const DEFAULT_CONTEXT = 'https://www.w3.org/2018/credentials/v1'; export const DEFAULT_VC_TYPE = 'VerifiableCredential'; @@ -22,6 +24,7 @@ export const DEFAULT_VC_TYPE = 'VerifiableCredential'; export type VcDataModel = ICredential; /** + * Options for creating a verifiable credential. * @param type Optional. The type of the credential, can be a string or an array of strings. * @param issuer The issuer URI of the credential, as a string. * @param subject The subject URI of the credential, as a string. @@ -39,14 +42,25 @@ export type VerifiableCredentialCreateOptions = { expirationDate?: string; }; -export type SignOptions = { - kid: string; - issuerDid: string; - subjectDid: string; - signer: Signer, -} +/** + * Options for signing a verifiable credential. + * @param did - The issuer DID of the credential, represented as a PortableDid. + */ +export type VerifiableCredentialSignOptions = { + did: PortableDid; +}; -type Signer = (data: Uint8Array) => Promise; +/** + * Options for `createJwt` + * @param issuerDid - The DID to sign with. + * @param subjectDid - The subject of the credential. + * @param payload - The payload to be signed. + */ +export type CreateJwtOptions = { + issuerDid: PortableDid, + subjectDid: string, + payload: any, +} type CredentialSubject = ICredentialSubject; @@ -105,17 +119,16 @@ export class VerifiableCredential { * Sign a verifiable credential using [signOptions] * * - * @param signOptions The sign options used to sign the credential. + * @param vcSignOptions The sign options used to sign the credential. * @return The JWT representing the signed verifiable credential. * * Example: * ``` - * const signedVc = verifiableCredential.sign(signOptions) + * const vcJwt = verifiableCredential.sign(vcSignOptions) * ``` */ - // TODO: Refactor to look like: sign(did: Did, assertionMethodId?: string) - public async sign(signOptions: SignOptions): Promise { - const vcJwt: string = await createJwt({ vc: this.vcDataModel }, signOptions); + public async sign(vcSignOptions: VerifiableCredentialSignOptions): Promise { + const vcJwt: string = await createJwt({issuerDid: vcSignOptions.did, subjectDid: this.subject, payload: { vc: this.vcDataModel }}); return vcJwt; } @@ -179,32 +192,32 @@ export class VerifiableCredential { } /** - * Verifies the integrity and authenticity of a Verifiable Credential (VC) encoded as a JSON Web Token (JWT). - * - * This function performs several crucial validation steps to ensure the trustworthiness of the provided VC: - * - Parses and validates the structure of the JWT. - * - Ensures the presence of critical header elements `alg` and `kid` in the JWT header. - * - Resolves the Decentralized Identifier (DID) and retrieves the associated DID Document. - * - Validates the DID and establishes a set of valid verification method IDs. - * - Identifies the correct Verification Method from the DID Document based on the `kid` parameter. - * - Verifies the JWT's signature using the public key associated with the Verification Method. - * - * If any of these steps fail, the function will throw a [Error] with a message indicating the nature of the failure. - * - * @param vcJwt The Verifiable Credential in JWT format as a [string]. - * @throws Error if the verification fails at any step, providing a message with failure details. - * @throws Error if critical JWT header elements are absent. - * - * ### Example: - * ``` - * try { - * VerifiableCredential.verify(signedVcJwt) - * console.log("VC Verification successful!") - * } catch (e: Error) { - * console.log("VC Verification failed: ${e.message}") - * } - * ``` - */ + * Verifies the integrity and authenticity of a Verifiable Credential (VC) encoded as a JSON Web Token (JWT). + * + * This function performs several crucial validation steps to ensure the trustworthiness of the provided VC: + * - Parses and validates the structure of the JWT. + * - Ensures the presence of critical header elements `alg` and `kid` in the JWT header. + * - Resolves the Decentralized Identifier (DID) and retrieves the associated DID Document. + * - Validates the DID and establishes a set of valid verification method IDs. + * - Identifies the correct Verification Method from the DID Document based on the `kid` parameter. + * - Verifies the JWT's signature using the public key associated with the Verification Method. + * + * If any of these steps fail, the function will throw a [Error] with a message indicating the nature of the failure. + * + * @param vcJwt The Verifiable Credential in JWT format as a [string]. + * @throws Error if the verification fails at any step, providing a message with failure details. + * @throws Error if critical JWT header elements are absent. + * + * ### Example: + * ``` + * try { + * VerifiableCredential.verify(signedVcJwt) + * console.log("VC Verification successful!") + * } catch (e: Error) { + * console.log("VC Verification failed: ${e.message}") + * } + * ``` + */ public static async verify(vcJwt: string): Promise { const jwt = decode(vcJwt); // Parse and validate JWT @@ -279,26 +292,28 @@ function decode(jwt: string): DecodedVcJwt { }; } -async function createJwt(payload: any, signOptions: SignOptions) { - const { issuerDid, subjectDid, signer, kid } = signOptions; +async function createJwt(createJwtOptions: CreateJwtOptions) { + const { issuerDid, subjectDid, payload } = createJwtOptions; + const privateKeyJwk = issuerDid.keySet.verificationMethodKeys![0].privateKeyJwk!; - const header: JwtHeaderParams = { alg: 'EdDSA', typ: 'JWT', kid: kid }; + const header: JwtHeaderParams = { typ: 'JWT', alg: privateKeyJwk.alg!, kid: issuerDid.document.verificationMethod![0].id }; + const base64UrlEncodedHeader = Convert.object(header).toBase64Url(); const jwtPayload = { - iss : issuerDid, + iss : issuerDid.did, sub : subjectDid, ...payload, }; - const encodedHeader = Convert.object(header).toBase64Url(); - const encodedPayload = Convert.object(jwtPayload).toBase64Url(); - const message = encodedHeader + '.' + encodedPayload; - const messageBytes = Convert.string(message).toUint8Array(); + const base64UrlEncodedPayload = Convert.object(jwtPayload).toBase64Url(); + + const toSign = `${base64UrlEncodedHeader}.${base64UrlEncodedPayload}`; + const toSignBytes = Convert.string(toSign).toUint8Array(); - const signature = await signer(messageBytes); + const { keyMaterial } = await Jose.jwkToKey({ key: privateKeyJwk }); - const encodedSignature = Convert.uint8Array(signature).toBase64Url(); - const jwt = message + '.' + encodedSignature; + const signatureBytes = await Ed25519.sign({ key: keyMaterial, data: toSignBytes }); + const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url(); - return jwt; + return `${toSign}.${base64UrlEncodedSignature}`; } \ No newline at end of file diff --git a/packages/credentials/tests/presentation-exchange.spec.ts b/packages/credentials/tests/presentation-exchange.spec.ts index 5e923a323..f11769ccc 100644 --- a/packages/credentials/tests/presentation-exchange.spec.ts +++ b/packages/credentials/tests/presentation-exchange.spec.ts @@ -1,10 +1,7 @@ import { expect } from 'chai'; -import { DidKeyMethod } from '@web5/dids'; -import { Ed25519, Jose } from '@web5/crypto'; +import { DidKeyMethod, PortableDid } from '@web5/dids'; import { PresentationExchange, Validated, PresentationDefinitionV2 } from '../src/presentation-exchange.js'; -import { VerifiableCredential, SignOptions } from '../src/verifiable-credential.js'; - -type Signer = (data: Uint8Array) => Promise; +import { VerifiableCredential } from '../src/verifiable-credential.js'; class BitcoinCredential { constructor( @@ -20,30 +17,21 @@ class OtherCredential { describe('PresentationExchange', () => { describe('Full Presentation Exchange', () => { - let signOptions: SignOptions; + let issuerDid: PortableDid; let btcCredentialJwt: string; let presentationDefinition: PresentationDefinitionV2; before(async () => { - const alice = await DidKeyMethod.create(); - const [signingKeyPair] = alice.keySet.verificationMethodKeys!; - const privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial; - const signer = EdDsaSigner(privateKey); - signOptions = { - issuerDid : alice.did, - subjectDid : alice.did, - kid : alice.did + '#' + alice.did.split(':')[2], - signer : signer - }; + issuerDid = await DidKeyMethod.create(); const vc = VerifiableCredential.create({ type : 'StreetCred', - issuer : alice.did, - subject : alice.did, + issuer : issuerDid.did, + subject : issuerDid.did, data : new BitcoinCredential('btcAddress123'), }); - btcCredentialJwt = await vc.sign(signOptions); + btcCredentialJwt = await vc.sign({did: issuerDid}); presentationDefinition = createPresentationDefinition(); }); @@ -59,12 +47,12 @@ describe('PresentationExchange', () => { it('should return the only one verifiable credential', async () => { const vc = VerifiableCredential.create({ type : 'StreetCred', - issuer : signOptions.issuerDid, - subject : signOptions.subjectDid, + issuer : issuerDid.did, + subject : issuerDid.did, data : new OtherCredential('otherstuff'), }); - const otherCredJwt = await vc.sign(signOptions); + const otherCredJwt = await vc.sign({did: issuerDid}); const actualSelectedVcJwts = PresentationExchange.selectCredentials([btcCredentialJwt, otherCredJwt], presentationDefinition); expect(actualSelectedVcJwts).to.deep.equal([btcCredentialJwt]); @@ -129,12 +117,12 @@ describe('PresentationExchange', () => { it('should fail to create a presentation with vc that does not match presentation definition', async() => { const vc = VerifiableCredential.create({ type : 'StreetCred', - issuer : signOptions.issuerDid, - subject : signOptions.subjectDid, + issuer : issuerDid.did, + subject : issuerDid.did, data : new OtherCredential('otherstuff'), }); - const otherCredJwt = await vc.sign(signOptions); + const otherCredJwt = await vc.sign({did: issuerDid}); await expectThrowsAsync(() => PresentationExchange.createPresentationFromCredentials([otherCredJwt], presentationDefinition), 'Failed to create Verifiable Presentation JWT due to: Required Credentials Not Present'); }); @@ -200,13 +188,6 @@ function createPresentationDefinition(): PresentationDefinitionV2 { }; } -function EdDsaSigner(privateKey: Uint8Array): Signer { - return async (data: Uint8Array): Promise => { - const signature = await Ed25519.sign({ data, key: privateKey}); - return signature; - }; -} - const expectThrowsAsync = async (method: any, errorMessage: string) => { let error: any = null; try { diff --git a/packages/credentials/tests/verifiable-credential.spec.ts b/packages/credentials/tests/verifiable-credential.spec.ts index 6cc2fe02a..80cee5197 100644 --- a/packages/credentials/tests/verifiable-credential.spec.ts +++ b/packages/credentials/tests/verifiable-credential.spec.ts @@ -1,14 +1,10 @@ import { expect } from 'chai'; -import { VerifiableCredential, SignOptions } from '../src/verifiable-credential.js'; -import { Ed25519, Jose } from '@web5/crypto'; +import { VerifiableCredential } from '../src/verifiable-credential.js'; import { DidDhtMethod, DidKeyMethod, PortableDid } from '@web5/dids'; import sinon from 'sinon'; -type Signer = (data: Uint8Array) => Promise; - describe('Verifiable Credential Tests', () => { - let signer: Signer; - let signOptions: SignOptions; + let issuerDid: PortableDid; class StreetCredibility { constructor( @@ -18,38 +14,52 @@ describe('Verifiable Credential Tests', () => { } beforeEach(async () => { - const alice = await DidKeyMethod.create(); - const [signingKeyPair] = alice.keySet.verificationMethodKeys!; - const privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial; - signer = EdDsaSigner(privateKey); - signOptions = { - issuerDid : alice.did, - subjectDid : alice.did, - kid : alice.did + '#' + alice.did.split(':')[2], - signer : signer - }; + issuerDid = await DidKeyMethod.create(); }); describe('Verifiable Credential (VC)', () => { it('create vc works', async () => { - // const keyManager = new InMemoryKeyManager(); - const issuerDid = signOptions.issuerDid; - const subjectDid = signOptions.subjectDid; + const subjectDid = issuerDid.did; const vc = VerifiableCredential.create({ type : 'StreetCred', - issuer : issuerDid, + issuer : issuerDid.did, subject : subjectDid, data : new StreetCredibility('high', true), }); - expect(vc.issuer).to.equal(issuerDid); + expect(vc.issuer).to.equal(issuerDid.did); expect(vc.subject).to.equal(subjectDid); expect(vc.type).to.equal('StreetCred'); expect(vc.vcDataModel.issuanceDate).to.not.be.undefined; expect(vc.vcDataModel.credentialSubject).to.deep.equal({ id: subjectDid, localRespect: 'high', legit: true }); }); + it('create and sign vc with did:key', async () => { + const did = await DidKeyMethod.create(); + + const vc = await VerifiableCredential.create({ + type : 'TBDeveloperCredential', + subject : did.did, + issuer : did.did, + data : { + username: 'nitro' + } + }); + + const vcJwt = await vc.sign({ did }); + + await VerifiableCredential.verify(vcJwt); + + for( const currentVc of [vc, VerifiableCredential.parseJwt(vcJwt)]){ + expect(currentVc.issuer).to.equal(did.did); + expect(currentVc.subject).to.equal(did.did); + expect(currentVc.type).to.equal('TBDeveloperCredential'); + expect(currentVc.vcDataModel.issuanceDate).to.not.be.undefined; + expect(currentVc.vcDataModel.credentialSubject).to.deep.equal({ id: did.did, username: 'nitro'}); + } + }); + it('should throw an error if data is not parseable into a JSON object', () => { const issuerDid = 'did:example:issuer'; const subjectDid = 'did:example:subject'; @@ -92,17 +102,16 @@ describe('Verifiable Credential Tests', () => { }); it('signing vc works', async () => { - const issuerDid = signOptions.issuerDid; - const subjectDid = signOptions.subjectDid; + const subjectDid = issuerDid.did; const vc = VerifiableCredential.create({ type : 'StreetCred', - issuer : issuerDid, + issuer : issuerDid.did, subject : subjectDid, data : new StreetCredibility('high', true), }); - const vcJwt = await vc.sign(signOptions); + const vcJwt = await vc.sign({did: issuerDid}); expect(vcJwt).to.not.be.null; expect(vcJwt).to.be.a('string'); @@ -116,26 +125,6 @@ describe('Verifiable Credential Tests', () => { }).to.throw('Not a valid jwt'); }); - it('verify fails with bad issuer did', async () => { - const vc = VerifiableCredential.create({ - type : 'StreetCred', - issuer : 'bad:did: invalidDid', - subject : signOptions.subjectDid, - data : new StreetCredibility('high', true) - }); - - const badSignOptions = { - issuerDid : 'bad:did: invalidDid', - subjectDid : signOptions.subjectDid, - kid : signOptions.issuerDid + '#' + signOptions.issuerDid.split(':')[2], - signer : signer - }; - - const vcJwt = await vc.sign(badSignOptions); - - await expectThrowsAsync(() => VerifiableCredential.verify(vcJwt), 'Unable to resolve DID'); - }); - it('parseJwt checks if missing vc property', async () => { await expectThrowsAsync(() => VerifiableCredential.parseJwt('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'), 'Jwt payload missing vc property'); }); @@ -143,12 +132,12 @@ describe('Verifiable Credential Tests', () => { it('parseJwt returns an instance of VerifiableCredential on success', async () => { const vc = VerifiableCredential.create({ type : 'StreetCred', - issuer : signOptions.issuerDid, - subject : signOptions.subjectDid, + issuer : issuerDid.did, + subject : issuerDid.did, data : new StreetCredibility('high', true), }); - const vcJwt = await vc.sign(signOptions); + const vcJwt = await vc.sign({did: issuerDid}); const parsedVc = VerifiableCredential.parseJwt(vcJwt); expect(parsedVc).to.not.be.null; @@ -172,12 +161,12 @@ describe('Verifiable Credential Tests', () => { it('verify does not throw an exception with vaild vc', async () => { const vc = VerifiableCredential.create({ type : 'StreetCred', - issuer : signOptions.issuerDid, - subject : signOptions.subjectDid, + issuer : issuerDid.did, + subject : issuerDid.did, data : new StreetCredibility('high', true), }); - const vcJwt = await vc.sign(signOptions); + const vcJwt = await vc.sign({did: issuerDid}); await VerifiableCredential.verify(vcJwt); }); @@ -280,20 +269,10 @@ describe('Verifiable Credential Tests', () => { const alice = await DidDhtMethod.create({ publish: true }); - const [signingKeyPair] = alice.keySet.verificationMethodKeys!; - const privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk! })).keyMaterial; - signer = EdDsaSigner(privateKey); - signOptions = { - issuerDid : alice.did, - subjectDid : alice.did, - kid : alice.did + '#0', - signer : signer - }; - const vc = VerifiableCredential.create({ type : 'StreetCred', - issuer : signOptions.issuerDid, - subject : signOptions.subjectDid, + issuer : alice.did, + subject : alice.did, data : new StreetCredibility('high', true), }); @@ -339,7 +318,7 @@ describe('Verifiable Credential Tests', () => { } }); - const vcJwt = await vc.sign(signOptions); + const vcJwt = await vc.sign({did: alice}); await VerifiableCredential.verify(vcJwt); @@ -350,13 +329,6 @@ describe('Verifiable Credential Tests', () => { }); }); -function EdDsaSigner(privateKey: Uint8Array): Signer { - return async (data: Uint8Array): Promise => { - const signature = await Ed25519.sign({ data, key: privateKey}); - return signature; - }; -} - const expectThrowsAsync = async (method: any, errorMessage: string) => { let error: any = null; try {