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 VC-JWT support and tests. #69

Merged
merged 12 commits into from
Aug 8, 2024
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# bedrock-vc-verifier ChangeLog

## 20.1.0 - 2024-08-dd

### Added
- Add feature to verify VC-JWT-enveloped credentials and presentations. These
credentials and presentations must be sent using an VC 2.x
`EnvelopedVerifiableCredential` or `EnvelopedVerifiablePresentation` to the
appropriate VC API endpoint. For presentations, any VCs inside the
presentation can be provided using `EnvelopedVerifiableCredential` or, if a
the `EnvelopedVerifiablePresentation` envelopes a 1.1 VP, the VCs can
be expressed directly as strings to allow for interoperability with
VC-JWT 1.1.

## 20.0.0 - 2024-08-02

### Added
Expand Down
63 changes: 63 additions & 0 deletions lib/di.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*!
* Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as vc from '@digitalbazaar/vc';
import {checkStatus} from './status.js';
import {createDocumentLoader} from './documentLoader.js';
import {createSuites} from './suites.js';

export async function verifyCredential({config, credential, checks} = {}) {
const documentLoader = await createDocumentLoader({config});
const suite = createSuites();

const result = await vc.verifyCredential({
credential,
documentLoader,
suite,
// only check credential status when option is set
checkStatus: checks.includes('credentialStatus') ?
checkStatus : () => ({verified: true})
});
// if proof should have been checked but wasn't due to an error,
// try to run the check again using the VC's issuance date
if(checks.includes('proof') &&
result.error && !result.proof && result.results?.[0] &&
typeof credential.issuanceDate === 'string') {
const proofResult = await vc.verifyCredential({
credential,
documentLoader,
suite,
now: new Date(credential.issuanceDate),
// only check credential status when option is set
checkStatus: checks.includes('credentialStatus') ?
checkStatus : () => ({verified: true})
});
if(proofResult.verified) {
// overlay original (failed) results on top of proof results
result.results[0] = {
...proofResult.results[0],
...result.results[0],
proofVerified: true
};
}
}
// ensure all proofs are verified in order to return `verified`
let {verified} = result;
verified = !!(verified && result?.results?.every(({verified}) => verified));
return {...result, verified};
}

export async function verifyPresentation({
config, presentation, challenge, domain, checks
} = {}) {
const verifyOptions = {
challenge,
domain,
presentation,
documentLoader: await createDocumentLoader({config}),
suite: createSuites(),
unsignedPresentation: !checks.includes('proof'),
checkStatus
};
return vc.verify(verifyOptions);
}
51 changes: 51 additions & 0 deletions lib/envelopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';
import * as vcjwt from './vcjwt.js';

const {util: {BedrockError}} = bedrock;

export async function verifyEnvelopedCredential({envelopedCredential} = {}) {
try {
const {contents: jwt} = _parseEnvelope({
envelope: envelopedCredential
});
return vcjwt.verifyEnvelopedCredential({jwt});
} catch(error) {
return {verified: false, error};
}
}

export async function verifyEnvelopedPresentation({
envelopedPresentation, challenge, domain
} = {}) {
try {
const {contents: jwt} = _parseEnvelope({
envelope: envelopedPresentation
});
return vcjwt.verifyEnvelopedPresentation({jwt, challenge, domain});
} catch(error) {
return {verified: false, error};
}
}

function _parseEnvelope({envelope}) {
const {id} = envelope;
let format;
const comma = id.indexOf(',');
if(id.startsWith('data:') && comma !== -1) {
format = id.slice('data:'.length, comma);
}
if(format !== 'application/jwt') {
throw new BedrockError(
`Unknown envelope format "${format}".`, {
name: 'DataError',
details: {
httpStatusCode: 400,
public: true
},
});
}
return {contents: id.slice(comma + 1), format};
}
69 changes: 12 additions & 57 deletions lib/http.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
/*!
* Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
* Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';
import * as vc from '@digitalbazaar/vc';
import {createChallenge, verifyChallenge} from './challenges.js';
import {
createChallengeBody,
verifyCredentialBody,
verifyPresentationBody
} from '../schemas/bedrock-vc-verifier.js';
import {metering, middleware} from '@bedrock/service-core';
import {verifyCredential, verifyPresentation} from './verify.js';
import {asyncHandler} from '@bedrock/express';
import bodyParser from 'body-parser';
import {checkStatus} from './status.js';
import cors from 'cors';
import {createDocumentLoader} from './documentLoader.js';
import {createSuites} from './suites.js';
import {serializeError} from 'serialize-error';
import {createValidateMiddleware as validate} from '@bedrock/validation';

Expand All @@ -33,7 +30,6 @@ bedrock.events.on('bedrock-express.configure.bodyParser', app => {

export async function addRoutes({app, service} = {}) {
const {routePrefix} = service;
const suite = createSuites();
const cfg = bedrock.config['vc-verifier'];
const baseUrl = `${routePrefix}/:localId`;
const routes = {
Expand Down Expand Up @@ -70,12 +66,11 @@ export async function addRoutes({app, service} = {}) {
app.post(
routes.credentialsVerify,
cors(),
validate({bodySchema: verifyCredentialBody}),
validate({bodySchema: verifyCredentialBody()}),
getConfigMiddleware,
middleware.authorizeServiceObjectRequest(),
asyncHandler(async (req, res) => {
const {config} = req.serviceObject;
const documentLoader = await createDocumentLoader({config});

let response;
try {
Expand All @@ -86,37 +81,7 @@ export async function addRoutes({app, service} = {}) {

const {checks} = options;
_validateChecks({checks});
const result = await vc.verifyCredential({
credential,
documentLoader,
suite,
// only check credential status when option is set
checkStatus: checks.includes('credentialStatus') ?
checkStatus : () => ({verified: true})
});
// if proof should have been checked but wasn't due to an error,
// try to run the check again using the VC's issuance date
if(checks.includes('proof') &&
result.error && !result.proof && result.results[0] &&
typeof credential.issuanceDate === 'string') {
const proofResult = await vc.verifyCredential({
credential,
documentLoader,
suite,
now: new Date(credential.issuanceDate),
// only check credential status when option is set
checkStatus: checks.includes('credentialStatus') ?
checkStatus : () => ({verified: true})
});
if(proofResult.verified) {
// overlay original (failed) results on top of proof results
result.results[0] = {
...proofResult.results[0],
...result.results[0],
proofVerified: true
};
}
}
const result = await verifyCredential({config, credential, checks});
response = _createResponse({credential, result, checks});
} catch(e) {
response = _createResponse({error: e});
Expand Down Expand Up @@ -150,17 +115,16 @@ export async function addRoutes({app, service} = {}) {
app.post(
routes.presentationsVerify,
cors(),
validate({bodySchema: verifyPresentationBody}),
validate({bodySchema: verifyPresentationBody()}),
getConfigMiddleware,
middleware.authorizeServiceObjectRequest(),
asyncHandler(async (req, res) => {
const {config} = req.serviceObject;
const documentLoader = await createDocumentLoader({config});

let response;
try {
const {
verifiablePresentation,
verifiablePresentation: presentation,
options = {}
} = req.body;

Expand All @@ -174,7 +138,6 @@ export async function addRoutes({app, service} = {}) {
}

_validateChecks({checks});
const unsignedPresentation = !checks.includes('proof');

// allow for `checks` to indicate whether or not the challenge
// should be checked
Expand All @@ -190,20 +153,12 @@ export async function addRoutes({app, service} = {}) {
({uses: challengeUses} = result);
}

const verifyOptions = {
challenge,
presentation: verifiablePresentation,
documentLoader,
suite,
unsignedPresentation,
checkStatus
};
const {proof} = verifiablePresentation;
if(proof && proof.domain) {
// FIXME: do not set a default
verifyOptions.domain = domain || 'issuer.example.com';
}
const result = await vc.verify(verifyOptions);
// FIXME: do not set a default domain
const expectedDomain = domain ??
(presentation?.proof?.domain && 'issuer.example.com');
const result = await verifyPresentation({
config, presentation, challenge, domain: expectedDomain, checks
});
response = _createResponse({result, challengeUses, checks});
} catch(e) {
response = _createResponse({error: e});
Expand Down
Loading