@pagopa/io-react-native-wallet 1.5.0 → 1.6.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/commonjs/credential/issuance/07-verify-and-parse-credential.js +33 -21
- package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
- package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js +192 -58
- package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
- package/lib/commonjs/credential/presentation/08-send-authorization-response.js +32 -15
- package/lib/commonjs/credential/presentation/08-send-authorization-response.js.map +1 -1
- package/lib/commonjs/credential/presentation/types.js +1 -1
- package/lib/commonjs/credential/presentation/types.js.map +1 -1
- package/lib/commonjs/entity/trust/chain.js.map +1 -1
- package/lib/commonjs/mdoc/index.js +45 -13
- package/lib/commonjs/mdoc/index.js.map +1 -1
- package/lib/commonjs/utils/crypto.js +70 -4
- package/lib/commonjs/utils/crypto.js.map +1 -1
- package/lib/commonjs/utils/string.js +4 -4
- package/lib/commonjs/utils/string.js.map +1 -1
- package/lib/module/credential/issuance/07-verify-and-parse-credential.js +33 -21
- package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
- package/lib/module/credential/presentation/07-evaluate-input-descriptor.js +186 -55
- package/lib/module/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
- package/lib/module/credential/presentation/08-send-authorization-response.js +32 -15
- package/lib/module/credential/presentation/08-send-authorization-response.js.map +1 -1
- package/lib/module/credential/presentation/types.js +1 -1
- package/lib/module/credential/presentation/types.js.map +1 -1
- package/lib/module/entity/trust/chain.js.map +1 -1
- package/lib/module/mdoc/index.js +43 -12
- package/lib/module/mdoc/index.js.map +1 -1
- package/lib/module/utils/crypto.js +67 -2
- package/lib/module/utils/crypto.js.map +1 -1
- package/lib/module/utils/string.js +4 -4
- package/lib/module/utils/string.js.map +1 -1
- package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts +1 -1
- package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
- package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts +49 -13
- package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts.map +1 -1
- package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts +3 -2
- package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts.map +1 -1
- package/lib/typescript/credential/presentation/types.d.ts +10 -7
- package/lib/typescript/credential/presentation/types.d.ts.map +1 -1
- package/lib/typescript/entity/trust/chain.d.ts.map +1 -1
- package/lib/typescript/mdoc/index.d.ts +6 -2
- package/lib/typescript/mdoc/index.d.ts.map +1 -1
- package/lib/typescript/utils/crypto.d.ts +8 -0
- package/lib/typescript/utils/crypto.d.ts.map +1 -1
- package/lib/typescript/utils/errors.d.ts.map +1 -1
- package/lib/typescript/utils/misc.d.ts.map +1 -1
- package/lib/typescript/utils/string.d.ts +3 -3
- package/lib/typescript/utils/string.d.ts.map +1 -1
- package/package.json +14 -12
- package/src/credential/issuance/07-verify-and-parse-credential.ts +37 -16
- package/src/credential/presentation/07-evaluate-input-descriptor.ts +278 -97
- package/src/credential/presentation/08-send-authorization-response.ts +35 -25
- package/src/credential/presentation/types.ts +9 -6
- package/src/entity/trust/chain.ts +14 -10
- package/src/mdoc/index.ts +72 -15
- package/src/utils/crypto.ts +61 -2
- package/src/utils/errors.ts +2 -2
- package/src/utils/misc.ts +2 -2
- package/src/utils/string.ts +4 -4
@@ -5,15 +5,21 @@ import { createCryptoContextFor } from "../../utils/crypto";
|
|
5
5
|
import { JSONPath } from "jsonpath-plus";
|
6
6
|
import { MissingDataError, CredentialNotFoundError } from "./errors";
|
7
7
|
import Ajv from "ajv";
|
8
|
-
import { base64ToBase64Url } from "../../utils/string";
|
9
8
|
import { CBOR } from "@pagopa/io-react-native-cbor";
|
9
|
+
import { prepareVpTokenMdoc } from "../../mdoc";
|
10
|
+
import { generateRandomAlphaNumericString } from "../../utils/misc";
|
11
|
+
|
10
12
|
const ajv = new Ajv({ allErrors: true });
|
11
|
-
const INDEX_CLAIM_NAME = 1;
|
12
13
|
|
13
14
|
type EvaluatedDisclosures = {
|
14
|
-
requiredDisclosures:
|
15
|
-
optionalDisclosures:
|
16
|
-
|
15
|
+
requiredDisclosures: EvaluatedDisclosure[];
|
16
|
+
optionalDisclosures: EvaluatedDisclosure[];
|
17
|
+
};
|
18
|
+
|
19
|
+
export type EvaluatedDisclosure = {
|
20
|
+
namespace?: string;
|
21
|
+
name: string;
|
22
|
+
value: unknown;
|
17
23
|
};
|
18
24
|
|
19
25
|
type EvaluateInputDescriptorSdJwt4VC = (
|
@@ -22,6 +28,11 @@ type EvaluateInputDescriptorSdJwt4VC = (
|
|
22
28
|
disclosures: DisclosureWithEncoded[]
|
23
29
|
) => EvaluatedDisclosures;
|
24
30
|
|
31
|
+
type EvaluateInputDescriptorMdoc = (
|
32
|
+
inputDescriptor: InputDescriptor,
|
33
|
+
issuerSigned: CBOR.IssuerSigned
|
34
|
+
) => EvaluatedDisclosures;
|
35
|
+
|
25
36
|
export type EvaluateInputDescriptors = (
|
26
37
|
descriptors: InputDescriptor[],
|
27
38
|
credentialsSdJwt: [string /* keyTag */, string /* credential */][],
|
@@ -42,9 +53,35 @@ export type PrepareRemotePresentations = (
|
|
42
53
|
credential: string;
|
43
54
|
keyTag: string;
|
44
55
|
}[],
|
45
|
-
|
46
|
-
|
47
|
-
|
56
|
+
authRequestObject: {
|
57
|
+
nonce: string;
|
58
|
+
clientId: string;
|
59
|
+
responseUri: string;
|
60
|
+
}
|
61
|
+
) => Promise<RemotePresentation>;
|
62
|
+
|
63
|
+
export const disclosureWithEncodedToEvaluatedDisclosure = (
|
64
|
+
disclosure: DisclosureWithEncoded
|
65
|
+
): EvaluatedDisclosure => {
|
66
|
+
const [, claimName, claimValue] = disclosure.decoded;
|
67
|
+
return {
|
68
|
+
name: claimName,
|
69
|
+
value: claimValue,
|
70
|
+
};
|
71
|
+
};
|
72
|
+
|
73
|
+
type DecodedCredentialMdoc = {
|
74
|
+
keyTag: string;
|
75
|
+
credential: string;
|
76
|
+
issuerSigned: CBOR.IssuerSigned;
|
77
|
+
};
|
78
|
+
|
79
|
+
type DecodedCredentialSdJwt = {
|
80
|
+
keyTag: string;
|
81
|
+
credential: string;
|
82
|
+
sdJwt: SdJwt4VC;
|
83
|
+
disclosures: DisclosureWithEncoded[];
|
84
|
+
};
|
48
85
|
|
49
86
|
/**
|
50
87
|
* Transforms an array of DisclosureWithEncoded objects into a key-value map.
|
@@ -54,11 +91,39 @@ export type PrepareRemotePresentations = (
|
|
54
91
|
const mapDisclosuresToObject = (
|
55
92
|
disclosures: DisclosureWithEncoded[]
|
56
93
|
): Record<string, unknown> => {
|
57
|
-
return disclosures.reduce(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
94
|
+
return disclosures.reduce(
|
95
|
+
(obj, { decoded }) => {
|
96
|
+
const [, claimName, claimValue] = decoded;
|
97
|
+
obj[claimName] = claimValue;
|
98
|
+
return obj;
|
99
|
+
},
|
100
|
+
{} as Record<string, unknown>
|
101
|
+
);
|
102
|
+
};
|
103
|
+
|
104
|
+
/**
|
105
|
+
* Transforms the issuer's namespaces from a CBOR structure into a plain JavaScript object.
|
106
|
+
*
|
107
|
+
* @param namespaces - The CBOR-based namespaces object where each key corresponds to a namespace,
|
108
|
+
* and each value is an array of elements containing identifiers and values.
|
109
|
+
* @returns A record (plain object) where each key is a namespace, and its value is another object
|
110
|
+
* mapping element identifiers to their corresponding element values.
|
111
|
+
*/
|
112
|
+
const mapNamespacesToObject = (
|
113
|
+
namespaces: CBOR.IssuerSigned["nameSpaces"]
|
114
|
+
): Record<string, unknown> => {
|
115
|
+
return Object.entries(namespaces).reduce(
|
116
|
+
(obj, [namespace, elements]) => {
|
117
|
+
obj[namespace] = Object.fromEntries(
|
118
|
+
elements.map((element) => [
|
119
|
+
element.elementIdentifier,
|
120
|
+
element.elementValue,
|
121
|
+
])
|
122
|
+
);
|
123
|
+
return obj;
|
124
|
+
},
|
125
|
+
{} as Record<string, unknown>
|
126
|
+
);
|
62
127
|
};
|
63
128
|
|
64
129
|
/**
|
@@ -113,13 +178,110 @@ const extractClaimName = (path: string): string | undefined => {
|
|
113
178
|
return match[1] || match[2];
|
114
179
|
}
|
115
180
|
|
116
|
-
// If the input doesn't match any of the expected formats, return null
|
117
|
-
|
118
181
|
throw new Error(
|
119
182
|
`Invalid input format: "${path}". Expected formats are "$.propertyName", "$['propertyName']", or '$["propertyName"]'.`
|
120
183
|
);
|
121
184
|
};
|
122
185
|
|
186
|
+
/**
|
187
|
+
* Extracts the namespace and claim name from a path in the following format:
|
188
|
+
* $['nameSpace']['propertyName']
|
189
|
+
*
|
190
|
+
* @param path - The path string containing the claim reference.
|
191
|
+
* @returns An object with the extracted namespace and claim name.
|
192
|
+
* @throws An error if the input format is invalid.
|
193
|
+
*/
|
194
|
+
const extractNamespaceAndClaimName = (
|
195
|
+
path: string
|
196
|
+
): { nameSpace?: string; propertyName?: string } => {
|
197
|
+
const regex = /^\$\[(?:'|")([^'"\]]+)(?:'|")\]\[(?:'|")([^'"\]]+)(?:'|")\]$/;
|
198
|
+
const match = path.match(regex);
|
199
|
+
if (match) {
|
200
|
+
return { nameSpace: match[1], propertyName: match[2] };
|
201
|
+
}
|
202
|
+
|
203
|
+
throw new Error(
|
204
|
+
`Invalid input format: "${path}". Expected format is "$['nameSpace']['propertyName']".`
|
205
|
+
);
|
206
|
+
};
|
207
|
+
/**
|
208
|
+
* Evaluates the input descriptor for an mDoc by verifying that the issuerSigned claims meet
|
209
|
+
* the constraints defined in the input descriptor. It categorizes disclosures as either required
|
210
|
+
* or optional based on the field definitions.
|
211
|
+
*
|
212
|
+
* @param inputDescriptor - Contains constraints and field definitions specifying required/optional claims.
|
213
|
+
* @param issuerSigned - Contains the issuerSigned with namespaces and their associated claims.
|
214
|
+
* @returns An object with two arrays: one for required disclosures and one for optional disclosures.
|
215
|
+
* @throws MissingDataError - If a required field is missing or if a claim fails JSON Schema validation.
|
216
|
+
*/
|
217
|
+
export const evaluateInputDescriptorForMdoc: EvaluateInputDescriptorMdoc = (
|
218
|
+
inputDescriptor,
|
219
|
+
issuerSigned
|
220
|
+
) => {
|
221
|
+
if (!inputDescriptor?.constraints?.fields) {
|
222
|
+
// No validation, no field are required
|
223
|
+
return {
|
224
|
+
requiredDisclosures: [],
|
225
|
+
optionalDisclosures: [],
|
226
|
+
};
|
227
|
+
}
|
228
|
+
|
229
|
+
const requiredDisclosures: EvaluatedDisclosure[] = [];
|
230
|
+
const optionalDisclosures: EvaluatedDisclosure[] = [];
|
231
|
+
|
232
|
+
// Convert issuer's namespaces into an object for easier lookup of claim values.
|
233
|
+
const namespacesAsPayload = mapNamespacesToObject(issuerSigned.nameSpaces);
|
234
|
+
|
235
|
+
const allFieldsValid = inputDescriptor.constraints.fields.every((field) => {
|
236
|
+
const [matchedPath, matchedValue] = findMatchedClaim(
|
237
|
+
field.path,
|
238
|
+
namespacesAsPayload
|
239
|
+
);
|
240
|
+
|
241
|
+
// If no matching claim is found, the field is valid only if it's marked as optional.
|
242
|
+
if (matchedValue === undefined || !matchedPath) {
|
243
|
+
return field?.optional;
|
244
|
+
} else {
|
245
|
+
// Extract the namespace and property name from the matched path.
|
246
|
+
const { nameSpace, propertyName } =
|
247
|
+
extractNamespaceAndClaimName(matchedPath);
|
248
|
+
if (nameSpace && propertyName) {
|
249
|
+
(field?.optional ? optionalDisclosures : requiredDisclosures).push({
|
250
|
+
namespace: nameSpace,
|
251
|
+
name: propertyName,
|
252
|
+
value: matchedValue,
|
253
|
+
});
|
254
|
+
}
|
255
|
+
}
|
256
|
+
|
257
|
+
if (field.filter) {
|
258
|
+
try {
|
259
|
+
const validateSchema = ajv.compile(field.filter);
|
260
|
+
if (!validateSchema(matchedValue)) {
|
261
|
+
throw new MissingDataError(
|
262
|
+
`Claim value "${matchedValue}" for path "${matchedPath}" does not match the provided JSON Schema.`
|
263
|
+
);
|
264
|
+
}
|
265
|
+
} catch (error) {
|
266
|
+
return false;
|
267
|
+
}
|
268
|
+
}
|
269
|
+
|
270
|
+
return true;
|
271
|
+
});
|
272
|
+
|
273
|
+
if (!allFieldsValid) {
|
274
|
+
throw new MissingDataError(
|
275
|
+
"Credential validation failed: Required fields are missing or do not match the input descriptor."
|
276
|
+
);
|
277
|
+
}
|
278
|
+
|
279
|
+
return {
|
280
|
+
requiredDisclosures,
|
281
|
+
optionalDisclosures,
|
282
|
+
};
|
283
|
+
};
|
284
|
+
|
123
285
|
/**
|
124
286
|
* Evaluates an InputDescriptor for an SD-JWT-based verifiable credential.
|
125
287
|
*
|
@@ -128,14 +290,12 @@ const extractClaimName = (path: string): string | undefined => {
|
|
128
290
|
* - Validates whether required fields are present (unless marked optional)
|
129
291
|
* and match any specified JSONPath.
|
130
292
|
* - If a field includes a JSON Schema filter, validates the claim value against that schema.
|
131
|
-
* - Enforces `limit_disclosure` rules by returning only disclosures, required and optional, matching the specified fields
|
132
|
-
* if set to "required". Otherwise also return the array unrequestedDisclosures with disclosures which can be passed for a particular use case.
|
133
293
|
* - Throws an error if a required field is invalid or missing.
|
134
294
|
*
|
135
295
|
* @param inputDescriptor - Describes constraints (fields, filters, etc.) that must be satisfied.
|
136
296
|
* @param payloadCredential - The credential payload to check against.
|
137
297
|
* @param disclosures - An array of DisclosureWithEncoded objects representing selective disclosures.
|
138
|
-
* @returns
|
298
|
+
* @returns An object with two arrays: one for required disclosures and one for optional disclosures.
|
139
299
|
* @throws Will throw an error if any required constraint fails or if JSONPath lookups are invalid.
|
140
300
|
*/
|
141
301
|
export const evaluateInputDescriptorForSdJwt4VC: EvaluateInputDescriptorSdJwt4VC =
|
@@ -145,13 +305,12 @@ export const evaluateInputDescriptorForSdJwt4VC: EvaluateInputDescriptorSdJwt4VC
|
|
145
305
|
return {
|
146
306
|
requiredDisclosures: [],
|
147
307
|
optionalDisclosures: [],
|
148
|
-
unrequestedDisclosures: disclosures,
|
149
308
|
};
|
150
309
|
}
|
151
|
-
const
|
152
|
-
const
|
310
|
+
const requiredDisclosures: EvaluatedDisclosure[] = [];
|
311
|
+
const optionalDisclosures: EvaluatedDisclosure[] = [];
|
153
312
|
|
154
|
-
// Transform disclosures
|
313
|
+
// Transform disclosures into an object for easier lookup of claim values.
|
155
314
|
const disclosuresAsPayload = mapDisclosuresToObject(disclosures);
|
156
315
|
|
157
316
|
// For each field, we need at least one matching path
|
@@ -179,9 +338,10 @@ export const evaluateInputDescriptorForSdJwt4VC: EvaluateInputDescriptorSdJwt4VC
|
|
179
338
|
// if match a disclouse we save which is required or optional
|
180
339
|
const claimName = extractClaimName(matchedPath);
|
181
340
|
if (claimName) {
|
182
|
-
(field?.optional ?
|
183
|
-
|
184
|
-
|
341
|
+
(field?.optional ? optionalDisclosures : requiredDisclosures).push({
|
342
|
+
value: matchedValue,
|
343
|
+
name: claimName,
|
344
|
+
});
|
185
345
|
}
|
186
346
|
}
|
187
347
|
|
@@ -211,43 +371,12 @@ export const evaluateInputDescriptorForSdJwt4VC: EvaluateInputDescriptorSdJwt4VC
|
|
211
371
|
);
|
212
372
|
}
|
213
373
|
|
214
|
-
// Categorizes disclosures into required and optional based on claim names and disclosure constraints.
|
215
|
-
|
216
|
-
const requiredDisclosures = disclosures.filter((disclosure) =>
|
217
|
-
requiredClaimNames.includes(disclosure.decoded[INDEX_CLAIM_NAME])
|
218
|
-
);
|
219
|
-
|
220
|
-
const optionalDisclosures = disclosures.filter((disclosure) =>
|
221
|
-
optionalClaimNames.includes(disclosure.decoded[INDEX_CLAIM_NAME])
|
222
|
-
);
|
223
|
-
|
224
|
-
const isNotLimitDisclosure = !(
|
225
|
-
inputDescriptor.constraints.limit_disclosure === "required"
|
226
|
-
);
|
227
|
-
|
228
|
-
const unrequestedDisclosures = isNotLimitDisclosure
|
229
|
-
? disclosures.filter(
|
230
|
-
(disclosure) =>
|
231
|
-
!optionalClaimNames.includes(
|
232
|
-
disclosure.decoded[INDEX_CLAIM_NAME]
|
233
|
-
) &&
|
234
|
-
!requiredClaimNames.includes(disclosure.decoded[INDEX_CLAIM_NAME])
|
235
|
-
)
|
236
|
-
: [];
|
237
|
-
|
238
374
|
return {
|
239
375
|
requiredDisclosures,
|
240
376
|
optionalDisclosures,
|
241
|
-
unrequestedDisclosures,
|
242
377
|
};
|
243
378
|
};
|
244
379
|
|
245
|
-
type DecodedCredentialSdJwt = {
|
246
|
-
keyTag: string;
|
247
|
-
credential: string;
|
248
|
-
sdJwt: SdJwt4VC;
|
249
|
-
disclosures: DisclosureWithEncoded[];
|
250
|
-
};
|
251
380
|
/**
|
252
381
|
* Finds the first credential that satisfies the input descriptor constraints.
|
253
382
|
* @param inputDescriptor The input descriptor to evaluate.
|
@@ -291,6 +420,43 @@ export const findCredentialSdJwt = (
|
|
291
420
|
);
|
292
421
|
};
|
293
422
|
|
423
|
+
/**
|
424
|
+
* Finds the first credential that satisfies the input descriptor constraints.
|
425
|
+
* @param inputDescriptor The input descriptor to evaluate.
|
426
|
+
* @param decodedMdocCredentials An array of decoded MDOC credentials.
|
427
|
+
* @returns An object containing the matched evaluation, keyTag, and credential.
|
428
|
+
*/
|
429
|
+
export const findCredentialMDoc = (
|
430
|
+
inputDescriptor: InputDescriptor,
|
431
|
+
decodedMDocCredentials: DecodedCredentialMdoc[]
|
432
|
+
): {
|
433
|
+
matchedEvaluation: EvaluatedDisclosures;
|
434
|
+
matchedKeyTag: string;
|
435
|
+
matchedCredential: string;
|
436
|
+
} => {
|
437
|
+
for (const { keyTag, credential, issuerSigned } of decodedMDocCredentials) {
|
438
|
+
try {
|
439
|
+
const evaluatedDisclosure = evaluateInputDescriptorForMdoc(
|
440
|
+
inputDescriptor,
|
441
|
+
issuerSigned
|
442
|
+
);
|
443
|
+
|
444
|
+
return {
|
445
|
+
matchedEvaluation: evaluatedDisclosure,
|
446
|
+
matchedKeyTag: keyTag,
|
447
|
+
matchedCredential: credential,
|
448
|
+
};
|
449
|
+
} catch {
|
450
|
+
// skip to next credential
|
451
|
+
continue;
|
452
|
+
}
|
453
|
+
}
|
454
|
+
|
455
|
+
throw new CredentialNotFoundError(
|
456
|
+
"None of the mso_mdoc credentials satisfy the requirements."
|
457
|
+
);
|
458
|
+
};
|
459
|
+
|
294
460
|
/**
|
295
461
|
* Evaluates multiple input descriptors against provided SD-JWT and MDOC credentials.
|
296
462
|
*
|
@@ -318,47 +484,37 @@ export const evaluateInputDescriptors: EvaluateInputDescriptors = async (
|
|
318
484
|
return { keyTag, credential, sdJwt, disclosures };
|
319
485
|
}) || [];
|
320
486
|
|
487
|
+
// We need decode Mdoc credentials for evaluation
|
488
|
+
const decodedMdocCredentials =
|
489
|
+
(await Promise.all(
|
490
|
+
credentialsMdoc?.map(async ([keyTag, credential]) => {
|
491
|
+
const issuerSigned = await CBOR.decodeIssuerSigned(credential);
|
492
|
+
if (!issuerSigned) {
|
493
|
+
throw new CredentialNotFoundError(
|
494
|
+
"mso_mdoc credential is not present."
|
495
|
+
);
|
496
|
+
}
|
497
|
+
return { keyTag, credential, issuerSigned };
|
498
|
+
})
|
499
|
+
)) || [];
|
500
|
+
|
321
501
|
const results = Promise.all(
|
322
502
|
inputDescriptors.map(async (descriptor) => {
|
323
503
|
if (descriptor.format?.mso_mdoc) {
|
324
|
-
if (!credentialsMdoc
|
504
|
+
if (!credentialsMdoc.length) {
|
325
505
|
throw new CredentialNotFoundError(
|
326
506
|
"mso_mdoc credential is not supported."
|
327
507
|
);
|
328
508
|
}
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
const [keyTag, credential] = credentialsMdoc[0];
|
334
|
-
const mdoc = await CBOR.decodeDocuments(credential);
|
335
|
-
if (!mdoc || !mdoc.documents || !mdoc.documents[0]) {
|
336
|
-
throw new CredentialNotFoundError(
|
337
|
-
"mso_mdoc credential is not present."
|
338
|
-
);
|
339
|
-
}
|
340
|
-
const document = mdoc.documents[0];
|
341
|
-
// We set requiredDisclosures to all the elements in the document, as we don't have a real implementation for this yet.
|
509
|
+
|
510
|
+
const { matchedEvaluation, matchedKeyTag, matchedCredential } =
|
511
|
+
findCredentialMDoc(descriptor, decodedMdocCredentials);
|
512
|
+
|
342
513
|
return {
|
343
|
-
evaluatedDisclosure:
|
344
|
-
requiredDisclosures: Object.entries(
|
345
|
-
document.issuerSigned.nameSpaces
|
346
|
-
).flatMap(([, elements]) =>
|
347
|
-
elements.map((element) => ({
|
348
|
-
encoded: "",
|
349
|
-
decoded: [
|
350
|
-
"",
|
351
|
-
element.elementIdentifier,
|
352
|
-
element.elementValue,
|
353
|
-
] as [string, string, unknown],
|
354
|
-
}))
|
355
|
-
),
|
356
|
-
optionalDisclosures: [],
|
357
|
-
unrequestedDisclosures: [],
|
358
|
-
},
|
514
|
+
evaluatedDisclosure: matchedEvaluation,
|
359
515
|
inputDescriptor: descriptor,
|
360
|
-
credential,
|
361
|
-
keyTag,
|
516
|
+
credential: matchedCredential,
|
517
|
+
keyTag: matchedKeyTag,
|
362
518
|
};
|
363
519
|
}
|
364
520
|
|
@@ -405,28 +561,48 @@ export const evaluateInputDescriptors: EvaluateInputDescriptors = async (
|
|
405
561
|
*/
|
406
562
|
export const prepareRemotePresentations: PrepareRemotePresentations = async (
|
407
563
|
credentialAndDescriptors,
|
408
|
-
|
409
|
-
client_id
|
564
|
+
authRequestObject
|
410
565
|
) => {
|
411
|
-
|
566
|
+
/* In case of ISO 18013-7 we need a nonce, it shall have a minimum entropy of 16 */
|
567
|
+
const generatedNonce = generateRandomAlphaNumericString(16);
|
568
|
+
|
569
|
+
const presentations = await Promise.all(
|
412
570
|
credentialAndDescriptors.map(async (item) => {
|
413
571
|
const descriptor = item.inputDescriptor;
|
414
572
|
|
415
573
|
if (descriptor.format?.mso_mdoc) {
|
574
|
+
const { vp_token } = await prepareVpTokenMdoc(
|
575
|
+
authRequestObject.nonce,
|
576
|
+
generatedNonce,
|
577
|
+
authRequestObject.clientId,
|
578
|
+
authRequestObject.responseUri,
|
579
|
+
descriptor.id,
|
580
|
+
item.keyTag,
|
581
|
+
[
|
582
|
+
item.credential,
|
583
|
+
item.requestedClaims,
|
584
|
+
createCryptoContextFor(item.keyTag),
|
585
|
+
]
|
586
|
+
);
|
587
|
+
|
416
588
|
return {
|
417
589
|
requestedClaims: item.requestedClaims,
|
418
590
|
inputDescriptor: descriptor,
|
419
|
-
vpToken:
|
591
|
+
vpToken: vp_token,
|
420
592
|
format: "mso_mdoc",
|
421
593
|
};
|
422
594
|
}
|
423
595
|
|
424
596
|
if (descriptor.format?.["vc+sd-jwt"]) {
|
425
|
-
const { vp_token } = await prepareVpToken(
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
597
|
+
const { vp_token } = await prepareVpToken(
|
598
|
+
authRequestObject.nonce,
|
599
|
+
authRequestObject.clientId,
|
600
|
+
[
|
601
|
+
item.credential,
|
602
|
+
item.requestedClaims,
|
603
|
+
createCryptoContextFor(item.keyTag),
|
604
|
+
]
|
605
|
+
);
|
430
606
|
|
431
607
|
return {
|
432
608
|
requestedClaims: item.requestedClaims,
|
@@ -441,4 +617,9 @@ export const prepareRemotePresentations: PrepareRemotePresentations = async (
|
|
441
617
|
);
|
442
618
|
})
|
443
619
|
);
|
620
|
+
|
621
|
+
return {
|
622
|
+
presentations,
|
623
|
+
generatedNonce,
|
624
|
+
};
|
444
625
|
};
|
@@ -11,6 +11,7 @@ import {
|
|
11
11
|
} from "./types";
|
12
12
|
import * as z from "zod";
|
13
13
|
import type { JWK } from "../../utils/jwk";
|
14
|
+
import { Base64 } from "js-base64";
|
14
15
|
|
15
16
|
export type AuthorizationResponse = z.infer<typeof AuthorizationResponse>;
|
16
17
|
export const AuthorizationResponse = z.object({
|
@@ -60,7 +61,7 @@ export const buildDirectPostBody = async (
|
|
60
61
|
payload: DirectAuthorizationBodyPayload
|
61
62
|
): Promise<string> => {
|
62
63
|
const formUrlEncodedBody = new URLSearchParams({
|
63
|
-
state: requestObject.state,
|
64
|
+
...(requestObject.state ? { state: requestObject.state } : {}),
|
64
65
|
...Object.fromEntries(
|
65
66
|
Object.entries(payload).map(([key, value]) => {
|
66
67
|
return [
|
@@ -82,13 +83,15 @@ export const buildDirectPostBody = async (
|
|
82
83
|
* @param jwkKeys - Array of JWKs from the Relying Party for encryption.
|
83
84
|
* @param requestObject - Contains state, nonce, and other relevant info.
|
84
85
|
* @param payload - Object that contains either the VP token to encrypt and the mapping of the credential disclosures or the error code
|
86
|
+
* @param generatedNonce - Optional nonce for the `apu` claim in the JWE header, it is used during ISO 18013-7.
|
85
87
|
* @returns A URL-encoded string for an `application/x-www-form-urlencoded` POST body,
|
86
88
|
* where `response` contains the encrypted JWE.
|
87
89
|
*/
|
88
90
|
export const buildDirectPostJwtBody = async (
|
89
91
|
jwkKeys: Out<FetchJwks>["keys"],
|
90
92
|
requestObject: Out<VerifyRequestObjectSignature>["requestObject"],
|
91
|
-
payload: DirectAuthorizationBodyPayload
|
93
|
+
payload: DirectAuthorizationBodyPayload,
|
94
|
+
generatedNonce?: string
|
92
95
|
): Promise<string> => {
|
93
96
|
// Prepare the authorization response payload to be encrypted
|
94
97
|
const authzResponsePayload = JSON.stringify({
|
@@ -98,7 +101,6 @@ export const buildDirectPostJwtBody = async (
|
|
98
101
|
|
99
102
|
// Choose a suitable RSA public key for encryption
|
100
103
|
const encPublicJwk = choosePublicKeyToEncrypt(jwkKeys);
|
101
|
-
|
102
104
|
// Encrypt the authorization payload
|
103
105
|
const { client_metadata } = requestObject;
|
104
106
|
const encryptedResponse = await new EncryptJwe(authzResponsePayload, {
|
@@ -111,12 +113,15 @@ export const buildDirectPostJwtBody = async (
|
|
111
113
|
| "A256CBC-HS512"
|
112
114
|
| "A128CBC-HS256") || "A256CBC-HS512",
|
113
115
|
kid: encPublicJwk.kid,
|
116
|
+
/* ISO 18013-7 */
|
117
|
+
apv: Base64.encodeURI(requestObject.nonce),
|
118
|
+
...(generatedNonce ? { apu: Base64.encodeURI(generatedNonce) } : {}),
|
114
119
|
}).encrypt(encPublicJwk);
|
115
120
|
|
116
121
|
// Build the x-www-form-urlencoded form body
|
117
122
|
const formBody = new URLSearchParams({
|
118
123
|
response: encryptedResponse,
|
119
|
-
state: requestObject.state,
|
124
|
+
...(requestObject.state ? { state: requestObject.state } : {}),
|
120
125
|
});
|
121
126
|
return formBody.toString();
|
122
127
|
};
|
@@ -129,7 +134,7 @@ export type SendAuthorizationResponse = (
|
|
129
134
|
requestObject: Out<VerifyRequestObjectSignature>["requestObject"],
|
130
135
|
presentationDefinitionId: string,
|
131
136
|
jwkKeys: Out<FetchJwks>["keys"],
|
132
|
-
|
137
|
+
remotePresentation: RemotePresentation,
|
133
138
|
context?: {
|
134
139
|
appFetch?: GlobalFetch["fetch"];
|
135
140
|
}
|
@@ -150,28 +155,25 @@ export const sendAuthorizationResponse: SendAuthorizationResponse = async (
|
|
150
155
|
requestObject,
|
151
156
|
presentationDefinitionId,
|
152
157
|
jwkKeys,
|
153
|
-
|
158
|
+
remotePresentation,
|
154
159
|
{ appFetch = fetch } = {}
|
155
160
|
): Promise<AuthorizationResponse> => {
|
161
|
+
const { generatedNonce, presentations } = remotePresentation;
|
156
162
|
/**
|
157
163
|
* 1. Prepare the VP token and presentation submission
|
158
164
|
* If there is only one credential, `vpToken` is a single string.
|
159
165
|
* If there are multiple credential, `vpToken` is an array of string.
|
160
166
|
**/
|
161
167
|
const vp_token =
|
162
|
-
|
163
|
-
?
|
164
|
-
:
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
path: remotePresentations.length === 1 ? `$` : `$[${index}]`,
|
172
|
-
format: remotePresentation.format,
|
173
|
-
})
|
174
|
-
);
|
168
|
+
presentations?.length === 1
|
169
|
+
? presentations[0]?.vpToken
|
170
|
+
: presentations.map((presentation) => presentation.vpToken);
|
171
|
+
|
172
|
+
const descriptor_map = presentations.map((presentation, index) => ({
|
173
|
+
id: presentation.inputDescriptor.id,
|
174
|
+
path: presentations?.length === 1 ? `$` : `$[${index}]`,
|
175
|
+
format: presentation.format,
|
176
|
+
}));
|
175
177
|
|
176
178
|
const presentation_submission = {
|
177
179
|
id: uuid.v4(),
|
@@ -182,17 +184,22 @@ export const sendAuthorizationResponse: SendAuthorizationResponse = async (
|
|
182
184
|
// 2. Choose the appropriate request body builder based on response mode
|
183
185
|
const requestBody =
|
184
186
|
requestObject.response_mode === "direct_post.jwt"
|
185
|
-
? await buildDirectPostJwtBody(
|
186
|
-
|
187
|
-
|
188
|
-
|
187
|
+
? await buildDirectPostJwtBody(
|
188
|
+
jwkKeys,
|
189
|
+
requestObject,
|
190
|
+
{
|
191
|
+
vp_token,
|
192
|
+
presentation_submission,
|
193
|
+
},
|
194
|
+
generatedNonce
|
195
|
+
)
|
189
196
|
: await buildDirectPostBody(requestObject, {
|
190
197
|
vp_token,
|
191
198
|
presentation_submission: presentation_submission,
|
192
199
|
});
|
193
200
|
|
194
201
|
// 3. Send the authorization response via HTTP POST and validate the response
|
195
|
-
|
202
|
+
const authResponse = await appFetch(requestObject.response_uri, {
|
196
203
|
method: "POST",
|
197
204
|
headers: {
|
198
205
|
"Content-Type": "application/x-www-form-urlencoded",
|
@@ -201,7 +208,10 @@ export const sendAuthorizationResponse: SendAuthorizationResponse = async (
|
|
201
208
|
})
|
202
209
|
.then(hasStatusOrThrow(200))
|
203
210
|
.then((res) => res.json())
|
204
|
-
.then(AuthorizationResponse.
|
211
|
+
.then(AuthorizationResponse.safeParse);
|
212
|
+
|
213
|
+
// Some Relying Parties may return an empty body.
|
214
|
+
return authResponse.success ? authResponse.data : {};
|
205
215
|
};
|
206
216
|
|
207
217
|
/**
|
@@ -9,17 +9,20 @@ import { JWKS } from "../../utils/jwk";
|
|
9
9
|
export type Presentation = [
|
10
10
|
/* verified credential token */ string,
|
11
11
|
/* claims */ string[],
|
12
|
-
/* the context for the key associated to the credential */ CryptoContext
|
12
|
+
/* the context for the key associated to the credential */ CryptoContext,
|
13
13
|
];
|
14
14
|
|
15
15
|
/**
|
16
16
|
* A object that associate the information needed to multiple remote presentation
|
17
17
|
*/
|
18
18
|
export type RemotePresentation = {
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
presentations: {
|
20
|
+
requestedClaims: string[];
|
21
|
+
inputDescriptor: InputDescriptor;
|
22
|
+
format: string;
|
23
|
+
vpToken: string;
|
24
|
+
}[];
|
25
|
+
generatedNonce?: string /* nonce generated by app, used in mdoc presentation */;
|
23
26
|
};
|
24
27
|
|
25
28
|
const Fields = z.object({
|
@@ -82,7 +85,7 @@ export const RequestObject = z.object({
|
|
82
85
|
iss: z.string().optional(), //optional by RFC 7519, mandatory for Potential
|
83
86
|
iat: UnixTime.optional(),
|
84
87
|
exp: UnixTime.optional(),
|
85
|
-
state: z.string(),
|
88
|
+
state: z.string().optional(),
|
86
89
|
nonce: z.string(),
|
87
90
|
response_uri: z.string(),
|
88
91
|
response_type: z.literal("vp_token"),
|