@bedrock/vc-verifier 23.1.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 +1 -0
- 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/verify.js +24 -4
- package/package.json +2 -1
- package/schemas/bedrock-vc-verifier.js +61 -1
package/lib/config.js
CHANGED
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/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.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-
|
|
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
|
+
}
|