@bedrock/vc-verifier 23.2.0 → 23.3.1
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/documentLoader.js +12 -2
- package/lib/http.js +1 -1
- package/lib/status.js +178 -13
- package/package.json +1 -1
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/http.js
CHANGED
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
|
+
}
|