@bedrock/vc-verifier 23.1.0 → 23.3.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
 
@@ -21,7 +21,7 @@ import '@bedrock/vc-status-list-context';
21
21
  import '@bedrock/veres-one-context';
22
22
 
23
23
  const serviceType = 'vc-verifier';
24
- let webLoader;
24
+ export let webLoader;
25
25
 
26
26
  bedrock.events.on('bedrock.init', () => {
27
27
  // build web loader if configuration calls for it
@@ -48,14 +48,24 @@ bedrock.events.on('bedrock.init', () => {
48
48
  * @param {object} options.config - The verifier instance config.
49
49
  * @param {Set} [options.remoteUrlAllowList] - Remote URLs that are
50
50
  * specifically allowed to be loaded (used for status list checks).
51
+ * @param {Map} [options.cache] - An optional cache of URL => document.
51
52
  *
52
53
  * @returns {Promise<Function>} The document loader.
53
54
  */
54
- export async function createDocumentLoader({config, remoteUrlAllowList} = {}) {
55
+ export async function createDocumentLoader({
56
+ config, remoteUrlAllowList, cache = new Map()
57
+ } = {}) {
55
58
  const contextDocumentLoader = await createContextDocumentLoader(
56
59
  {config, serviceType});
57
60
 
58
61
  return async function documentLoader(url) {
62
+ if(cache) {
63
+ const document = cache.get(url);
64
+ if(document) {
65
+ return {contextUrl: null, documentUrl: url, document};
66
+ }
67
+ }
68
+
59
69
  // handle DID URLs...
60
70
  if(url.startsWith('did:')) {
61
71
  let document;
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
- config, envelopedPresentation, challenge, domain, checks
37
+ config, envelopedPresentation, challenge, domain, checks, options
37
38
  } = {}) {
38
39
  let format;
39
40
  try {
@@ -50,6 +51,10 @@ export async function verifyEnvelopedPresentation({
50
51
  result = await vcb.verifyEnvelopedPresentation({
51
52
  config, contents, format, challenge, checks
52
53
  });
54
+ } else if(format.typeAndSubType === 'application/mdl-vp-token') {
55
+ result = await mdl.verifyEnvelopedPresentation({
56
+ config, contents, format, challenge, domain, checks, options
57
+ });
53
58
  } else {
54
59
  _throwUnknownFormat(format);
55
60
  }
@@ -79,7 +84,7 @@ function _parseEnvelope({envelope}) {
79
84
  function _throwUnknownFormat(format) {
80
85
  throw new BedrockError(
81
86
  `Unknown envelope format "${format.mediaType}".`, {
82
- name: 'DataError',
87
+ name: 'NotSupportedError',
83
88
  details: {
84
89
  httpStatusCode: 400,
85
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/status.js CHANGED
@@ -1,10 +1,12 @@
1
1
  /*!
2
- * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2019-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
+ import * as bedrock from '@bedrock/core';
4
5
  import {
5
6
  checkStatus as bitstringStatusListCheckStatus,
6
7
  statusTypeMatches as bitstringStatusListStatusTypeMatches
7
8
  } from '@digitalbazaar/vc-bitstring-status-list';
9
+ import {createDocumentLoader, webLoader} from './documentLoader.js';
8
10
  import {
9
11
  checkStatus as revocationListCheckStatus,
10
12
  statusTypeMatches as revocationListStatusTypeMatches
@@ -14,13 +16,21 @@ import {
14
16
  statusTypeMatches as statusList2020StatusTypeMatches
15
17
  } from '@digitalbazaar/vc-status-list';
16
18
  import assert from 'assert-plus';
17
- import {createDocumentLoader} from './documentLoader.js';
19
+
20
+ const {util: {BedrockError}} = bedrock;
21
+
22
+ const TERSE_BITSTRING_STATUS_LIST_ENTRY = 'TerseBitstringStatusListEntry';
23
+ // always 2^26 = 67108864 per vc-barcodes spec
24
+ const TERSE_BITSTRING_STATUS_LIST_LENGTH = 67108864;
25
+ const TERSE_STATUS_PURPOSES = ['revocation', 'suspension'];
26
+ const VC_BARCODES_V1_CONTEXT_URL = 'https://w3id.org/vc-barcodes/v1';
18
27
 
19
28
  const handlerMap = new Map();
20
29
  handlerMap.set('BitstringStatusListEntry', {
21
30
  checkStatus: bitstringStatusListCheckStatus,
22
31
  statusTypeMatches: bitstringStatusListStatusTypeMatches
23
32
  });
33
+ // legacy status entry types
24
34
  handlerMap.set('RevocationList2020Status', {
25
35
  checkStatus: revocationListCheckStatus,
26
36
  statusTypeMatches: revocationListStatusTypeMatches
@@ -36,30 +46,66 @@ export function createCheckStatus({config} = {}) {
36
46
  assert.object(options.credential, 'options.credential');
37
47
 
38
48
  try {
39
- const {credential} = options;
40
- const {credentialStatus} = credential;
41
- if(!credentialStatus) {
49
+ if(!options.credential.credentialStatus) {
42
50
  // no status to check
43
51
  return {verified: true};
44
52
  }
45
53
 
46
- const handlers = handlerMap.get(credentialStatus.type);
54
+ // expand every `TerseBitstringStatusListEntry`
55
+ const cache = new Map();
56
+ const credential = await _expandAllTerseEntries({
57
+ credential: options.credential, cache
58
+ });
59
+ const {credentialStatus} = credential;
60
+
61
+ // normalize credential status to an array
62
+ const credentialStatuses = Array.isArray(credentialStatus) ?
63
+ credentialStatus : [credentialStatus];
64
+
65
+ // combination of different status types not supported at this time
66
+ const expectedType = credentialStatuses?.[0]?.type;
67
+ if(credentialStatuses.some(({type}) => type !== expectedType)) {
68
+ throw new BedrockError(
69
+ 'Combinations of different credential status types are not ' +
70
+ 'presently supported.', {
71
+ name: 'NotSupportedError',
72
+ details: {
73
+ httpStatusCode: 400,
74
+ public: true
75
+ }
76
+ });
77
+ }
78
+
79
+ // get handlers for `expectedType`
80
+ const handlers = handlerMap.get(expectedType);
47
81
  if(!(handlers && handlers.statusTypeMatches({credential}))) {
48
- throw new Error(
49
- `Unsupported credentialStatus type "${credentialStatus.type}".`);
82
+ throw new BedrockError(
83
+ `Unsupported credentialStatus type "${expectedType}".`, {
84
+ name: 'NotSupportedError',
85
+ details: {
86
+ httpStatusCode: 400,
87
+ public: true
88
+ }
89
+ });
90
+ }
91
+
92
+ // create remote URL allow list from status lists
93
+ const remoteUrlAllowList = new Set();
94
+ for(const cs of credentialStatuses) {
95
+ const url = cs.statusListCredential ?? cs.revocationListCredential;
96
+ if(url) {
97
+ remoteUrlAllowList.add(url);
98
+ }
50
99
  }
51
100
 
52
101
  // document loader needs to only allow web loading of status
53
102
  // list VCs, nothing else
54
103
  const documentLoader = await createDocumentLoader({
55
- config,
56
- remoteUrlAllowList: new Set([
57
- credentialStatus.statusListCredential ??
58
- credentialStatus.revocationListCredential
59
- ])
104
+ config, remoteUrlAllowList, cache
60
105
  });
61
106
  options = {
62
107
  ...options,
108
+ credential,
63
109
  documentLoader
64
110
  };
65
111
  return await handlers.checkStatus(options);
@@ -68,3 +114,122 @@ export function createCheckStatus({config} = {}) {
68
114
  }
69
115
  };
70
116
  }
117
+
118
+ async function _expandAllTerseEntries({credential, cache} = {}) {
119
+ try {
120
+ // check for any terse entries
121
+ let hasTerseEntries = false;
122
+ const {credentialStatus} = credential;
123
+ if(Array.isArray(credentialStatus)) {
124
+ hasTerseEntries = credentialStatus.some(
125
+ cs => cs?.type === TERSE_BITSTRING_STATUS_LIST_ENTRY);
126
+ } else if(credentialStatus?.type === TERSE_BITSTRING_STATUS_LIST_ENTRY) {
127
+ hasTerseEntries = true;
128
+ }
129
+
130
+ if(!hasTerseEntries) {
131
+ return credential;
132
+ }
133
+
134
+ // check for expected context
135
+ const {'@context': contexts} = credential;
136
+ if(!Array.isArray(contexts)) {
137
+ throw new TypeError('"@context" must be an array.');
138
+ }
139
+ if(!contexts.includes(VC_BARCODES_V1_CONTEXT_URL)) {
140
+ throw new TypeError(
141
+ `The "@context" array must include "${VC_BARCODES_V1_CONTEXT_URL}".`);
142
+ }
143
+
144
+ // expand any `TerseBitstringStatusListEntry` to `BitstringStatusListEntry`
145
+ credential = structuredClone(credential);
146
+ if(Array.isArray(credentialStatus)) {
147
+ credential.credentialStatus = (await Promise.all(
148
+ credentialStatus.map(
149
+ async credentialStatus => _expandIfTerseEntry({
150
+ credentialStatus, cache
151
+ })))).flat();
152
+ } else {
153
+ credential.credentialStatus = await _expandIfTerseEntry({
154
+ credentialStatus, cache
155
+ });
156
+ }
157
+ return credential;
158
+ } catch(cause) {
159
+ throw new BedrockError(
160
+ `Could not expand terse bitstring status list entries: ${cause.message}`,
161
+ {
162
+ name: 'DataError',
163
+ cause,
164
+ details: {
165
+ httpStatusCode: 400,
166
+ public: true
167
+ }
168
+ });
169
+ }
170
+ }
171
+
172
+ async function _expandIfTerseEntry({credentialStatus, cache}) {
173
+ if(credentialStatus?.type !== TERSE_BITSTRING_STATUS_LIST_ENTRY) {
174
+ // nothing to expand
175
+ return credentialStatus;
176
+ }
177
+
178
+ if(!webLoader) {
179
+ throw new BedrockError(
180
+ `Web loader disabled; cannot load credential status list(s) for `
181
+ `status type "${credentialStatus.type}".`, {
182
+ name: 'NotSupportedError',
183
+ details: {
184
+ httpStatusCode: 400,
185
+ public: true
186
+ }
187
+ });
188
+ }
189
+
190
+ // compute two possible expanded statuses, for purposes `revocation` and
191
+ // `suspension`...
192
+ const credentialStatuses = (await Promise.all(
193
+ TERSE_STATUS_PURPOSES.map(async statusPurpose => {
194
+ const expanded = _expandTerseEntry({credentialStatus, statusPurpose});
195
+ const exists = await _fetchStatusListIfExists({expanded, cache});
196
+ return exists ? expanded : undefined;
197
+ }))).filter(cs => !!cs);
198
+
199
+ return credentialStatuses;
200
+ }
201
+
202
+ function _expandTerseEntry({credentialStatus, statusPurpose}) {
203
+ // compute `statusListCredential` from other params
204
+ const listIndex = Math.floor(
205
+ credentialStatus.terseStatusListIndex / TERSE_BITSTRING_STATUS_LIST_LENGTH);
206
+ const statusListIndex = credentialStatus.terseStatusListIndex %
207
+ TERSE_BITSTRING_STATUS_LIST_LENGTH;
208
+ const {terseStatusListBaseUrl} = credentialStatus;
209
+ const statusListCredential =
210
+ `${terseStatusListBaseUrl}/${statusPurpose}/${listIndex}`;
211
+ return {
212
+ type: 'BitstringStatusListEntry',
213
+ statusListCredential,
214
+ statusListIndex: `${statusListIndex}`,
215
+ statusPurpose
216
+ };
217
+ }
218
+
219
+ async function _fetchStatusListIfExists({expanded, cache}) {
220
+ try {
221
+ const {statusListCredential} = expanded;
222
+ if(cache.has(statusListCredential)) {
223
+ return true;
224
+ }
225
+ const {document} = await webLoader(statusListCredential);
226
+ cache.set(statusListCredential, document);
227
+ return true;
228
+ } catch(e) {
229
+ if(e.message === 'NotFoundError') {
230
+ // ok for a terse bitstring list to not exist
231
+ return false;
232
+ }
233
+ throw e;
234
+ }
235
+ }
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
- config, envelopedPresentation: presentation, challenge, domain, checks
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.1.0",
3
+ "version": "23.3.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
+ }