@bedrock/vc-verifier 23.0.0 → 23.2.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/lib/config.js CHANGED
@@ -42,6 +42,7 @@ cfg.supportedSuites = [
42
42
  cfg.routes = {
43
43
  challenges: '/challenges',
44
44
  credentialsVerify: '/credentials/verify',
45
+ mdl: '/mdl',
45
46
  presentationsVerify: '/presentations/verify'
46
47
  };
47
48
 
package/lib/envelopes.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Copyright (c) 2019-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
+ import * as mdl from './mdl.js';
5
6
  import * as vcb from './vcb.js';
6
7
  import * as vcjwt from './vcjwt.js';
7
8
 
@@ -33,7 +34,7 @@ export async function verifyEnvelopedCredential({
33
34
  }
34
35
 
35
36
  export async function verifyEnvelopedPresentation({
36
- envelopedPresentation, challenge, domain
37
+ config, envelopedPresentation, challenge, domain, checks, options
37
38
  } = {}) {
38
39
  let format;
39
40
  try {
@@ -46,6 +47,14 @@ export async function verifyEnvelopedPresentation({
46
47
  result = await vcjwt.verifyEnvelopedPresentation({
47
48
  jwt: contents, challenge, domain
48
49
  });
50
+ } else if(format.typeAndSubType === 'application/vcb') {
51
+ result = await vcb.verifyEnvelopedPresentation({
52
+ config, contents, format, challenge, checks
53
+ });
54
+ } else if(format.typeAndSubType === 'application/mdl-vp-token') {
55
+ result = await mdl.verifyEnvelopedPresentation({
56
+ config, contents, format, challenge, domain, checks, options
57
+ });
49
58
  } else {
50
59
  _throwUnknownFormat(format);
51
60
  }
@@ -75,7 +84,7 @@ function _parseEnvelope({envelope}) {
75
84
  function _throwUnknownFormat(format) {
76
85
  throw new BedrockError(
77
86
  `Unknown envelope format "${format.mediaType}".`, {
78
- name: 'DataError',
87
+ name: 'NotSupportedError',
79
88
  details: {
80
89
  httpStatusCode: 400,
81
90
  public: true
package/lib/http.js CHANGED
@@ -157,7 +157,8 @@ export async function addRoutes({app, service} = {}) {
157
157
  const expectedDomain = domain ??
158
158
  (presentation?.proof?.domain && 'issuer.example.com');
159
159
  const result = await verifyPresentation({
160
- config, presentation, challenge, domain: expectedDomain, checks
160
+ config, presentation, challenge, domain: expectedDomain, checks,
161
+ options
161
162
  });
162
163
  response = _createResponse({result, challengeUses, checks});
163
164
  } catch(e) {
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  addCborldRoutes, addContextRoutes
7
7
  } from '@bedrock/service-context-store';
8
8
  import {createService, schemas} from '@bedrock/service-core';
9
+ import {addCaStoreRoutes as addMdlCaStoreRoutes} from './mdl.js';
9
10
  import {addRoutes} from './http.js';
10
11
  import {initializeServiceAgent} from '@bedrock/service-agent';
11
12
  import {verifyOptions} from '../schemas/bedrock-vc-verifier.js';
@@ -53,6 +54,7 @@ bedrock.events.on('bedrock.init', async () => {
53
54
  bedrock.events.on('bedrock-express.configure.routes', async app => {
54
55
  await addCborldRoutes({app, service});
55
56
  await addContextRoutes({app, service});
57
+ await addMdlCaStoreRoutes({app, service});
56
58
  await addRoutes({app, service});
57
59
  });
58
60
 
@@ -72,10 +74,13 @@ async function validateConfigFn({config} = {}) {
72
74
  // set default `verifyOptions` if not given
73
75
  const {verifyOptions} = config;
74
76
  if(verifyOptions === undefined) {
77
+ config.verifyOptions = {};
78
+ }
79
+ // set default `documentLoader` options
80
+ if(config.verifyOptions.documentLoader === undefined) {
75
81
  config.verifyOptions = {
76
- documentLoader: {
77
- allowRemoteContexts: false
78
- }
82
+ ...config.verifyOptions,
83
+ documentLoader: {allowRemoteContexts: false}
79
84
  };
80
85
  }
81
86
  } catch(error) {
package/lib/mdl.js ADDED
@@ -0,0 +1,193 @@
1
+ /*
2
+ * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import {addDocumentRoutes, documentStores} from '@bedrock/service-agent';
6
+ import {
7
+ createMdlCaStoreBody, updateMdlCaStoreBody
8
+ } from '../schemas/bedrock-vc-verifier.js';
9
+ import {DataItem, Verifier} from '@auth0/mdl';
10
+
11
+ const {util: {BedrockError}} = bedrock;
12
+
13
+ const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
14
+
15
+ const MDL_NAMESPACE = 'org.iso.18013.5.1';
16
+ const MDOC_TYPE_MDL = `${MDL_NAMESPACE}.mDL`;
17
+ const MDL_CA_STORE_TYPE = 'MdlCAStore';
18
+
19
+ const serviceType = 'vc-verifier';
20
+
21
+ export async function addCaStoreRoutes({app, service} = {}) {
22
+ const cfg = bedrock.config['vc-verifier'];
23
+ const basePath = `${cfg.routes.mdl}/ca-stores`;
24
+ addDocumentRoutes({
25
+ app, service,
26
+ type: MDL_CA_STORE_TYPE,
27
+ typeName: 'mDL Certificate Authority Store',
28
+ contentProperty: 'trustedCertificates',
29
+ basePath,
30
+ pathParam: 'caStoreId',
31
+ createBodySchema: createMdlCaStoreBody(),
32
+ updateBodySchema: updateMdlCaStoreBody()
33
+ });
34
+ }
35
+
36
+ export async function verifyEnvelopedPresentation({
37
+ config, contents, format, challenge, domain, options
38
+ } = {}) {
39
+ // base64 encoding must NOT be used; vp token is `base64url` encoded
40
+ if(format.parameters.has('base64')) {
41
+ throw new BedrockError(
42
+ `Unknown envelope format "${format.mediaType}".`, {
43
+ name: 'DataError',
44
+ details: {
45
+ httpStatusCode: 400,
46
+ public: true
47
+ },
48
+ });
49
+ }
50
+
51
+ // decoded `contents` is the mDL device response
52
+ const deviceResponse = Buffer.from(contents, 'base64url');
53
+
54
+ // session transcription must be provided modulo:
55
+ // `domain` which is used as the `responseUri`
56
+ // `challenge` which is used as the `verifierGeneratedNonce`
57
+ const sessionTranscript = {
58
+ ..._getSessionTranscriptFromOptions({options}),
59
+ responseUri: domain,
60
+ verifierGeneratedNonce: challenge
61
+ };
62
+
63
+ // fetch CA store(s)
64
+ const {documentStore} = await documentStores.get({config, serviceType});
65
+ const caStoreIds = config.verifyOptions?.mdl?.caStores;
66
+ const caStore = new Set();
67
+ // FIXME: implement parallel lookup w/limits, for now one CA store is
68
+ // expected and multiples will be slow, discouraging too many from being used
69
+ for(const url of caStoreIds) {
70
+ const doc = await _getCAStoreDocument({documentStore, url});
71
+ doc.content.trustedCertificates.forEach(caStore.add, caStore);
72
+ }
73
+
74
+ return verifyPresentation({
75
+ deviceResponse, sessionTranscript, trustedCertificates: [...caStore]
76
+ });
77
+ }
78
+
79
+ export async function verifyPresentation({
80
+ deviceResponse, sessionTranscript, trustedCertificates
81
+ } = {}) {
82
+ try {
83
+ const verifier = new Verifier(trustedCertificates);
84
+ const encodedSessionTranscript = _encodeSessionTranscript(
85
+ sessionTranscript);
86
+
87
+ // uncomment to debug
88
+ /*
89
+ const diagnostic = await verifier.getDiagnosticInformation(
90
+ deviceResponse, {encodedSessionTranscript});
91
+ console.debug('Diagnostic information:', diagnostic);
92
+ */
93
+
94
+ // verify device response and get selectively disclosed mdoc result
95
+ const mdoc = await verifier.verify(deviceResponse, {
96
+ encodedSessionTranscript
97
+ });
98
+
99
+ // ensure `mdoc` has one document and its `type` is `MDOC_TYPE_MDL`
100
+ if(!(mdoc.documents?.length === 1 &&
101
+ mdoc.documents[0].docType === MDOC_TYPE_MDL)) {
102
+ throw new BedrockError(
103
+ `Unknown mdoc document type "${mdoc.documents[0].docType}"; ` +
104
+ `expecting "${MDOC_TYPE_MDL}".`, {
105
+ name: 'NotSupportedError',
106
+ details: {
107
+ httpStatusCode: 400,
108
+ public: true
109
+ }
110
+ });
111
+ }
112
+
113
+ // express CBOR-encoded mdoc as an enveloped VC in a VP
114
+ const cborMdoc = mdoc.encode();
115
+ const b64Mdl = Buffer.from(cborMdoc).toString('base64');
116
+ const presentation = {
117
+ '@context': [VC_CONTEXT_2],
118
+ type: 'VerifiablePresentation',
119
+ verifiableCredential: {
120
+ id: `data:application/mdl;base64,${b64Mdl}`,
121
+ type: 'EnvelopedVerifiableCredential'
122
+ }
123
+ };
124
+ return {verified: true, presentation};
125
+ } catch(err) {
126
+ // capture `err` message, name, and code
127
+ const cause = new Error(err.message);
128
+ cause.name = err.name;
129
+ cause.code = err.code;
130
+ const error = new Error('Verification error.');
131
+ error.name = 'VerificationError';
132
+ error.errors = [cause];
133
+ return {verified: false, error};
134
+ }
135
+ }
136
+
137
+ // returns a full or partial session transcript
138
+ function _getSessionTranscriptFromOptions({options}) {
139
+ if(!options.mdl?.sessionTranscript) {
140
+ // no `mdl` session transcription options given
141
+ return {};
142
+ }
143
+
144
+ // `mdocGeneratedNonce` and `verifierGeneratedNonce` are base64url-encoded
145
+ // values; the others are passthrough strings
146
+ const sessionTranscript = {...options.mdl.sessionTranscript};
147
+ if(sessionTranscript.mdocGeneratedNonce) {
148
+ sessionTranscript.mdocGeneratedNonce = Buffer
149
+ .from(sessionTranscript.mdocGeneratedNonce, 'base64url')
150
+ // note: ISO 18013-7 requires `verifierGeneratedNonce` to be a string
151
+ .toString('utf8');
152
+ }
153
+ if(sessionTranscript.verifierGeneratedNonce) {
154
+ sessionTranscript.verifierGeneratedNonce = Buffer
155
+ .from(sessionTranscript.verifierGeneratedNonce, 'base64url')
156
+ // note: ISO 18013-7 requires `verifierGeneratedNonce` to be a string
157
+ .toString('utf8');
158
+ }
159
+ return sessionTranscript;
160
+ }
161
+
162
+ function _encodeSessionTranscript(sessionTranscript) {
163
+ const {
164
+ mdocGeneratedNonce,
165
+ clientId,
166
+ responseUri,
167
+ verifierGeneratedNonce
168
+ } = sessionTranscript;
169
+ const encoded = DataItem.fromData([
170
+ // deviceEngagementBytes
171
+ null,
172
+ // eReaderKeyBytes
173
+ null,
174
+ [mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce],
175
+ ]);
176
+ return DataItem.fromData(encoded).buffer;
177
+ }
178
+
179
+ async function _getCAStoreDocument({documentStore, url}) {
180
+ const doc = await documentStore.get({id: url});
181
+ if(doc.meta.type !== MDL_CA_STORE_TYPE) {
182
+ // wrong meta type; treat as not found error
183
+ throw new BedrockError(`Document "${url}" not found.`, {
184
+ name: 'NotFoundError',
185
+ details: {
186
+ url,
187
+ httpStatusCode: 404,
188
+ public: true
189
+ }
190
+ });
191
+ }
192
+ return doc;
193
+ }
package/lib/vcb.js CHANGED
@@ -29,6 +29,41 @@ const SUPPORTED_BARCODE_FORMATS = new Set([
29
29
 
30
30
  const TEXT_DECODER = new TextDecoder();
31
31
 
32
+ export async function verifyEnvelopedPresentation({
33
+ config, contents, format, challenge, checks
34
+ } = {}) {
35
+ // handle base64 encoding
36
+ let {parameters} = format;
37
+ if(parameters.has('base64')) {
38
+ contents = new Uint8Array(Buffer.from(contents, 'base64'));
39
+ parameters = new Map(parameters);
40
+ parameters.delete('base64');
41
+ }
42
+ // only parameter understood is `barcode-format` with values of:
43
+ // 'qr_code' (default)
44
+ const barcodeFormat = parameters.size === 1 ?
45
+ parameters.get('barcode-format') : (parameters.size === 0 && 'qr_code');
46
+ if(barcodeFormat !== 'qr_code') {
47
+ _throwUnknownFormat(format);
48
+ }
49
+ // create loaders for JSON-LD contexts and CBOR-LD type tables
50
+ const documentLoader = await createDocumentLoader({config});
51
+ const typeTableLoader = await createCborldTypeTableLoader({
52
+ config, serviceType: 'vc-verifier'
53
+ });
54
+ // parse credential and any verification options from contents...
55
+ const {jsonldDocument: presentation, options} = await _parseQrCodeEnvelope({
56
+ contents, documentLoader, typeTableLoader, expectedHeader: 'VP1-'
57
+ });
58
+ // checks for VCB VPs only applies if there is actually a proof to check
59
+ checks = presentation.proof ? checks : [];
60
+ // verify VP
61
+ const result = await di.verifyPresentation({
62
+ config, presentation, options, challenge, checks
63
+ });
64
+ return {...result, presentation};
65
+ }
66
+
32
67
  export async function verifyEnvelopedCredential({
33
68
  config, contents, format, checks
34
69
  } = {}) {
@@ -58,8 +93,8 @@ export async function verifyEnvelopedCredential({
58
93
  let credential;
59
94
  let options;
60
95
  if(barcodeFormat === 'qr_code') {
61
- ({credential, options} = await _parseQrCodeEnvelope({
62
- contents, documentLoader, typeTableLoader
96
+ ({jsonldDocument: credential, options} = await _parseQrCodeEnvelope({
97
+ contents, documentLoader, typeTableLoader, expectedHeader: 'VC1-'
63
98
  }));
64
99
  }
65
100
  if(barcodeFormat === 'pdf417') {
@@ -76,19 +111,19 @@ export async function verifyEnvelopedCredential({
76
111
  }
77
112
 
78
113
  async function _parseQrCodeEnvelope({
79
- contents, documentLoader, typeTableLoader
114
+ contents, documentLoader, typeTableLoader, expectedHeader
80
115
  }) {
81
116
  // `fromQrCode` requires text, so convert to text as needed
82
117
  if(contents instanceof Uint8Array) {
83
118
  contents = TEXT_DECODER.decode(contents);
84
119
  }
85
- const {jsonldDocument: credential} = await util.fromQrCode({
120
+ const {jsonldDocument} = await util.fromQrCode({
86
121
  text: contents,
87
122
  documentLoader,
88
123
  typeTableLoader,
89
- expectedHeader: 'VC1-'
124
+ expectedHeader
90
125
  });
91
- return {credential};
126
+ return {jsonldDocument};
92
127
  }
93
128
 
94
129
  async function _parsePdf417Envelope({
package/lib/verify.js CHANGED
@@ -30,7 +30,7 @@ export async function verifyCredential({config, credential, checks} = {}) {
30
30
  }
31
31
 
32
32
  export async function verifyPresentation({
33
- config, presentation, challenge, domain, checks
33
+ config, presentation, challenge, domain, checks, options
34
34
  } = {}) {
35
35
  if(presentation?.type !== 'EnvelopedVerifiablePresentation') {
36
36
  const result = await di.verifyPresentation({
@@ -74,20 +74,40 @@ export async function verifyPresentation({
74
74
  }
75
75
 
76
76
  const presentationResult = await verifyEnvelopedPresentation({
77
- envelopedPresentation: presentation, challenge, domain
77
+ config, envelopedPresentation: presentation, challenge, domain, checks,
78
+ options
78
79
  });
79
- // verify each `verifiableCredential` in the resulting VP
80
+ // verify each `verifiableCredential` in the resulting VP, unless the VP
81
+ // format was mDL, which means there will always be one VC (an enveloped mDL)
82
+ // and it will already have been verified
80
83
  let verified = presentationResult.verified;
81
84
  let credentialResults;
82
85
  if(!verified) {
83
86
  credentialResults = [];
87
+ } else if(
88
+ presentationResult.format.typeAndSubType === 'application/mdl-vp-token') {
89
+ let {verifiableCredential} = presentationResult.presentation;
90
+ if(Array.isArray(verifiableCredential)) {
91
+ verifiableCredential = verifiableCredential[0];
92
+ }
93
+ credentialResults = [{
94
+ verified: true,
95
+ credential: verifiableCredential,
96
+ format: {
97
+ mediaType: 'application/mdl',
98
+ typeAndSubType: 'application/mdl',
99
+ type: 'application',
100
+ subType: 'mdl',
101
+ parameters: {}
102
+ }
103
+ }];
84
104
  } else if(presentationResult.presentation?.proof &&
85
105
  presentationResult.format.typeAndSubType === 'application/jwt') {
86
106
  // presentation in the envelope has a `proof` and envelope format is JWT,
87
107
  // so recurse to check `proof` field
88
108
  const proofResult = await verifyPresentation({
89
109
  config, presentation: presentationResult.presentation,
90
- challenge, domain, checks
110
+ challenge, domain, checks, options
91
111
  });
92
112
  verified = !!(verified && proofResult.presentationResult?.verified);
93
113
  presentationResult.proofResult = proofResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-verifier",
3
- "version": "23.0.0",
3
+ "version": "23.2.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock VC Verifier",
6
6
  "main": "./lib/index.js",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "homepage": "https://github.com/digitalbazaar/bedrock-vc-verifier",
27
27
  "dependencies": {
28
+ "@auth0/mdl": "^3.0.0",
28
29
  "@digitalbazaar/bbs-2023-cryptosuite": "^2.0.1",
29
30
  "@digitalbazaar/cborld": "^8.0.0",
30
31
  "@digitalbazaar/data-integrity": "^2.5.0",
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import {schemas} from '@bedrock/validation';
5
5
 
@@ -182,6 +182,8 @@ export const verifyOptions = {
182
182
  required: ['didResolver']
183
183
  }, {
184
184
  required: ['documentLoader']
185
+ }, {
186
+ required: ['mdl']
185
187
  }],
186
188
  additionalProperties: false,
187
189
  properties: {
@@ -207,6 +209,20 @@ export const verifyOptions = {
207
209
  type: 'boolean'
208
210
  }
209
211
  }
212
+ },
213
+ mdl: {
214
+ title: 'mDL Verification Options',
215
+ type: 'object',
216
+ required: ['caStores'],
217
+ additionalProperties: false,
218
+ properties: {
219
+ caStores: {
220
+ title: 'mDL Certificate Authority Store IDs',
221
+ type: 'array',
222
+ minItems: 1,
223
+ items: {type: 'string'}
224
+ }
225
+ }
210
226
  }
211
227
  }
212
228
  };
@@ -258,3 +274,47 @@ export function verifyPresentationBody() {
258
274
  }
259
275
  };
260
276
  }
277
+
278
+ const sequence = {
279
+ title: 'sequence',
280
+ type: 'integer',
281
+ minimum: 0,
282
+ maximum: Number.MAX_SAFE_INTEGER - 1
283
+ };
284
+
285
+ const mdlCaStoreBody = {
286
+ title: 'mDL Certificate Authority Store Record',
287
+ type: 'object',
288
+ required: ['id', 'trustedCertificates'],
289
+ additionalProperties: false,
290
+ properties: {
291
+ id: {
292
+ title: 'CA Store ID',
293
+ type: 'string',
294
+ pattern: '^urn:mdl-ca-store:'
295
+ },
296
+ trustedCertificates: {
297
+ type: 'array',
298
+ items: {type: 'string'}
299
+ }
300
+ }
301
+ };
302
+
303
+ export function createMdlCaStoreBody() {
304
+ return {
305
+ ...mdlCaStoreBody,
306
+ title: 'createMdlCaStoreBody'
307
+ };
308
+ }
309
+
310
+ export function updateMdlCaStoreBody() {
311
+ return {
312
+ ...mdlCaStoreBody,
313
+ required: ['id', 'trustedCertificates', 'sequence'],
314
+ properties: {
315
+ ...mdlCaStoreBody.properties,
316
+ sequence
317
+ },
318
+ title: 'updateMdlCaStoreBody'
319
+ };
320
+ }