@bedrock/vc-delivery 5.0.1 → 5.1.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/http.js +10 -73
- package/lib/oid4/http.js +314 -0
- package/lib/oid4/oid4vci.js +530 -0
- package/lib/oid4/oid4vp.js +330 -0
- package/lib/vcapi.js +77 -1
- package/package.json +1 -1
- package/lib/openId.js +0 -1057
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as exchanges from '../exchanges.js';
|
|
6
|
+
import {
|
|
7
|
+
evaluateTemplate, getWorkflowIssuerInstances
|
|
8
|
+
} from '../helpers.js';
|
|
9
|
+
import {importJWK, SignJWT} from 'jose';
|
|
10
|
+
import {checkAccessToken} from '@bedrock/oauth2-verifier';
|
|
11
|
+
import {getAuthorizationRequest} from './oid4vp.js';
|
|
12
|
+
import {issue} from '../issue.js';
|
|
13
|
+
import {timingSafeEqual} from 'node:crypto';
|
|
14
|
+
import {verifyDidProofJwt} from '../verify.js';
|
|
15
|
+
|
|
16
|
+
const {util: {BedrockError}} = bedrock;
|
|
17
|
+
|
|
18
|
+
const PRE_AUTH_GRANT_TYPE =
|
|
19
|
+
'urn:ietf:params:oauth:grant-type:pre-authorized_code';
|
|
20
|
+
|
|
21
|
+
export async function getAuthorizationServerConfig({req}) {
|
|
22
|
+
// note that technically, we should not need to serve any credential
|
|
23
|
+
// issuer metadata, but we do for backwards compatibility purposes as
|
|
24
|
+
// previous versions of OID4VCI required it
|
|
25
|
+
return getCredentialIssuerConfig({req});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getCredentialIssuerConfig({req}) {
|
|
29
|
+
const {config: workflow} = req.serviceObject;
|
|
30
|
+
const {exchange} = await req.getExchange();
|
|
31
|
+
_assertOID4VCISupported({exchange});
|
|
32
|
+
|
|
33
|
+
// build `credential_configurations_supported`...
|
|
34
|
+
const {openId: {expectedCredentialRequests}} = exchange;
|
|
35
|
+
const supportedFormats = [..._getSupportedFormats({workflow})];
|
|
36
|
+
|
|
37
|
+
// for every expected credential definition, set `format` default to
|
|
38
|
+
// `supportedFormats` and for every format, generate a new supported
|
|
39
|
+
// credential configuration
|
|
40
|
+
const credential_configurations_supported = {};
|
|
41
|
+
for(const credentialRequest of expectedCredentialRequests) {
|
|
42
|
+
const configurations = _createCredentialConfigurations({
|
|
43
|
+
credentialRequest, supportedFormats
|
|
44
|
+
});
|
|
45
|
+
for(const {id, configuration} of configurations) {
|
|
46
|
+
credential_configurations_supported[id] = configuration;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
|
|
51
|
+
return {
|
|
52
|
+
credential_issuer: exchangeId,
|
|
53
|
+
issuer: exchangeId,
|
|
54
|
+
jwks_uri: `${exchangeId}/openid/jwks`,
|
|
55
|
+
token_endpoint: `${exchangeId}/openid/token`,
|
|
56
|
+
credential_endpoint: `${exchangeId}/openid/credential`,
|
|
57
|
+
batch_credential_endpoint: `${exchangeId}/openid/batch_credential`,
|
|
58
|
+
'pre-authorized_grant_anonymous_access_supported': true,
|
|
59
|
+
credential_configurations_supported
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getJwks({req}) {
|
|
64
|
+
const {exchange} = await req.getExchange();
|
|
65
|
+
_assertOID4VCISupported({exchange});
|
|
66
|
+
return [exchange.openId.oauth2.keyPair.publicKeyJwk];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function processAccessTokenRequest({req}) {
|
|
70
|
+
const exchangeRecord = await req.getExchange();
|
|
71
|
+
const {exchange} = exchangeRecord;
|
|
72
|
+
_assertOID4VCISupported({exchange});
|
|
73
|
+
|
|
74
|
+
/* Examples of types of token requests:
|
|
75
|
+
pre-authz code:
|
|
76
|
+
grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code
|
|
77
|
+
&pre-authorized_code=SplxlOBeZQQYbYS6WxSbIA
|
|
78
|
+
&user_pin=493536
|
|
79
|
+
|
|
80
|
+
authz code:
|
|
81
|
+
grant_type=authorization_code
|
|
82
|
+
&code=SplxlOBeZQQYbYS6WxSbIA
|
|
83
|
+
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
|
|
84
|
+
&redirect_uri=https%3A%2F%2FWallet.example.org%2Fcb */
|
|
85
|
+
|
|
86
|
+
const {config: workflow} = req.serviceObject;
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
grant_type: grantType,
|
|
90
|
+
'pre-authorized_code': preAuthorizedCode,
|
|
91
|
+
// FIXME: `user_pin` now called `tx_code`
|
|
92
|
+
//user_pin: userPin
|
|
93
|
+
} = req.body;
|
|
94
|
+
|
|
95
|
+
if(grantType !== PRE_AUTH_GRANT_TYPE) {
|
|
96
|
+
// unsupported grant type
|
|
97
|
+
// FIXME: throw proper oauth2 formatted error
|
|
98
|
+
throw new Error('Unsupported grant type.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// validate grant type
|
|
102
|
+
const {openId: {preAuthorizedCode: expectedCode}} = exchange;
|
|
103
|
+
if(expectedCode) {
|
|
104
|
+
// ensure expected pre-authz code matches
|
|
105
|
+
if(!timingSafeEqual(
|
|
106
|
+
Buffer.from(expectedCode, 'utf8'),
|
|
107
|
+
Buffer.from(preAuthorizedCode, 'utf8'))) {
|
|
108
|
+
// FIXME: throw proper oauth2 formatted error
|
|
109
|
+
throw new Error('invalid pre-authorized-code or user pin');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// create access token
|
|
114
|
+
const {accessToken, ttl} = await _createExchangeAccessToken({
|
|
115
|
+
workflow, exchangeRecord
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
access_token: accessToken,
|
|
119
|
+
token_type: 'bearer',
|
|
120
|
+
expires_in: ttl
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function processCredentialRequests({req, res, isBatchRequest}) {
|
|
125
|
+
const {config: workflow} = req.serviceObject;
|
|
126
|
+
const exchangeRecord = await req.getExchange();
|
|
127
|
+
const {exchange} = exchangeRecord;
|
|
128
|
+
_assertOID4VCISupported({exchange});
|
|
129
|
+
|
|
130
|
+
// ensure oauth2 access token is valid
|
|
131
|
+
await _checkAuthz({req, workflow, exchange});
|
|
132
|
+
|
|
133
|
+
// validate body against expected credential requests
|
|
134
|
+
const {openId: {expectedCredentialRequests}} = exchange;
|
|
135
|
+
let credentialRequests;
|
|
136
|
+
if(isBatchRequest) {
|
|
137
|
+
({credential_requests: credentialRequests} = req.body);
|
|
138
|
+
} else {
|
|
139
|
+
if(expectedCredentialRequests.length > 1) {
|
|
140
|
+
// FIXME: it is no longer the case that the batch endpoint must be used
|
|
141
|
+
// for multiple requests; determine if the request has changed
|
|
142
|
+
|
|
143
|
+
// clients interacting with exchanges with more than one VC to be
|
|
144
|
+
// delivered must use the "batch credential" endpoint
|
|
145
|
+
// FIXME: improve error
|
|
146
|
+
throw new Error('batch_credential_endpoint must be used');
|
|
147
|
+
}
|
|
148
|
+
credentialRequests = [req.body];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// before asserting, normalize credential requests to use `type` instead of
|
|
152
|
+
// `types`; this is to allow for OID4VCI draft implementers that followed
|
|
153
|
+
// the non-normative examples
|
|
154
|
+
_normalizeCredentialDefinitionTypes({credentialRequests});
|
|
155
|
+
const {format} = _assertCredentialRequests({
|
|
156
|
+
workflow, credentialRequests, expectedCredentialRequests
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// process exchange step if present
|
|
160
|
+
const currentStep = exchange.step;
|
|
161
|
+
if(currentStep) {
|
|
162
|
+
let step = workflow.steps[exchange.step];
|
|
163
|
+
if(step.stepTemplate) {
|
|
164
|
+
// generate step from the template; assume the template type is
|
|
165
|
+
// `jsonata` per the JSON schema
|
|
166
|
+
step = await evaluateTemplate(
|
|
167
|
+
{workflow, exchange, typedTemplate: step.stepTemplate});
|
|
168
|
+
if(Object.keys(step).length === 0) {
|
|
169
|
+
throw new BedrockError('Could not create exchange step.', {
|
|
170
|
+
name: 'DataError',
|
|
171
|
+
details: {httpStatusCode: 500, public: true}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// do late workflow configuration validation
|
|
177
|
+
const {jwtDidProofRequest, openId} = step;
|
|
178
|
+
// use of `jwtDidProofRequest` and `openId` together is prohibited
|
|
179
|
+
if(jwtDidProofRequest && openId) {
|
|
180
|
+
throw new BedrockError(
|
|
181
|
+
'Invalid workflow configuration; only one of ' +
|
|
182
|
+
'"jwtDidProofRequest" and "openId" is permitted in a step.', {
|
|
183
|
+
name: 'DataError',
|
|
184
|
+
details: {httpStatusCode: 500, public: true}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// check to see if step supports OID4VP during OID4VCI
|
|
189
|
+
if(step.openId) {
|
|
190
|
+
// if there is no `presentationSubmission`, request one
|
|
191
|
+
const {results} = exchange.variables;
|
|
192
|
+
if(!results?.[exchange.step]?.openId?.presentationSubmission) {
|
|
193
|
+
// FIXME: optimize away double step-template processing that currently
|
|
194
|
+
// occurs when calling `_getAuthorizationRequest`
|
|
195
|
+
const {
|
|
196
|
+
authorizationRequest
|
|
197
|
+
} = await getAuthorizationRequest({req});
|
|
198
|
+
return _requestOID4VP({authorizationRequest, res});
|
|
199
|
+
}
|
|
200
|
+
// otherwise drop down below to complete exchange...
|
|
201
|
+
} else if(jwtDidProofRequest) {
|
|
202
|
+
// handle OID4VCI specialized JWT DID Proof request...
|
|
203
|
+
|
|
204
|
+
// `proof` must be in every credential request; if any request is missing
|
|
205
|
+
// `proof` then request a DID proof
|
|
206
|
+
if(credentialRequests.some(cr => !cr.proof?.jwt)) {
|
|
207
|
+
return _requestDidProof({res, exchangeRecord});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// verify every DID proof and get resulting DIDs
|
|
211
|
+
const results = await Promise.all(
|
|
212
|
+
credentialRequests.map(async cr => {
|
|
213
|
+
const {proof: {jwt}} = cr;
|
|
214
|
+
const {did} = await verifyDidProofJwt({workflow, exchange, jwt});
|
|
215
|
+
return did;
|
|
216
|
+
}));
|
|
217
|
+
// require `did` to be the same for every proof
|
|
218
|
+
// FIXME: determine if this needs to be more flexible
|
|
219
|
+
const did = results[0];
|
|
220
|
+
if(results.some(d => did !== d)) {
|
|
221
|
+
// FIXME: improve error
|
|
222
|
+
throw new Error('every DID must be the same');
|
|
223
|
+
}
|
|
224
|
+
// store did results in variables associated with current step
|
|
225
|
+
if(!exchange.variables.results) {
|
|
226
|
+
exchange.variables.results = {};
|
|
227
|
+
}
|
|
228
|
+
exchange.variables.results[currentStep] = {
|
|
229
|
+
// common use case of DID Authentication; provide `did` for ease
|
|
230
|
+
// of use in templates
|
|
231
|
+
did
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// mark exchange complete
|
|
237
|
+
exchange.sequence++;
|
|
238
|
+
await exchanges.complete({workflowId: workflow.id, exchange});
|
|
239
|
+
|
|
240
|
+
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
241
|
+
// replay attack detected) after exchange has been marked complete
|
|
242
|
+
|
|
243
|
+
// issue VCs
|
|
244
|
+
return issue({workflow, exchange, format});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function _assertCredentialRequests({
|
|
248
|
+
workflow, credentialRequests, expectedCredentialRequests
|
|
249
|
+
}) {
|
|
250
|
+
// ensure that every credential request is for the same format
|
|
251
|
+
/* credential requests look like:
|
|
252
|
+
{
|
|
253
|
+
format: 'ldp_vc',
|
|
254
|
+
credential_definition: { '@context': [Array], type: [Array] }
|
|
255
|
+
}
|
|
256
|
+
*/
|
|
257
|
+
let sharedFormat;
|
|
258
|
+
if(!credentialRequests.every(({format}) => {
|
|
259
|
+
if(sharedFormat === undefined) {
|
|
260
|
+
sharedFormat = format;
|
|
261
|
+
}
|
|
262
|
+
return sharedFormat === format;
|
|
263
|
+
})) {
|
|
264
|
+
throw new BedrockError(
|
|
265
|
+
'Credential requests must all use the same format in this workflow.', {
|
|
266
|
+
name: 'DataError',
|
|
267
|
+
details: {httpStatusCode: 400, public: true}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ensure that the shared format is supported by the workflow
|
|
272
|
+
const supportedFormats = _getSupportedFormats({workflow});
|
|
273
|
+
if(!supportedFormats.has(sharedFormat)) {
|
|
274
|
+
throw new BedrockError(
|
|
275
|
+
`Credential request format "${sharedFormat}" is not supported ` +
|
|
276
|
+
'by this workflow.', {
|
|
277
|
+
name: 'DataError',
|
|
278
|
+
details: {httpStatusCode: 400, public: true}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ensure every credential request matches against an expected one and none
|
|
283
|
+
// are missing; `expectedCredentialRequests` formats are ignored based on the
|
|
284
|
+
// issuer instance supported formats and have already been checked
|
|
285
|
+
if(!(credentialRequests.length === expectedCredentialRequests.length &&
|
|
286
|
+
credentialRequests.every(cr => expectedCredentialRequests.some(
|
|
287
|
+
expected => _matchCredentialRequest(expected, cr))))) {
|
|
288
|
+
throw new BedrockError(
|
|
289
|
+
'Unexpected credential request.', {
|
|
290
|
+
name: 'DataError',
|
|
291
|
+
details: {httpStatusCode: 400, public: true}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {format: sharedFormat};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function _assertOID4VCISupported({exchange}) {
|
|
299
|
+
if(!exchange.openId?.expectedCredentialRequests) {
|
|
300
|
+
throw new BedrockError('OID4VCI is not supported by this exchange.', {
|
|
301
|
+
name: 'NotSupportedError',
|
|
302
|
+
details: {httpStatusCode: 400, public: true}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function _checkAuthz({req, workflow, exchange}) {
|
|
308
|
+
// optional oauth2 options
|
|
309
|
+
const {oauth2} = exchange.openId;
|
|
310
|
+
const {maxClockSkew} = oauth2;
|
|
311
|
+
|
|
312
|
+
// audience is always the `exchangeId` and cannot be configured; this
|
|
313
|
+
// prevents attacks where access tokens could otherwise be generated
|
|
314
|
+
// if the AS keys were compromised; the `exchangeId` must also be known
|
|
315
|
+
const exchangeId = `${workflow.id}/exchanges/${req.params.exchangeId}`;
|
|
316
|
+
const audience = exchangeId;
|
|
317
|
+
|
|
318
|
+
// `issuerConfigUrl` is always based off of the `exchangeId` as well
|
|
319
|
+
const parsedIssuer = new URL(exchangeId);
|
|
320
|
+
const issuerConfigUrl =
|
|
321
|
+
`${parsedIssuer.origin}/.well-known/oauth-authorization-server` +
|
|
322
|
+
parsedIssuer.pathname;
|
|
323
|
+
|
|
324
|
+
// FIXME: `allowedAlgorithms` should be computed from `oauth2.keyPair`
|
|
325
|
+
// const allowedAlgorithms =
|
|
326
|
+
|
|
327
|
+
// ensure access token is valid
|
|
328
|
+
await checkAccessToken({req, issuerConfigUrl, maxClockSkew, audience});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function _createExchangeAccessToken({workflow, exchangeRecord}) {
|
|
332
|
+
// FIXME: set `exp` to max of 15 minutes / configured max minutes
|
|
333
|
+
const expires = exchangeRecord.meta.expires;
|
|
334
|
+
const exp = Math.floor(expires.getTime() / 1000);
|
|
335
|
+
|
|
336
|
+
// create access token
|
|
337
|
+
const {exchange} = exchangeRecord;
|
|
338
|
+
const {openId: {oauth2: {keyPair: {privateKeyJwk}}}} = exchange;
|
|
339
|
+
const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
|
|
340
|
+
const {accessToken, ttl} = await _createOAuth2AccessToken({
|
|
341
|
+
privateKeyJwk, audience: exchangeId, action: 'write', target: exchangeId,
|
|
342
|
+
exp, iss: exchangeId
|
|
343
|
+
});
|
|
344
|
+
return {accessToken, ttl};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function _createOAuth2AccessToken({
|
|
348
|
+
privateKeyJwk, audience, action, target, exp, iss, nbf, typ = 'at+jwt'
|
|
349
|
+
}) {
|
|
350
|
+
const alg = _getAlgFromPrivateKey({privateKeyJwk});
|
|
351
|
+
const scope = `${action}:${target}`;
|
|
352
|
+
const builder = new SignJWT({scope})
|
|
353
|
+
.setProtectedHeader({alg, typ})
|
|
354
|
+
.setIssuer(iss)
|
|
355
|
+
.setAudience(audience);
|
|
356
|
+
let ttl;
|
|
357
|
+
if(exp !== undefined) {
|
|
358
|
+
builder.setExpirationTime(exp);
|
|
359
|
+
ttl = Math.max(0, exp - Math.floor(Date.now() / 1000));
|
|
360
|
+
} else {
|
|
361
|
+
// default to 15 minute expiration time
|
|
362
|
+
builder.setExpirationTime('15m');
|
|
363
|
+
ttl = Math.floor(Date.now() / 1000) + 15 * 60;
|
|
364
|
+
}
|
|
365
|
+
if(nbf !== undefined) {
|
|
366
|
+
builder.setNotBefore(nbf);
|
|
367
|
+
}
|
|
368
|
+
const key = await importJWK({...privateKeyJwk, alg});
|
|
369
|
+
const accessToken = await builder.sign(key);
|
|
370
|
+
return {accessToken, ttl};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function _createCredentialConfigurationId({format, credential_definition}) {
|
|
374
|
+
let types = (credential_definition.type ?? credential_definition.types);
|
|
375
|
+
if(types.length > 1) {
|
|
376
|
+
types = types.filter(t => t !== 'VerifiableCredential');
|
|
377
|
+
}
|
|
378
|
+
return types.join('_') + '_' + format;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function _createCredentialConfigurations({
|
|
382
|
+
credentialRequest, supportedFormats
|
|
383
|
+
}) {
|
|
384
|
+
const configurations = [];
|
|
385
|
+
|
|
386
|
+
let {format: formats = supportedFormats} = credentialRequest;
|
|
387
|
+
if(!Array.isArray(formats)) {
|
|
388
|
+
formats = [formats];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for(const format of formats) {
|
|
392
|
+
const {credential_definition} = credentialRequest;
|
|
393
|
+
const id = _createCredentialConfigurationId({
|
|
394
|
+
format, credential_definition
|
|
395
|
+
});
|
|
396
|
+
const configuration = {format, credential_definition};
|
|
397
|
+
// FIXME: if `jwtDidProofRequest` exists in (any) step in the exchange,
|
|
398
|
+
// then must include:
|
|
399
|
+
/*
|
|
400
|
+
"proof_types_supported": {
|
|
401
|
+
"jwt": {
|
|
402
|
+
"proof_signing_alg_values_supported": [
|
|
403
|
+
"ES256"
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
*/
|
|
408
|
+
configurations.push({id, configuration});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return configurations;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function _getAlgFromPrivateKey({privateKeyJwk}) {
|
|
415
|
+
if(privateKeyJwk.alg) {
|
|
416
|
+
return privateKeyJwk.alg;
|
|
417
|
+
}
|
|
418
|
+
if(privateKeyJwk.kty === 'EC' && privateKeyJwk.crv) {
|
|
419
|
+
if(privateKeyJwk.crv.startsWith('P-')) {
|
|
420
|
+
return `ES${privateKeyJwk.crv.slice(2)}`;
|
|
421
|
+
}
|
|
422
|
+
if(privateKeyJwk.crv === 'secp256k1') {
|
|
423
|
+
return 'ES256K';
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if(privateKeyJwk.kty === 'OKP' && privateKeyJwk.crv?.startsWith('Ed')) {
|
|
427
|
+
return 'EdDSA';
|
|
428
|
+
}
|
|
429
|
+
if(privateKeyJwk.kty === 'RSA') {
|
|
430
|
+
return 'PS256';
|
|
431
|
+
}
|
|
432
|
+
return 'invalid';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function _getSupportedFormats({workflow}) {
|
|
436
|
+
// get all supported formats from available issuer instances; for simple
|
|
437
|
+
// workflow configs, a single issuer instance is used
|
|
438
|
+
const supportedFormats = new Set();
|
|
439
|
+
const issuerInstances = getWorkflowIssuerInstances({workflow});
|
|
440
|
+
issuerInstances.forEach(
|
|
441
|
+
instance => instance.supportedFormats.forEach(
|
|
442
|
+
supportedFormats.add, supportedFormats));
|
|
443
|
+
return supportedFormats;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function _matchCredentialRequest(expected, cr) {
|
|
447
|
+
const {credential_definition: {'@context': c1, type: t1}} = expected;
|
|
448
|
+
const {credential_definition: {'@context': c2, type: t2}} = cr;
|
|
449
|
+
// contexts must match exact order but types can have different order
|
|
450
|
+
return (c1.length === c2.length && t1.length === t2.length &&
|
|
451
|
+
c1.every((c, i) => c === c2[i]) && t1.every(t => t2.some(x => t === x)));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function _normalizeCredentialDefinitionTypes({credentialRequests}) {
|
|
455
|
+
// normalize credential requests to use `type` instead of `types`
|
|
456
|
+
for(const cr of credentialRequests) {
|
|
457
|
+
if(cr?.credential_definition?.types) {
|
|
458
|
+
if(!cr?.credential_definition?.type) {
|
|
459
|
+
cr.credential_definition.type = cr.credential_definition.types;
|
|
460
|
+
}
|
|
461
|
+
delete cr.credential_definition.types;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function _requestDidProof({res, exchangeRecord}) {
|
|
467
|
+
/* `9.4 Credential Issuer-provided nonce` allows the credential
|
|
468
|
+
issuer infrastructure to provide the nonce via an error:
|
|
469
|
+
|
|
470
|
+
HTTP/1.1 400 Bad Request
|
|
471
|
+
Content-Type: application/json
|
|
472
|
+
Cache-Control: no-store
|
|
473
|
+
|
|
474
|
+
{
|
|
475
|
+
"error": "invalid_or_missing_proof"
|
|
476
|
+
"error_description":
|
|
477
|
+
"Credential issuer requires proof element in Credential Request"
|
|
478
|
+
"c_nonce": "8YE9hCnyV2",
|
|
479
|
+
"c_nonce_expires_in": 86400
|
|
480
|
+
}*/
|
|
481
|
+
|
|
482
|
+
/* OID4VCI exchanges themselves are not replayable and single-step, so the
|
|
483
|
+
challenge to be signed is just the exchange ID itself. An exchange cannot
|
|
484
|
+
be reused and neither can a challenge. */
|
|
485
|
+
const {exchange, meta: {expires}} = exchangeRecord;
|
|
486
|
+
const ttl = Math.floor((expires.getTime() - Date.now()) / 1000);
|
|
487
|
+
|
|
488
|
+
res.status(400).json({
|
|
489
|
+
error: 'invalid_or_missing_proof',
|
|
490
|
+
error_description:
|
|
491
|
+
'Credential issuer requires proof element in Credential Request',
|
|
492
|
+
// use exchange ID
|
|
493
|
+
c_nonce: exchange.id,
|
|
494
|
+
// use exchange expiration period
|
|
495
|
+
c_nonce_expires_in: ttl
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function _requestOID4VP({authorizationRequest, res}) {
|
|
500
|
+
/* Error thrown when OID4VP is required to complete OID4VCI:
|
|
501
|
+
|
|
502
|
+
HTTP/1.1 400 Bad Request
|
|
503
|
+
Content-Type: application/json
|
|
504
|
+
Cache-Control: no-store
|
|
505
|
+
|
|
506
|
+
{
|
|
507
|
+
"error": "presentation_required"
|
|
508
|
+
"error_description":
|
|
509
|
+
"Credential issuer requires presentation before Credential Request"
|
|
510
|
+
"authorization_request": {
|
|
511
|
+
"response_type": "vp_token",
|
|
512
|
+
"presentation_definition": {
|
|
513
|
+
id: "<urn:uuid>",
|
|
514
|
+
input_descriptors: {...}
|
|
515
|
+
},
|
|
516
|
+
"response_mode": "direct_post"
|
|
517
|
+
}
|
|
518
|
+
}*/
|
|
519
|
+
|
|
520
|
+
/* OID4VCI exchanges themselves are not replayable and single-step, so the
|
|
521
|
+
challenge to be signed is just the exchange ID itself. An exchange cannot
|
|
522
|
+
be reused and neither can a challenge. */
|
|
523
|
+
|
|
524
|
+
res.status(400).json({
|
|
525
|
+
error: 'presentation_required',
|
|
526
|
+
error_description:
|
|
527
|
+
'Credential issuer requires presentation before Credential Request',
|
|
528
|
+
authorization_request: authorizationRequest
|
|
529
|
+
});
|
|
530
|
+
}
|