@bedrock/vc-delivery 5.0.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 +60 -23
- package/lib/openId.js +45 -14
- package/lib/vcapi.js +12 -5
- package/lib/vcjwt.js +375 -0
- package/lib/verify.js +5 -1
- package/package.json +1 -1
- package/schemas/bedrock-vc-workflow.js +1 -1
package/lib/helpers.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
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
|
-
import {decodeJwt} from 'jose';
|
|
7
7
|
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
|
|
8
8
|
import {httpsAgent} from '@bedrock/https-agent';
|
|
9
9
|
import jsonata from 'jsonata';
|
|
10
10
|
import {serviceAgents} from '@bedrock/service-agent';
|
|
11
11
|
import {ZcapClient} from '@digitalbazaar/ezcap';
|
|
12
12
|
|
|
13
|
-
const {config} = bedrock;
|
|
13
|
+
const {config, util: {BedrockError}} = bedrock;
|
|
14
14
|
|
|
15
15
|
export async function evaluateTemplate({
|
|
16
16
|
workflow, exchange, typedTemplate
|
|
@@ -104,29 +104,66 @@ export function decodeLocalId({localId} = {}) {
|
|
|
104
104
|
}));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
export async function unenvelopeCredential({
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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];
|
|
117
129
|
}
|
|
118
|
-
|
|
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};
|
|
119
143
|
}
|
|
120
144
|
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
if(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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};
|
|
130
162
|
}
|
|
131
|
-
|
|
163
|
+
|
|
164
|
+
throw new BedrockError(
|
|
165
|
+
`Unsupported credential or presentation envelope format "${format}".`, {
|
|
166
|
+
name: 'NotSupportedError',
|
|
167
|
+
details: {httpStatusCode: 400, public: true}
|
|
168
|
+
});
|
|
132
169
|
}
|
package/lib/openId.js
CHANGED
|
@@ -6,7 +6,9 @@ import * as exchanges from './exchanges.js';
|
|
|
6
6
|
import {
|
|
7
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,
|
|
@@ -57,6 +59,8 @@ instantiating a new authorization server instance per VC exchange. */
|
|
|
57
59
|
const PRE_AUTH_GRANT_TYPE =
|
|
58
60
|
'urn:ietf:params:oauth:grant-type:pre-authorized_code';
|
|
59
61
|
|
|
62
|
+
const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
|
|
63
|
+
|
|
60
64
|
// creates OID4VCI Authorization Server + Credential Delivery Server
|
|
61
65
|
// endpoints for each individual exchange
|
|
62
66
|
export async function createRoutes({
|
|
@@ -379,14 +383,33 @@ export async function createRoutes({
|
|
|
379
383
|
const {vp_token, presentation_submission} = req.body;
|
|
380
384
|
|
|
381
385
|
// JSON parse and validate `vp_token` and `presentation_submission`
|
|
382
|
-
|
|
386
|
+
let presentation = _jsonParse(vp_token, 'vp_token');
|
|
383
387
|
const presentationSubmission = _jsonParse(
|
|
384
388
|
presentation_submission, 'presentation_submission');
|
|
385
389
|
_validate(validatePresentationSubmission, presentationSubmission);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
{
|
|
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
|
+
});
|
|
390
413
|
res.json(result);
|
|
391
414
|
}));
|
|
392
415
|
|
|
@@ -906,7 +929,7 @@ function _matchCredentialRequest(expected, cr) {
|
|
|
906
929
|
}
|
|
907
930
|
|
|
908
931
|
async function _processAuthorizationResponse({
|
|
909
|
-
req, presentation, presentationSubmission
|
|
932
|
+
req, presentation, envelope, presentationSubmission
|
|
910
933
|
}) {
|
|
911
934
|
const {config: workflow} = req.serviceObject;
|
|
912
935
|
const exchangeRecord = await req.getExchange();
|
|
@@ -917,17 +940,17 @@ async function _processAuthorizationResponse({
|
|
|
917
940
|
const {authorizationRequest, step} = arRequest;
|
|
918
941
|
({exchange} = arRequest);
|
|
919
942
|
|
|
920
|
-
// FIXME: if the VP is enveloped, remove the envelope to validate or
|
|
921
|
-
// run validation code after verification if necessary
|
|
922
|
-
|
|
923
943
|
// FIXME: check the VP against the presentation submission if requested
|
|
924
944
|
// FIXME: check the VP against "trustedIssuer" in VPR, if provided
|
|
925
945
|
const {presentationSchema} = step;
|
|
926
946
|
if(presentationSchema) {
|
|
927
|
-
// validate the
|
|
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
|
@@ -87,9 +87,13 @@ export async function verify({
|
|
|
87
87
|
|
|
88
88
|
// generate useful error to return to client
|
|
89
89
|
const {name, errors, message} = cause.data.error;
|
|
90
|
+
const causeError = _stripStacktrace({...cause.data.error});
|
|
91
|
+
delete causeError.errors;
|
|
90
92
|
const error = new BedrockError(message ?? 'Verification error.', {
|
|
91
|
-
name: name === 'VerificationError'
|
|
93
|
+
name: (name === 'VerificationError' || name === 'DataError') ?
|
|
94
|
+
'DataError' : 'OperationError',
|
|
92
95
|
details: {
|
|
96
|
+
error: causeError,
|
|
93
97
|
verified,
|
|
94
98
|
credentialResults,
|
|
95
99
|
presentationResult,
|
package/package.json
CHANGED