@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.
Files changed (58) hide show
  1. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js +33 -21
  2. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  3. package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js +192 -58
  4. package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
  5. package/lib/commonjs/credential/presentation/08-send-authorization-response.js +32 -15
  6. package/lib/commonjs/credential/presentation/08-send-authorization-response.js.map +1 -1
  7. package/lib/commonjs/credential/presentation/types.js +1 -1
  8. package/lib/commonjs/credential/presentation/types.js.map +1 -1
  9. package/lib/commonjs/entity/trust/chain.js.map +1 -1
  10. package/lib/commonjs/mdoc/index.js +45 -13
  11. package/lib/commonjs/mdoc/index.js.map +1 -1
  12. package/lib/commonjs/utils/crypto.js +70 -4
  13. package/lib/commonjs/utils/crypto.js.map +1 -1
  14. package/lib/commonjs/utils/string.js +4 -4
  15. package/lib/commonjs/utils/string.js.map +1 -1
  16. package/lib/module/credential/issuance/07-verify-and-parse-credential.js +33 -21
  17. package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  18. package/lib/module/credential/presentation/07-evaluate-input-descriptor.js +186 -55
  19. package/lib/module/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
  20. package/lib/module/credential/presentation/08-send-authorization-response.js +32 -15
  21. package/lib/module/credential/presentation/08-send-authorization-response.js.map +1 -1
  22. package/lib/module/credential/presentation/types.js +1 -1
  23. package/lib/module/credential/presentation/types.js.map +1 -1
  24. package/lib/module/entity/trust/chain.js.map +1 -1
  25. package/lib/module/mdoc/index.js +43 -12
  26. package/lib/module/mdoc/index.js.map +1 -1
  27. package/lib/module/utils/crypto.js +67 -2
  28. package/lib/module/utils/crypto.js.map +1 -1
  29. package/lib/module/utils/string.js +4 -4
  30. package/lib/module/utils/string.js.map +1 -1
  31. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts +1 -1
  32. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
  33. package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts +49 -13
  34. package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts.map +1 -1
  35. package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts +3 -2
  36. package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts.map +1 -1
  37. package/lib/typescript/credential/presentation/types.d.ts +10 -7
  38. package/lib/typescript/credential/presentation/types.d.ts.map +1 -1
  39. package/lib/typescript/entity/trust/chain.d.ts.map +1 -1
  40. package/lib/typescript/mdoc/index.d.ts +6 -2
  41. package/lib/typescript/mdoc/index.d.ts.map +1 -1
  42. package/lib/typescript/utils/crypto.d.ts +8 -0
  43. package/lib/typescript/utils/crypto.d.ts.map +1 -1
  44. package/lib/typescript/utils/errors.d.ts.map +1 -1
  45. package/lib/typescript/utils/misc.d.ts.map +1 -1
  46. package/lib/typescript/utils/string.d.ts +3 -3
  47. package/lib/typescript/utils/string.d.ts.map +1 -1
  48. package/package.json +14 -12
  49. package/src/credential/issuance/07-verify-and-parse-credential.ts +37 -16
  50. package/src/credential/presentation/07-evaluate-input-descriptor.ts +278 -97
  51. package/src/credential/presentation/08-send-authorization-response.ts +35 -25
  52. package/src/credential/presentation/types.ts +9 -6
  53. package/src/entity/trust/chain.ts +14 -10
  54. package/src/mdoc/index.ts +72 -15
  55. package/src/utils/crypto.ts +61 -2
  56. package/src/utils/errors.ts +2 -2
  57. package/src/utils/misc.ts +2 -2
  58. 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: DisclosureWithEncoded[];
15
- optionalDisclosures: DisclosureWithEncoded[];
16
- unrequestedDisclosures: DisclosureWithEncoded[];
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
- nonce: string,
46
- client_id: string
47
- ) => Promise<RemotePresentation[]>;
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((obj, { decoded }) => {
58
- const [, claimName, claimValue] = decoded;
59
- obj[claimName] = claimValue;
60
- return obj;
61
- }, {} as Record<string, unknown>);
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 A filtered list of disclosures satisfying the descriptor constraints, or throws an error if not.
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 requiredClaimNames: string[] = [];
152
- const optionalClaimNames: string[] = [];
310
+ const requiredDisclosures: EvaluatedDisclosure[] = [];
311
+ const optionalDisclosures: EvaluatedDisclosure[] = [];
153
312
 
154
- // Transform disclosures to find claim using JSONPath
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 ? optionalClaimNames : requiredClaimNames).push(
183
- claimName
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 || !credentialsMdoc[0]) {
504
+ if (!credentialsMdoc.length) {
325
505
  throw new CredentialNotFoundError(
326
506
  "mso_mdoc credential is not supported."
327
507
  );
328
508
  }
329
- /**
330
- * The current implementation for the "mso_mdoc" format is temporary and always returns the first credential (mDL).
331
- * [WLEO-266] This will be replaced with the real implementation once the evaluateInputDescriptorForMdoc function is available.
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
- nonce,
409
- client_id
564
+ authRequestObject
410
565
  ) => {
411
- return Promise.all(
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: base64ToBase64Url(item.credential),
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(nonce, client_id, [
426
- item.credential,
427
- item.requestedClaims,
428
- createCryptoContextFor(item.keyTag),
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
- remotePresentations: RemotePresentation[],
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
- remotePresentations,
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
- remotePresentations?.length === 1
163
- ? remotePresentations[0]?.vpToken
164
- : remotePresentations.map(
165
- (remotePresentation) => remotePresentation.vpToken
166
- );
167
-
168
- const descriptor_map = remotePresentations.map(
169
- (remotePresentation, index) => ({
170
- id: remotePresentation.inputDescriptor.id,
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(jwkKeys, requestObject, {
186
- vp_token,
187
- presentation_submission,
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
- return await appFetch(requestObject.response_uri, {
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.parse);
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
- requestedClaims: string[];
20
- inputDescriptor: InputDescriptor;
21
- format: string;
22
- vpToken: string;
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"),