@bedrock/vc-verifier 19.1.0 → 20.1.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Bedrock VC Verifier API module _(@bedrock/vc-verifier)_
2
2
 
3
- [![Build Status](https://img.shields.io/github/workflow/status/digitalbazaar/bedrock-vc-verifier/Bedrock%20Node.js%20CI)](https://github.com/digitalbazaar/bedrock-vc-verifier/actions?query=workflow%3A%22Bedrock+Node.js+CI%22)
3
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/digitalbazaar/bedrock-vc-verifier/main.yml)](https://github.com/digitalbazaar/bedrock-vc-verifier/actions/workflows/main.yml)
4
4
  [![NPM Version](https://img.shields.io/npm/v/bedrock-vc-verifier.svg)](https://npm.im/bedrock-vc-verifier)
5
5
 
6
6
  > A VC Verifier API library for use with Bedrock applications.
package/lib/config.js CHANGED
@@ -17,9 +17,11 @@ cfg.challenges = {
17
17
  // also allow any verifier instance to optionally load `http` and `https`
18
18
  // documents directly from the Web
19
19
  cfg.documentLoader = {
20
- // `true` enables all verifiers to fetch `http` documents from the Web
20
+ // `true` enables all verifier instances that do not have document loader
21
+ // instance-specific constraints to fetch `http` documents from the Web
21
22
  http: false,
22
- // `true` enables all verifiers to fetch `https` documents from the Web
23
+ // `true` enables all verifier instances that do not have document loader
24
+ // instance-specific constraints to fetch `https` documents from the Web
23
25
  https: true
24
26
  };
25
27
 
package/lib/di.js ADDED
@@ -0,0 +1,63 @@
1
+ /*!
2
+ * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as vc from '@digitalbazaar/vc';
5
+ import {checkStatus} from './status.js';
6
+ import {createDocumentLoader} from './documentLoader.js';
7
+ import {createSuites} from './suites.js';
8
+
9
+ export async function verifyCredential({config, credential, checks} = {}) {
10
+ const documentLoader = await createDocumentLoader({config});
11
+ const suite = createSuites();
12
+
13
+ const result = await vc.verifyCredential({
14
+ credential,
15
+ documentLoader,
16
+ suite,
17
+ // only check credential status when option is set
18
+ checkStatus: checks.includes('credentialStatus') ?
19
+ checkStatus : () => ({verified: true})
20
+ });
21
+ // if proof should have been checked but wasn't due to an error,
22
+ // try to run the check again using the VC's issuance date
23
+ if(checks.includes('proof') &&
24
+ result.error && !result.proof && result.results?.[0] &&
25
+ typeof credential.issuanceDate === 'string') {
26
+ const proofResult = await vc.verifyCredential({
27
+ credential,
28
+ documentLoader,
29
+ suite,
30
+ now: new Date(credential.issuanceDate),
31
+ // only check credential status when option is set
32
+ checkStatus: checks.includes('credentialStatus') ?
33
+ checkStatus : () => ({verified: true})
34
+ });
35
+ if(proofResult.verified) {
36
+ // overlay original (failed) results on top of proof results
37
+ result.results[0] = {
38
+ ...proofResult.results[0],
39
+ ...result.results[0],
40
+ proofVerified: true
41
+ };
42
+ }
43
+ }
44
+ // ensure all proofs are verified in order to return `verified`
45
+ let {verified} = result;
46
+ verified = !!(verified && result?.results?.every(({verified}) => verified));
47
+ return {...result, verified};
48
+ }
49
+
50
+ export async function verifyPresentation({
51
+ config, presentation, challenge, domain, checks
52
+ } = {}) {
53
+ const verifyOptions = {
54
+ challenge,
55
+ domain,
56
+ presentation,
57
+ documentLoader: await createDocumentLoader({config}),
58
+ suite: createSuites(),
59
+ unsignedPresentation: !checks.includes('proof'),
60
+ checkStatus
61
+ };
62
+ return vc.verify(verifyOptions);
63
+ }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import {
@@ -9,6 +9,7 @@ import {
9
9
  } from '@bedrock/jsonld-document-loader';
10
10
  import {createContextDocumentLoader} from '@bedrock/service-context-store';
11
11
  import {didIo} from '@bedrock/did-io';
12
+ import {klona} from 'klona';
12
13
  import '@bedrock/credentials-context';
13
14
  import '@bedrock/data-integrity-context';
14
15
  import '@bedrock/did-context';
@@ -53,9 +54,17 @@ export async function createDocumentLoader({config} = {}) {
53
54
  {config, serviceType});
54
55
 
55
56
  return async function documentLoader(url) {
56
- // resolve all DID URLs through did-io
57
+ // handle DID URLs...
57
58
  if(url.startsWith('did:')) {
58
- const document = await didIo.get({url});
59
+ let document;
60
+ if(config.verifyOptions?.didResolver) {
61
+ // resolve via configured DID resolver
62
+ const {verifyOptions: {didResolver}} = config;
63
+ document = await _resolve({didResolver, didUrl: url});
64
+ } else {
65
+ // resolve via did-io
66
+ document = await didIo.get({url});
67
+ }
59
68
  return {
60
69
  contextUrl: null,
61
70
  documentUrl: url,
@@ -75,11 +84,74 @@ export async function createDocumentLoader({config} = {}) {
75
84
  // try to resolve URL through context doc loader
76
85
  return await contextDocumentLoader(url);
77
86
  } catch(e) {
78
- // use web loader if configured
79
- if(url.startsWith('http') && e.name === 'NotFoundError' && webLoader) {
87
+ // use web loader if configured and instance config allows it and
88
+ // the url starts with `http`, and the core config allows it
89
+ const allowRemoteContexts = !config.verifyOptions?.documentLoader ||
90
+ config.verifyOptions.documentLoader.allowRemoteContexts;
91
+ if(allowRemoteContexts &&
92
+ url.startsWith('http') && e.name === 'NotFoundError' && webLoader) {
80
93
  return webLoader(url);
81
94
  }
82
95
  throw e;
83
96
  }
84
97
  };
85
98
  }
99
+
100
+ async function _resolve({didResolver, didUrl}) {
101
+ // split on `?` query or `#` fragment
102
+ const [did] = didUrl.split(/(?=[\?#])/);
103
+
104
+ // fetch DID document using DID resolver, assume DID param is prepended
105
+ const url = didResolver.url.endsWith('/') ?
106
+ `${didResolver.url}${encodeURIComponent(did)}` :
107
+ `${didResolver.url}/${encodeURIComponent(did)}`;
108
+ const data = await httpClientHandler.get({url});
109
+
110
+ if(data?.didDocument?.id !== did) {
111
+ throw new Error(`DID document for DID "${did}" not found.`);
112
+ }
113
+
114
+ // FIXME: perform DID document validation
115
+ // FIXME: handle URL query param / services
116
+ const {didDocument} = data;
117
+
118
+ // if a fragment was found use the fragment to dereference a subnode
119
+ // in the did doc
120
+ const [, fragment] = didUrl.split('#');
121
+ if(fragment) {
122
+ const id = `${didDocument.id}#${fragment}`;
123
+ return _getNode({didDocument, id});
124
+ }
125
+ // resolve the full DID Document
126
+ return didDocument;
127
+ }
128
+
129
+ function _getNode({didDocument, id}) {
130
+ // do verification method search first
131
+ let match = didDocument?.verificationMethod?.find(vm => vm?.id === id);
132
+ if(!match) {
133
+ // check other top-level nodes
134
+ for(const [key, value] of Object.entries(didDocument)) {
135
+ if(key === '@context' || key === 'verificationMethod') {
136
+ continue;
137
+ }
138
+ if(Array.isArray(value)) {
139
+ match = value.find(e => e?.id === id);
140
+ } else if(value?.id === id) {
141
+ match = value;
142
+ }
143
+ if(match) {
144
+ break;
145
+ }
146
+ }
147
+ }
148
+
149
+ if(!match) {
150
+ throw new Error(`DID document entity with id "${id}" not found.`);
151
+ }
152
+
153
+ return {
154
+ '@context': klona(didDocument['@context']),
155
+ ...klona(match)
156
+ };
157
+ }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import * as vcjwt from './vcjwt.js';
6
+
7
+ const {util: {BedrockError}} = bedrock;
8
+
9
+ export async function verifyEnvelopedCredential({envelopedCredential} = {}) {
10
+ try {
11
+ const {contents: jwt} = _parseEnvelope({
12
+ envelope: envelopedCredential
13
+ });
14
+ return vcjwt.verifyEnvelopedCredential({jwt});
15
+ } catch(error) {
16
+ return {verified: false, error};
17
+ }
18
+ }
19
+
20
+ export async function verifyEnvelopedPresentation({
21
+ envelopedPresentation, challenge, domain
22
+ } = {}) {
23
+ try {
24
+ const {contents: jwt} = _parseEnvelope({
25
+ envelope: envelopedPresentation
26
+ });
27
+ return vcjwt.verifyEnvelopedPresentation({jwt, challenge, domain});
28
+ } catch(error) {
29
+ return {verified: false, error};
30
+ }
31
+ }
32
+
33
+ function _parseEnvelope({envelope}) {
34
+ const {id} = envelope;
35
+ let format;
36
+ const comma = id.indexOf(',');
37
+ if(id.startsWith('data:') && comma !== -1) {
38
+ format = id.slice('data:'.length, comma);
39
+ }
40
+ if(format !== 'application/jwt') {
41
+ throw new BedrockError(
42
+ `Unknown envelope format "${format}".`, {
43
+ name: 'DataError',
44
+ details: {
45
+ httpStatusCode: 400,
46
+ public: true
47
+ },
48
+ });
49
+ }
50
+ return {contents: id.slice(comma + 1), format};
51
+ }
package/lib/http.js CHANGED
@@ -1,8 +1,7 @@
1
1
  /*!
2
- * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
- import * as vc from '@digitalbazaar/vc';
6
5
  import {createChallenge, verifyChallenge} from './challenges.js';
7
6
  import {
8
7
  createChallengeBody,
@@ -10,12 +9,10 @@ import {
10
9
  verifyPresentationBody
11
10
  } from '../schemas/bedrock-vc-verifier.js';
12
11
  import {metering, middleware} from '@bedrock/service-core';
12
+ import {verifyCredential, verifyPresentation} from './verify.js';
13
13
  import {asyncHandler} from '@bedrock/express';
14
14
  import bodyParser from 'body-parser';
15
- import {checkStatus} from './status.js';
16
15
  import cors from 'cors';
17
- import {createDocumentLoader} from './documentLoader.js';
18
- import {createSuites} from './suites.js';
19
16
  import {serializeError} from 'serialize-error';
20
17
  import {createValidateMiddleware as validate} from '@bedrock/validation';
21
18
 
@@ -33,7 +30,6 @@ bedrock.events.on('bedrock-express.configure.bodyParser', app => {
33
30
 
34
31
  export async function addRoutes({app, service} = {}) {
35
32
  const {routePrefix} = service;
36
- const suite = createSuites();
37
33
  const cfg = bedrock.config['vc-verifier'];
38
34
  const baseUrl = `${routePrefix}/:localId`;
39
35
  const routes = {
@@ -70,12 +66,11 @@ export async function addRoutes({app, service} = {}) {
70
66
  app.post(
71
67
  routes.credentialsVerify,
72
68
  cors(),
73
- validate({bodySchema: verifyCredentialBody}),
69
+ validate({bodySchema: verifyCredentialBody()}),
74
70
  getConfigMiddleware,
75
71
  middleware.authorizeServiceObjectRequest(),
76
72
  asyncHandler(async (req, res) => {
77
73
  const {config} = req.serviceObject;
78
- const documentLoader = await createDocumentLoader({config});
79
74
 
80
75
  let response;
81
76
  try {
@@ -86,37 +81,7 @@ export async function addRoutes({app, service} = {}) {
86
81
 
87
82
  const {checks} = options;
88
83
  _validateChecks({checks});
89
- const result = await vc.verifyCredential({
90
- credential,
91
- documentLoader,
92
- suite,
93
- // only check credential status when option is set
94
- checkStatus: checks.includes('credentialStatus') ?
95
- checkStatus : () => ({verified: true})
96
- });
97
- // if proof should have been checked but wasn't due to an error,
98
- // try to run the check again using the VC's issuance date
99
- if(checks.includes('proof') &&
100
- result.error && !result.proof && result.results[0] &&
101
- typeof credential.issuanceDate === 'string') {
102
- const proofResult = await vc.verifyCredential({
103
- credential,
104
- documentLoader,
105
- suite,
106
- now: new Date(credential.issuanceDate),
107
- // only check credential status when option is set
108
- checkStatus: checks.includes('credentialStatus') ?
109
- checkStatus : () => ({verified: true})
110
- });
111
- if(proofResult.verified) {
112
- // overlay original (failed) results on top of proof results
113
- result.results[0] = {
114
- ...proofResult.results[0],
115
- ...result.results[0],
116
- proofVerified: true
117
- };
118
- }
119
- }
84
+ const result = await verifyCredential({config, credential, checks});
120
85
  response = _createResponse({credential, result, checks});
121
86
  } catch(e) {
122
87
  response = _createResponse({error: e});
@@ -150,17 +115,16 @@ export async function addRoutes({app, service} = {}) {
150
115
  app.post(
151
116
  routes.presentationsVerify,
152
117
  cors(),
153
- validate({bodySchema: verifyPresentationBody}),
118
+ validate({bodySchema: verifyPresentationBody()}),
154
119
  getConfigMiddleware,
155
120
  middleware.authorizeServiceObjectRequest(),
156
121
  asyncHandler(async (req, res) => {
157
122
  const {config} = req.serviceObject;
158
- const documentLoader = await createDocumentLoader({config});
159
123
 
160
124
  let response;
161
125
  try {
162
126
  const {
163
- verifiablePresentation,
127
+ verifiablePresentation: presentation,
164
128
  options = {}
165
129
  } = req.body;
166
130
 
@@ -174,7 +138,6 @@ export async function addRoutes({app, service} = {}) {
174
138
  }
175
139
 
176
140
  _validateChecks({checks});
177
- const unsignedPresentation = !checks.includes('proof');
178
141
 
179
142
  // allow for `checks` to indicate whether or not the challenge
180
143
  // should be checked
@@ -190,20 +153,12 @@ export async function addRoutes({app, service} = {}) {
190
153
  ({uses: challengeUses} = result);
191
154
  }
192
155
 
193
- const verifyOptions = {
194
- challenge,
195
- presentation: verifiablePresentation,
196
- documentLoader,
197
- suite,
198
- unsignedPresentation,
199
- checkStatus
200
- };
201
- const {proof} = verifiablePresentation;
202
- if(proof && proof.domain) {
203
- // FIXME: do not set a default
204
- verifyOptions.domain = domain || 'issuer.example.com';
205
- }
206
- const result = await vc.verify(verifyOptions);
156
+ // FIXME: do not set a default domain
157
+ const expectedDomain = domain ??
158
+ (presentation?.proof?.domain && 'issuer.example.com');
159
+ const result = await verifyPresentation({
160
+ config, presentation, challenge, domain: expectedDomain, checks
161
+ });
207
162
  response = _createResponse({result, challengeUses, checks});
208
163
  } catch(e) {
209
164
  response = _createResponse({error: e});
package/lib/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  /*!
2
- * Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
+ import {createService, schemas} from '@bedrock/service-core';
5
6
  import {
6
7
  addRoutes as addContextStoreRoutes
7
8
  } from '@bedrock/service-context-store';
8
9
  import {addRoutes} from './http.js';
9
- import {createService} from '@bedrock/service-core';
10
10
  import {initializeServiceAgent} from '@bedrock/service-agent';
11
+ import {klona} from 'klona';
12
+ import {verifyOptions} from '../schemas/bedrock-vc-verifier.js';
11
13
 
12
14
  // load config defaults
13
15
  import './config.js';
@@ -15,6 +17,15 @@ import './config.js';
15
17
  const serviceType = 'vc-verifier';
16
18
 
17
19
  bedrock.events.on('bedrock.init', async () => {
20
+ // add customizations to config validators...
21
+ const createConfigBody = klona(schemas.createConfigBody);
22
+ const updateConfigBody = klona(schemas.updateConfigBody);
23
+ const schemasToUpdate = [createConfigBody, updateConfigBody];
24
+ for(const schema of schemasToUpdate) {
25
+ // verify options
26
+ schema.properties.verifyOptions = verifyOptions;
27
+ }
28
+
18
29
  // create `vc-verifier` service
19
30
  const service = await createService({
20
31
  serviceType,
@@ -24,6 +35,8 @@ bedrock.events.on('bedrock.init', async () => {
24
35
  revocation: 1
25
36
  },
26
37
  validation: {
38
+ createConfigBody,
39
+ updateConfigBody,
27
40
  // require these zcaps (by reference ID)
28
41
  zcapReferenceIds: [{
29
42
  referenceId: 'edv',