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 all 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
2 changes: 1 addition & 1 deletion packages/credentials/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@web5/credentials",
"version": "0.4.1",
"version": "0.4.2",
"description": "Verifiable Credentials",
"type": "module",
"main": "./dist/cjs/index.js",
Expand Down
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 @@
} 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 @@
}
}

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}"`);
}

Check warning on line 42 in packages/credentials/src/validators.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/validators.ts#L41-L42

Added lines #L41 - L42 were not covered by tests
}

static validateCredentialSubject(value: ICredentialSubject | ICredentialSubject[]): void {
if (Object.keys(value).length === 0) {
throw new Error(`credentialSubject must not be empty`);
Expand Down
4 changes: 4 additions & 0 deletions packages/credentials/src/verifiable-credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export class VerifiableCredential {
throw new Error('Issuer and subject must be defined');
}

if(typeof issuer !== 'string' || typeof subject !== 'string') {
throw new Error('Issuer and subject must be of type string');
}

const credentialSubject: CredentialSubject = {
id: subject,
...jsonData
Expand Down
232 changes: 232 additions & 0 deletions packages/credentials/src/verifiable-presentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import type { BearerDid } 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: BearerDid;
};

/**
* `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[];
}

Check warning on line 65 in packages/credentials/src/verifiable-presentation.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/verifiable-presentation.ts#L64-L65

Added lines #L64 - L65 were not covered by tests

/**
* 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.uri,
sub : options.did.uri,
}
});

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);
}

Check warning on line 98 in packages/credentials/src/verifiable-presentation.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/verifiable-presentation.ts#L97-L98

Added lines #L97 - L98 were not covered by tests

/**
* 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');
}

Check warning on line 124 in packages/credentials/src/verifiable-presentation.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/verifiable-presentation.ts#L123-L124

Added lines #L123 - L124 were not covered by tests
}

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

if(typeof holder !== 'string') {
throw new Error('Holder must be of type string');
}

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.');
}

Check warning on line 185 in packages/credentials/src/verifiable-presentation.ts

View check run for this annotation

Codecov / codecov/patch

packages/credentials/src/verifiable-presentation.ts#L184-L185

Added lines #L184 - L185 were not covered by tests

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!);
}
18 changes: 18 additions & 0 deletions packages/credentials/tests/verifiable-credential.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ describe('Verifiable Credential Tests', async() => {
}
});

it('should throw an error if issuer is not string', async () => {
const subjectDid = issuerDid.uri;

const anyTypeIssuer: any = DidKey.create();

try {
await VerifiableCredential.create({
type : 'StreetCred',
issuer : anyTypeIssuer,
subject : subjectDid,
data : new StreetCredibility('high', true),
});
expect.fail();
} catch(e: any) {
expect(e.message).to.include('Issuer and subject must be of type string');
}
});

it('should throw an error if data is not parseable into a JSON object', async () => {
const issuerDid = 'did:example:issuer';
const subjectDid = 'did:example:subject';
Expand Down
Loading
Loading