@bedrock/vc-delivery 7.13.2 → 7.14.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/ExchangeProcessor.js +72 -46
- package/lib/helpers.js +39 -0
- package/lib/issue.js +54 -18
- package/lib/oid4/authorizationResponse.js +0 -1
- package/lib/oid4/http.js +13 -3
- package/lib/oid4/oid4vci.js +478 -273
- package/lib/oid4/oid4vciDraft13.js +197 -0
- package/lib/oid4/oid4vp.js +6 -1
- package/lib/vcapi.js +27 -2
- package/package.json +2 -2
- package/schemas/bedrock-vc-workflow.js +220 -180
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import {deepEqual, getWorkflowIssuerInstances} from '../helpers.js';
|
|
6
|
+
import {randomUUID as uuid} from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
const {util: {BedrockError}} = bedrock;
|
|
9
|
+
|
|
10
|
+
export function createSupportedCredentialConfigurations({
|
|
11
|
+
exchange, issuerInstances
|
|
12
|
+
} = {}) {
|
|
13
|
+
// get legacy `expectedCredentialRequests`
|
|
14
|
+
const {
|
|
15
|
+
openId: {expectedCredentialRequests}
|
|
16
|
+
} = exchange;
|
|
17
|
+
|
|
18
|
+
// build legacy supported credential configurations...
|
|
19
|
+
const supported = new Map();
|
|
20
|
+
|
|
21
|
+
// get supported formats from issuer instances
|
|
22
|
+
const supportedFormats = new Set();
|
|
23
|
+
issuerInstances.forEach(instance => instance.supportedFormats.forEach(
|
|
24
|
+
supportedFormats.add, supportedFormats));
|
|
25
|
+
|
|
26
|
+
// for every expected credential request and supported format, generate a
|
|
27
|
+
// supported credential configuration
|
|
28
|
+
for(const credentialRequest of expectedCredentialRequests) {
|
|
29
|
+
const configurations = _createCredentialConfigurations({
|
|
30
|
+
credentialRequest, supportedFormats
|
|
31
|
+
});
|
|
32
|
+
for(const {id, configuration} of configurations) {
|
|
33
|
+
supported.set(id, configuration);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Object.fromEntries(supported.entries());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createSupportedCredentialRequests({
|
|
41
|
+
workflow, exchange, issueRequestsParams
|
|
42
|
+
} = {}) {
|
|
43
|
+
const issuerInstances = getWorkflowIssuerInstances({workflow});
|
|
44
|
+
const supported = createSupportedCredentialConfigurations({
|
|
45
|
+
exchange, issuerInstances
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// for each `issueRequest` params, create a duplicate for each supported
|
|
49
|
+
// credential configuration
|
|
50
|
+
const ids = Object.keys(supported);
|
|
51
|
+
const supportedCredentialRequests = [];
|
|
52
|
+
for(let i = 0; i < issueRequestsParams.length; ++i) {
|
|
53
|
+
for(const credentialConfigurationId of ids) {
|
|
54
|
+
supportedCredentialRequests.push({
|
|
55
|
+
credentialConfigurationId,
|
|
56
|
+
credentialIdentifier: uuid(),
|
|
57
|
+
issueRequestsParamsIndex: i,
|
|
58
|
+
processed: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return supportedCredentialRequests;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function normalizeCredentialRequestsToVersion1({
|
|
67
|
+
credentialRequests, supportedCredentialConfigurations
|
|
68
|
+
}) {
|
|
69
|
+
// normalize credential requests to use `type` instead of `types`; this is to
|
|
70
|
+
// allow for OID4VCI draft implementers that followed the non-normative
|
|
71
|
+
// examples
|
|
72
|
+
credentialRequests = _normalizeCredentialDefinitionTypes({
|
|
73
|
+
credentialRequests
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// match draft 13 requests against credential configurations
|
|
77
|
+
return _matchCredentialRequests({
|
|
78
|
+
credentialRequests, supportedCredentialConfigurations
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _createCredentialConfigurationId({
|
|
83
|
+
format, credential_definition
|
|
84
|
+
}) {
|
|
85
|
+
let types = (credential_definition.type ?? credential_definition.types);
|
|
86
|
+
if(types.length > 1) {
|
|
87
|
+
types = types.filter(t => t !== 'VerifiableCredential');
|
|
88
|
+
}
|
|
89
|
+
return types.join('_') + '_' + format;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _matchCredentialRequest(expected, cr) {
|
|
93
|
+
const {credential_definition: {'@context': c1, type: t1}} = expected;
|
|
94
|
+
const {credential_definition: {'@context': c2, type: t2}} = cr;
|
|
95
|
+
// contexts must match exactly but types can have different order
|
|
96
|
+
return (c1.length === c2.length && t1.length === t2.length &&
|
|
97
|
+
deepEqual(c1, c2) && t1.every(t => t2.some(x => t === x)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _matchCredentialRequests({
|
|
101
|
+
credentialRequests, supportedCredentialConfigurations
|
|
102
|
+
}) {
|
|
103
|
+
// ensure that every credential request is for the same format
|
|
104
|
+
/* credential requests look like:
|
|
105
|
+
{
|
|
106
|
+
format: 'ldp_vc',
|
|
107
|
+
credential_definition: { '@context': [Array], type: [Array] }
|
|
108
|
+
}
|
|
109
|
+
*/
|
|
110
|
+
let sharedFormat;
|
|
111
|
+
if(!credentialRequests.every(({format}) => {
|
|
112
|
+
if(sharedFormat === undefined) {
|
|
113
|
+
sharedFormat = format;
|
|
114
|
+
}
|
|
115
|
+
return sharedFormat === format;
|
|
116
|
+
})) {
|
|
117
|
+
throw new BedrockError(
|
|
118
|
+
'Credential requests in a batch must all use the same format.', {
|
|
119
|
+
name: 'DataError',
|
|
120
|
+
details: {httpStatusCode: 400, public: true}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ensure every credential request matches a supported configuration
|
|
125
|
+
const entries = Object.entries(supportedCredentialConfigurations);
|
|
126
|
+
return credentialRequests.map(cr => {
|
|
127
|
+
for(const [credential_configuration_id, configuration] of entries) {
|
|
128
|
+
if(_matchCredentialRequest(configuration, cr)) {
|
|
129
|
+
const newRequest = {
|
|
130
|
+
type: 'openid_credential',
|
|
131
|
+
credential_configuration_id
|
|
132
|
+
};
|
|
133
|
+
// only proof type supported for draft 13 is `jwt` per JSON schema that
|
|
134
|
+
// has already run
|
|
135
|
+
if(cr.proof) {
|
|
136
|
+
newRequest.proofs = {
|
|
137
|
+
jwt: [cr.proof.jwt]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return newRequest;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
throw new BedrockError(
|
|
144
|
+
'Unexpected credential request.', {
|
|
145
|
+
name: 'DataError',
|
|
146
|
+
details: {httpStatusCode: 400, public: true}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _createCredentialConfigurations({
|
|
152
|
+
credentialRequest, supportedFormats
|
|
153
|
+
}) {
|
|
154
|
+
const configurations = [];
|
|
155
|
+
|
|
156
|
+
let {format: formats = supportedFormats} = credentialRequest;
|
|
157
|
+
if(!Array.isArray(formats)) {
|
|
158
|
+
formats = [formats];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for(const format of formats) {
|
|
162
|
+
const {credential_definition} = credentialRequest;
|
|
163
|
+
const id = _createCredentialConfigurationId({
|
|
164
|
+
format, credential_definition
|
|
165
|
+
});
|
|
166
|
+
const configuration = {format, credential_definition};
|
|
167
|
+
// FIXME: if `jwtDidProofRequest` exists in (any) step in the exchange,
|
|
168
|
+
// then must include:
|
|
169
|
+
/*
|
|
170
|
+
"proof_types_supported": {
|
|
171
|
+
"jwt": {
|
|
172
|
+
"proof_signing_alg_values_supported": [
|
|
173
|
+
"ES256"
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
*/
|
|
178
|
+
configurations.push({id, configuration});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return configurations;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _normalizeCredentialDefinitionTypes({credentialRequests}) {
|
|
185
|
+
// normalize credential requests to use `type` instead of `types`
|
|
186
|
+
return credentialRequests.map(cr => {
|
|
187
|
+
if(!cr?.credential_definition?.types) {
|
|
188
|
+
return cr;
|
|
189
|
+
}
|
|
190
|
+
cr = {...cr};
|
|
191
|
+
if(!cr.credential_definition.type) {
|
|
192
|
+
cr.credential_definition.type = cr.credential_definition.types;
|
|
193
|
+
}
|
|
194
|
+
delete cr.credential_definition.types;
|
|
195
|
+
return cr;
|
|
196
|
+
});
|
|
197
|
+
}
|
package/lib/oid4/oid4vp.js
CHANGED
|
@@ -191,7 +191,12 @@ export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
|
191
191
|
}) {
|
|
192
192
|
const {authorizationRequest} = result;
|
|
193
193
|
verifyPresentationOptions.challenge = authorizationRequest.nonce;
|
|
194
|
-
|
|
194
|
+
const usesRedirectUri = oid4vp.authzRequest.usesClientIdScheme({
|
|
195
|
+
authorizationRequest, scheme: 'redirect_uri'
|
|
196
|
+
});
|
|
197
|
+
verifyPresentationOptions.domain = usesRedirectUri ?
|
|
198
|
+
authorizationRequest.client_id : authorizationRequest.response_uri;
|
|
199
|
+
expectedDomain = verifyPresentationOptions.domain;
|
|
195
200
|
|
|
196
201
|
const {envelope} = parseResponseResult;
|
|
197
202
|
if(envelope?.mediaType === 'application/mdl-vp-token') {
|
package/lib/vcapi.js
CHANGED
|
@@ -107,6 +107,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
|
107
107
|
const receivedPresentation = req?.body?.verifiablePresentation;
|
|
108
108
|
|
|
109
109
|
// use exchange processor to generate a response
|
|
110
|
+
let stepComplete = true;
|
|
110
111
|
const exchangeProcessor = new ExchangeProcessor({
|
|
111
112
|
workflow, exchangeRecord,
|
|
112
113
|
prepareStep({workflow, step}) {
|
|
@@ -118,8 +119,32 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
|
|
|
118
119
|
});
|
|
119
120
|
}
|
|
120
121
|
},
|
|
121
|
-
inputRequired({step}) {
|
|
122
|
-
|
|
122
|
+
inputRequired({exchange, step, receivedPresentation}) {
|
|
123
|
+
// default to completing step
|
|
124
|
+
stepComplete = true;
|
|
125
|
+
|
|
126
|
+
// if no VP is requested then no input is required
|
|
127
|
+
if(!step.verifiablePresentationRequest) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// a VP is requested; if one was provided, allow it to be stored in the
|
|
132
|
+
// step results without issuing anything, but mark the step as incomplete
|
|
133
|
+
// to allow for reprocessing against the stored results
|
|
134
|
+
if(receivedPresentation) {
|
|
135
|
+
step.issueRequests = [];
|
|
136
|
+
step.verifiablePresentation = undefined;
|
|
137
|
+
stepComplete = false;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// if no VP was previously stored in the step results, then input is
|
|
142
|
+
// required, otherwise it is not
|
|
143
|
+
const stepResults = exchange.variables.results[exchange.step];
|
|
144
|
+
return !stepResults?.verifiablePresentation;
|
|
145
|
+
},
|
|
146
|
+
isStepComplete() {
|
|
147
|
+
return stepComplete;
|
|
123
148
|
}
|
|
124
149
|
});
|
|
125
150
|
const response = await exchangeProcessor.process({receivedPresentation});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock Verifiable Credential Delivery",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@digitalbazaar/ed25519-signature-2020": "^5.4.0",
|
|
41
41
|
"@digitalbazaar/ezcap": "^4.1.0",
|
|
42
42
|
"@digitalbazaar/http-client": "^4.2.0",
|
|
43
|
-
"@digitalbazaar/oid4-client": "^5.
|
|
43
|
+
"@digitalbazaar/oid4-client": "^5.10.0",
|
|
44
44
|
"@digitalbazaar/vc": "^7.2.0",
|
|
45
45
|
"@digitalbazaar/webkms-client": "^14.2.0",
|
|
46
46
|
"assert-plus": "^1.0.0",
|