@bedrock/vc-delivery 4.7.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/helpers.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import {decodeId, generateId} from 'bnid';
6
+ import {decodeJwt} from 'jose';
6
7
  import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
7
8
  import {httpsAgent} from '@bedrock/https-agent';
8
9
  import jsonata from 'jsonata';
@@ -102,3 +103,30 @@ export function decodeLocalId({localId} = {}) {
102
103
  expectedSize: 16
103
104
  }));
104
105
  }
106
+
107
+ export async function unenvelopeCredential({envelopedCredential} = {}) {
108
+ let credential;
109
+ const {id} = envelopedCredential;
110
+ if(id?.startsWith('data:application/jwt,')) {
111
+ const format = 'application/jwt';
112
+ const jwt = id.slice('data:application/jwt,'.length);
113
+ const claimset = decodeJwt(jwt);
114
+ // FIXME: perform various field mappings as needed
115
+ console.log('VC-JWT claimset', credential);
116
+ return {credential: claimset.vc, format};
117
+ }
118
+ throw new Error('Not implemented.');
119
+ }
120
+
121
+ export async function unenvelopePresentation({envelopedPresentation} = {}) {
122
+ const {id} = envelopedPresentation;
123
+ if(id?.startsWith('data:application/jwt,')) {
124
+ const format = 'application/jwt';
125
+ const jwt = id.slice('data:application/jwt,'.length);
126
+ const claimset = decodeJwt(jwt);
127
+ // FIXME: perform various field mappings as needed
128
+ console.log('VC-JWT claimset', claimset);
129
+ return {presentation: claimset.vp, format};
130
+ }
131
+ throw new Error('Not implemented.');
132
+ }
package/lib/openId.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from './exchanges.js';
6
6
  import {
7
- compile, schemas, createValidateMiddleware as validate
7
+ compile, createValidateMiddleware as validate
8
8
  } from '@bedrock/validation';
9
9
  import {evaluateTemplate, getWorkflowIssuerInstances} from './helpers.js';
10
10
  import {importJWK, SignJWT} from 'jose';
@@ -13,7 +13,8 @@ import {
13
13
  openIdBatchCredentialBody,
14
14
  openIdCredentialBody,
15
15
  openIdTokenBody,
16
- presentationSubmission as presentationSubmissionSchema
16
+ presentationSubmission as presentationSubmissionSchema,
17
+ verifiablePresentation as verifiablePresentationSchema
17
18
  } from '../schemas/bedrock-vc-workflow.js';
18
19
  import {verify, verifyDidProofJwt} from './verify.js';
19
20
  import {asyncHandler} from '@bedrock/express';
@@ -84,7 +85,7 @@ export async function createRoutes({
84
85
 
85
86
  // create validators for x-www-form-urlencoded parsed data
86
87
  const validatePresentation = compile(
87
- {schema: schemas.verifiablePresentation()});
88
+ {schema: verifiablePresentationSchema()});
88
89
  const validatePresentationSubmission = compile(
89
90
  {schema: presentationSubmissionSchema});
90
91
 
@@ -916,6 +917,22 @@ async function _processAuthorizationResponse({
916
917
  const {authorizationRequest, step} = arRequest;
917
918
  ({exchange} = arRequest);
918
919
 
920
+ // FIXME: if the VP is enveloped, remove the envelope to validate or
921
+ // run validation code after verification if necessary
922
+
923
+ // FIXME: check the VP against the presentation submission if requested
924
+ // FIXME: check the VP against "trustedIssuer" in VPR, if provided
925
+ const {presentationSchema} = step;
926
+ if(presentationSchema) {
927
+ // validate the received VP
928
+ const {jsonSchema: schema} = presentationSchema;
929
+ const validate = compile({schema});
930
+ const {valid, error} = validate(presentation);
931
+ if(!valid) {
932
+ throw error;
933
+ }
934
+ }
935
+
919
936
  // verify the received VP
920
937
  const {verifiablePresentationRequest} = await oid4vp.toVpr(
921
938
  {authorizationRequest});
@@ -928,9 +945,6 @@ async function _processAuthorizationResponse({
928
945
  expectedChallenge: authorizationRequest.nonce
929
946
  });
930
947
 
931
- // FIXME: check the VP against the presentation submission if requested
932
- // FIXME: check the VP against "trustedIssuer" in VPR, if provided
933
-
934
948
  // store VP results in variables associated with current step
935
949
  const currentStep = exchange.step;
936
950
  if(!exchange.variables.results) {
package/lib/vcapi.js CHANGED
@@ -4,6 +4,7 @@
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 {compile} from '@bedrock/validation';
7
8
  import {evaluateTemplate} from './helpers.js';
8
9
  import {issue} from './issue.js';
9
10
  import {klona} from 'klona';
@@ -95,6 +96,20 @@ export async function processExchange({req, res, workflow, exchange}) {
95
96
  return;
96
97
  }
97
98
 
99
+ // FIXME: if the VP is enveloped, remove the envelope to validate or
100
+ // run validation code after verification if necessary
101
+
102
+ const {presentationSchema} = step;
103
+ if(presentationSchema) {
104
+ // validate the received VP
105
+ const {jsonSchema: schema} = presentationSchema;
106
+ const validate = compile({schema});
107
+ const {valid, error} = validate(receivedPresentation);
108
+ if(!valid) {
109
+ throw error;
110
+ }
111
+ }
112
+
98
113
  // verify the received VP
99
114
  const expectedChallenge = isInitialStep ? exchange.id : undefined;
100
115
  const {allowUnprotectedPresentation = false} = step;
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});
@@ -134,23 +139,50 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
134
139
  const audience = exchangeId;
135
140
 
136
141
  let issuer;
137
- const resolveKey = async protectedHeader => {
138
- const vm = await didIo.get({url: protectedHeader.kid});
142
+ // `resolveKey` is passed `protectedHeader`
143
+ const resolveKey = async ({alg, kid}) => {
144
+ const isEcdsa = ECDSA_ALGS.includes(alg);
145
+ const isEddsa = !isEcdsa && EDDSA_ALGS.includes(alg);
146
+ if(!(isEcdsa || isEddsa)) {
147
+ throw new BedrockError(
148
+ `Unsupported JWT "alg": "${alg}".`, {
149
+ name: 'DataError',
150
+ details: {
151
+ httpStatusCode: 400,
152
+ public: true
153
+ }
154
+ });
155
+ }
156
+
157
+ const vm = await didIo.get({url: kid});
139
158
  // `vm.controller` must be the issuer of the DID JWT; also ensure that
140
159
  // the specified controller authorized `vm` for the purpose of
141
160
  // authentication
142
161
  issuer = vm.controller;
143
162
  const didDoc = await didIo.get({url: issuer});
144
- if(!(didDoc?.authentication?.some(e => e === vm.id || e.id === vm.id))) {
163
+ let match = didDoc?.authentication?.find?.(
164
+ e => e === vm.id || e.id === vm.id);
165
+ if(typeof match === 'string') {
166
+ match = didDoc?.verificationMethod?.find?.(e => e.id === vm.id);
167
+ }
168
+ if(!(match && Array.isArray(match.controller) ?
169
+ match.controller.includes(vm.controller) :
170
+ match.controller === vm.controller)) {
145
171
  throw new BedrockError(
146
172
  `Verification method controller "${issuer}" did not authorize ` +
147
173
  `verification method "${vm.id}" for the purpose of "authentication".`,
148
174
  {name: 'NotAllowedError'});
149
175
  }
150
- // FIXME: support other key types
151
- const keyPair = await Ed25519Multikey.from(vm);
152
- const jwk = await Ed25519Multikey.toJwk({keyPair});
153
- jwk.alg = 'EdDSA';
176
+ let jwk;
177
+ if(isEcdsa) {
178
+ const keyPair = await EcdsaMultikey.from(vm);
179
+ jwk = await EcdsaMultikey.toJwk({keyPair});
180
+ jwk.alg = alg;
181
+ } else {
182
+ const keyPair = await Ed25519Multikey.from(vm);
183
+ jwk = await Ed25519Multikey.toJwk({keyPair});
184
+ jwk.alg = 'EdDSA';
185
+ }
154
186
  return importJWK(jwk);
155
187
  };
156
188
 
@@ -186,7 +218,7 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
186
218
  }
187
219
 
188
220
  // check `iss` claim
189
- if(!(verifyResult?.payload?.iss === issuer)) {
221
+ if(!(issuer && verifyResult?.payload?.iss === issuer)) {
190
222
  throw new BedrockError('DID proof JWT validation failed.', {
191
223
  name: 'NotAllowedError',
192
224
  details: {
@@ -200,7 +232,7 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
200
232
  }
201
233
 
202
234
  // check `nonce` claim
203
- if(!(verifyResult?.payload?.nonce === exchange.id)) {
235
+ if(verifyResult?.payload?.nonce !== exchange.id) {
204
236
  throw new BedrockError('DID proof JWT validation failed.', {
205
237
  name: 'NotAllowedError',
206
238
  details: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "4.7.0",
3
+ "version": "5.0.0",
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.2.0",
40
- "@digitalbazaar/ezcap": "^4.0.0",
41
- "@digitalbazaar/oid4-client": "^3.1.0",
42
- "@digitalbazaar/vc": "^6.0.1",
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.1",
46
+ "body-parser": "^1.20.2",
46
47
  "cors": "^2.8.5",
47
- "jose": "^4.10.4",
48
- "jsonata": "^2.0.3",
49
- "klona": "^2.0.5"
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.0.1",
54
- "@bedrock/did-io": "^10.0.0",
55
- "@bedrock/express": "^8.0.0",
56
- "@bedrock/https-agent": "^4.0.0",
57
- "@bedrock/mongodb": "^10.0.0",
58
- "@bedrock/oauth2-verifier": "^2.0.0",
59
- "@bedrock/service-agent": "^8.0.0",
60
- "@bedrock/service-core": "^9.0.0",
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.41.0",
68
- "eslint-config-digitalbazaar": "^5.0.1",
69
- "eslint-plugin-jsdoc": "^46.3.0",
70
- "eslint-plugin-unicorn": "^47.0.0",
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: 2,
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: 2,
168
+ minItems: 1,
31
169
  item: {
32
170
  type: 'string'
33
171
  }
@@ -228,6 +366,19 @@ const step = {
228
366
  verifiablePresentationRequest: {
229
367
  type: 'object'
230
368
  },
369
+ presentationSchema: {
370
+ type: 'object',
371
+ required: ['type', 'jsonSchema'],
372
+ additionalProperties: false,
373
+ properties: {
374
+ type: {
375
+ type: 'string'
376
+ },
377
+ jsonSchema: {
378
+ type: 'object'
379
+ }
380
+ }
381
+ },
231
382
  jwtDidProofRequest: {
232
383
  type: 'object',
233
384
  additionalProperties: false,
@@ -314,7 +465,7 @@ export function useExchangeBody() {
314
465
  type: 'object',
315
466
  additionalProperties: false,
316
467
  properties: {
317
- verifiablePresentation: schemas.verifiablePresentation()
468
+ verifiablePresentation: verifiablePresentation()
318
469
  }
319
470
  };
320
471
  }