Skip to content

Commit

Permalink
update vc sign
Browse files Browse the repository at this point in the history
  • Loading branch information
nitro-neal committed Dec 5, 2023
1 parent acc0020 commit 2ff1e2c
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 153 deletions.
115 changes: 65 additions & 50 deletions packages/credentials/src/verifiable-credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -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<Uint8Array>;
/**
* 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;

Expand Down Expand Up @@ -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<string> {
const vcJwt: string = await createJwt({ vc: this.vcDataModel }, signOptions);
public async sign(vcSignOptions: VerifiableCredentialSignOptions): Promise<string> {
const vcJwt: string = await createJwt({issuerDid: vcSignOptions.did, subjectDid: this.subject, payload: { vc: this.vcDataModel }});
return vcJwt;
}

Expand Down Expand Up @@ -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<void> {
const jwt = decode(vcJwt); // Parse and validate JWT

Expand Down Expand Up @@ -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}`;
}
45 changes: 13 additions & 32 deletions packages/credentials/tests/presentation-exchange.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array>;
import { VerifiableCredential } from '../src/verifiable-credential.js';

class BitcoinCredential {
constructor(
Expand All @@ -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();
});

Expand All @@ -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]);
Expand Down Expand Up @@ -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');
});

Expand Down Expand Up @@ -200,13 +188,6 @@ function createPresentationDefinition(): PresentationDefinitionV2 {
};
}

function EdDsaSigner(privateKey: Uint8Array): Signer {
return async (data: Uint8Array): Promise<Uint8Array> => {
const signature = await Ed25519.sign({ data, key: privateKey});
return signature;
};
}

const expectThrowsAsync = async (method: any, errorMessage: string) => {
let error: any = null;
try {
Expand Down
Loading

0 comments on commit 2ff1e2c

Please sign in to comment.