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

Accept VCs as JSON-LD or JWT when creating Verifiable Presentation #226

Closed
Closed
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
11 changes: 6 additions & 5 deletions packages/credentials/src/ssi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
validateDefinition,
validateSubmission,
Validated,
OriginalVerifiableCredential,
resetPex
} from './types.js';

Expand All @@ -35,7 +36,7 @@ export type CreateVcOptions = {

export type CreateVpOptions = {
presentationDefinition: PresentationDefinition,
verifiableCredentialJwts: string[]
verifiableCredentials: OriginalVerifiableCredential[]
}

export type SignOptions = {
Expand Down Expand Up @@ -176,7 +177,7 @@ export class VerifiablePresentation {
/**
* Creates a Verifiable Presentation (VP) JWT from a presentation definition and set of credentials.
* @param signOptions - Options for creating the VP including subjectDid, issuerDid, kid, and the sign function.
* @param createVpOptions - Options for creating the VP including presentationDefinition, verifiableCredentialJwts.
* @param createVpOptions - Options for creating the VP including presentationDefinition, verifiableCredentials.
* @returns A promise that resolves to a VP JWT.
*/
public static async create(signOptions: SignOptions, createVpOptions: CreateVpOptions,): Promise<VpJwt> {
Expand All @@ -185,15 +186,15 @@ export class VerifiablePresentation {
const pdValidated: Validated = validateDefinition(createVpOptions.presentationDefinition);
isValid(pdValidated);

const evaluationResults: EvaluationResults = evaluateCredentials(createVpOptions.presentationDefinition, createVpOptions.verifiableCredentialJwts);
const evaluationResults: EvaluationResults = evaluateCredentials(createVpOptions.presentationDefinition, createVpOptions.verifiableCredentials);

if (evaluationResults.warnings?.length) {
console.warn('Warnings were generated during the evaluation process: ' + JSON.stringify(evaluationResults.warnings));
}

if (evaluationResults.areRequiredCredentialsPresent.toString() !== 'info' || evaluationResults.errors?.length) {
let errorMessage = 'Failed to create Verifiable Presentation JWT due to: ';
if(evaluationResults.areRequiredCredentialsPresent) {
if (evaluationResults.areRequiredCredentialsPresent) {
errorMessage += 'Required Credentials Not Present: ' + JSON.stringify(evaluationResults.areRequiredCredentialsPresent);
}

Expand All @@ -204,7 +205,7 @@ export class VerifiablePresentation {
throw new Error(errorMessage);
}

const presentationResult: PresentationResult = presentationFrom(createVpOptions.presentationDefinition, createVpOptions.verifiableCredentialJwts);
const presentationResult: PresentationResult = presentationFrom(createVpOptions.presentationDefinition, createVpOptions.verifiableCredentials);

const submissionValidated: Validated = validateSubmission(presentationResult.presentationSubmission);
isValid(submissionValidated);
Expand Down
16 changes: 12 additions & 4 deletions packages/credentials/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ICredentialSchemaType,
ICredentialContextType,
AdditionalClaims,
OriginalVerifiableCredential as OriginVerifiableCredential,
} from '@sphereon/ssi-types';
import type {
Descriptor,
Expand Down Expand Up @@ -42,6 +43,13 @@ export const resetPex = () => {
*/
export type VerifiableCredentialTypeV1 = ICredential;

/**
* An Original Verifiable Credential is used in a Verifiable Presentation.
*
* @see {@link https://www.w3.org/TR/vc-data-model/#credentials | VC Data Model}
*/
export type OriginalVerifiableCredential = OriginVerifiableCredential;

/**
* A Credential Context is to convey the meaning of the data and term definitions of the data in a verifiable credential.
*
Expand Down Expand Up @@ -165,7 +173,7 @@ export type Validated = PexValidated;
*/
export const evaluateCredentials = (
presentationDefinition: PresentationDefinition,
verifiableCredentials: string[]
verifiableCredentials: OriginalVerifiableCredential[]
): EvaluationResults => {
return pex.evaluateCredentials(presentationDefinition, verifiableCredentials);
};
Expand All @@ -177,7 +185,7 @@ export const evaluateCredentials = (
export const evaluatePresentation = (
presentationDefinition: PresentationDefinition,
presentation: VerifiablePresentationV1
): EvaluationResults => {
): EvaluationResults => {
return pex.evaluatePresentation(presentationDefinition, presentation);
};

Expand All @@ -187,7 +195,7 @@ export const evaluatePresentation = (
*/
export const presentationFrom = (
presentationDefinition: PresentationDefinition,
verifiableCredentials: string[]
verifiableCredentials: OriginalVerifiableCredential[]
): PresentationResult => {
return pex.presentationFrom(presentationDefinition, verifiableCredentials);
};
Expand Down Expand Up @@ -287,7 +295,7 @@ export type DisplayMapping = {
type: 'string' | 'boolean' | 'number' | 'integer';
/** If the `type` property is "string", this property is used to format the string in any rendered UI */
format?: 'date-time' | 'time' | 'date' | 'email' | 'idn-email' | 'hostname' | 'idn-hostname' |
'ipv4' | 'ipv6' | 'uri' | 'uri-reference' | 'iri' | 'iri-reference';
'ipv4' | 'ipv6' | 'uri' | 'uri-reference' | 'iri' | 'iri-reference';
}
/**
* String to be rendered into the UI if all the `path` property's item's value is
Expand Down
142 changes: 105 additions & 37 deletions packages/credentials/tests/ssi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from 'chai';
import { VcJwt, VpJwt, VerifiableCredentialTypeV1, PresentationDefinition} from '../src/types.js';
import {VerifiableCredential, VerifiablePresentation, CreateVcOptions, CreateVpOptions, SignOptions} from '../src/ssi.js';
import { VcJwt, VpJwt, VerifiableCredentialTypeV1, PresentationDefinition, OriginalVerifiableCredential } from '../src/types.js';
import { VerifiableCredential, VerifiablePresentation, CreateVcOptions, CreateVpOptions, SignOptions } from '../src/ssi.js';
import { Ed25519, Jose } from '@web5/crypto';
import { DidKeyMethod } from '@web5/dids';
import { getCurrentXmlSchema112Timestamp } from '../src/utils.js';
Expand All @@ -18,7 +18,7 @@ describe('SSI Tests', () => {
beforeEach(async () => {
alice = await DidKeyMethod.create();
[signingKeyPair] = alice.keySet.verificationMethodKeys!;
privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial;
privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk! })).keyMaterial;
subjectIssuerDid = alice.did;
signer = EdDsaSigner(privateKey);
signOptions = {
Expand All @@ -41,7 +41,7 @@ describe('SSI Tests', () => {
});

it('creates a VC JWT with VerifiableCredentialV1 type', async () => {
const vc:VerifiableCredentialTypeV1 = {
const vc: VerifiableCredentialTypeV1 = {
id : 'id123',
'@context' : ['https://www.w3.org/2018/credentials/v1'],
credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' },
Expand All @@ -55,7 +55,7 @@ describe('SSI Tests', () => {
});

it('fails to create a VC JWT with CreateVCOptions and VC', async () => {
const vc:VerifiableCredentialTypeV1 = {
const vc: VerifiableCredentialTypeV1 = {
id : 'id123',
'@context' : ['https://www.w3.org/2018/credentials/v1'],
credentialSubject : { id: subjectIssuerDid, btcAddress: 'abc123' },
Expand All @@ -69,11 +69,11 @@ describe('SSI Tests', () => {
issuer : { id: subjectIssuerDid }
};

await expectThrowsAsync(() => VerifiableCredential.create(signOptions, vcCreateOptions, vc), 'options and verifiableCredentials are mutually exclusive, either include the full verifiableCredential or the options to create one');
await expectThrowsAsync(() => VerifiableCredential.create(signOptions, vcCreateOptions, vc), 'options and verifiableCredentials are mutually exclusive, either include the full verifiableCredential or the options to create one');
});

it('fails to create a VC JWT with no CreateVCOptions and no VC', async () => {
await expectThrowsAsync(() => VerifiableCredential.create(signOptions, undefined, undefined), 'options or verifiableCredential must be provided');
await expectThrowsAsync(() => VerifiableCredential.create(signOptions, undefined, undefined), 'options or verifiableCredential must be provided');
});

it('creates a VC JWT with a VC', async () => {
Expand All @@ -93,7 +93,7 @@ describe('SSI Tests', () => {
});

it('fails to verify an invalid VC JWT', async () => {
await expectThrowsAsync(() => VerifiableCredential.verify('invalid-jwt'), 'Incorrect format JWT');
await expectThrowsAsync(() => VerifiableCredential.verify('invalid-jwt'), 'Incorrect format JWT');
});

it('decodes a VC JWT', async () => {
Expand Down Expand Up @@ -138,7 +138,7 @@ describe('SSI Tests', () => {
};

const vcJwt: VcJwt = await VerifiableCredential.create(vcSignOptions, vcCreateOptions);
await expectThrowsAsync(() => VerifiableCredential.verify(vcJwt), 'resolver_error: Unable to resolve DID document for bad:did: invalidDid');
await expectThrowsAsync(() => VerifiableCredential.verify(vcJwt), 'resolver_error: Unable to resolve DID document for bad:did: invalidDid');
});
});

Expand All @@ -148,15 +148,15 @@ describe('SSI Tests', () => {
let vcJwt: VcJwt;

beforeEach(async () => {
vcCreateOptions = {credentialSubject: {id: subjectIssuerDid, btcAddress: 'abc123'}, issuer: {id: subjectIssuerDid}};
signOptions = {issuerDid: alice.did, subjectDid: alice.did, kid: '#' + alice.did.split(':')[2], signer: signer};
vcCreateOptions = { credentialSubject: { id: subjectIssuerDid, btcAddress: 'abc123' }, issuer: { id: subjectIssuerDid } };
signOptions = { issuerDid: alice.did, subjectDid: alice.did, kid: '#' + alice.did.split(':')[2], signer: signer };
vcJwt = await VerifiableCredential.create(signOptions, vcCreateOptions);
});

it('creates a VP JWT', async () => {
const vpCreateOptions: CreateVpOptions = {
presentationDefinition : getPresentationDefinition(),
verifiableCredentialJwts : [vcJwt]
presentationDefinition : getPresentationDefinition(),
verifiableCredentials : [vcJwt]
};

const vpJwt: VpJwt = await VerifiablePresentation.create(signOptions, vpCreateOptions);
Expand All @@ -170,8 +170,8 @@ describe('SSI Tests', () => {

it('verifies a VP JWT', async () => {
const vpCreateOptions: CreateVpOptions = {
presentationDefinition : getPresentationDefinition(),
verifiableCredentialJwts : [vcJwt],
presentationDefinition : getPresentationDefinition(),
verifiableCredentials : [vcJwt],
};

const vpJwt: VpJwt = await VerifiablePresentation.create(signOptions, vpCreateOptions);
Expand All @@ -180,8 +180,8 @@ describe('SSI Tests', () => {

it('evaluates an invalid VP with empty VCs', async () => {
const vpCreateOptions: CreateVpOptions = {
presentationDefinition : getPresentationDefinition(),
verifiableCredentialJwts : []
presentationDefinition : getPresentationDefinition(),
verifiableCredentials : []
};

try {
Expand All @@ -193,13 +193,13 @@ describe('SSI Tests', () => {
});

it('evaluates an invalid VP with invalid subject', async () => {
vcCreateOptions = {credentialSubject: {id: subjectIssuerDid, badSubject: 'abc123'}, issuer: {id: subjectIssuerDid}};
signOptions = {issuerDid: alice.did, subjectDid: alice.did, kid: '#' + alice.did.split(':')[2], signer: signer};
vcCreateOptions = { credentialSubject: { id: subjectIssuerDid, badSubject: 'abc123' }, issuer: { id: subjectIssuerDid } };
signOptions = { issuerDid: alice.did, subjectDid: alice.did, kid: '#' + alice.did.split(':')[2], signer: signer };
vcJwt = await VerifiableCredential.create(signOptions, vcCreateOptions);

const vpCreateOptions: CreateVpOptions = {
presentationDefinition : getPresentationDefinition(),
verifiableCredentialJwts : [vcJwt]
presentationDefinition : getPresentationDefinition(),
verifiableCredentials : [vcJwt]
};

try {
Expand All @@ -212,28 +212,28 @@ describe('SSI Tests', () => {

it('evaluates an invalid VP with bad presentation definition', async () => {
const presentationDefinition = getPresentationDefinition();
presentationDefinition.input_descriptors[0].constraints!.fields![0].path = ['$.credentialSubject.badSubject'];

const vpCreateOptions: CreateVpOptions = {
presentationDefinition : presentationDefinition,
verifiableCredentialJwts : [vcJwt]
};

try {
await VerifiablePresentation.create(signOptions, vpCreateOptions);
} catch (err: any) {
expect(err).instanceOf(Error);
expect(err!.message).to.equal('Failed to create Verifiable Presentation JWT due to: Required Credentials Not Present: "error"Errors: [{"tag":"FilterEvaluation","status":"error","message":"Input candidate does not contain property: $.input_descriptors[0]: $.verifiableCredential[0]"},{"tag":"MarkForSubmissionEvaluation","status":"error","message":"The input candidate is not eligible for submission: $.input_descriptors[0]: $.verifiableCredential[0]"}]');
}
presentationDefinition.input_descriptors[0].constraints!.fields![0].path = ['$.credentialSubject.badSubject'];

const vpCreateOptions: CreateVpOptions = {
presentationDefinition : presentationDefinition,
verifiableCredentials : [vcJwt]
};

try {
await VerifiablePresentation.create(signOptions, vpCreateOptions);
} catch (err: any) {
expect(err).instanceOf(Error);
expect(err!.message).to.equal('Failed to create Verifiable Presentation JWT due to: Required Credentials Not Present: "error"Errors: [{"tag":"FilterEvaluation","status":"error","message":"Input candidate does not contain property: $.input_descriptors[0]: $.verifiableCredential[0]"},{"tag":"MarkForSubmissionEvaluation","status":"error","message":"The input candidate is not eligible for submission: $.input_descriptors[0]: $.verifiableCredential[0]"}]');
}
});

it('evaluates an invalid VP with an invalid presentation definition', async () => {
const presentationDefinition = getPresentationDefinition();
presentationDefinition.frame = { '@id': 'this is not valid' };

const vpCreateOptions: CreateVpOptions = {
presentationDefinition : presentationDefinition,
verifiableCredentialJwts : [vcJwt]
presentationDefinition : presentationDefinition,
verifiableCredentials : [vcJwt]
};

try {
Expand All @@ -243,6 +243,23 @@ describe('SSI Tests', () => {
expect(err!.message).to.equal('Failed to pass validation check due to: Validation Errors: [{"tag":"presentation_definition.frame","status":"error","message":"frame value is not valid"}]');
}
});

it('accepts JSON-LD VC for presentation', async () => {
const presentationDefinition = getPresentationDefinition2();

const vpCreateOptions: CreateVpOptions = {
presentationDefinition : presentationDefinition,
verifiableCredentials : [getExampleAlumniVC()]
};

const vpJwt: VpJwt = await VerifiablePresentation.create(signOptions, vpCreateOptions);
expect(vpJwt).to.exist;

const decodedVp = VerifiablePresentation.decode(vpJwt);
expect(decodedVp).to.have.property('header');
expect(decodedVp).to.have.property('payload');
expect(decodedVp).to.have.property('signature');
});
});
});

Expand Down Expand Up @@ -283,9 +300,60 @@ function getPresentationDefinition(): PresentationDefinition {
};
}

function getPresentationDefinition2(): PresentationDefinition {
return {
'id' : 'test-pd-id',
'name' : 'simple PD',
'purpose' : 'pd for testing',
'input_descriptors' : [
{
'id' : 'whatever',
'purpose' : 'id for testing',
'constraints' : {
'fields': [
{
'path': [
'$.credentialSubject.alumniOf',
]
}
]
}
}
]
};
}

function getExampleAlumniVC(): OriginalVerifiableCredential {
return {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://www.w3.org/2018/credentials/examples/v1',
'https://w3id.org/security/suites/ed25519-2020/v1'
],
'id' : 'https://example.com/credentials/1872',
'type' : [
'VerifiableCredential',
'AlumniCredential'
],
'issuer' : 'https://example.edu/issuers/565049',
'issuanceDate' : '2010-01-01T19:23:24Z',
'credentialSubject' : {
'id' : 'did:example:ebfeb1f712ebc6f1c276e12ec21',
'alumniOf' : 'Example University'
},
'proof': {
'type' : 'Ed25519Signature2020',
'created' : '2010-01-01T19:23:24Z',
'proofPurpose' : 'assertionMethod',
'proofValue' : 'zXLdRHYXUXAFtoD7LXcMfT4oNWg52SGFSYux3HdPnM7GLwcivi7PKXA5YJiqQQfiXp1tygzfzLj3i7fTLouEEuFr',
'verificationMethod' : 'https://example.edu/issuers/565049#key-1',
}
};
}

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