@bedrock/vc-verifier 20.0.0 → 20.1.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/di.js +63 -0
- package/lib/envelopes.js +51 -0
- package/lib/http.js +12 -57
- package/lib/vcjwt.js +537 -0
- package/lib/verify.js +103 -0
- package/package.json +4 -1
- package/schemas/bedrock-vc-verifier.js +203 -38
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/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/vcjwt.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
6
|
+
import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey';
|
|
7
|
+
import {importJWK, jwtVerify} from 'jose';
|
|
8
|
+
import {didIo} from '@bedrock/did-io';
|
|
9
|
+
|
|
10
|
+
const {util: {BedrockError}} = bedrock;
|
|
11
|
+
|
|
12
|
+
// supported JWT algs
|
|
13
|
+
const ECDSA_ALGS = ['ES256', 'ES384'];
|
|
14
|
+
const EDDSA_ALGS = ['Ed25519', 'EdDSA'];
|
|
15
|
+
|
|
16
|
+
const VC_CONTEXT_1 = 'https://www.w3.org/2018/credentials/v1';
|
|
17
|
+
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
18
|
+
|
|
19
|
+
export async function verifyEnvelopedCredential({jwt} = {}) {
|
|
20
|
+
try {
|
|
21
|
+
const {
|
|
22
|
+
verified, controller, verificationMethod, verifyResult
|
|
23
|
+
} = await _verifyJwt({jwt, proofPurpose: 'assertionMethod'});
|
|
24
|
+
// if verified, parse credential from payload...
|
|
25
|
+
let credential;
|
|
26
|
+
if(verified) {
|
|
27
|
+
credential = _jwtPayloadToCredential({verifyResult});
|
|
28
|
+
}
|
|
29
|
+
const results = [{
|
|
30
|
+
verified,
|
|
31
|
+
verificationMethod,
|
|
32
|
+
controller,
|
|
33
|
+
verifyResult,
|
|
34
|
+
credential
|
|
35
|
+
}];
|
|
36
|
+
return {verified, controller, results, credential};
|
|
37
|
+
} catch(error) {
|
|
38
|
+
return {verified: false, error};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function verifyEnvelopedPresentation({
|
|
43
|
+
jwt, challenge, domain
|
|
44
|
+
} = {}) {
|
|
45
|
+
try {
|
|
46
|
+
const {
|
|
47
|
+
verified, controller, verificationMethod, verifyResult
|
|
48
|
+
} = await _verifyJwt({
|
|
49
|
+
jwt, proofPurpose: 'authentication', audience: domain
|
|
50
|
+
});
|
|
51
|
+
// if verified, parse presentation from payload...
|
|
52
|
+
let presentation;
|
|
53
|
+
if(verified) {
|
|
54
|
+
presentation = _jwtPayloadToPresentation({
|
|
55
|
+
verifyResult, challenge
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const results = [{
|
|
59
|
+
verified,
|
|
60
|
+
verificationMethod,
|
|
61
|
+
controller,
|
|
62
|
+
verifyResult,
|
|
63
|
+
presentation
|
|
64
|
+
}];
|
|
65
|
+
return {verified, controller, results, presentation};
|
|
66
|
+
} catch(error) {
|
|
67
|
+
return {verified: false, error};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function _verifyJwt({jwt, proofPurpose, audience} = {}) {
|
|
72
|
+
let verificationMethod;
|
|
73
|
+
let controller;
|
|
74
|
+
// `resolveKey` is passed `protectedHeader`
|
|
75
|
+
const resolveKey = async ({alg, kid}) => {
|
|
76
|
+
const isEcdsa = ECDSA_ALGS.includes(alg);
|
|
77
|
+
const isEddsa = !isEcdsa && EDDSA_ALGS.includes(alg);
|
|
78
|
+
if(!(isEcdsa || isEddsa)) {
|
|
79
|
+
throw new BedrockError(
|
|
80
|
+
`Unsupported JWT "alg": "${alg}".`, {
|
|
81
|
+
name: 'DataError',
|
|
82
|
+
details: {
|
|
83
|
+
httpStatusCode: 400,
|
|
84
|
+
public: true
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const vm = await didIo.get({url: kid});
|
|
90
|
+
// `vm.controller` must be the issuer of the JWT; also ensure that
|
|
91
|
+
// the specified controller authorized `vm` for the given proof purpose
|
|
92
|
+
({controller} = vm);
|
|
93
|
+
verificationMethod = vm;
|
|
94
|
+
const didDoc = await didIo.get({url: controller});
|
|
95
|
+
let match = didDoc?.authentication?.find?.(
|
|
96
|
+
e => e === vm.id || e.id === vm.id);
|
|
97
|
+
if(typeof match === 'string') {
|
|
98
|
+
match = didDoc?.verificationMethod?.find?.(e => e.id === vm.id);
|
|
99
|
+
}
|
|
100
|
+
if(!(match && Array.isArray(match.controller) ?
|
|
101
|
+
match.controller.includes(vm.controller) :
|
|
102
|
+
match.controller === vm.controller)) {
|
|
103
|
+
throw new BedrockError(
|
|
104
|
+
`Verification method controller "${controller}" did not authorize ` +
|
|
105
|
+
`verification method "${vm.id}" for the purpose ` +
|
|
106
|
+
`of "${proofPurpose}".`, {
|
|
107
|
+
name: 'DataError',
|
|
108
|
+
details: {
|
|
109
|
+
httpStatusCode: 400,
|
|
110
|
+
public: true
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
let jwk;
|
|
115
|
+
if(isEcdsa) {
|
|
116
|
+
const keyPair = await EcdsaMultikey.from(vm);
|
|
117
|
+
jwk = await EcdsaMultikey.toJwk({keyPair});
|
|
118
|
+
jwk.alg = alg;
|
|
119
|
+
} else {
|
|
120
|
+
const keyPair = await Ed25519Multikey.from(vm);
|
|
121
|
+
jwk = await Ed25519Multikey.toJwk({keyPair});
|
|
122
|
+
jwk.alg = 'EdDSA';
|
|
123
|
+
}
|
|
124
|
+
return importJWK(jwk);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// FIXME: enable allowed algorithms to be configurable per instance
|
|
128
|
+
const allowedAlgorithms = ['EdDSA', 'Ed25519', 'ES256', 'ES256K', 'ES384'];
|
|
129
|
+
// FIXME: enable `maxClockSkew` to be configurable per instance
|
|
130
|
+
// default is 300 secs
|
|
131
|
+
const maxClockSkew = 300;
|
|
132
|
+
|
|
133
|
+
// use `jose` lib (for now) to verify JWT and return `payload`;
|
|
134
|
+
// pass optional supported algorithms as allow list ... note
|
|
135
|
+
// that `jose` *always* prohibits the `none` algorithm
|
|
136
|
+
let verifyResult;
|
|
137
|
+
try {
|
|
138
|
+
// `jwtVerify` checks claims: `aud`, `exp`, `nbf`
|
|
139
|
+
const {payload, protectedHeader} = await jwtVerify(jwt, resolveKey, {
|
|
140
|
+
algorithms: allowedAlgorithms,
|
|
141
|
+
clockTolerance: maxClockSkew,
|
|
142
|
+
audience
|
|
143
|
+
});
|
|
144
|
+
verifyResult = {payload, protectedHeader};
|
|
145
|
+
} catch(e) {
|
|
146
|
+
const details = {
|
|
147
|
+
httpStatusCode: 403,
|
|
148
|
+
public: true,
|
|
149
|
+
code: e.code,
|
|
150
|
+
reason: e.message
|
|
151
|
+
};
|
|
152
|
+
if(e.claim) {
|
|
153
|
+
details.claim = e.claim;
|
|
154
|
+
}
|
|
155
|
+
throw new BedrockError('JWT validation failed.', {
|
|
156
|
+
name: 'DataError',
|
|
157
|
+
details
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// check `iss` claim
|
|
162
|
+
if(!(controller && verifyResult?.payload?.iss === controller)) {
|
|
163
|
+
throw new BedrockError('JWT validation failed.', {
|
|
164
|
+
name: 'DataError',
|
|
165
|
+
details: {
|
|
166
|
+
httpStatusCode: 400,
|
|
167
|
+
public: true,
|
|
168
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
169
|
+
reason: 'unexpected "iss" claim value.',
|
|
170
|
+
claim: 'iss'
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {verified: true, verificationMethod, controller, verifyResult};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function _jwtPayloadToCredential({verifyResult} = {}) {
|
|
179
|
+
/* Example:
|
|
180
|
+
{
|
|
181
|
+
"alg": <signer.algorithm>,
|
|
182
|
+
"kid": <signer.id>
|
|
183
|
+
}.
|
|
184
|
+
{
|
|
185
|
+
"iss": <verifiableCredential.issuer>,
|
|
186
|
+
"jti": <verifiableCredential.id>
|
|
187
|
+
"sub": <verifiableCredential.credentialSubject>
|
|
188
|
+
"nbf": <verifiableCredential.[issuanceDate | validFrom]>
|
|
189
|
+
"exp": <verifiableCredential.[expirationDate | validUntil]>
|
|
190
|
+
"vc": <verifiableCredential>
|
|
191
|
+
}
|
|
192
|
+
*/
|
|
193
|
+
const {vc} = verifyResult.payload;
|
|
194
|
+
if(!(vc && typeof vc === 'object')) {
|
|
195
|
+
throw new BedrockError('JWT validation failed.', {
|
|
196
|
+
name: 'DataError',
|
|
197
|
+
details: {
|
|
198
|
+
httpStatusCode: 400,
|
|
199
|
+
public: true,
|
|
200
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
201
|
+
reason: 'missing or unexpected "vc" claim value.',
|
|
202
|
+
claim: 'vc'
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let {'@context': context = []} = vc;
|
|
208
|
+
if(!Array.isArray(context)) {
|
|
209
|
+
context = [context];
|
|
210
|
+
}
|
|
211
|
+
const isVersion1 = context.includes(VC_CONTEXT_1);
|
|
212
|
+
const isVersion2 = context.includes(VC_CONTEXT_2);
|
|
213
|
+
if(!(isVersion1 ^ isVersion2)) {
|
|
214
|
+
throw new BedrockError(
|
|
215
|
+
'Verifiable credential is neither version "1.x" nor "2.x".', {
|
|
216
|
+
name: 'DataError',
|
|
217
|
+
details: {
|
|
218
|
+
httpStatusCode: 400,
|
|
219
|
+
public: true
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const credential = {...vc};
|
|
225
|
+
const {iss, jti, sub, nbf, exp} = verifyResult.payload;
|
|
226
|
+
|
|
227
|
+
// inject `issuer` value
|
|
228
|
+
if(vc.issuer === undefined) {
|
|
229
|
+
vc.issuer = iss;
|
|
230
|
+
} else if(vc.issuer && typeof vc.issuer === 'object' &&
|
|
231
|
+
vc.issuer.id === undefined) {
|
|
232
|
+
vc.issuer.id = iss;
|
|
233
|
+
} else if(iss !== vc.issuer && iss !== vc.issuer?.id) {
|
|
234
|
+
throw new BedrockError(
|
|
235
|
+
'VC-JWT "iss" claim does not equal nor does it exclusively ' +
|
|
236
|
+
'provide verifiable credential "issuer" / "issuer.id".', {
|
|
237
|
+
name: 'DataError',
|
|
238
|
+
details: {
|
|
239
|
+
httpStatusCode: 400,
|
|
240
|
+
public: true
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if(jti !== undefined && jti !== vc.id) {
|
|
246
|
+
// inject `id` value
|
|
247
|
+
if(vc.id === undefined) {
|
|
248
|
+
vc.id = jti;
|
|
249
|
+
} else {
|
|
250
|
+
throw new BedrockError(
|
|
251
|
+
'VC-JWT "jti" claim does not equal nor does it exclusively ' +
|
|
252
|
+
'provide verifiable credential "id".', {
|
|
253
|
+
name: 'DataError',
|
|
254
|
+
details: {
|
|
255
|
+
httpStatusCode: 400,
|
|
256
|
+
public: true
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if(sub !== undefined && sub !== vc.credentialSubject?.id) {
|
|
263
|
+
// inject `credentialSubject.id` value
|
|
264
|
+
if(!vc.credentialSubject) {
|
|
265
|
+
throw new BedrockError(
|
|
266
|
+
'Verifiable credential has no "credentialSubject".', {
|
|
267
|
+
name: 'DataError',
|
|
268
|
+
details: {
|
|
269
|
+
httpStatusCode: 400,
|
|
270
|
+
public: true
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if(Array.isArray(vc.credentialSubject)) {
|
|
275
|
+
throw new BedrockError(
|
|
276
|
+
'Verifiable credential has multiple credential subjects, which is ' +
|
|
277
|
+
'not supported in VC-JWT.', {
|
|
278
|
+
name: 'DataError',
|
|
279
|
+
details: {
|
|
280
|
+
httpStatusCode: 400,
|
|
281
|
+
public: true
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if(vc.credentialSubject?.id === undefined) {
|
|
286
|
+
vc.credentialSubject.id = sub;
|
|
287
|
+
} else {
|
|
288
|
+
throw new BedrockError(
|
|
289
|
+
'VC-JWT "sub" claim does not equal nor does it exclusively ' +
|
|
290
|
+
'provide verifiable credential "credentialSubject.id".', {
|
|
291
|
+
name: 'DataError',
|
|
292
|
+
details: {
|
|
293
|
+
httpStatusCode: 400,
|
|
294
|
+
public: true
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if(nbf === undefined && isVersion1) {
|
|
301
|
+
throw new BedrockError('JWT validation failed.', {
|
|
302
|
+
name: 'DataError',
|
|
303
|
+
details: {
|
|
304
|
+
httpStatusCode: 400,
|
|
305
|
+
public: true,
|
|
306
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
307
|
+
reason: 'missing "nbf" claim value.',
|
|
308
|
+
claim: 'nbf'
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if(nbf !== undefined) {
|
|
314
|
+
// fuzzy convert `nbf` into `issuanceDate` / `validFrom`, only require
|
|
315
|
+
// second-level precision
|
|
316
|
+
const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
|
|
317
|
+
const dateProperty = isVersion1 ? 'issuanceDate' : 'validFrom';
|
|
318
|
+
// inject dateProperty value
|
|
319
|
+
if(vc[dateProperty] === undefined) {
|
|
320
|
+
vc[dateProperty] = dateString + 'Z';
|
|
321
|
+
} else if(!(vc[dateProperty].startsWith(dateString) &&
|
|
322
|
+
vc[dateProperty].endsWith('Z'))) {
|
|
323
|
+
throw new BedrockError(
|
|
324
|
+
'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
|
|
325
|
+
`verifiable credential "${dateProperty}".`, {
|
|
326
|
+
name: 'DataError',
|
|
327
|
+
details: {
|
|
328
|
+
httpStatusCode: 400,
|
|
329
|
+
public: true
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if(exp !== undefined) {
|
|
336
|
+
// fuzzy convert `exp` into `expirationDate` / `validUntil`, only require
|
|
337
|
+
// second-level precision
|
|
338
|
+
const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
|
|
339
|
+
const dateProperty = isVersion1 ? 'expirationDate' : 'validUntil';
|
|
340
|
+
// inject dateProperty value
|
|
341
|
+
if(vc[dateProperty] === undefined) {
|
|
342
|
+
vc[dateProperty] = dateString + 'Z';
|
|
343
|
+
} else if(!(vc[dateProperty].startsWith(dateString) &&
|
|
344
|
+
vc[dateProperty].endsWith('Z'))) {
|
|
345
|
+
throw new BedrockError(
|
|
346
|
+
'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
|
|
347
|
+
`verifiable credential "${dateProperty}".`, {
|
|
348
|
+
name: 'DataError',
|
|
349
|
+
details: {
|
|
350
|
+
httpStatusCode: 400,
|
|
351
|
+
public: true
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return credential;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function _jwtPayloadToPresentation({verifyResult, challenge} = {}) {
|
|
361
|
+
/* Example:
|
|
362
|
+
{
|
|
363
|
+
"alg": <signer.algorithm>,
|
|
364
|
+
"kid": <signer.id>
|
|
365
|
+
}.
|
|
366
|
+
{
|
|
367
|
+
"iss": <verifiablePresentation.holder>,
|
|
368
|
+
"aud": <verifiablePresentation.domain>,
|
|
369
|
+
"nonce": <verifiablePresentation.nonce>,
|
|
370
|
+
"jti": <verifiablePresentation.id>
|
|
371
|
+
"nbf": <verifiablePresentation.[validFrom]>
|
|
372
|
+
"exp": <verifiablePresentation.[validUntil]>
|
|
373
|
+
"vp": <verifiablePresentation>
|
|
374
|
+
}
|
|
375
|
+
*/
|
|
376
|
+
const {vp} = verifyResult.payload;
|
|
377
|
+
if(!(vp && typeof vp === 'object')) {
|
|
378
|
+
throw new BedrockError('JWT validation failed.', {
|
|
379
|
+
name: 'DataError',
|
|
380
|
+
details: {
|
|
381
|
+
httpStatusCode: 400,
|
|
382
|
+
public: true,
|
|
383
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
384
|
+
reason: 'missing or unexpected "vp" claim value.',
|
|
385
|
+
claim: 'vp'
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let {'@context': context = []} = vp;
|
|
391
|
+
if(!Array.isArray(context)) {
|
|
392
|
+
context = [context];
|
|
393
|
+
}
|
|
394
|
+
const isVersion1 = context.includes(VC_CONTEXT_1);
|
|
395
|
+
const isVersion2 = context.includes(VC_CONTEXT_2);
|
|
396
|
+
if(!(isVersion1 ^ isVersion2)) {
|
|
397
|
+
throw new BedrockError(
|
|
398
|
+
'Verifiable presentation is not either version "1.x" or "2.x".', {
|
|
399
|
+
name: 'DataError',
|
|
400
|
+
details: {
|
|
401
|
+
httpStatusCode: 400,
|
|
402
|
+
public: true
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const presentation = {...vp};
|
|
408
|
+
const {iss, nonce, jti, nbf, exp} = verifyResult.payload;
|
|
409
|
+
|
|
410
|
+
// inject `holder` value
|
|
411
|
+
if(vp.holder === undefined) {
|
|
412
|
+
vp.holder = iss;
|
|
413
|
+
} else if(vp.holder && typeof vp.holder === 'object' &&
|
|
414
|
+
vp.holder.id === undefined) {
|
|
415
|
+
vp.holder.id = iss;
|
|
416
|
+
} else if(iss !== vp.holder && iss !== vp.holder?.id) {
|
|
417
|
+
throw new BedrockError(
|
|
418
|
+
'VC-JWT "iss" claim does not equal nor does it exclusively ' +
|
|
419
|
+
'provide verifiable presentation "holder" / "holder.id".', {
|
|
420
|
+
name: 'DataError',
|
|
421
|
+
details: {
|
|
422
|
+
httpStatusCode: 400,
|
|
423
|
+
public: true
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if(jti !== undefined && jti !== vp.id) {
|
|
429
|
+
// inject `id` value
|
|
430
|
+
if(vp.id === undefined) {
|
|
431
|
+
vp.id = jti;
|
|
432
|
+
} else {
|
|
433
|
+
throw new BedrockError(
|
|
434
|
+
'VC-JWT "jti" claim does not equal nor does it exclusively ' +
|
|
435
|
+
'provide verifiable presentation "id".', {
|
|
436
|
+
name: 'DataError',
|
|
437
|
+
details: {
|
|
438
|
+
httpStatusCode: 400,
|
|
439
|
+
public: true
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// version 1.x VPs do not support `validFrom`/`validUntil`
|
|
446
|
+
if(nbf !== undefined && isVersion2) {
|
|
447
|
+
// fuzzy convert `nbf` into `validFrom`, only require
|
|
448
|
+
// second-level precision
|
|
449
|
+
const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
|
|
450
|
+
|
|
451
|
+
// inject `validFrom` value
|
|
452
|
+
if(vp.validFrom === undefined) {
|
|
453
|
+
vp.validFrom = dateString + 'Z';
|
|
454
|
+
} else if(!(vp.validFrom?.startsWith(dateString) &&
|
|
455
|
+
vp.validFrom.endsWith('Z'))) {
|
|
456
|
+
throw new BedrockError(
|
|
457
|
+
'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
|
|
458
|
+
'verifiable presentation "validFrom".', {
|
|
459
|
+
name: 'DataError',
|
|
460
|
+
details: {
|
|
461
|
+
httpStatusCode: 400,
|
|
462
|
+
public: true
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if(exp !== undefined && isVersion2) {
|
|
468
|
+
// fuzzy convert `exp` into `validUntil`, only require
|
|
469
|
+
// second-level precision
|
|
470
|
+
const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
|
|
471
|
+
|
|
472
|
+
// inject `validUntil` value
|
|
473
|
+
if(vp.validUntil === undefined) {
|
|
474
|
+
vp.validUntil = dateString + 'Z';
|
|
475
|
+
} else if(!(vp.validUntil?.startsWith(dateString) &&
|
|
476
|
+
vp.validUntil?.endsWith('Z'))) {
|
|
477
|
+
throw new BedrockError(
|
|
478
|
+
'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
|
|
479
|
+
'verifiable presentation "validUntil".', {
|
|
480
|
+
name: 'DataError',
|
|
481
|
+
details: {
|
|
482
|
+
httpStatusCode: 400,
|
|
483
|
+
public: true
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if(challenge !== undefined && nonce !== challenge) {
|
|
490
|
+
throw new BedrockError('JWT validation failed.', {
|
|
491
|
+
name: 'DataError',
|
|
492
|
+
details: {
|
|
493
|
+
httpStatusCode: 400,
|
|
494
|
+
public: true,
|
|
495
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
496
|
+
reason: 'missing or unexpected "nonce" claim value.',
|
|
497
|
+
claim: 'nonce'
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// do some validation on `verifiableCredential`
|
|
503
|
+
let {verifiableCredential = []} = presentation;
|
|
504
|
+
if(!Array.isArray(verifiableCredential)) {
|
|
505
|
+
verifiableCredential = [verifiableCredential];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ensure version 2 VPs only have objects in `verifiableCredential`
|
|
509
|
+
const hasVCJWTs = verifiableCredential.some(vc => typeof vc !== 'object');
|
|
510
|
+
if(isVersion2 && hasVCJWTs) {
|
|
511
|
+
throw new BedrockError(
|
|
512
|
+
'Version 2.x verifiable presentations must only use objects in the ' +
|
|
513
|
+
'"verifiableCredential" field.', {
|
|
514
|
+
name: 'DataError',
|
|
515
|
+
details: {
|
|
516
|
+
httpStatusCode: 400,
|
|
517
|
+
public: true
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// transform any VC-JWT VCs to enveloped VCs
|
|
523
|
+
if(presentation.verifiableCredential && hasVCJWTs) {
|
|
524
|
+
presentation.verifiableCredential = verifiableCredential.map(vc => {
|
|
525
|
+
if(typeof vc !== 'string') {
|
|
526
|
+
return vc;
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
'@context': VC_CONTEXT_2,
|
|
530
|
+
id: `data:application/jwt,${vc}`,
|
|
531
|
+
type: 'EnvelopedVerifiableCredential',
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return presentation;
|
|
537
|
+
}
|
package/lib/verify.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as di from './di.js';
|
|
5
|
+
import {
|
|
6
|
+
verifyEnvelopedCredential, verifyEnvelopedPresentation
|
|
7
|
+
} from './envelopes.js';
|
|
8
|
+
|
|
9
|
+
export async function verifyCredential({config, credential, checks} = {}) {
|
|
10
|
+
if(credential?.type !== 'EnvelopedVerifiableCredential') {
|
|
11
|
+
return di.verifyCredential({config, credential, checks});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const result = await verifyEnvelopedCredential({
|
|
15
|
+
envelopedCredential: credential, checks
|
|
16
|
+
});
|
|
17
|
+
// if the credential has a `proof` field, do DI verification
|
|
18
|
+
let {verified} = result;
|
|
19
|
+
if(verified && result.credential.proof) {
|
|
20
|
+
const proofResult = await di.verifyCredential({
|
|
21
|
+
config, credential: result.credential, checks
|
|
22
|
+
});
|
|
23
|
+
result.proofResult = proofResult;
|
|
24
|
+
verified = verified && proofResult.verified;
|
|
25
|
+
}
|
|
26
|
+
return {...result, verified};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function verifyPresentation({
|
|
30
|
+
config, presentation, challenge, domain, checks
|
|
31
|
+
} = {}) {
|
|
32
|
+
if(presentation?.type !== 'EnvelopedVerifiablePresentation') {
|
|
33
|
+
const result = await di.verifyPresentation({
|
|
34
|
+
config, presentation, challenge, domain, checks
|
|
35
|
+
});
|
|
36
|
+
if(result.verified || !result.presentationResult?.verified) {
|
|
37
|
+
// the whole VP and all its VCs were verified or the VP itself failed
|
|
38
|
+
// verification, so no extra work needed below
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// note that the presentation itself verified, but the VCs therein might
|
|
43
|
+
// not because some of them might be enveloped VCs and the underlying
|
|
44
|
+
// `vc` library doesn't support this; therefore only use the presentation
|
|
45
|
+
// result and let the code below check VCs to ensure any enveloped VCs
|
|
46
|
+
// will also be checked
|
|
47
|
+
let {verifiableCredential = []} = presentation;
|
|
48
|
+
if(!Array.isArray(verifiableCredential)) {
|
|
49
|
+
verifiableCredential = [verifiableCredential];
|
|
50
|
+
}
|
|
51
|
+
const hasEnvelopedCredential = verifiableCredential.some(
|
|
52
|
+
vc => vc?.type === 'EnvelopedVerifiableCredential');
|
|
53
|
+
if(!hasEnvelopedCredential) {
|
|
54
|
+
// no enveloped VCs, return result
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// try to verify each VC in the VP again but with envelope support
|
|
59
|
+
const credentialResults = await Promise.all(verifiableCredential.map(
|
|
60
|
+
credential => verifyCredential({config, credential, checks})));
|
|
61
|
+
const verified = credentialResults.every(({verified}) => verified);
|
|
62
|
+
if(verified) {
|
|
63
|
+
result.verified = true;
|
|
64
|
+
}
|
|
65
|
+
result.credentialResults = credentialResults;
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const presentationResult = await verifyEnvelopedPresentation({
|
|
70
|
+
envelopedPresentation: presentation, challenge, domain
|
|
71
|
+
});
|
|
72
|
+
// verify each `verifiableCredential` in the resulting VP
|
|
73
|
+
let verified = presentationResult.verified;
|
|
74
|
+
let credentialResults;
|
|
75
|
+
if(!verified) {
|
|
76
|
+
credentialResults = [];
|
|
77
|
+
} else if(presentationResult.presentation?.proof) {
|
|
78
|
+
// presentation in the envelope has a `proof`, so recurse to check it
|
|
79
|
+
const proofResult = await verifyPresentation({
|
|
80
|
+
config, presentation: presentationResult.presentation,
|
|
81
|
+
challenge, domain, checks
|
|
82
|
+
});
|
|
83
|
+
verified = !!(verified && proofResult.presentationResult?.verified);
|
|
84
|
+
presentationResult.proofResult = proofResult;
|
|
85
|
+
({credentialResults} = proofResult);
|
|
86
|
+
} else {
|
|
87
|
+
// verify each VC in the VP
|
|
88
|
+
let {verifiableCredential = []} = presentationResult.presentation;
|
|
89
|
+
if(!Array.isArray(verifiableCredential)) {
|
|
90
|
+
verifiableCredential = [verifiableCredential];
|
|
91
|
+
}
|
|
92
|
+
credentialResults = await Promise.all(verifiableCredential.map(
|
|
93
|
+
credential => verifyCredential({config, credential, checks})));
|
|
94
|
+
verified = verified && credentialResults.every(
|
|
95
|
+
({verified}) => verified);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
...presentationResult,
|
|
99
|
+
verified,
|
|
100
|
+
presentationResult,
|
|
101
|
+
credentialResults
|
|
102
|
+
};
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-verifier",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock VC Verifier",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
"@digitalbazaar/bbs-2023-cryptosuite": "^1.2.0",
|
|
29
29
|
"@digitalbazaar/data-integrity": "^2.2.0",
|
|
30
30
|
"@digitalbazaar/ecdsa-2019-cryptosuite": "^2.0.0",
|
|
31
|
+
"@digitalbazaar/ecdsa-multikey": "^1.7.0",
|
|
31
32
|
"@digitalbazaar/ecdsa-rdfc-2019-cryptosuite": "^1.1.0",
|
|
32
33
|
"@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.2.1",
|
|
34
|
+
"@digitalbazaar/ed25519-multikey": "^1.1.0",
|
|
33
35
|
"@digitalbazaar/ed25519-signature-2018": "^4.0.0",
|
|
34
36
|
"@digitalbazaar/ed25519-signature-2020": "^5.4.0",
|
|
35
37
|
"@digitalbazaar/eddsa-2022-cryptosuite": "^1.0.0",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
"bnid": "^3.0.0",
|
|
42
44
|
"body-parser": "^1.20.2",
|
|
43
45
|
"cors": "^2.8.5",
|
|
46
|
+
"jose": "^5.6.3",
|
|
44
47
|
"klona": "^2.0.6",
|
|
45
48
|
"serialize-error": "^11.0.3"
|
|
46
49
|
},
|
|
@@ -1,12 +1,177 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import {schemas} from '@bedrock/validation';
|
|
5
|
+
|
|
6
|
+
const VC_CONTEXT_1 = 'https://www.w3.org/2018/credentials/v1';
|
|
7
|
+
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
8
|
+
|
|
9
|
+
const vcContext = {
|
|
6
10
|
type: 'array',
|
|
7
11
|
minItems: 1,
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
// the first context must be the VC context
|
|
13
|
+
items: [{
|
|
14
|
+
oneOf: [{
|
|
15
|
+
const: VC_CONTEXT_1
|
|
16
|
+
}, {
|
|
17
|
+
const: VC_CONTEXT_2
|
|
18
|
+
}]
|
|
19
|
+
}],
|
|
20
|
+
// additional contexts maybe strings or objects
|
|
21
|
+
additionalItems: {
|
|
22
|
+
anyOf: [{type: 'string'}, {type: 'object'}]
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function idOrObjectWithId() {
|
|
27
|
+
return {
|
|
28
|
+
title: 'identifier or an object with an id',
|
|
29
|
+
anyOf: [
|
|
30
|
+
schemas.identifier(),
|
|
31
|
+
{
|
|
32
|
+
type: 'object',
|
|
33
|
+
required: ['id'],
|
|
34
|
+
additionalProperties: true,
|
|
35
|
+
properties: {id: schemas.identifier()}
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function verifiableCredential() {
|
|
42
|
+
return {
|
|
43
|
+
title: 'Verifiable Credential',
|
|
44
|
+
type: 'object',
|
|
45
|
+
required: [
|
|
46
|
+
'@context',
|
|
47
|
+
'credentialSubject',
|
|
48
|
+
'issuer',
|
|
49
|
+
'type'
|
|
50
|
+
],
|
|
51
|
+
additionalProperties: true,
|
|
52
|
+
properties: {
|
|
53
|
+
'@context': vcContext,
|
|
54
|
+
credentialSubject: {
|
|
55
|
+
anyOf: [
|
|
56
|
+
{type: 'object'},
|
|
57
|
+
{type: 'array', minItems: 1, items: {type: 'object'}}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
id: {
|
|
61
|
+
type: 'string'
|
|
62
|
+
},
|
|
63
|
+
issuer: idOrObjectWithId(),
|
|
64
|
+
type: {
|
|
65
|
+
type: 'array',
|
|
66
|
+
minItems: 1,
|
|
67
|
+
// this first type must be VerifiableCredential
|
|
68
|
+
items: [
|
|
69
|
+
{const: 'VerifiableCredential'},
|
|
70
|
+
],
|
|
71
|
+
// additional types must be strings
|
|
72
|
+
additionalItems: {
|
|
73
|
+
type: 'string'
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
proof: schemas.proof()
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const envelopedVerifiableCredential = {
|
|
82
|
+
title: 'Enveloped Verifiable Credential',
|
|
83
|
+
type: 'object',
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
required: ['@context', 'id', 'type'],
|
|
86
|
+
properties: {
|
|
87
|
+
'@context': {
|
|
88
|
+
anyOf: [{
|
|
89
|
+
const: VC_CONTEXT_2
|
|
90
|
+
}, {
|
|
91
|
+
type: 'array',
|
|
92
|
+
minItems: 1,
|
|
93
|
+
maxItems: 1,
|
|
94
|
+
// the first context must be the VC context
|
|
95
|
+
items: [{
|
|
96
|
+
const: VC_CONTEXT_2
|
|
97
|
+
}]
|
|
98
|
+
}]
|
|
99
|
+
},
|
|
100
|
+
id: {
|
|
101
|
+
type: 'string'
|
|
102
|
+
},
|
|
103
|
+
type: {
|
|
104
|
+
const: 'EnvelopedVerifiableCredential'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export function verifiablePresentation() {
|
|
110
|
+
return {
|
|
111
|
+
title: 'Verifiable Presentation',
|
|
112
|
+
type: 'object',
|
|
113
|
+
required: ['@context', 'type'],
|
|
114
|
+
additionalProperties: true,
|
|
115
|
+
properties: {
|
|
116
|
+
'@context': vcContext,
|
|
117
|
+
id: {
|
|
118
|
+
type: 'string'
|
|
119
|
+
},
|
|
120
|
+
type: {
|
|
121
|
+
type: 'array',
|
|
122
|
+
minItems: 1,
|
|
123
|
+
// this first type must be VerifiablePresentation
|
|
124
|
+
items: [
|
|
125
|
+
{const: 'VerifiablePresentation'},
|
|
126
|
+
],
|
|
127
|
+
// additional types must be strings
|
|
128
|
+
additionalItems: {
|
|
129
|
+
type: 'string'
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
verifiableCredential: {
|
|
133
|
+
anyOf: [
|
|
134
|
+
verifiableCredential(),
|
|
135
|
+
envelopedVerifiableCredential, {
|
|
136
|
+
type: 'array',
|
|
137
|
+
minItems: 1,
|
|
138
|
+
items: {
|
|
139
|
+
anyOf: [verifiableCredential(), envelopedVerifiableCredential]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
holder: idOrObjectWithId(),
|
|
145
|
+
proof: schemas.proof()
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const envelopedVerifiablePresentation = {
|
|
151
|
+
title: 'Enveloped Verifiable Presentation',
|
|
152
|
+
type: 'object',
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
required: ['@context', 'id', 'type'],
|
|
155
|
+
properties: {
|
|
156
|
+
'@context': {
|
|
157
|
+
anyOf: [{
|
|
158
|
+
const: VC_CONTEXT_2
|
|
159
|
+
}, {
|
|
160
|
+
type: 'array',
|
|
161
|
+
minItems: 1,
|
|
162
|
+
maxItems: 1,
|
|
163
|
+
// the first context must be the VC context
|
|
164
|
+
items: [{
|
|
165
|
+
const: VC_CONTEXT_2
|
|
166
|
+
}]
|
|
167
|
+
}]
|
|
168
|
+
},
|
|
169
|
+
id: {
|
|
170
|
+
type: 'string'
|
|
171
|
+
},
|
|
172
|
+
type: {
|
|
173
|
+
const: 'EnvelopedVerifiablePresentation'
|
|
174
|
+
}
|
|
10
175
|
}
|
|
11
176
|
};
|
|
12
177
|
|
|
@@ -54,42 +219,42 @@ export const createChallengeBody = {
|
|
|
54
219
|
properties: {}
|
|
55
220
|
};
|
|
56
221
|
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
222
|
+
export function verifyCredentialBody() {
|
|
223
|
+
return {
|
|
224
|
+
title: 'Verify Credential Body',
|
|
225
|
+
type: 'object',
|
|
226
|
+
required: ['verifiableCredential'],
|
|
227
|
+
additionalProperties: false,
|
|
228
|
+
properties: {
|
|
229
|
+
options: {
|
|
230
|
+
type: 'object'
|
|
231
|
+
},
|
|
232
|
+
verifiableCredential: {
|
|
233
|
+
anyOf: [
|
|
234
|
+
verifiableCredential(),
|
|
235
|
+
envelopedVerifiableCredential
|
|
236
|
+
]
|
|
72
237
|
}
|
|
73
238
|
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
76
241
|
|
|
77
|
-
export
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
242
|
+
export function verifyPresentationBody() {
|
|
243
|
+
return {
|
|
244
|
+
title: 'Verify Presentation Body',
|
|
245
|
+
type: 'object',
|
|
246
|
+
required: ['verifiablePresentation'],
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
properties: {
|
|
249
|
+
options: {
|
|
250
|
+
type: 'object'
|
|
251
|
+
},
|
|
252
|
+
verifiablePresentation: {
|
|
253
|
+
anyOf: [
|
|
254
|
+
verifiablePresentation(),
|
|
255
|
+
envelopedVerifiablePresentation
|
|
256
|
+
]
|
|
92
257
|
}
|
|
93
258
|
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
259
|
+
};
|
|
260
|
+
}
|