@bedrock/vc-verifier 19.1.0 → 20.1.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/README.md +1 -1
- package/lib/config.js +4 -2
- package/lib/di.js +63 -0
- package/lib/documentLoader.js +77 -5
- package/lib/envelopes.js +51 -0
- package/lib/http.js +12 -57
- package/lib/index.js +15 -2
- package/lib/vcjwt.js +537 -0
- package/lib/verify.js +86 -0
- package/package.json +39 -37
- package/schemas/bedrock-vc-verifier.js +231 -30
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Bedrock VC Verifier API module _(@bedrock/vc-verifier)_
|
|
2
2
|
|
|
3
|
-
[](https://github.com/digitalbazaar/bedrock-vc-verifier/actions/workflows/main.yml)
|
|
4
4
|
[](https://npm.im/bedrock-vc-verifier)
|
|
5
5
|
|
|
6
6
|
> A VC Verifier API library for use with Bedrock applications.
|
package/lib/config.js
CHANGED
|
@@ -17,9 +17,11 @@ cfg.challenges = {
|
|
|
17
17
|
// also allow any verifier instance to optionally load `http` and `https`
|
|
18
18
|
// documents directly from the Web
|
|
19
19
|
cfg.documentLoader = {
|
|
20
|
-
// `true` enables all
|
|
20
|
+
// `true` enables all verifier instances that do not have document loader
|
|
21
|
+
// instance-specific constraints to fetch `http` documents from the Web
|
|
21
22
|
http: false,
|
|
22
|
-
// `true` enables all
|
|
23
|
+
// `true` enables all verifier instances that do not have document loader
|
|
24
|
+
// instance-specific constraints to fetch `https` documents from the Web
|
|
23
25
|
https: true
|
|
24
26
|
};
|
|
25
27
|
|
package/lib/di.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as vc from '@digitalbazaar/vc';
|
|
5
|
+
import {checkStatus} from './status.js';
|
|
6
|
+
import {createDocumentLoader} from './documentLoader.js';
|
|
7
|
+
import {createSuites} from './suites.js';
|
|
8
|
+
|
|
9
|
+
export async function verifyCredential({config, credential, checks} = {}) {
|
|
10
|
+
const documentLoader = await createDocumentLoader({config});
|
|
11
|
+
const suite = createSuites();
|
|
12
|
+
|
|
13
|
+
const result = await vc.verifyCredential({
|
|
14
|
+
credential,
|
|
15
|
+
documentLoader,
|
|
16
|
+
suite,
|
|
17
|
+
// only check credential status when option is set
|
|
18
|
+
checkStatus: checks.includes('credentialStatus') ?
|
|
19
|
+
checkStatus : () => ({verified: true})
|
|
20
|
+
});
|
|
21
|
+
// if proof should have been checked but wasn't due to an error,
|
|
22
|
+
// try to run the check again using the VC's issuance date
|
|
23
|
+
if(checks.includes('proof') &&
|
|
24
|
+
result.error && !result.proof && result.results?.[0] &&
|
|
25
|
+
typeof credential.issuanceDate === 'string') {
|
|
26
|
+
const proofResult = await vc.verifyCredential({
|
|
27
|
+
credential,
|
|
28
|
+
documentLoader,
|
|
29
|
+
suite,
|
|
30
|
+
now: new Date(credential.issuanceDate),
|
|
31
|
+
// only check credential status when option is set
|
|
32
|
+
checkStatus: checks.includes('credentialStatus') ?
|
|
33
|
+
checkStatus : () => ({verified: true})
|
|
34
|
+
});
|
|
35
|
+
if(proofResult.verified) {
|
|
36
|
+
// overlay original (failed) results on top of proof results
|
|
37
|
+
result.results[0] = {
|
|
38
|
+
...proofResult.results[0],
|
|
39
|
+
...result.results[0],
|
|
40
|
+
proofVerified: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ensure all proofs are verified in order to return `verified`
|
|
45
|
+
let {verified} = result;
|
|
46
|
+
verified = !!(verified && result?.results?.every(({verified}) => verified));
|
|
47
|
+
return {...result, verified};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function verifyPresentation({
|
|
51
|
+
config, presentation, challenge, domain, checks
|
|
52
|
+
} = {}) {
|
|
53
|
+
const verifyOptions = {
|
|
54
|
+
challenge,
|
|
55
|
+
domain,
|
|
56
|
+
presentation,
|
|
57
|
+
documentLoader: await createDocumentLoader({config}),
|
|
58
|
+
suite: createSuites(),
|
|
59
|
+
unsignedPresentation: !checks.includes('proof'),
|
|
60
|
+
checkStatus
|
|
61
|
+
};
|
|
62
|
+
return vc.verify(verifyOptions);
|
|
63
|
+
}
|
package/lib/documentLoader.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2019-
|
|
2
|
+
* Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@bedrock/jsonld-document-loader';
|
|
10
10
|
import {createContextDocumentLoader} from '@bedrock/service-context-store';
|
|
11
11
|
import {didIo} from '@bedrock/did-io';
|
|
12
|
+
import {klona} from 'klona';
|
|
12
13
|
import '@bedrock/credentials-context';
|
|
13
14
|
import '@bedrock/data-integrity-context';
|
|
14
15
|
import '@bedrock/did-context';
|
|
@@ -53,9 +54,17 @@ export async function createDocumentLoader({config} = {}) {
|
|
|
53
54
|
{config, serviceType});
|
|
54
55
|
|
|
55
56
|
return async function documentLoader(url) {
|
|
56
|
-
//
|
|
57
|
+
// handle DID URLs...
|
|
57
58
|
if(url.startsWith('did:')) {
|
|
58
|
-
|
|
59
|
+
let document;
|
|
60
|
+
if(config.verifyOptions?.didResolver) {
|
|
61
|
+
// resolve via configured DID resolver
|
|
62
|
+
const {verifyOptions: {didResolver}} = config;
|
|
63
|
+
document = await _resolve({didResolver, didUrl: url});
|
|
64
|
+
} else {
|
|
65
|
+
// resolve via did-io
|
|
66
|
+
document = await didIo.get({url});
|
|
67
|
+
}
|
|
59
68
|
return {
|
|
60
69
|
contextUrl: null,
|
|
61
70
|
documentUrl: url,
|
|
@@ -75,11 +84,74 @@ export async function createDocumentLoader({config} = {}) {
|
|
|
75
84
|
// try to resolve URL through context doc loader
|
|
76
85
|
return await contextDocumentLoader(url);
|
|
77
86
|
} catch(e) {
|
|
78
|
-
// use web loader if configured
|
|
79
|
-
|
|
87
|
+
// use web loader if configured and instance config allows it and
|
|
88
|
+
// the url starts with `http`, and the core config allows it
|
|
89
|
+
const allowRemoteContexts = !config.verifyOptions?.documentLoader ||
|
|
90
|
+
config.verifyOptions.documentLoader.allowRemoteContexts;
|
|
91
|
+
if(allowRemoteContexts &&
|
|
92
|
+
url.startsWith('http') && e.name === 'NotFoundError' && webLoader) {
|
|
80
93
|
return webLoader(url);
|
|
81
94
|
}
|
|
82
95
|
throw e;
|
|
83
96
|
}
|
|
84
97
|
};
|
|
85
98
|
}
|
|
99
|
+
|
|
100
|
+
async function _resolve({didResolver, didUrl}) {
|
|
101
|
+
// split on `?` query or `#` fragment
|
|
102
|
+
const [did] = didUrl.split(/(?=[\?#])/);
|
|
103
|
+
|
|
104
|
+
// fetch DID document using DID resolver, assume DID param is prepended
|
|
105
|
+
const url = didResolver.url.endsWith('/') ?
|
|
106
|
+
`${didResolver.url}${encodeURIComponent(did)}` :
|
|
107
|
+
`${didResolver.url}/${encodeURIComponent(did)}`;
|
|
108
|
+
const data = await httpClientHandler.get({url});
|
|
109
|
+
|
|
110
|
+
if(data?.didDocument?.id !== did) {
|
|
111
|
+
throw new Error(`DID document for DID "${did}" not found.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// FIXME: perform DID document validation
|
|
115
|
+
// FIXME: handle URL query param / services
|
|
116
|
+
const {didDocument} = data;
|
|
117
|
+
|
|
118
|
+
// if a fragment was found use the fragment to dereference a subnode
|
|
119
|
+
// in the did doc
|
|
120
|
+
const [, fragment] = didUrl.split('#');
|
|
121
|
+
if(fragment) {
|
|
122
|
+
const id = `${didDocument.id}#${fragment}`;
|
|
123
|
+
return _getNode({didDocument, id});
|
|
124
|
+
}
|
|
125
|
+
// resolve the full DID Document
|
|
126
|
+
return didDocument;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _getNode({didDocument, id}) {
|
|
130
|
+
// do verification method search first
|
|
131
|
+
let match = didDocument?.verificationMethod?.find(vm => vm?.id === id);
|
|
132
|
+
if(!match) {
|
|
133
|
+
// check other top-level nodes
|
|
134
|
+
for(const [key, value] of Object.entries(didDocument)) {
|
|
135
|
+
if(key === '@context' || key === 'verificationMethod') {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if(Array.isArray(value)) {
|
|
139
|
+
match = value.find(e => e?.id === id);
|
|
140
|
+
} else if(value?.id === id) {
|
|
141
|
+
match = value;
|
|
142
|
+
}
|
|
143
|
+
if(match) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if(!match) {
|
|
150
|
+
throw new Error(`DID document entity with id "${id}" not found.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
'@context': klona(didDocument['@context']),
|
|
155
|
+
...klona(match)
|
|
156
|
+
};
|
|
157
|
+
}
|
package/lib/envelopes.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as vcjwt from './vcjwt.js';
|
|
6
|
+
|
|
7
|
+
const {util: {BedrockError}} = bedrock;
|
|
8
|
+
|
|
9
|
+
export async function verifyEnvelopedCredential({envelopedCredential} = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const {contents: jwt} = _parseEnvelope({
|
|
12
|
+
envelope: envelopedCredential
|
|
13
|
+
});
|
|
14
|
+
return vcjwt.verifyEnvelopedCredential({jwt});
|
|
15
|
+
} catch(error) {
|
|
16
|
+
return {verified: false, error};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function verifyEnvelopedPresentation({
|
|
21
|
+
envelopedPresentation, challenge, domain
|
|
22
|
+
} = {}) {
|
|
23
|
+
try {
|
|
24
|
+
const {contents: jwt} = _parseEnvelope({
|
|
25
|
+
envelope: envelopedPresentation
|
|
26
|
+
});
|
|
27
|
+
return vcjwt.verifyEnvelopedPresentation({jwt, challenge, domain});
|
|
28
|
+
} catch(error) {
|
|
29
|
+
return {verified: false, error};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _parseEnvelope({envelope}) {
|
|
34
|
+
const {id} = envelope;
|
|
35
|
+
let format;
|
|
36
|
+
const comma = id.indexOf(',');
|
|
37
|
+
if(id.startsWith('data:') && comma !== -1) {
|
|
38
|
+
format = id.slice('data:'.length, comma);
|
|
39
|
+
}
|
|
40
|
+
if(format !== 'application/jwt') {
|
|
41
|
+
throw new BedrockError(
|
|
42
|
+
`Unknown envelope format "${format}".`, {
|
|
43
|
+
name: 'DataError',
|
|
44
|
+
details: {
|
|
45
|
+
httpStatusCode: 400,
|
|
46
|
+
public: true
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return {contents: id.slice(comma + 1), format};
|
|
51
|
+
}
|
package/lib/http.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2018-
|
|
2
|
+
* Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
|
-
import * as vc from '@digitalbazaar/vc';
|
|
6
5
|
import {createChallenge, verifyChallenge} from './challenges.js';
|
|
7
6
|
import {
|
|
8
7
|
createChallengeBody,
|
|
@@ -10,12 +9,10 @@ import {
|
|
|
10
9
|
verifyPresentationBody
|
|
11
10
|
} from '../schemas/bedrock-vc-verifier.js';
|
|
12
11
|
import {metering, middleware} from '@bedrock/service-core';
|
|
12
|
+
import {verifyCredential, verifyPresentation} from './verify.js';
|
|
13
13
|
import {asyncHandler} from '@bedrock/express';
|
|
14
14
|
import bodyParser from 'body-parser';
|
|
15
|
-
import {checkStatus} from './status.js';
|
|
16
15
|
import cors from 'cors';
|
|
17
|
-
import {createDocumentLoader} from './documentLoader.js';
|
|
18
|
-
import {createSuites} from './suites.js';
|
|
19
16
|
import {serializeError} from 'serialize-error';
|
|
20
17
|
import {createValidateMiddleware as validate} from '@bedrock/validation';
|
|
21
18
|
|
|
@@ -33,7 +30,6 @@ bedrock.events.on('bedrock-express.configure.bodyParser', app => {
|
|
|
33
30
|
|
|
34
31
|
export async function addRoutes({app, service} = {}) {
|
|
35
32
|
const {routePrefix} = service;
|
|
36
|
-
const suite = createSuites();
|
|
37
33
|
const cfg = bedrock.config['vc-verifier'];
|
|
38
34
|
const baseUrl = `${routePrefix}/:localId`;
|
|
39
35
|
const routes = {
|
|
@@ -70,12 +66,11 @@ export async function addRoutes({app, service} = {}) {
|
|
|
70
66
|
app.post(
|
|
71
67
|
routes.credentialsVerify,
|
|
72
68
|
cors(),
|
|
73
|
-
validate({bodySchema: verifyCredentialBody}),
|
|
69
|
+
validate({bodySchema: verifyCredentialBody()}),
|
|
74
70
|
getConfigMiddleware,
|
|
75
71
|
middleware.authorizeServiceObjectRequest(),
|
|
76
72
|
asyncHandler(async (req, res) => {
|
|
77
73
|
const {config} = req.serviceObject;
|
|
78
|
-
const documentLoader = await createDocumentLoader({config});
|
|
79
74
|
|
|
80
75
|
let response;
|
|
81
76
|
try {
|
|
@@ -86,37 +81,7 @@ export async function addRoutes({app, service} = {}) {
|
|
|
86
81
|
|
|
87
82
|
const {checks} = options;
|
|
88
83
|
_validateChecks({checks});
|
|
89
|
-
const result = await
|
|
90
|
-
credential,
|
|
91
|
-
documentLoader,
|
|
92
|
-
suite,
|
|
93
|
-
// only check credential status when option is set
|
|
94
|
-
checkStatus: checks.includes('credentialStatus') ?
|
|
95
|
-
checkStatus : () => ({verified: true})
|
|
96
|
-
});
|
|
97
|
-
// if proof should have been checked but wasn't due to an error,
|
|
98
|
-
// try to run the check again using the VC's issuance date
|
|
99
|
-
if(checks.includes('proof') &&
|
|
100
|
-
result.error && !result.proof && result.results[0] &&
|
|
101
|
-
typeof credential.issuanceDate === 'string') {
|
|
102
|
-
const proofResult = await vc.verifyCredential({
|
|
103
|
-
credential,
|
|
104
|
-
documentLoader,
|
|
105
|
-
suite,
|
|
106
|
-
now: new Date(credential.issuanceDate),
|
|
107
|
-
// only check credential status when option is set
|
|
108
|
-
checkStatus: checks.includes('credentialStatus') ?
|
|
109
|
-
checkStatus : () => ({verified: true})
|
|
110
|
-
});
|
|
111
|
-
if(proofResult.verified) {
|
|
112
|
-
// overlay original (failed) results on top of proof results
|
|
113
|
-
result.results[0] = {
|
|
114
|
-
...proofResult.results[0],
|
|
115
|
-
...result.results[0],
|
|
116
|
-
proofVerified: true
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
84
|
+
const result = await verifyCredential({config, credential, checks});
|
|
120
85
|
response = _createResponse({credential, result, checks});
|
|
121
86
|
} catch(e) {
|
|
122
87
|
response = _createResponse({error: e});
|
|
@@ -150,17 +115,16 @@ export async function addRoutes({app, service} = {}) {
|
|
|
150
115
|
app.post(
|
|
151
116
|
routes.presentationsVerify,
|
|
152
117
|
cors(),
|
|
153
|
-
validate({bodySchema: verifyPresentationBody}),
|
|
118
|
+
validate({bodySchema: verifyPresentationBody()}),
|
|
154
119
|
getConfigMiddleware,
|
|
155
120
|
middleware.authorizeServiceObjectRequest(),
|
|
156
121
|
asyncHandler(async (req, res) => {
|
|
157
122
|
const {config} = req.serviceObject;
|
|
158
|
-
const documentLoader = await createDocumentLoader({config});
|
|
159
123
|
|
|
160
124
|
let response;
|
|
161
125
|
try {
|
|
162
126
|
const {
|
|
163
|
-
verifiablePresentation,
|
|
127
|
+
verifiablePresentation: presentation,
|
|
164
128
|
options = {}
|
|
165
129
|
} = req.body;
|
|
166
130
|
|
|
@@ -174,7 +138,6 @@ export async function addRoutes({app, service} = {}) {
|
|
|
174
138
|
}
|
|
175
139
|
|
|
176
140
|
_validateChecks({checks});
|
|
177
|
-
const unsignedPresentation = !checks.includes('proof');
|
|
178
141
|
|
|
179
142
|
// allow for `checks` to indicate whether or not the challenge
|
|
180
143
|
// should be checked
|
|
@@ -190,20 +153,12 @@ export async function addRoutes({app, service} = {}) {
|
|
|
190
153
|
({uses: challengeUses} = result);
|
|
191
154
|
}
|
|
192
155
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
presentation
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
checkStatus
|
|
200
|
-
};
|
|
201
|
-
const {proof} = verifiablePresentation;
|
|
202
|
-
if(proof && proof.domain) {
|
|
203
|
-
// FIXME: do not set a default
|
|
204
|
-
verifyOptions.domain = domain || 'issuer.example.com';
|
|
205
|
-
}
|
|
206
|
-
const result = await vc.verify(verifyOptions);
|
|
156
|
+
// FIXME: do not set a default domain
|
|
157
|
+
const expectedDomain = domain ??
|
|
158
|
+
(presentation?.proof?.domain && 'issuer.example.com');
|
|
159
|
+
const result = await verifyPresentation({
|
|
160
|
+
config, presentation, challenge, domain: expectedDomain, checks
|
|
161
|
+
});
|
|
207
162
|
response = _createResponse({result, challengeUses, checks});
|
|
208
163
|
} catch(e) {
|
|
209
164
|
response = _createResponse({error: e});
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2021-
|
|
2
|
+
* Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import {createService, schemas} from '@bedrock/service-core';
|
|
5
6
|
import {
|
|
6
7
|
addRoutes as addContextStoreRoutes
|
|
7
8
|
} from '@bedrock/service-context-store';
|
|
8
9
|
import {addRoutes} from './http.js';
|
|
9
|
-
import {createService} from '@bedrock/service-core';
|
|
10
10
|
import {initializeServiceAgent} from '@bedrock/service-agent';
|
|
11
|
+
import {klona} from 'klona';
|
|
12
|
+
import {verifyOptions} from '../schemas/bedrock-vc-verifier.js';
|
|
11
13
|
|
|
12
14
|
// load config defaults
|
|
13
15
|
import './config.js';
|
|
@@ -15,6 +17,15 @@ import './config.js';
|
|
|
15
17
|
const serviceType = 'vc-verifier';
|
|
16
18
|
|
|
17
19
|
bedrock.events.on('bedrock.init', async () => {
|
|
20
|
+
// add customizations to config validators...
|
|
21
|
+
const createConfigBody = klona(schemas.createConfigBody);
|
|
22
|
+
const updateConfigBody = klona(schemas.updateConfigBody);
|
|
23
|
+
const schemasToUpdate = [createConfigBody, updateConfigBody];
|
|
24
|
+
for(const schema of schemasToUpdate) {
|
|
25
|
+
// verify options
|
|
26
|
+
schema.properties.verifyOptions = verifyOptions;
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
// create `vc-verifier` service
|
|
19
30
|
const service = await createService({
|
|
20
31
|
serviceType,
|
|
@@ -24,6 +35,8 @@ bedrock.events.on('bedrock.init', async () => {
|
|
|
24
35
|
revocation: 1
|
|
25
36
|
},
|
|
26
37
|
validation: {
|
|
38
|
+
createConfigBody,
|
|
39
|
+
updateConfigBody,
|
|
27
40
|
// require these zcaps (by reference ID)
|
|
28
41
|
zcapReferenceIds: [{
|
|
29
42
|
referenceId: 'edv',
|