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

Verifiable Presentation Implementation #382

Merged
merged 5 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
8 changes: 8 additions & 0 deletions packages/credentials/src/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from './verifiable-credential.js';

import { isValidXmlSchema112Timestamp } from './utils.js';
import { DEFAULT_VP_TYPE } from './verifiable-presentation.js';

export class SsiValidator {
static validateCredentialPayload(vc: VerifiableCredential): void {
Expand All @@ -34,6 +35,13 @@ export class SsiValidator {
}
}

static validateVpType(value: string | string[]): void {
const input = this.asArray(value);
if (input.length < 1 || input.indexOf(DEFAULT_VP_TYPE) === -1) {
throw new Error(`type is missing default "${DEFAULT_VP_TYPE}"`);
}
}

static validateCredentialSubject(value: ICredentialSubject | ICredentialSubject[]): void {
if (Object.keys(value).length === 0) {
throw new Error(`credentialSubject must not be empty`);
Expand Down
228 changes: 228 additions & 0 deletions packages/credentials/src/verifiable-presentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import type { PortableDid } from '@web5/dids';
import type { IPresentation} from '@sphereon/ssi-types';

import { utils as cryptoUtils } from '@web5/crypto';

import { Jwt } from './jwt.js';
import { SsiValidator } from './validators.js';

export const DEFAULT_CONTEXT = 'https://www.w3.org/2018/credentials/v1';
export const DEFAULT_VP_TYPE = 'VerifiablePresentation';

/**
* A Verifiable Presentation
*
* @see {@link https://www.w3.org/TR/vc-data-model/#credentials | VC Data Model}
*/
export type VpDataModel = IPresentation;

/**
* Options for creating a verifiable presentation.
* @param holder The holder URI of the presentation, as a string.
* @param vcJwts The JWTs of the credentials to be included in the presentation.
* @param type Optional. The type of the presentation, can be a string or an array of strings.
* @param additionalData Optional additional data to be included in the presentation.
*/
export type VerifiablePresentationCreateOptions = {
holder: string,
vcJwts: string[],
type?: string | string[];
additionalData?: Record<string, any>
};

/**
* Options for signing a verifiable presentation.
* @param did - The holder DID of the presentation, represented as a PortableDid.
*/
export type VerifiablePresentationSignOptions = {
did: PortableDid;
};

/**
* `VerifiablePresentation` is a tamper-evident presentation encoded in such a way that authorship of the data
* can be trusted after a process of cryptographic verification.
* [W3C Verifiable Presentation Data Model](https://www.w3.org/TR/vc-data-model/#presentations).
*
* It provides functionalities to sign, verify, and create presentations, offering a concise API to
* work with JWT representations of verifiable presentations and ensuring that the signatures
* and claims within those JWTs can be validated.
*
* @property vpDataModel The [vpDataModel] instance representing the core data model of a verifiable presentation.
*/
export class VerifiablePresentation {
constructor(public vpDataModel: VpDataModel) {}

get type(): string {
return this.vpDataModel.type![this.vpDataModel.type!.length - 1];
}

get holder(): string {
return this.vpDataModel.holder!.toString();
}

get verifiableCredential(): string[] {
return this.vpDataModel.verifiableCredential! as string[];
}

/**
* Signs the verifiable presentation and returns it as a signed JWT.
*
* @example
* ```ts
* const vpJwt = verifiablePresentation.sign({ did: myDid });
* ```
*
* @param options - The sign options used to sign the presentation.
* @returns The JWT representing the signed verifiable presentation.
*/
public async sign(options: VerifiablePresentationSignOptions): Promise<string> {
const vpJwt: string = await Jwt.sign({
signerDid : options.did,
payload : {
vp : this.vpDataModel,
iss : options.did.did,
sub : options.did.did,
}
});

return vpJwt;
}

/**
* Converts the current object to its JSON representation.
*
* @returns The JSON representation of the object.
*/
public toString(): string {
return JSON.stringify(this.vpDataModel);
}

/**
* Create a [VerifiablePresentation] based on the provided parameters.
*
* @example
* ```ts
* const vp = await VerifiablePresentation.create({
* type: 'PresentationSubmission',
* holder: 'did:ex:holder',
* vcJwts: vcJwts,
* additionalData: { 'arbitrary': 'data' }
* })
* ```
*
* @param options - The options to use when creating the Verifiable Presentation.
* @returns A [VerifiablePresentation] instance.
*/
public static async create(options: VerifiablePresentationCreateOptions): Promise<VerifiablePresentation> {
const { type, holder, vcJwts, additionalData } = options;

if (additionalData) {
const jsonData = JSON.parse(JSON.stringify(additionalData));

if (typeof jsonData !== 'object') {
throw new Error('Expected data to be parseable into a JSON object');
}
}

if(!holder) {
throw new Error('Holder must be defined');
}

const vpDataModel: VpDataModel = {
'@context' : [DEFAULT_CONTEXT],
type : Array.isArray(type)
? [DEFAULT_VP_TYPE, ...type]
: (type ? [DEFAULT_VP_TYPE, type] : [DEFAULT_VP_TYPE]),
id : `urn:uuid:${cryptoUtils.randomUuid()}`,
holder : holder,
verifiableCredential : vcJwts,
...additionalData,
};

validatePayload(vpDataModel);

return new VerifiablePresentation(vpDataModel);
}

/**
* Verifies the integrity and authenticity of a Verifiable Presentation (VP) encoded as a JSON Web Token (JWT).
*
* This function performs several crucial validation steps to ensure the trustworthiness of the provided VP:
* - 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.
*
* @example
* ```ts
* try {
* VerifiablePresentation.verify({ vpJwt: signedVpJwt })
* console.log("VC Verification successful!")
* } catch (e: Error) {
* console.log("VC Verification failed: ${e.message}")
* }
* ```
*
* @param vpJwt The Verifiable Presentation 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.
*/
public static async verify({ vpJwt }: {
vpJwt: string
}) {
const { payload } = await Jwt.verify({ jwt: vpJwt });
const vp = payload['vp'] as VpDataModel;
if (!vp) {
throw new Error('vp property missing.');
}

validatePayload(vp);

for (const vcJwt of vp.verifiableCredential!) {
await Jwt.verify({ jwt: vcJwt as string });
}

return {
issuer : payload.iss!,
subject : payload.sub!,
vc : payload['vp'] as VpDataModel
};
}

/**
* Parses a JWT into a [VerifiablePresentation] instance.
*
* @example
* ```ts
* const vp = VerifiablePresentation.parseJwt({ vpJwt: signedVpJwt })
* ```
*
* @param vpJwt The verifiable presentation JWT as a [String].
* @returns A [VerifiablePresentation] instance derived from the JWT.
*/
public static parseJwt({ vpJwt }: { vpJwt: string }): VerifiablePresentation {
const parsedJwt = Jwt.parse({ jwt: vpJwt });
const vpDataModel: VpDataModel = parsedJwt.decoded.payload['vp'] as VpDataModel;

if(!vpDataModel) {
throw Error('Jwt payload missing vp property');
}

return new VerifiablePresentation(vpDataModel);
}
}

/**
* Validates the structure and integrity of a Verifiable Presentation payload.
*
* @param vp - The Verifiable Presentaation object to validate.
* @throws Error if any validation check fails.
*/
function validatePayload(vp: VpDataModel): void {
SsiValidator.validateContext(vp['@context']);
SsiValidator.validateVpType(vp.type!);
}
131 changes: 131 additions & 0 deletions packages/credentials/tests/verifiable-presentation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { PortableDid } from '@web5/dids';

import { expect } from 'chai';
import { DidKeyMethod } from '@web5/dids';

import { Jwt } from '../src/jwt.js';
import { VerifiablePresentation } from '../src/verifiable-presentation.js';
import { PresentationSubmission } from '@sphereon/pex-models';

const validVcJwt = 'eyJraWQiOiJkaWQ6a2V5OnpRM3NoZ0NqVmZucldxOUw3cjFRc3oxcmlRUldvb3pid2dKYkptTGdxRFB2OXNnNGIjelEzc' +
'2hnQ2pWZm5yV3E5TDdyMVFzejFyaVFSV29vemJ3Z0piSm1MZ3FEUHY5c2c0YiIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2SyJ9.eyJpc3Mi' +
'OiJkaWQ6a2V5OnpRM3NoZ0NqVmZucldxOUw3cjFRc3oxcmlRUldvb3pid2dKYkptTGdxRFB2OXNnNGIiLCJzdWIiOiJkaWQ6a2V5OnpRM3No' +
'd2Q0eVVBZldnZkdFUnFVazQ3eEc5NXFOVXNpc0Q3NzZKTHVaN3l6OW5RaWoiLCJpYXQiOjE3MDQ5MTgwODMsInZjIjp7IkBjb250ZXh0Ijpb' +
'Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTdHJlZXRD' +
'cmVkIl0sImlkIjoidXJuOnV1aWQ6NTU2OGQyZTEtYjA0NS00MTQ3LTkxNjUtZTU3YTIxMGM2ZGVlIiwiaXNzdWVyIjoiZGlkOmtleTp6UTNz' +
'aGdDalZmbnJXcTlMN3IxUXN6MXJpUVJXb296YndnSmJKbUxncURQdjlzZzRiIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wMS0xMFQyMDoyMToy' +
'M1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6elEzc2h3ZDR5VUFmV2dmR0VScVVrNDd4Rzk1cU5Vc2lzRDc3NkpMdVo3' +
'eXo5blFpaiIsImxvY2FsUmVzcGVjdCI6ImhpZ2giLCJsZWdpdCI6dHJ1ZX19fQ.Bx0JrQERWRLpYeg3TnfrOIo4zexo3q1exPZ-Ej6j0T0YO' +
'BVZaZ9-RqpiAM-fHKrdGUzVyXr77pOl7yGgwIO90g';

describe('Verifiable Credential Tests', () => {
let holderDid: PortableDid;

beforeEach(async () => {
holderDid = await DidKeyMethod.create();
});

describe('Verifiable Presentation (VP)', () => {
it('create simple vp', async () => {
const vcJwts = ['vcjwt1'];

const vp = await VerifiablePresentation.create({
holder : holderDid.did,
vcJwts : vcJwts
});

expect(vp.holder).to.equal(holderDid.did);
expect(vp.type).to.equal('VerifiablePresentation');
expect(vp.vpDataModel.verifiableCredential).to.not.be.undefined;
expect(vp.vpDataModel.verifiableCredential).to.deep.equal(vcJwts);
});

it('create and sign vp with did:key', async () => {
const vp = await VerifiablePresentation.create({
holder : holderDid.did,
vcJwts : [validVcJwt]
});

const vpJwt = await vp.sign({ did: holderDid });

await VerifiablePresentation.verify({ vpJwt });

const parsedVp = await VerifiablePresentation.parseJwt({ vpJwt });

expect(vpJwt).to.not.be.undefined;
expect(parsedVp.holder).to.equal(holderDid.did);
expect(parsedVp.type).to.equal('VerifiablePresentation');
expect(parsedVp.vpDataModel.verifiableCredential).to.not.be.undefined;
expect(parsedVp.vpDataModel.verifiableCredential).to.deep.equal([validVcJwt]);
});

it('create and sign presentatin submission vp', async () => {
const presentationSubmission: PresentationSubmission = {
id : 'presentationSubmissionId',
definition_id : 'definitionId',
descriptor_map : [
{
id : 'descriptorId',
format : 'format',
path : 'path'
}
]
};

const vp = await VerifiablePresentation.create({
holder : holderDid.did,
vcJwts : [validVcJwt],
additionalData : {
presentation_submission: presentationSubmission
},
type: 'PresentationSubmission'
});

const vpJwt = await vp.sign({ did: holderDid });

await VerifiablePresentation.verify({ vpJwt });

const parsedVp = await VerifiablePresentation.parseJwt({ vpJwt });

expect(vpJwt).to.not.be.undefined;
expect(parsedVp.holder).to.equal(holderDid.did);
expect(parsedVp.type).to.equal('PresentationSubmission');
expect(parsedVp.vpDataModel.verifiableCredential).to.not.be.undefined;
expect(parsedVp.vpDataModel.verifiableCredential).to.deep.equal([validVcJwt]);
});

it('parseJwt throws ParseException if argument is not a valid JWT', async () => {
expect(() =>
VerifiablePresentation.parseJwt({ vpJwt: 'hi' })
).to.throw('Malformed JWT');
});

it('parseJwt checks if missing vp property', async () => {
const did = await DidKeyMethod.create();
const jwt = await Jwt.sign({
signerDid : did,
payload : {
iss : did.did,
sub : did.did
}
});

expect(() =>
VerifiablePresentation.parseJwt({ vpJwt: jwt })
).to.throw('Jwt payload missing vp property');
});

it('should throw an error if holder is not defined', async () => {
try {
await VerifiablePresentation.create({
holder : '',
vcJwts : [validVcJwt]
});

expect.fail();
} catch(e: any) {
expect(e.message).to.include('Holder must be defined');
}
});
});
});