@bedrock/vc-delivery 4.8.0 → 5.0.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/helpers.js +66 -1
- package/lib/openId.js +49 -18
- package/lib/vcapi.js +12 -5
- package/lib/vcjwt.js +375 -0
- package/lib/verify.js +46 -10
- package/package.json +23 -24
- package/schemas/bedrock-vc-workflow.js +142 -4
package/lib/helpers.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as vcjwt from './vcjwt.js';
|
|
5
6
|
import {decodeId, generateId} from 'bnid';
|
|
6
7
|
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
7
8
|
import {httpsAgent} from '@bedrock/https-agent';
|
|
@@ -9,7 +10,7 @@ import jsonata from 'jsonata';
|
|
|
9
10
|
import {serviceAgents} from '@bedrock/service-agent';
|
|
10
11
|
import {ZcapClient} from '@digitalbazaar/ezcap';
|
|
11
12
|
|
|
12
|
-
const {config} = bedrock;
|
|
13
|
+
const {config, util: {BedrockError}} = bedrock;
|
|
13
14
|
|
|
14
15
|
export async function evaluateTemplate({
|
|
15
16
|
workflow, exchange, typedTemplate
|
|
@@ -102,3 +103,67 @@ export function decodeLocalId({localId} = {}) {
|
|
|
102
103
|
expectedSize: 16
|
|
103
104
|
}));
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
export async function unenvelopeCredential({
|
|
108
|
+
envelopedCredential, format
|
|
109
|
+
} = {}) {
|
|
110
|
+
const result = _getEnvelope({envelope: envelopedCredential, format});
|
|
111
|
+
|
|
112
|
+
// only supported format is VC-JWT at this time
|
|
113
|
+
const credential = vcjwt.decodeVCJWTCredential({jwt: result.envelope});
|
|
114
|
+
return {credential, ...result};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function unenvelopePresentation({
|
|
118
|
+
envelopedPresentation, format
|
|
119
|
+
} = {}) {
|
|
120
|
+
const result = _getEnvelope({envelope: envelopedPresentation, format});
|
|
121
|
+
|
|
122
|
+
// only supported format is VC-JWT at this time
|
|
123
|
+
const presentation = vcjwt.decodeVCJWTPresentation({jwt: result.envelope});
|
|
124
|
+
|
|
125
|
+
// unenvelope any VCs in the presentation
|
|
126
|
+
let {verifiableCredential = []} = presentation;
|
|
127
|
+
if(!Array.isArray(verifiableCredential)) {
|
|
128
|
+
verifiableCredential = [verifiableCredential];
|
|
129
|
+
}
|
|
130
|
+
if(verifiableCredential.length > 0) {
|
|
131
|
+
presentation.verifiableCredential = await Promise.all(
|
|
132
|
+
verifiableCredential.map(async vc => {
|
|
133
|
+
if(vc?.type !== 'EnvelopedVerifiableCredential') {
|
|
134
|
+
return vc;
|
|
135
|
+
}
|
|
136
|
+
const {credential} = await unenvelopeCredential({
|
|
137
|
+
envelopedCredential: vc
|
|
138
|
+
});
|
|
139
|
+
return credential;
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
return {presentation, ...result};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _getEnvelope({envelope, format}) {
|
|
146
|
+
const isString = typeof envelope === 'string';
|
|
147
|
+
if(isString) {
|
|
148
|
+
// supported formats
|
|
149
|
+
if(format === 'application/jwt' || format === 'jwt_vc_json-ld') {
|
|
150
|
+
format = 'application/jwt';
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
const {id} = envelope;
|
|
154
|
+
if(id?.startsWith('data:application/jwt,')) {
|
|
155
|
+
format = 'application/jwt';
|
|
156
|
+
envelope = id.slice('data:application/jwt,'.length);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if(format === 'application/jwt' && envelope !== undefined) {
|
|
161
|
+
return {envelope, format};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new BedrockError(
|
|
165
|
+
`Unsupported credential or presentation envelope format "${format}".`, {
|
|
166
|
+
name: 'NotSupportedError',
|
|
167
|
+
details: {httpStatusCode: 400, public: true}
|
|
168
|
+
});
|
|
169
|
+
}
|
package/lib/openId.js
CHANGED
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as exchanges from './exchanges.js';
|
|
6
6
|
import {
|
|
7
|
-
compile,
|
|
7
|
+
compile, createValidateMiddleware as validate
|
|
8
8
|
} from '@bedrock/validation';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
evaluateTemplate, getWorkflowIssuerInstances, unenvelopePresentation
|
|
11
|
+
} from './helpers.js';
|
|
10
12
|
import {importJWK, SignJWT} from 'jose';
|
|
11
13
|
import {
|
|
12
14
|
openIdAuthorizationResponseBody,
|
|
13
15
|
openIdBatchCredentialBody,
|
|
14
16
|
openIdCredentialBody,
|
|
15
17
|
openIdTokenBody,
|
|
16
|
-
presentationSubmission as presentationSubmissionSchema
|
|
18
|
+
presentationSubmission as presentationSubmissionSchema,
|
|
19
|
+
verifiablePresentation as verifiablePresentationSchema
|
|
17
20
|
} from '../schemas/bedrock-vc-workflow.js';
|
|
18
21
|
import {verify, verifyDidProofJwt} from './verify.js';
|
|
19
22
|
import {asyncHandler} from '@bedrock/express';
|
|
@@ -56,6 +59,8 @@ instantiating a new authorization server instance per VC exchange. */
|
|
|
56
59
|
const PRE_AUTH_GRANT_TYPE =
|
|
57
60
|
'urn:ietf:params:oauth:grant-type:pre-authorized_code';
|
|
58
61
|
|
|
62
|
+
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
63
|
+
|
|
59
64
|
// creates OID4VCI Authorization Server + Credential Delivery Server
|
|
60
65
|
// endpoints for each individual exchange
|
|
61
66
|
export async function createRoutes({
|
|
@@ -84,7 +89,7 @@ export async function createRoutes({
|
|
|
84
89
|
|
|
85
90
|
// create validators for x-www-form-urlencoded parsed data
|
|
86
91
|
const validatePresentation = compile(
|
|
87
|
-
{schema:
|
|
92
|
+
{schema: verifiablePresentationSchema()});
|
|
88
93
|
const validatePresentationSubmission = compile(
|
|
89
94
|
{schema: presentationSubmissionSchema});
|
|
90
95
|
|
|
@@ -378,14 +383,33 @@ export async function createRoutes({
|
|
|
378
383
|
const {vp_token, presentation_submission} = req.body;
|
|
379
384
|
|
|
380
385
|
// JSON parse and validate `vp_token` and `presentation_submission`
|
|
381
|
-
|
|
386
|
+
let presentation = _jsonParse(vp_token, 'vp_token');
|
|
382
387
|
const presentationSubmission = _jsonParse(
|
|
383
388
|
presentation_submission, 'presentation_submission');
|
|
384
389
|
_validate(validatePresentationSubmission, presentationSubmission);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
{
|
|
390
|
+
let envelope;
|
|
391
|
+
if(typeof presentation === 'string') {
|
|
392
|
+
// handle enveloped presentation
|
|
393
|
+
const {
|
|
394
|
+
envelope: raw, presentation: contents, format
|
|
395
|
+
} = await unenvelopePresentation({
|
|
396
|
+
envelopedPresentation: presentation,
|
|
397
|
+
// FIXME: check presentationSubmission for VP format
|
|
398
|
+
format: 'jwt_vc_json-ld'
|
|
399
|
+
});
|
|
400
|
+
_validate(validatePresentation, contents);
|
|
401
|
+
presentation = {
|
|
402
|
+
'@context': VC_CONTEXT_2,
|
|
403
|
+
id: `data:${format},${raw}`,
|
|
404
|
+
type: 'EnvelopedVerifiablePresentation'
|
|
405
|
+
};
|
|
406
|
+
envelope = {raw, contents, format};
|
|
407
|
+
} else {
|
|
408
|
+
_validate(validatePresentation, presentation);
|
|
409
|
+
}
|
|
410
|
+
const result = await _processAuthorizationResponse({
|
|
411
|
+
req, presentation, envelope, presentationSubmission
|
|
412
|
+
});
|
|
389
413
|
res.json(result);
|
|
390
414
|
}));
|
|
391
415
|
|
|
@@ -905,7 +929,7 @@ function _matchCredentialRequest(expected, cr) {
|
|
|
905
929
|
}
|
|
906
930
|
|
|
907
931
|
async function _processAuthorizationResponse({
|
|
908
|
-
req, presentation, presentationSubmission
|
|
932
|
+
req, presentation, envelope, presentationSubmission
|
|
909
933
|
}) {
|
|
910
934
|
const {config: workflow} = req.serviceObject;
|
|
911
935
|
const exchangeRecord = await req.getExchange();
|
|
@@ -916,18 +940,17 @@ async function _processAuthorizationResponse({
|
|
|
916
940
|
const {authorizationRequest, step} = arRequest;
|
|
917
941
|
({exchange} = arRequest);
|
|
918
942
|
|
|
919
|
-
// FIXME: if the VP is enveloped, remove the envelope to validate or
|
|
920
|
-
// run validation code after verification if necessary
|
|
921
|
-
|
|
922
943
|
// FIXME: check the VP against the presentation submission if requested
|
|
923
944
|
// FIXME: check the VP against "trustedIssuer" in VPR, if provided
|
|
924
945
|
const {presentationSchema} = step;
|
|
925
946
|
if(presentationSchema) {
|
|
926
|
-
// validate the
|
|
927
|
-
|
|
947
|
+
// if the VP is enveloped, validate the contents of the envelope
|
|
948
|
+
const toValidate = envelope ? envelope.contents : presentation;
|
|
949
|
+
|
|
950
|
+
// validate the received VP / envelope contents
|
|
928
951
|
const {jsonSchema: schema} = presentationSchema;
|
|
929
952
|
const validate = compile({schema});
|
|
930
|
-
const {valid, error} = validate(
|
|
953
|
+
const {valid, error} = validate(toValidate);
|
|
931
954
|
if(!valid) {
|
|
932
955
|
throw error;
|
|
933
956
|
}
|
|
@@ -937,20 +960,21 @@ async function _processAuthorizationResponse({
|
|
|
937
960
|
const {verifiablePresentationRequest} = await oid4vp.toVpr(
|
|
938
961
|
{authorizationRequest});
|
|
939
962
|
const {allowUnprotectedPresentation = false} = step;
|
|
940
|
-
const
|
|
963
|
+
const verifyResult = await verify({
|
|
941
964
|
workflow,
|
|
942
965
|
verifiablePresentationRequest,
|
|
943
966
|
presentation,
|
|
944
967
|
allowUnprotectedPresentation,
|
|
945
968
|
expectedChallenge: authorizationRequest.nonce
|
|
946
969
|
});
|
|
970
|
+
const {verificationMethod} = verifyResult;
|
|
947
971
|
|
|
948
972
|
// store VP results in variables associated with current step
|
|
949
973
|
const currentStep = exchange.step;
|
|
950
974
|
if(!exchange.variables.results) {
|
|
951
975
|
exchange.variables.results = {};
|
|
952
976
|
}
|
|
953
|
-
|
|
977
|
+
const results = {
|
|
954
978
|
// common use case of DID Authentication; provide `did` for ease
|
|
955
979
|
// of use in template
|
|
956
980
|
did: verificationMethod?.controller || null,
|
|
@@ -961,6 +985,13 @@ async function _processAuthorizationResponse({
|
|
|
961
985
|
presentationSubmission
|
|
962
986
|
}
|
|
963
987
|
};
|
|
988
|
+
if(envelope) {
|
|
989
|
+
// normalize VP from inside envelope to `verifiablePresentation`
|
|
990
|
+
results.envelopedPresentation = presentation;
|
|
991
|
+
results.verifiablePresentation = verifyResult
|
|
992
|
+
.presentationResult.presentation;
|
|
993
|
+
}
|
|
994
|
+
exchange.variables.results[currentStep] = results;
|
|
964
995
|
exchange.sequence++;
|
|
965
996
|
|
|
966
997
|
// if there is something to issue, update exchange, do not complete it
|
package/lib/vcapi.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as exchanges from './exchanges.js';
|
|
6
6
|
import {createChallenge as _createChallenge, verify} from './verify.js';
|
|
7
|
+
import {evaluateTemplate, unenvelopePresentation} from './helpers.js';
|
|
7
8
|
import {compile} from '@bedrock/validation';
|
|
8
|
-
import {evaluateTemplate} from './helpers.js';
|
|
9
9
|
import {issue} from './issue.js';
|
|
10
10
|
import {klona} from 'klona';
|
|
11
11
|
import {logger} from './logger.js';
|
|
@@ -96,15 +96,22 @@ export async function processExchange({req, res, workflow, exchange}) {
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// FIXME: if the VP is enveloped, remove the envelope to validate or
|
|
100
|
-
// run validation code after verification if necessary
|
|
101
|
-
|
|
102
99
|
const {presentationSchema} = step;
|
|
103
100
|
if(presentationSchema) {
|
|
101
|
+
// if the VP is enveloped, get the presentation from the envelope
|
|
102
|
+
let presentation;
|
|
103
|
+
if(receivedPresentation?.type === 'EnvelopedVerifiablePresentation') {
|
|
104
|
+
({presentation} = await unenvelopePresentation({
|
|
105
|
+
envelopedPresentation: receivedPresentation
|
|
106
|
+
}));
|
|
107
|
+
} else {
|
|
108
|
+
presentation = receivedPresentation;
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
// validate the received VP
|
|
105
112
|
const {jsonSchema: schema} = presentationSchema;
|
|
106
113
|
const validate = compile({schema});
|
|
107
|
-
const {valid, error} = validate(
|
|
114
|
+
const {valid, error} = validate(presentation);
|
|
108
115
|
if(!valid) {
|
|
109
116
|
throw error;
|
|
110
117
|
}
|
package/lib/vcjwt.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import {decodeJwt} from 'jose';
|
|
6
|
+
|
|
7
|
+
const {util: {BedrockError}} = bedrock;
|
|
8
|
+
|
|
9
|
+
const VC_CONTEXT_1 = 'https://www.w3.org/2018/credentials/v1';
|
|
10
|
+
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
11
|
+
|
|
12
|
+
export function decodeVCJWTCredential({jwt} = {}) {
|
|
13
|
+
const payload = decodeJwt(jwt);
|
|
14
|
+
|
|
15
|
+
/* Example:
|
|
16
|
+
{
|
|
17
|
+
"alg": <signer.algorithm>,
|
|
18
|
+
"kid": <signer.id>
|
|
19
|
+
}.
|
|
20
|
+
{
|
|
21
|
+
"iss": <verifiableCredential.issuer>,
|
|
22
|
+
"jti": <verifiableCredential.id>
|
|
23
|
+
"sub": <verifiableCredential.credentialSubject>
|
|
24
|
+
"nbf": <verifiableCredential.[issuanceDate | validFrom]>
|
|
25
|
+
"exp": <verifiableCredential.[expirationDate | validUntil]>
|
|
26
|
+
"vc": <verifiableCredential>
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
const {vc} = payload;
|
|
30
|
+
if(!(vc && typeof vc === 'object')) {
|
|
31
|
+
throw new BedrockError('JWT validation failed.', {
|
|
32
|
+
name: 'DataError',
|
|
33
|
+
details: {
|
|
34
|
+
httpStatusCode: 400,
|
|
35
|
+
public: true,
|
|
36
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
37
|
+
reason: 'missing or unexpected "vc" claim value.',
|
|
38
|
+
claim: 'vc'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let {'@context': context = []} = vc;
|
|
44
|
+
if(!Array.isArray(context)) {
|
|
45
|
+
context = [context];
|
|
46
|
+
}
|
|
47
|
+
const isVersion1 = context.includes(VC_CONTEXT_1);
|
|
48
|
+
const isVersion2 = context.includes(VC_CONTEXT_2);
|
|
49
|
+
if(!(isVersion1 ^ isVersion2)) {
|
|
50
|
+
throw new BedrockError(
|
|
51
|
+
'Verifiable credential is neither version "1.x" nor "2.x".', {
|
|
52
|
+
name: 'DataError',
|
|
53
|
+
details: {
|
|
54
|
+
httpStatusCode: 400,
|
|
55
|
+
public: true
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const credential = {...vc};
|
|
61
|
+
const {iss, jti, sub, nbf, exp} = payload;
|
|
62
|
+
|
|
63
|
+
// inject `issuer` value
|
|
64
|
+
if(vc.issuer === undefined) {
|
|
65
|
+
vc.issuer = iss;
|
|
66
|
+
} else if(vc.issuer && typeof vc.issuer === 'object' &&
|
|
67
|
+
vc.issuer.id === undefined) {
|
|
68
|
+
vc.issuer = {id: iss, ...vc.issuer};
|
|
69
|
+
} else if(iss !== vc.issuer && iss !== vc.issuer?.id) {
|
|
70
|
+
throw new BedrockError(
|
|
71
|
+
'VC-JWT "iss" claim does not equal nor does it exclusively ' +
|
|
72
|
+
'provide verifiable credential "issuer" / "issuer.id".', {
|
|
73
|
+
name: 'DataError',
|
|
74
|
+
details: {
|
|
75
|
+
httpStatusCode: 400,
|
|
76
|
+
public: true
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if(jti !== undefined && jti !== vc.id) {
|
|
82
|
+
// inject `id` value
|
|
83
|
+
if(vc.id === undefined) {
|
|
84
|
+
vc.id = jti;
|
|
85
|
+
} else {
|
|
86
|
+
throw new BedrockError(
|
|
87
|
+
'VC-JWT "jti" claim does not equal nor does it exclusively ' +
|
|
88
|
+
'provide verifiable credential "id".', {
|
|
89
|
+
name: 'DataError',
|
|
90
|
+
details: {
|
|
91
|
+
httpStatusCode: 400,
|
|
92
|
+
public: true
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if(sub !== undefined && sub !== vc.credentialSubject?.id) {
|
|
99
|
+
// inject `credentialSubject.id` value
|
|
100
|
+
if(!vc.credentialSubject) {
|
|
101
|
+
throw new BedrockError(
|
|
102
|
+
'Verifiable credential has no "credentialSubject".', {
|
|
103
|
+
name: 'DataError',
|
|
104
|
+
details: {
|
|
105
|
+
httpStatusCode: 400,
|
|
106
|
+
public: true
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if(Array.isArray(vc.credentialSubject)) {
|
|
111
|
+
throw new BedrockError(
|
|
112
|
+
'Verifiable credential has multiple credential subjects, which is ' +
|
|
113
|
+
'not supported in VC-JWT.', {
|
|
114
|
+
name: 'DataError',
|
|
115
|
+
details: {
|
|
116
|
+
httpStatusCode: 400,
|
|
117
|
+
public: true
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if(vc.credentialSubject?.id === undefined) {
|
|
122
|
+
vc.credentialSubject = {id: sub, ...vc.credentialSubject};
|
|
123
|
+
} else {
|
|
124
|
+
throw new BedrockError(
|
|
125
|
+
'VC-JWT "sub" claim does not equal nor does it exclusively ' +
|
|
126
|
+
'provide verifiable credential "credentialSubject.id".', {
|
|
127
|
+
name: 'DataError',
|
|
128
|
+
details: {
|
|
129
|
+
httpStatusCode: 400,
|
|
130
|
+
public: true
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if(nbf === undefined && isVersion1) {
|
|
137
|
+
throw new BedrockError('JWT validation failed.', {
|
|
138
|
+
name: 'DataError',
|
|
139
|
+
details: {
|
|
140
|
+
httpStatusCode: 400,
|
|
141
|
+
public: true,
|
|
142
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
143
|
+
reason: 'missing "nbf" claim value.',
|
|
144
|
+
claim: 'nbf'
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if(nbf !== undefined) {
|
|
150
|
+
// fuzzy convert `nbf` into `issuanceDate` / `validFrom`, only require
|
|
151
|
+
// second-level precision
|
|
152
|
+
const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
|
|
153
|
+
const dateProperty = isVersion1 ? 'issuanceDate' : 'validFrom';
|
|
154
|
+
// inject dateProperty value
|
|
155
|
+
if(vc[dateProperty] === undefined) {
|
|
156
|
+
vc[dateProperty] = dateString + 'Z';
|
|
157
|
+
} else if(!(vc[dateProperty].startsWith(dateString) &&
|
|
158
|
+
vc[dateProperty].endsWith('Z'))) {
|
|
159
|
+
throw new BedrockError(
|
|
160
|
+
'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
|
|
161
|
+
`verifiable credential "${dateProperty}".`, {
|
|
162
|
+
name: 'DataError',
|
|
163
|
+
details: {
|
|
164
|
+
httpStatusCode: 400,
|
|
165
|
+
public: true
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if(exp !== undefined) {
|
|
172
|
+
// fuzzy convert `exp` into `expirationDate` / `validUntil`, only require
|
|
173
|
+
// second-level precision
|
|
174
|
+
const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
|
|
175
|
+
const dateProperty = isVersion1 ? 'expirationDate' : 'validUntil';
|
|
176
|
+
// inject dateProperty value
|
|
177
|
+
if(vc[dateProperty] === undefined) {
|
|
178
|
+
vc[dateProperty] = dateString + 'Z';
|
|
179
|
+
} else if(!(vc[dateProperty].startsWith(dateString) &&
|
|
180
|
+
vc[dateProperty].endsWith('Z'))) {
|
|
181
|
+
throw new BedrockError(
|
|
182
|
+
'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
|
|
183
|
+
`verifiable credential "${dateProperty}".`, {
|
|
184
|
+
name: 'DataError',
|
|
185
|
+
details: {
|
|
186
|
+
httpStatusCode: 400,
|
|
187
|
+
public: true
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return credential;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function decodeVCJWTPresentation({jwt, challenge} = {}) {
|
|
197
|
+
/* Example:
|
|
198
|
+
{
|
|
199
|
+
"alg": <signer.algorithm>,
|
|
200
|
+
"kid": <signer.id>
|
|
201
|
+
}.
|
|
202
|
+
{
|
|
203
|
+
"iss": <verifiablePresentation.holder>,
|
|
204
|
+
"aud": <verifiablePresentation.domain>,
|
|
205
|
+
"nonce": <verifiablePresentation.nonce>,
|
|
206
|
+
"jti": <verifiablePresentation.id>
|
|
207
|
+
"nbf": <verifiablePresentation.[validFrom]>
|
|
208
|
+
"exp": <verifiablePresentation.[validUntil]>
|
|
209
|
+
"vp": <verifiablePresentation>
|
|
210
|
+
}
|
|
211
|
+
*/
|
|
212
|
+
const payload = decodeJwt(jwt);
|
|
213
|
+
|
|
214
|
+
const {vp} = payload;
|
|
215
|
+
if(!(vp && typeof vp === 'object')) {
|
|
216
|
+
throw new BedrockError('JWT validation failed.', {
|
|
217
|
+
name: 'DataError',
|
|
218
|
+
details: {
|
|
219
|
+
httpStatusCode: 400,
|
|
220
|
+
public: true,
|
|
221
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
222
|
+
reason: 'missing or unexpected "vp" claim value.',
|
|
223
|
+
claim: 'vp'
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let {'@context': context = []} = vp;
|
|
229
|
+
if(!Array.isArray(context)) {
|
|
230
|
+
context = [context];
|
|
231
|
+
}
|
|
232
|
+
const isVersion1 = context.includes(VC_CONTEXT_1);
|
|
233
|
+
const isVersion2 = context.includes(VC_CONTEXT_2);
|
|
234
|
+
if(!(isVersion1 ^ isVersion2)) {
|
|
235
|
+
throw new BedrockError(
|
|
236
|
+
'Verifiable presentation is not either version "1.x" or "2.x".', {
|
|
237
|
+
name: 'DataError',
|
|
238
|
+
details: {
|
|
239
|
+
httpStatusCode: 400,
|
|
240
|
+
public: true
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const presentation = {...vp};
|
|
246
|
+
const {iss, nonce, jti, nbf, exp} = payload;
|
|
247
|
+
|
|
248
|
+
// inject `holder` value
|
|
249
|
+
if(vp.holder === undefined) {
|
|
250
|
+
vp.holder = iss;
|
|
251
|
+
} else if(vp.holder && typeof vp.holder === 'object' &&
|
|
252
|
+
vp.holder.id === undefined) {
|
|
253
|
+
vp.holder = {id: iss, ...vp.holder};
|
|
254
|
+
} else if(iss !== vp.holder && iss !== vp.holder?.id) {
|
|
255
|
+
throw new BedrockError(
|
|
256
|
+
'VC-JWT "iss" claim does not equal nor does it exclusively ' +
|
|
257
|
+
'provide verifiable presentation "holder" / "holder.id".', {
|
|
258
|
+
name: 'DataError',
|
|
259
|
+
details: {
|
|
260
|
+
httpStatusCode: 400,
|
|
261
|
+
public: true
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if(jti !== undefined && jti !== vp.id) {
|
|
267
|
+
// inject `id` value
|
|
268
|
+
if(vp.id === undefined) {
|
|
269
|
+
vp.id = jti;
|
|
270
|
+
} else {
|
|
271
|
+
throw new BedrockError(
|
|
272
|
+
'VC-JWT "jti" claim does not equal nor does it exclusively ' +
|
|
273
|
+
'provide verifiable presentation "id".', {
|
|
274
|
+
name: 'DataError',
|
|
275
|
+
details: {
|
|
276
|
+
httpStatusCode: 400,
|
|
277
|
+
public: true
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// version 1.x VPs do not support `validFrom`/`validUntil`
|
|
284
|
+
if(nbf !== undefined && isVersion2) {
|
|
285
|
+
// fuzzy convert `nbf` into `validFrom`, only require
|
|
286
|
+
// second-level precision
|
|
287
|
+
const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
|
|
288
|
+
|
|
289
|
+
// inject `validFrom` value
|
|
290
|
+
if(vp.validFrom === undefined) {
|
|
291
|
+
vp.validFrom = dateString + 'Z';
|
|
292
|
+
} else if(!(vp.validFrom?.startsWith(dateString) &&
|
|
293
|
+
vp.validFrom.endsWith('Z'))) {
|
|
294
|
+
throw new BedrockError(
|
|
295
|
+
'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
|
|
296
|
+
'verifiable presentation "validFrom".', {
|
|
297
|
+
name: 'DataError',
|
|
298
|
+
details: {
|
|
299
|
+
httpStatusCode: 400,
|
|
300
|
+
public: true
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if(exp !== undefined && isVersion2) {
|
|
306
|
+
// fuzzy convert `exp` into `validUntil`, only require
|
|
307
|
+
// second-level precision
|
|
308
|
+
const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
|
|
309
|
+
|
|
310
|
+
// inject `validUntil` value
|
|
311
|
+
if(vp.validUntil === undefined) {
|
|
312
|
+
vp.validUntil = dateString + 'Z';
|
|
313
|
+
} else if(!(vp.validUntil?.startsWith(dateString) &&
|
|
314
|
+
vp.validUntil?.endsWith('Z'))) {
|
|
315
|
+
throw new BedrockError(
|
|
316
|
+
'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
|
|
317
|
+
'verifiable presentation "validUntil".', {
|
|
318
|
+
name: 'DataError',
|
|
319
|
+
details: {
|
|
320
|
+
httpStatusCode: 400,
|
|
321
|
+
public: true
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if(challenge !== undefined && nonce !== challenge) {
|
|
328
|
+
throw new BedrockError('JWT validation failed.', {
|
|
329
|
+
name: 'DataError',
|
|
330
|
+
details: {
|
|
331
|
+
httpStatusCode: 400,
|
|
332
|
+
public: true,
|
|
333
|
+
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
|
|
334
|
+
reason: 'missing or unexpected "nonce" claim value.',
|
|
335
|
+
claim: 'nonce'
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// do some validation on `verifiableCredential`
|
|
341
|
+
let {verifiableCredential = []} = presentation;
|
|
342
|
+
if(!Array.isArray(verifiableCredential)) {
|
|
343
|
+
verifiableCredential = [verifiableCredential];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ensure version 2 VPs only have objects in `verifiableCredential`
|
|
347
|
+
const hasVCJWTs = verifiableCredential.some(vc => typeof vc !== 'object');
|
|
348
|
+
if(isVersion2 && hasVCJWTs) {
|
|
349
|
+
throw new BedrockError(
|
|
350
|
+
'Version 2.x verifiable presentations must only use objects in the ' +
|
|
351
|
+
'"verifiableCredential" field.', {
|
|
352
|
+
name: 'DataError',
|
|
353
|
+
details: {
|
|
354
|
+
httpStatusCode: 400,
|
|
355
|
+
public: true
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// transform any VC-JWT VCs to enveloped VCs
|
|
361
|
+
if(presentation.verifiableCredential && hasVCJWTs) {
|
|
362
|
+
presentation.verifiableCredential = verifiableCredential.map(vc => {
|
|
363
|
+
if(typeof vc !== 'string') {
|
|
364
|
+
return vc;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
'@context': VC_CONTEXT_2,
|
|
368
|
+
id: `data:application/jwt,${vc}`,
|
|
369
|
+
type: 'EnvelopedVerifiableCredential',
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return presentation;
|
|
375
|
+
}
|
package/lib/verify.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
5
6
|
import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey';
|
|
6
7
|
import {importJWK, jwtVerify} from 'jose';
|
|
7
8
|
import {didIo} from '@bedrock/did-io';
|
|
@@ -9,6 +10,10 @@ import {getZcapClient} from './helpers.js';
|
|
|
9
10
|
|
|
10
11
|
const {util: {BedrockError}} = bedrock;
|
|
11
12
|
|
|
13
|
+
// supported JWT algs
|
|
14
|
+
const ECDSA_ALGS = ['ES256', 'ES384'];
|
|
15
|
+
const EDDSA_ALGS = ['Ed25519', 'EdDSA'];
|
|
16
|
+
|
|
12
17
|
export async function createChallenge({workflow} = {}) {
|
|
13
18
|
// create zcap client for creating challenges
|
|
14
19
|
const {zcapClient, zcaps} = await getZcapClient({workflow});
|
|
@@ -82,9 +87,13 @@ export async function verify({
|
|
|
82
87
|
|
|
83
88
|
// generate useful error to return to client
|
|
84
89
|
const {name, errors, message} = cause.data.error;
|
|
90
|
+
const causeError = _stripStacktrace({...cause.data.error});
|
|
91
|
+
delete causeError.errors;
|
|
85
92
|
const error = new BedrockError(message ?? 'Verification error.', {
|
|
86
|
-
name: name === 'VerificationError'
|
|
93
|
+
name: (name === 'VerificationError' || name === 'DataError') ?
|
|
94
|
+
'DataError' : 'OperationError',
|
|
87
95
|
details: {
|
|
96
|
+
error: causeError,
|
|
88
97
|
verified,
|
|
89
98
|
credentialResults,
|
|
90
99
|
presentationResult,
|
|
@@ -134,23 +143,50 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
|
|
|
134
143
|
const audience = exchangeId;
|
|
135
144
|
|
|
136
145
|
let issuer;
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
// `resolveKey` is passed `protectedHeader`
|
|
147
|
+
const resolveKey = async ({alg, kid}) => {
|
|
148
|
+
const isEcdsa = ECDSA_ALGS.includes(alg);
|
|
149
|
+
const isEddsa = !isEcdsa && EDDSA_ALGS.includes(alg);
|
|
150
|
+
if(!(isEcdsa || isEddsa)) {
|
|
151
|
+
throw new BedrockError(
|
|
152
|
+
`Unsupported JWT "alg": "${alg}".`, {
|
|
153
|
+
name: 'DataError',
|
|
154
|
+
details: {
|
|
155
|
+
httpStatusCode: 400,
|
|
156
|
+
public: true
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const vm = await didIo.get({url: kid});
|
|
139
162
|
// `vm.controller` must be the issuer of the DID JWT; also ensure that
|
|
140
163
|
// the specified controller authorized `vm` for the purpose of
|
|
141
164
|
// authentication
|
|
142
165
|
issuer = vm.controller;
|
|
143
166
|
const didDoc = await didIo.get({url: issuer});
|
|
144
|
-
|
|
167
|
+
let match = didDoc?.authentication?.find?.(
|
|
168
|
+
e => e === vm.id || e.id === vm.id);
|
|
169
|
+
if(typeof match === 'string') {
|
|
170
|
+
match = didDoc?.verificationMethod?.find?.(e => e.id === vm.id);
|
|
171
|
+
}
|
|
172
|
+
if(!(match && Array.isArray(match.controller) ?
|
|
173
|
+
match.controller.includes(vm.controller) :
|
|
174
|
+
match.controller === vm.controller)) {
|
|
145
175
|
throw new BedrockError(
|
|
146
176
|
`Verification method controller "${issuer}" did not authorize ` +
|
|
147
177
|
`verification method "${vm.id}" for the purpose of "authentication".`,
|
|
148
178
|
{name: 'NotAllowedError'});
|
|
149
179
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
180
|
+
let jwk;
|
|
181
|
+
if(isEcdsa) {
|
|
182
|
+
const keyPair = await EcdsaMultikey.from(vm);
|
|
183
|
+
jwk = await EcdsaMultikey.toJwk({keyPair});
|
|
184
|
+
jwk.alg = alg;
|
|
185
|
+
} else {
|
|
186
|
+
const keyPair = await Ed25519Multikey.from(vm);
|
|
187
|
+
jwk = await Ed25519Multikey.toJwk({keyPair});
|
|
188
|
+
jwk.alg = 'EdDSA';
|
|
189
|
+
}
|
|
154
190
|
return importJWK(jwk);
|
|
155
191
|
};
|
|
156
192
|
|
|
@@ -186,7 +222,7 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
|
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
// check `iss` claim
|
|
189
|
-
if(!(verifyResult?.payload?.iss === issuer)) {
|
|
225
|
+
if(!(issuer && verifyResult?.payload?.iss === issuer)) {
|
|
190
226
|
throw new BedrockError('DID proof JWT validation failed.', {
|
|
191
227
|
name: 'NotAllowedError',
|
|
192
228
|
details: {
|
|
@@ -200,7 +236,7 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
|
|
|
200
236
|
}
|
|
201
237
|
|
|
202
238
|
// check `nonce` claim
|
|
203
|
-
if(
|
|
239
|
+
if(verifyResult?.payload?.nonce !== exchange.id) {
|
|
204
240
|
throw new BedrockError('DID proof JWT validation failed.', {
|
|
205
241
|
name: 'NotAllowedError',
|
|
206
242
|
details: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock Verifiable Credential Delivery",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"schemas/**/*.js"
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
|
-
"lint": "eslint ."
|
|
12
|
+
"lint": "eslint --ext .cjs,.js ."
|
|
13
13
|
},
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
@@ -35,41 +35,40 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/digitalbazaar/bedrock-vc-delivery",
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@digitalbazaar/ecdsa-multikey": "^1.7.0",
|
|
38
39
|
"@digitalbazaar/ed25519-multikey": "^1.1.0",
|
|
39
|
-
"@digitalbazaar/ed25519-signature-2020": "^5.
|
|
40
|
-
"@digitalbazaar/ezcap": "^4.
|
|
41
|
-
"@digitalbazaar/oid4-client": "^3.1
|
|
42
|
-
"@digitalbazaar/vc": "^
|
|
40
|
+
"@digitalbazaar/ed25519-signature-2020": "^5.4.0",
|
|
41
|
+
"@digitalbazaar/ezcap": "^4.1.0",
|
|
42
|
+
"@digitalbazaar/oid4-client": "^3.4.1",
|
|
43
|
+
"@digitalbazaar/vc": "^7.0.0",
|
|
43
44
|
"assert-plus": "^1.0.0",
|
|
44
45
|
"bnid": "^3.0.0",
|
|
45
|
-
"body-parser": "^1.20.
|
|
46
|
+
"body-parser": "^1.20.2",
|
|
46
47
|
"cors": "^2.8.5",
|
|
47
|
-
"jose": "^
|
|
48
|
-
"jsonata": "^2.0.
|
|
49
|
-
"klona": "^2.0.
|
|
48
|
+
"jose": "^5.6.3",
|
|
49
|
+
"jsonata": "^2.0.5",
|
|
50
|
+
"klona": "^2.0.6"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
53
|
"@bedrock/app-identity": "4.0.0",
|
|
53
|
-
"@bedrock/core": "^6.
|
|
54
|
-
"@bedrock/did-io": "^10.
|
|
55
|
-
"@bedrock/express": "^8.
|
|
56
|
-
"@bedrock/https-agent": "^4.
|
|
57
|
-
"@bedrock/mongodb": "^10.
|
|
58
|
-
"@bedrock/oauth2-verifier": "^2.
|
|
59
|
-
"@bedrock/service-agent": "^
|
|
60
|
-
"@bedrock/service-core": "^
|
|
54
|
+
"@bedrock/core": "^6.1.3",
|
|
55
|
+
"@bedrock/did-io": "^10.3.1",
|
|
56
|
+
"@bedrock/express": "^8.3.1",
|
|
57
|
+
"@bedrock/https-agent": "^4.1.0",
|
|
58
|
+
"@bedrock/mongodb": "^10.2.0",
|
|
59
|
+
"@bedrock/oauth2-verifier": "^2.1.0",
|
|
60
|
+
"@bedrock/service-agent": "^9.0.2",
|
|
61
|
+
"@bedrock/service-core": "^10.0.0",
|
|
61
62
|
"@bedrock/validation": "^7.1.0"
|
|
62
63
|
},
|
|
63
64
|
"directories": {
|
|
64
65
|
"lib": "./lib"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
|
-
"eslint": "^8.
|
|
68
|
-
"eslint-config-digitalbazaar": "^5.0
|
|
69
|
-
"eslint-plugin-jsdoc": "^
|
|
70
|
-
"eslint-plugin-unicorn": "^
|
|
71
|
-
"jsdoc": "^4.0.2",
|
|
72
|
-
"jsdoc-to-markdown": "^8.0.0"
|
|
68
|
+
"eslint": "^8.57.0",
|
|
69
|
+
"eslint-config-digitalbazaar": "^5.2.0",
|
|
70
|
+
"eslint-plugin-jsdoc": "^48.11.0",
|
|
71
|
+
"eslint-plugin-unicorn": "^55.0.0"
|
|
73
72
|
},
|
|
74
73
|
"engines": {
|
|
75
74
|
"node": ">=18"
|
|
@@ -4,6 +4,144 @@
|
|
|
4
4
|
import {MAX_ISSUER_INSTANCES} from '../lib/constants.js';
|
|
5
5
|
import {schemas} from '@bedrock/validation';
|
|
6
6
|
|
|
7
|
+
const VC_CONTEXT_1 = 'https://www.w3.org/2018/credentials/v1';
|
|
8
|
+
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
9
|
+
|
|
10
|
+
const vcContext = {
|
|
11
|
+
type: 'array',
|
|
12
|
+
minItems: 1,
|
|
13
|
+
// the first context must be the VC context
|
|
14
|
+
items: [{
|
|
15
|
+
oneOf: [{
|
|
16
|
+
const: VC_CONTEXT_1
|
|
17
|
+
}, {
|
|
18
|
+
const: VC_CONTEXT_2
|
|
19
|
+
}]
|
|
20
|
+
}],
|
|
21
|
+
// additional contexts maybe strings or objects
|
|
22
|
+
additionalItems: {
|
|
23
|
+
anyOf: [{type: 'string'}, {type: 'object'}]
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function idOrObjectWithId() {
|
|
28
|
+
return {
|
|
29
|
+
title: 'identifier or an object with an id',
|
|
30
|
+
anyOf: [
|
|
31
|
+
schemas.identifier(),
|
|
32
|
+
{
|
|
33
|
+
type: 'object',
|
|
34
|
+
required: ['id'],
|
|
35
|
+
additionalProperties: true,
|
|
36
|
+
properties: {id: schemas.identifier()}
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function verifiableCredential() {
|
|
43
|
+
return {
|
|
44
|
+
title: 'Verifiable Credential',
|
|
45
|
+
type: 'object',
|
|
46
|
+
required: [
|
|
47
|
+
'@context',
|
|
48
|
+
'credentialSubject',
|
|
49
|
+
'issuer',
|
|
50
|
+
'type'
|
|
51
|
+
],
|
|
52
|
+
additionalProperties: true,
|
|
53
|
+
properties: {
|
|
54
|
+
'@context': vcContext,
|
|
55
|
+
credentialSubject: {
|
|
56
|
+
anyOf: [
|
|
57
|
+
{type: 'object'},
|
|
58
|
+
{type: 'array', minItems: 1, items: {type: 'object'}}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
id: {
|
|
62
|
+
type: 'string'
|
|
63
|
+
},
|
|
64
|
+
issuer: idOrObjectWithId(),
|
|
65
|
+
type: {
|
|
66
|
+
type: 'array',
|
|
67
|
+
minItems: 1,
|
|
68
|
+
// this first type must be VerifiableCredential
|
|
69
|
+
items: [
|
|
70
|
+
{const: 'VerifiableCredential'},
|
|
71
|
+
],
|
|
72
|
+
// additional types must be strings
|
|
73
|
+
additionalItems: {
|
|
74
|
+
type: 'string'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
proof: schemas.proof()
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const envelopedVerifiableCredential = {
|
|
83
|
+
title: 'Enveloped Verifiable Credential',
|
|
84
|
+
type: 'object',
|
|
85
|
+
additionalProperties: true,
|
|
86
|
+
properties: {
|
|
87
|
+
'@context': {
|
|
88
|
+
const: VC_CONTEXT_2
|
|
89
|
+
},
|
|
90
|
+
id: {
|
|
91
|
+
type: 'string'
|
|
92
|
+
},
|
|
93
|
+
type: {
|
|
94
|
+
const: 'EnvelopedVerifiableCredential'
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
required: [
|
|
98
|
+
'@context',
|
|
99
|
+
'id',
|
|
100
|
+
'type'
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function verifiablePresentation() {
|
|
105
|
+
return {
|
|
106
|
+
title: 'Verifiable Presentation',
|
|
107
|
+
type: 'object',
|
|
108
|
+
required: ['@context', 'type'],
|
|
109
|
+
additionalProperties: true,
|
|
110
|
+
properties: {
|
|
111
|
+
'@context': vcContext,
|
|
112
|
+
id: {
|
|
113
|
+
type: 'string'
|
|
114
|
+
},
|
|
115
|
+
type: {
|
|
116
|
+
type: 'array',
|
|
117
|
+
minItems: 1,
|
|
118
|
+
// this first type must be VerifiablePresentation
|
|
119
|
+
items: [
|
|
120
|
+
{const: 'VerifiablePresentation'},
|
|
121
|
+
],
|
|
122
|
+
// additional types must be strings
|
|
123
|
+
additionalItems: {
|
|
124
|
+
type: 'string'
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
verifiableCredential: {
|
|
128
|
+
anyOf: [
|
|
129
|
+
verifiableCredential(),
|
|
130
|
+
envelopedVerifiableCredential, {
|
|
131
|
+
type: 'array',
|
|
132
|
+
minItems: 1,
|
|
133
|
+
items: {
|
|
134
|
+
anyOf: [verifiableCredential(), envelopedVerifiableCredential]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
holder: idOrObjectWithId(),
|
|
140
|
+
proof: schemas.proof()
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
7
145
|
const credentialDefinition = {
|
|
8
146
|
title: 'OID4VCI Verifiable Credential Definition',
|
|
9
147
|
type: 'object',
|
|
@@ -19,7 +157,7 @@ const credentialDefinition = {
|
|
|
19
157
|
},
|
|
20
158
|
type: {
|
|
21
159
|
type: 'array',
|
|
22
|
-
minItems:
|
|
160
|
+
minItems: 1,
|
|
23
161
|
item: {
|
|
24
162
|
type: 'string'
|
|
25
163
|
}
|
|
@@ -27,7 +165,7 @@ const credentialDefinition = {
|
|
|
27
165
|
// allow `types` to be flexible for OID4VCI draft 20 implementers
|
|
28
166
|
types: {
|
|
29
167
|
type: 'array',
|
|
30
|
-
minItems:
|
|
168
|
+
minItems: 1,
|
|
31
169
|
item: {
|
|
32
170
|
type: 'string'
|
|
33
171
|
}
|
|
@@ -165,7 +303,7 @@ const vcFormats = {
|
|
|
165
303
|
const issuerInstance = {
|
|
166
304
|
title: 'Issuer Instance',
|
|
167
305
|
type: 'object',
|
|
168
|
-
required: ['zcapReferenceIds'],
|
|
306
|
+
required: ['supportedFormats', 'zcapReferenceIds'],
|
|
169
307
|
additionalProperties: false,
|
|
170
308
|
properties: {
|
|
171
309
|
id: {
|
|
@@ -327,7 +465,7 @@ export function useExchangeBody() {
|
|
|
327
465
|
type: 'object',
|
|
328
466
|
additionalProperties: false,
|
|
329
467
|
properties: {
|
|
330
|
-
verifiablePresentation:
|
|
468
|
+
verifiablePresentation: verifiablePresentation()
|
|
331
469
|
}
|
|
332
470
|
};
|
|
333
471
|
}
|