@bedrock/vc-verifier 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ /*!
2
+ * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import '@bedrock/credentials-context';
6
+ import '@bedrock/did-context';
7
+ import '@bedrock/did-io';
8
+ import '@bedrock/security-context';
9
+ import '@bedrock/vc-status-list-context';
10
+ import '@bedrock/vc-revocation-list-context';
11
+ import '@bedrock/veres-one-context';
12
+ import {createContextDocumentLoader} from '@bedrock/service-context-store';
13
+ import {didIo} from '@bedrock/did-io';
14
+ import {
15
+ documentLoader as brDocLoader,
16
+ httpClientHandler,
17
+ JsonLdDocumentLoader
18
+ } from '@bedrock/jsonld-document-loader';
19
+
20
+ const serviceType = 'vc-verifier';
21
+ let webLoader;
22
+
23
+ bedrock.events.on('bedrock.init', () => {
24
+ // build web loader if configuration calls for it
25
+ const cfg = bedrock.config['vc-verifier'];
26
+ if(cfg.documentLoader.http || cfg.documentLoader.https) {
27
+ const jdl = new JsonLdDocumentLoader();
28
+
29
+ if(cfg.documentLoader.http) {
30
+ jdl.setProtocolHandler({protocol: 'http', handler: httpClientHandler});
31
+ }
32
+ if(cfg.documentLoader.https) {
33
+ jdl.setProtocolHandler({protocol: 'https', handler: httpClientHandler});
34
+ }
35
+
36
+ webLoader = jdl.build();
37
+ }
38
+ });
39
+
40
+ /**
41
+ * Creates a document loader for the verifier instance identified via the
42
+ * given config.
43
+ *
44
+ * @param {object} options - The options to use.
45
+ * @param {object} options.config - The verifier instance config.
46
+ *
47
+ * @returns {Promise<Function>} The document loader.
48
+ */
49
+ export async function createDocumentLoader({config} = {}) {
50
+ const contextDocumentLoader = await createContextDocumentLoader(
51
+ {config, serviceType});
52
+
53
+ return async function documentLoader(url) {
54
+ // resolve all DID URLs through did-io
55
+ if(url.startsWith('did:')) {
56
+ const document = await didIo.get({url});
57
+ return {
58
+ contextUrl: null,
59
+ documentUrl: url,
60
+ document
61
+ };
62
+ }
63
+
64
+ try {
65
+ // try to resolve URL through built-in doc loader
66
+ return await brDocLoader(url);
67
+ } catch(e) {
68
+ // FIXME: improve to check for `NotFoundError` once `e.name`
69
+ // supports it
70
+ }
71
+
72
+ try {
73
+ // try to resolve URL through context doc loader
74
+ return await contextDocumentLoader(url);
75
+ } catch(e) {
76
+ // use web loader if configured
77
+ if(url.startsWith('http') && e.name === 'NotFoundError' && webLoader) {
78
+ return webLoader(url);
79
+ }
80
+ throw e;
81
+ }
82
+ };
83
+ }
package/lib/http.js ADDED
@@ -0,0 +1,284 @@
1
+ /*!
2
+ * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import {createChallenge, verifyChallenge} from './challenges.js';
6
+ import {asyncHandler} from '@bedrock/express';
7
+ import bodyParser from 'body-parser';
8
+ import {checkStatus} from './status.js';
9
+ import cors from 'cors';
10
+ import {
11
+ createChallengeBody,
12
+ verifyCredentialBody,
13
+ verifyPresentationBody
14
+ } from '../schemas/bedrock-vc-verifier.js';
15
+ import {createDocumentLoader} from './documentLoader.js';
16
+ import {createRequire} from 'module';
17
+ import {createValidateMiddleware as validate} from '@bedrock/validation';
18
+ import {metering, middleware} from '@bedrock/service-core';
19
+ const require = createRequire(import.meta.url);
20
+ const {Ed25519Signature2020} =
21
+ require('@digitalbazaar/ed25519-signature-2020');
22
+ const {Ed25519Signature2018} =
23
+ require('@digitalbazaar/ed25519-signature-2018');
24
+ const vc = require('@digitalbazaar/vc');
25
+
26
+ const {util: {BedrockError}} = bedrock;
27
+
28
+ // FIXME: remove and apply at top-level application
29
+ bedrock.events.on('bedrock-express.configure.bodyParser', app => {
30
+ app.use(bodyParser.json({limit: '10MB', type: ['json', '+json']}));
31
+ });
32
+
33
+ export async function addRoutes({app, service} = {}) {
34
+ const {routePrefix} = service;
35
+
36
+ const cfg = bedrock.config['vc-verifier'];
37
+ const baseUrl = `${routePrefix}/:localId`;
38
+ const routes = {
39
+ challenges: `${baseUrl}${cfg.routes.challenges}`,
40
+ credentialsVerify: `${baseUrl}${cfg.routes.credentialsVerify}`,
41
+ presentationsVerify: `${baseUrl}${cfg.routes.presentationsVerify}`
42
+ };
43
+
44
+ const {supportedSuites} = cfg;
45
+ const suite = [];
46
+ if(supportedSuites.includes('Ed25519Signature2018')) {
47
+ suite.push(new Ed25519Signature2018());
48
+ }
49
+ if(supportedSuites.includes('Ed25519Signature2020')) {
50
+ suite.push(new Ed25519Signature2020());
51
+ }
52
+
53
+ const getConfigMiddleware = middleware.createGetConfigMiddleware({service});
54
+
55
+ /* Note: CORS is used on all endpoints. This is safe because authorization
56
+ uses HTTP signatures + capabilities or OAuth2, not cookies; CSRF is not
57
+ possible. */
58
+
59
+ // create a challenge
60
+ app.options(routes.challenges, cors());
61
+ app.post(
62
+ routes.challenges,
63
+ cors(),
64
+ validate({bodySchema: createChallengeBody}),
65
+ getConfigMiddleware,
66
+ // FIXME: add middleware to switch between oauth2 / zcap based on headers
67
+ middleware.authorizeConfigZcapInvocation(),
68
+ asyncHandler(async (req, res) => {
69
+ const {config} = req.serviceObject;
70
+ const challenge = await createChallenge({verifierId: config.id});
71
+ res.json({challenge});
72
+
73
+ // meter operation usage
74
+ metering.reportOperationUsage({req});
75
+ }));
76
+
77
+ // verify a credential
78
+ app.options(routes.credentialsVerify, cors());
79
+ app.post(
80
+ routes.credentialsVerify,
81
+ cors(),
82
+ validate({bodySchema: verifyCredentialBody}),
83
+ getConfigMiddleware,
84
+ // FIXME: add middleware to switch between oauth2 / zcap based on headers
85
+ middleware.authorizeConfigZcapInvocation(),
86
+ asyncHandler(async (req, res) => {
87
+ const {config} = req.serviceObject;
88
+ const documentLoader = await createDocumentLoader({config});
89
+
90
+ let response;
91
+ try {
92
+ const {
93
+ options = {},
94
+ verifiableCredential: credential,
95
+ } = req.body;
96
+
97
+ const {checks} = options;
98
+ _validateChecks({checks});
99
+ const result = await vc.verifyCredential({
100
+ credential,
101
+ documentLoader,
102
+ suite,
103
+ // only check credential status when option is set
104
+ checkStatus: checks.includes('credentialStatus') ?
105
+ checkStatus : () => ({verified: true})
106
+ });
107
+ response = _createResponse({credential, result, checks});
108
+ } catch(e) {
109
+ response = _createResponse({error: e});
110
+ }
111
+
112
+ if(response.verified) {
113
+ res.status(200).json(response);
114
+ } else {
115
+ res.status(400).json(response).end();
116
+ }
117
+
118
+ // meter operation usage
119
+ metering.reportOperationUsage({req});
120
+ }));
121
+
122
+ // verify a presentation
123
+ app.options(routes.presentationsVerify, cors());
124
+ /**
125
+ * Verifies a Verifiable Presentation.
126
+ *
127
+ * POST /verifiers/z1234/presentations/verify
128
+ * {
129
+ * "verifiablePresentation": {...},
130
+ * "options": {
131
+ * "challenge": "...",
132
+ * "checks": ["proof", "credentialStatus"],
133
+ * "domain": "issuer.example.com"
134
+ * }
135
+ * }.
136
+ */
137
+ app.post(
138
+ routes.presentationsVerify,
139
+ cors(),
140
+ validate({bodySchema: verifyPresentationBody}),
141
+ getConfigMiddleware,
142
+ // FIXME: add middleware to switch between oauth2 / zcap based on headers
143
+ middleware.authorizeConfigZcapInvocation(),
144
+ asyncHandler(async (req, res) => {
145
+ const {config} = req.serviceObject;
146
+ const documentLoader = await createDocumentLoader({config});
147
+
148
+ let response;
149
+ try {
150
+ const {
151
+ verifiablePresentation,
152
+ options = {}
153
+ } = req.body;
154
+
155
+ const {challenge, checks, domain} = options;
156
+ if(!challenge) {
157
+ throw new BedrockError(
158
+ '"options.challenge" is required.', 'TypeError', {
159
+ httpStatusCode: 400,
160
+ public: true
161
+ });
162
+ }
163
+
164
+ _validateChecks({checks});
165
+ const unsignedPresentation = !checks.includes('proof');
166
+
167
+ // FIXME: allow for `checks` to request whether or not the challenge
168
+ // should be checked; for now, default to checking it
169
+
170
+ // first, check the challenge
171
+ const {verified, uses: challengeUses, error} = await verifyChallenge(
172
+ {challenge, verifierId: config.id});
173
+ if(!verified) {
174
+ throw error;
175
+ }
176
+
177
+ const verifyOptions = {
178
+ challenge,
179
+ presentation: verifiablePresentation,
180
+ documentLoader,
181
+ suite,
182
+ unsignedPresentation,
183
+ checkStatus
184
+ };
185
+ const {proof} = verifiablePresentation;
186
+ if(proof && proof.domain) {
187
+ // FIXME: do not set a default
188
+ verifyOptions.domain = domain || 'issuer.example.com';
189
+ }
190
+ const result = await vc.verify(verifyOptions);
191
+ response = _createResponse({result, challengeUses, checks});
192
+ } catch(e) {
193
+ response = _createResponse({error: e});
194
+ }
195
+
196
+ if(response.verified) {
197
+ res.status(200).json(response);
198
+ } else {
199
+ res.status(400).json(response);
200
+ }
201
+
202
+ // meter operation usage
203
+ metering.reportOperationUsage({req});
204
+ }));
205
+ }
206
+
207
+ function _validateChecks({checks}) {
208
+ if(!Array.isArray(checks)) {
209
+ throw new BedrockError(
210
+ '"options.checks" must be an array.', 'TypeError', {
211
+ httpStatusCode: 400,
212
+ public: true
213
+ });
214
+ }
215
+ }
216
+
217
+ function _createResponse({credential, result, challengeUses, error, checks}) {
218
+ result = result || {verified: false, error};
219
+
220
+ let response;
221
+ if(result.verified) {
222
+ result.checks = checks;
223
+ response = {...result, challengeUses, checks};
224
+ } else {
225
+ // debugging purposes
226
+ // console.log('RESULT:', JSON.stringify(result, null, 2));
227
+
228
+ // get verification method for presentation/credential
229
+ let verificationMethod;
230
+ const results = result.results ||
231
+ (result.presentationResult && result.presentationResult.results);
232
+ if(results && results.length > 0) {
233
+ const [{proof}] = results;
234
+ ({verificationMethod} = proof);
235
+ }
236
+
237
+ if(!result.error) {
238
+ // try to get error from credential results
239
+ const firstCredentialResult = (result.presentationResult &&
240
+ result.credentialResults && result.credentialResults[0]) ||
241
+ result;
242
+ if(!firstCredentialResult.error &&
243
+ firstCredentialResult.statusResult &&
244
+ !firstCredentialResult.statusResult.verified) {
245
+ if(firstCredentialResult.statusResult.error) {
246
+ error = {
247
+ message: 'The credential status could not be checked.',
248
+ cause: firstCredentialResult.statusResult.error.message
249
+ };
250
+ } else {
251
+ // FIXME: surface status type information so it can be included here
252
+ error = {
253
+ message: 'The credential failed a status check.'
254
+ };
255
+ }
256
+ } else {
257
+ error = firstCredentialResult.error;
258
+ }
259
+ error = error || {message: 'Verification error.'};
260
+ } else {
261
+ error = result.error;
262
+ if(!error.message) {
263
+ error.message = 'Verification error.';
264
+ }
265
+ }
266
+
267
+ let message = error.message;
268
+ if(error.errors && error.errors.length > 0) {
269
+ message = error.errors[0].message;
270
+ }
271
+ response = {
272
+ ...result,
273
+ verified: false,
274
+ error,
275
+ checks: [{
276
+ check: checks,
277
+ id: credential && credential.id,
278
+ error: message,
279
+ verificationMethod
280
+ }]
281
+ };
282
+ }
283
+ return response;
284
+ }
package/lib/index.js ADDED
@@ -0,0 +1,55 @@
1
+ /*!
2
+ * Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import {addRoutes} from './http.js';
6
+ import {
7
+ addRoutes as addContextStoreRoutes
8
+ } from '@bedrock/service-context-store';
9
+ import {createService} from '@bedrock/service-core';
10
+ import {initializeServiceAgent} from '@bedrock/service-agent';
11
+
12
+ // load config defaults
13
+ import './config.js';
14
+
15
+ const serviceType = 'vc-verifier';
16
+
17
+ bedrock.events.on('bedrock.init', async () => {
18
+ // create `vc-verifier` service
19
+ const service = await createService({
20
+ serviceType,
21
+ routePrefix: '/verifiers',
22
+ storageCost: {
23
+ config: 1,
24
+ revocation: 1
25
+ },
26
+ validation: {
27
+ // require these zcaps (by reference ID)
28
+ zcapReferenceIds: [{
29
+ referenceId: 'edv',
30
+ required: true
31
+ }, {
32
+ referenceId: 'hmac',
33
+ required: true
34
+ }, {
35
+ referenceId: 'keyAgreementKey',
36
+ required: true
37
+ }]
38
+ }
39
+ });
40
+
41
+ bedrock.events.on('bedrock-express.configure.routes', async app => {
42
+ await addContextStoreRoutes({app, service});
43
+ await addRoutes({app, service});
44
+ });
45
+
46
+ // initialize vc-verifier service agent early (after database is ready) if
47
+ // KMS system is externalized; otherwise we must wait until KMS system
48
+ // is ready
49
+ const externalKms = !bedrock.config['service-agent'].kms.baseUrl.startsWith(
50
+ bedrock.config.server.baseUri);
51
+ const event = externalKms ? 'bedrock-mongodb.ready' : 'bedrock.ready';
52
+ bedrock.events.on(event, async () => {
53
+ await initializeServiceAgent({serviceType});
54
+ });
55
+ });
package/lib/status.js ADDED
@@ -0,0 +1,52 @@
1
+ /*!
2
+ * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import assert from 'assert-plus';
5
+ import {createRequire} from 'module';
6
+ const require = createRequire(import.meta.url);
7
+ const {
8
+ checkStatus: statusListCheckStatus,
9
+ statusTypeMatches: statusListStatusTypeMatches
10
+ } = require('@digitalbazaar/vc-status-list');
11
+ const {
12
+ checkStatus: revocationListCheckStatus,
13
+ statusTypeMatches: revocationListStatusTypeMatches
14
+ } = require('vc-revocation-list');
15
+
16
+ const handlerMap = new Map();
17
+ handlerMap.set('RevocationList2020Status', {
18
+ checkStatus: revocationListCheckStatus,
19
+ statusTypeMatches: revocationListStatusTypeMatches
20
+ });
21
+ handlerMap.set('RevocationList2021Status', {
22
+ checkStatus: statusListCheckStatus,
23
+ statusTypeMatches: statusListStatusTypeMatches
24
+ });
25
+ handlerMap.set('SuspensionList2021Status', {
26
+ checkStatus: statusListCheckStatus,
27
+ statusTypeMatches: statusListStatusTypeMatches
28
+ });
29
+
30
+ export async function checkStatus(options = {}) {
31
+ assert.object(options, 'options');
32
+ assert.object(options.credential, 'options.credential');
33
+
34
+ try {
35
+ const {credential} = options;
36
+ const {credentialStatus} = credential;
37
+ if(!credentialStatus) {
38
+ // no status to check
39
+ return {verified: true};
40
+ }
41
+
42
+ const handlers = handlerMap.get(credentialStatus.type);
43
+ if(!(handlers && handlers.statusTypeMatches({credential}))) {
44
+ throw new Error(
45
+ `Unsupported credentialStatus type "${credentialStatus.type}".`);
46
+ }
47
+
48
+ return await handlers.checkStatus(options);
49
+ } catch(error) {
50
+ return {verified: false, error};
51
+ }
52
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@bedrock/vc-verifier",
3
+ "version": "6.0.0",
4
+ "type": "module",
5
+ "description": "Bedrock VC Verifier",
6
+ "main": "./lib/index.js",
7
+ "scripts": {
8
+ "lint": "eslint ."
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/digitalbazaar/bedrock-vc-verifier.git"
13
+ },
14
+ "author": {
15
+ "name": "Digital Bazaar, Inc.",
16
+ "email": "support@digitalbazaar.com",
17
+ "url": "https://digitalbazaar.com"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/digitalbazaar/bedrock-vc-verifier/issues"
21
+ },
22
+ "homepage": "https://github.com/digitalbazaar/bedrock-vc-verifier",
23
+ "dependencies": {
24
+ "@digitalbazaar/ed25519-signature-2018": "^2.1.0",
25
+ "@digitalbazaar/ed25519-signature-2020": "^3.0.0",
26
+ "@digitalbazaar/vc": "^2.1.0",
27
+ "@digitalbazaar/vc-status-list": "^2.1.0",
28
+ "assert-plus": "^1.0.0",
29
+ "bnid": "^2.1.0",
30
+ "body-parser": "^1.19.0",
31
+ "cors": "^2.8.5",
32
+ "vc-revocation-list": "^3.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "@bedrock/core": "^5.0.0",
36
+ "@bedrock/credentials-context": "^2.0.0",
37
+ "@bedrock/did-context": "^3.0.0",
38
+ "@bedrock/did-io": "^7.0.0",
39
+ "@bedrock/express": "^7.0.0",
40
+ "@bedrock/https-agent": "^3.0.0",
41
+ "@bedrock/jsonld-document-loader": "^2.0.0",
42
+ "@bedrock/mongodb": "^9.0.0",
43
+ "@bedrock/security-context": "^6.0.0",
44
+ "@bedrock/service-agent": "^3.0.0",
45
+ "@bedrock/service-context-store": "^4.0.0",
46
+ "@bedrock/service-core": "^4.0.0",
47
+ "@bedrock/vc-status-list-context": "^2.0.0",
48
+ "@bedrock/vc-revocation-list-context": "^2.0.0",
49
+ "@bedrock/veres-one-context": "^13.0.0",
50
+ "@bedrock/validation": "^6.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "eslint": "^7.32.0",
54
+ "eslint-config-digitalbazaar": "^2.8.0",
55
+ "eslint-plugin-jsdoc": "^28.6.1",
56
+ "jsdoc": "^3.6.4",
57
+ "jsdoc-to-markdown": "^7.1.1"
58
+ },
59
+ "engines": {
60
+ "node": ">=14"
61
+ }
62
+ }
@@ -0,0 +1,59 @@
1
+ /*!
2
+ * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ const context = {
5
+ title: '@context',
6
+ type: 'array',
7
+ minItems: 1,
8
+ items: {
9
+ type: ['string', 'object']
10
+ }
11
+ };
12
+
13
+ export const createChallengeBody = {
14
+ title: 'Create Challenge Body',
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ // body must be empty
18
+ properties: {}
19
+ };
20
+
21
+ export const verifyCredentialBody = {
22
+ title: 'Verify Credential Body',
23
+ type: 'object',
24
+ required: ['verifiableCredential'],
25
+ additionalProperties: false,
26
+ properties: {
27
+ options: {
28
+ type: 'object'
29
+ },
30
+ verifiableCredential: {
31
+ type: 'object',
32
+ additionalProperties: true,
33
+ required: ['@context'],
34
+ properties: {
35
+ '@context': context
36
+ }
37
+ }
38
+ }
39
+ };
40
+
41
+ export const verifyPresentationBody = {
42
+ title: 'Verify Presentation Body',
43
+ type: 'object',
44
+ required: ['verifiablePresentation'],
45
+ additionalProperties: false,
46
+ properties: {
47
+ options: {
48
+ type: 'object'
49
+ },
50
+ verifiablePresentation: {
51
+ type: 'object',
52
+ additionalProperties: true,
53
+ required: ['@context'],
54
+ properties: {
55
+ '@context': context
56
+ }
57
+ }
58
+ }
59
+ };
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ env: {
3
+ mocha: true
4
+ },
5
+ globals: {
6
+ assertNoError: true,
7
+ should: true
8
+ }
9
+ };