@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 +1 -0
- package/lib/documentLoader.js +12 -2
- package/lib/envelopes.js +7 -2
- package/lib/http.js +2 -1
- package/lib/index.js +8 -3
- package/lib/mdl.js +193 -0
- package/lib/status.js +178 -13
- package/lib/verify.js +24 -4
- package/package.json +2 -1
- package/schemas/bedrock-vc-verifier.js +61 -1
package/lib/config.js
CHANGED
package/lib/documentLoader.js
CHANGED
|
@@ -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({
|
|
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: '
|
|
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
|
-
|
|
77
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
49
|
-
`Unsupported 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.
|
|
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-
|
|
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
|
+
}
|