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

Add presentation schema feature. #82

Merged
merged 2 commits into from
Jul 18, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# bedrock-vc-delivery ChangeLog

## 4.8.0 - 2024-07-dd

### Added
- Add `presentationSchema` option to workflow step to enable passing
a JSON schema to be run against a submitted presentation.

## 4.7.0 - 2024-07-15

### Added
Expand Down
20 changes: 17 additions & 3 deletions lib/openId.js
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,23 @@ async function _processAuthorizationResponse({
const {authorizationRequest, step} = arRequest;
({exchange} = arRequest);

// FIXME: if the VP is enveloped, remove the envelope to validate or
// run validation code after verification if necessary

// FIXME: check the VP against the presentation submission if requested
// FIXME: check the VP against "trustedIssuer" in VPR, if provided
const {presentationSchema} = step;
if(presentationSchema) {
// validate the received VP
console.log('run presentation schema');
const {jsonSchema: schema} = presentationSchema;
const validate = compile({schema});
const {valid, error} = validate(presentation);
if(!valid) {
throw error;
}
}

// verify the received VP
const {verifiablePresentationRequest} = await oid4vp.toVpr(
{authorizationRequest});
Expand All @@ -928,9 +945,6 @@ async function _processAuthorizationResponse({
expectedChallenge: authorizationRequest.nonce
});

// FIXME: check the VP against the presentation submission if requested
// FIXME: check the VP against "trustedIssuer" in VPR, if provided

// store VP results in variables associated with current step
const currentStep = exchange.step;
if(!exchange.variables.results) {
Expand Down
15 changes: 15 additions & 0 deletions lib/vcapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import * as bedrock from '@bedrock/core';
import * as exchanges from './exchanges.js';
import {createChallenge as _createChallenge, verify} from './verify.js';
import {compile} from '@bedrock/validation';
import {evaluateTemplate} from './helpers.js';
import {issue} from './issue.js';
import {klona} from 'klona';
Expand Down Expand Up @@ -95,6 +96,20 @@ export async function processExchange({req, res, workflow, exchange}) {
return;
}

// FIXME: if the VP is enveloped, remove the envelope to validate or
// run validation code after verification if necessary

const {presentationSchema} = step;
if(presentationSchema) {
// validate the received VP
const {jsonSchema: schema} = presentationSchema;
const validate = compile({schema});
const {valid, error} = validate(receivedPresentation);
if(!valid) {
throw error;
}
}

// verify the received VP
const expectedChallenge = isInitialStep ? exchange.id : undefined;
const {allowUnprotectedPresentation = false} = step;
Expand Down
13 changes: 13 additions & 0 deletions schemas/bedrock-vc-workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,19 @@ const step = {
verifiablePresentationRequest: {
type: 'object'
},
presentationSchema: {
type: 'object',
required: ['type', 'jsonSchema'],
additionalProperties: false,
properties: {
type: {
type: 'string'
},
jsonSchema: {
type: 'object'
}
}
},
jwtDidProofRequest: {
type: 'object',
additionalProperties: false,
Expand Down
56 changes: 55 additions & 1 deletion test/mocha/22-vcapi-verify-vc-issue-vc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import * as helpers from './helpers.js';
import {agent} from '@bedrock/https-agent';
import {httpClient} from '@digitalbazaar/http-client';
import {klona} from 'klona';
import {mockData} from './mock.data.js';
import {v4 as uuid} from 'uuid';

const {
baseUrl, didAuthnCredentialTemplate
baseUrl, didAuthnCredentialTemplate, strictDegreePresentationSchema
} = mockData;

describe('exchange w/ VC-API delivery + DID authn + VC request', () => {
Expand Down Expand Up @@ -115,6 +116,12 @@ describe('exchange w/ VC-API delivery + DID authn + VC request', () => {
type: 'jsonata',
template: didAuthnCredentialTemplate
}];
const jsonSchema = klona(strictDegreePresentationSchema);
// FIXME: create a function to inject required `issuer` value
jsonSchema.properties.verifiableCredential.oneOf[0]
.properties.issuer = {const: verifiableCredential.issuer};
jsonSchema.properties.verifiableCredential.oneOf[1].items
.properties.issuer = {const: verifiableCredential.issuer};
// require semantically-named workflow steps
const steps = {
// DID Authn step, additionally require VC that was issued from
Expand All @@ -139,6 +146,10 @@ describe('exchange w/ VC-API delivery + DID authn + VC request', () => {
}]
}],
domain: baseUrl
},
presentationSchema: {
type: 'JsonSchema',
jsonSchema
}
}
};
Expand Down Expand Up @@ -207,6 +218,49 @@ describe('exchange w/ VC-API delivery + DID authn + VC request', () => {
}
});

it('should fail when sending VC w/unacceptable issuer', async () => {
const credentialId = `urn:uuid:${uuid()}`;
const {exchangeId} = await helpers.createCredentialOffer({
// local target user
userId: 'urn:uuid:01cc3771-7c51-47ab-a3a3-6d34b47ae3c4',
credentialDefinition: mockData.credentialDefinition,
credentialId,
preAuthorized: true,
userPinRequired: false,
capabilityAgent,
workflowId,
workflowRootZcap
});

const invalidVerifiableCredential = klona(verifiableCredential);
invalidVerifiableCredential.issuer = 'invalid:issuer';

// generate VP
const {verifiablePresentation} = await helpers.createDidAuthnVP({
domain: baseUrl,
challenge: exchangeId.slice(exchangeId.lastIndexOf('/') + 1),
did, signer, verifiableCredential: invalidVerifiableCredential
});

// post VP to get VP in response, should produce a validation error on
// the "issuer" field
let err;
let response;
try {
response = await httpClient.post(
exchangeId, {agent, json: {verifiablePresentation}});
} catch(e) {
err = e;
}
should.not.exist(response);
should.exist(err);
err.status.should.equal(400);
err.data.name.should.equal('ValidationError');
const issuerError = err.data.details.errors[0];
issuerError.name.should.equal('ValidationError');
issuerError.details.path.should.equal('.verifiableCredential.issuer');
});

it('should pass when sending VP in second call', async () => {
const credentialId = `urn:uuid:${uuid()}`;
const {exchangeId} = await helpers.createCredentialOffer({
Expand Down
18 changes: 17 additions & 1 deletion test/mocha/35-oid4vci-oid4vp.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import {
} from '@digitalbazaar/oid4-client';
import {agent} from '@bedrock/https-agent';
import {httpClient} from '@digitalbazaar/http-client';
import {klona} from 'klona';
import {mockData} from './mock.data.js';
import {v4 as uuid} from 'uuid';

const {baseUrl, didAuthnCredentialTemplate} = mockData;
const {
baseUrl, didAuthnCredentialTemplate, strictDegreePresentationSchema
} = mockData;

describe('exchange w/OID4VCI delivery + OID4VP VC requirement', () => {
let capabilityAgent;
Expand Down Expand Up @@ -126,6 +129,7 @@ describe('exchange w/OID4VCI delivery + OID4VP VC requirement', () => {
{
"createChallenge": true,
"verifiablePresentationRequest": verifiablePresentationRequest,
"presentationSchema": presentationSchema,
"openId": {
"createAuthorizationRequest": "authorizationRequest",
"client_id_scheme": "redirect_uri",
Expand All @@ -148,6 +152,8 @@ describe('exchange w/OID4VCI delivery + OID4VP VC requirement', () => {
workflowRootZcap = `urn:zcap:root:${encodeURIComponent(workflowId)}`;
});

// FIXME: add invalid issuer test that will fail against `presentationSchema`

it('should pass w/ pre-authorized code flow', async () => {
// pre-authorized flow, issuer-initiated
const credentialId = `urn:uuid:${uuid()}`;
Expand All @@ -171,6 +177,12 @@ describe('exchange w/OID4VCI delivery + OID4VP VC requirement', () => {
}],
domain: baseUrl
};
const jsonSchema = klona(strictDegreePresentationSchema);
// FIXME: create a function to inject required `issuer` value
jsonSchema.properties.verifiableCredential.oneOf[0]
.properties.issuer = {const: verifiableCredential.issuer};
jsonSchema.properties.verifiableCredential.oneOf[1].items
.properties.issuer = {const: verifiableCredential.issuer};
const {
exchangeId,
openIdUrl: issuanceUrl
Expand All @@ -187,6 +199,10 @@ describe('exchange w/OID4VCI delivery + OID4VP VC requirement', () => {
variables: {
credentialId,
verifiablePresentationRequest: vpr,
presentationSchema: {
type: 'JsonSchema',
jsonSchema
},
openId: {
createAuthorizationRequest: 'authorizationRequest'
}
Expand Down
97 changes: 97 additions & 0 deletions test/mocha/mock.data.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,4 +342,101 @@ mockData.prcCredentialContext = {
"Person": "http://schema.org/Person"
}
};

mockData.strictDegreeCredentialSchema = {
title: 'Strict Degree Credential',
type: 'object',
required: ['@context', 'type', 'issuer', 'credentialSubject'],
additionalProperties: false,
properties: {
'@context': {
type: 'array',
items: [{
const: 'https://www.w3.org/2018/credentials/v1'
}, {
const: 'https://www.w3.org/2018/credentials/examples/v1'
}]
},
id: {
type: 'string'
},
// a real system would make `issuer` value a very specific requirement
issuer: {
type: 'string'
},
type: {
type: 'array',
items: [{
const: 'VerifiableCredential'
}, {
const: 'UniversityDegreeCredential'
}]
},
issuanceDate: {
type: 'string'
},
credentialSubject: {
type: 'object',
required: ['degree'],
additionalProperties: false,
properties: {
id: {
type: 'string'
},
degree: {
type: 'object',
required: ['type', 'name'],
additionalProperties: false,
properties: {
type: {
const: 'BachelorDegree'
},
name: {
const: 'Bachelor of Science and Arts'
}
}
}
}
},
proof: {
oneOf: [{
type: 'object',
}, {
type: 'array'
}]
}
}
};

mockData.strictDegreePresentationSchema = {
title: 'Presentation',
type: 'object',
required: ['@context', 'type', 'verifiableCredential'],
additionalProperties: false,
properties: {
'@context': {
type: 'array'
},
holder: {
type: 'string'
},
type: {
type: 'array'
},
proof: {
oneOf: [{
type: 'object',
}, {
type: 'array'
}]
},
verifiableCredential: {
oneOf: [mockData.strictDegreeCredentialSchema, {
type: 'array',
minItems: 1,
items: mockData.strictDegreeCredentialSchema
}]
}
}
};
/* eslint-enable */
Loading