@pagopa/io-react-native-wallet 1.5.0 → 1.6.1

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 (64) hide show
  1. package/lib/commonjs/credential/issuance/06-obtain-credential.js +5 -1
  2. package/lib/commonjs/credential/issuance/06-obtain-credential.js.map +1 -1
  3. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js +33 -21
  4. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  5. package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js +192 -58
  6. package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
  7. package/lib/commonjs/credential/presentation/08-send-authorization-response.js +45 -18
  8. package/lib/commonjs/credential/presentation/08-send-authorization-response.js.map +1 -1
  9. package/lib/commonjs/credential/presentation/types.js +1 -1
  10. package/lib/commonjs/credential/presentation/types.js.map +1 -1
  11. package/lib/commonjs/entity/trust/chain.js.map +1 -1
  12. package/lib/commonjs/mdoc/index.js +45 -13
  13. package/lib/commonjs/mdoc/index.js.map +1 -1
  14. package/lib/commonjs/utils/crypto.js +70 -4
  15. package/lib/commonjs/utils/crypto.js.map +1 -1
  16. package/lib/commonjs/utils/string.js +4 -4
  17. package/lib/commonjs/utils/string.js.map +1 -1
  18. package/lib/module/credential/issuance/06-obtain-credential.js +5 -1
  19. package/lib/module/credential/issuance/06-obtain-credential.js.map +1 -1
  20. package/lib/module/credential/issuance/07-verify-and-parse-credential.js +33 -21
  21. package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  22. package/lib/module/credential/presentation/07-evaluate-input-descriptor.js +186 -55
  23. package/lib/module/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
  24. package/lib/module/credential/presentation/08-send-authorization-response.js +45 -18
  25. package/lib/module/credential/presentation/08-send-authorization-response.js.map +1 -1
  26. package/lib/module/credential/presentation/types.js +1 -1
  27. package/lib/module/credential/presentation/types.js.map +1 -1
  28. package/lib/module/entity/trust/chain.js.map +1 -1
  29. package/lib/module/mdoc/index.js +43 -12
  30. package/lib/module/mdoc/index.js.map +1 -1
  31. package/lib/module/utils/crypto.js +67 -2
  32. package/lib/module/utils/crypto.js.map +1 -1
  33. package/lib/module/utils/string.js +4 -4
  34. package/lib/module/utils/string.js.map +1 -1
  35. package/lib/typescript/credential/issuance/06-obtain-credential.d.ts.map +1 -1
  36. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts +1 -1
  37. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
  38. package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts +49 -13
  39. package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts.map +1 -1
  40. package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts +6 -2
  41. package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts.map +1 -1
  42. package/lib/typescript/credential/presentation/types.d.ts +10 -7
  43. package/lib/typescript/credential/presentation/types.d.ts.map +1 -1
  44. package/lib/typescript/entity/trust/chain.d.ts.map +1 -1
  45. package/lib/typescript/mdoc/index.d.ts +6 -2
  46. package/lib/typescript/mdoc/index.d.ts.map +1 -1
  47. package/lib/typescript/utils/crypto.d.ts +8 -0
  48. package/lib/typescript/utils/crypto.d.ts.map +1 -1
  49. package/lib/typescript/utils/errors.d.ts.map +1 -1
  50. package/lib/typescript/utils/misc.d.ts.map +1 -1
  51. package/lib/typescript/utils/string.d.ts +3 -3
  52. package/lib/typescript/utils/string.d.ts.map +1 -1
  53. package/package.json +14 -12
  54. package/src/credential/issuance/06-obtain-credential.ts +3 -1
  55. package/src/credential/issuance/07-verify-and-parse-credential.ts +37 -16
  56. package/src/credential/presentation/07-evaluate-input-descriptor.ts +278 -97
  57. package/src/credential/presentation/08-send-authorization-response.ts +50 -27
  58. package/src/credential/presentation/types.ts +9 -6
  59. package/src/entity/trust/chain.ts +14 -10
  60. package/src/mdoc/index.ts +72 -15
  61. package/src/utils/crypto.ts +61 -2
  62. package/src/utils/errors.ts +2 -2
  63. package/src/utils/misc.ts +2 -2
  64. 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({
@@ -29,6 +30,9 @@ export const AuthorizationResponse = z.object({
29
30
  * Selects a public key (with `use = enc`) from the set of JWK keys
30
31
  * offered by the Relying Party (RP) for encryption.
31
32
  *
33
+ * Preference is given to EC keys (P-256 or P-384), followed by RSA keys,
34
+ * based on compatibility and common usage for encryption.
35
+ *
32
36
  * @param rpJwkKeys - The array of JWKs retrieved from the RP entity configuration.
33
37
  * @returns The first suitable public key found in the list.
34
38
  * @throws {NoSuitableKeysFoundInEntityConfiguration} If no suitable encryption key is found.
@@ -36,7 +40,18 @@ export const AuthorizationResponse = z.object({
36
40
  export const choosePublicKeyToEncrypt = (
37
41
  rpJwkKeys: Out<FetchJwks>["keys"]
38
42
  ): JWK => {
39
- const [encKey] = rpJwkKeys.filter((jwk) => jwk.use === "enc");
43
+ // First try to find RSA keys which are more commonly used for encryption
44
+ const encKeys = rpJwkKeys.filter((jwk) => jwk.use === "enc");
45
+
46
+ // Prioritize EC keys first, then fall back to RSA keys if needed
47
+ // io-react-native-jwt support only EC keys with P-256 or P-384 curves
48
+ const ecEncKeys = encKeys.filter(
49
+ (jwk) => jwk.kty === "EC" && (jwk.crv === "P-256" || jwk.crv === "P-384")
50
+ );
51
+ const rsaEncKeys = encKeys.filter((jwk) => jwk.kty === "RSA");
52
+
53
+ // Select the first available key based on priority
54
+ const encKey = ecEncKeys[0] || rsaEncKeys[0] || encKeys[0];
40
55
 
41
56
  if (encKey) {
42
57
  return encKey;
@@ -60,7 +75,7 @@ export const buildDirectPostBody = async (
60
75
  payload: DirectAuthorizationBodyPayload
61
76
  ): Promise<string> => {
62
77
  const formUrlEncodedBody = new URLSearchParams({
63
- state: requestObject.state,
78
+ ...(requestObject.state ? { state: requestObject.state } : {}),
64
79
  ...Object.fromEntries(
65
80
  Object.entries(payload).map(([key, value]) => {
66
81
  return [
@@ -82,13 +97,15 @@ export const buildDirectPostBody = async (
82
97
  * @param jwkKeys - Array of JWKs from the Relying Party for encryption.
83
98
  * @param requestObject - Contains state, nonce, and other relevant info.
84
99
  * @param payload - Object that contains either the VP token to encrypt and the mapping of the credential disclosures or the error code
100
+ * @param generatedNonce - Optional nonce for the `apu` claim in the JWE header, it is used during ISO 18013-7.
85
101
  * @returns A URL-encoded string for an `application/x-www-form-urlencoded` POST body,
86
102
  * where `response` contains the encrypted JWE.
87
103
  */
88
104
  export const buildDirectPostJwtBody = async (
89
105
  jwkKeys: Out<FetchJwks>["keys"],
90
106
  requestObject: Out<VerifyRequestObjectSignature>["requestObject"],
91
- payload: DirectAuthorizationBodyPayload
107
+ payload: DirectAuthorizationBodyPayload,
108
+ generatedNonce?: string
92
109
  ): Promise<string> => {
93
110
  // Prepare the authorization response payload to be encrypted
94
111
  const authzResponsePayload = JSON.stringify({
@@ -96,9 +113,7 @@ export const buildDirectPostJwtBody = async (
96
113
  ...payload,
97
114
  });
98
115
 
99
- // Choose a suitable RSA public key for encryption
100
116
  const encPublicJwk = choosePublicKeyToEncrypt(jwkKeys);
101
-
102
117
  // Encrypt the authorization payload
103
118
  const { client_metadata } = requestObject;
104
119
  const encryptedResponse = await new EncryptJwe(authzResponsePayload, {
@@ -111,12 +126,15 @@ export const buildDirectPostJwtBody = async (
111
126
  | "A256CBC-HS512"
112
127
  | "A128CBC-HS256") || "A256CBC-HS512",
113
128
  kid: encPublicJwk.kid,
129
+ /* ISO 18013-7 */
130
+ apv: Base64.encodeURI(requestObject.nonce),
131
+ ...(generatedNonce ? { apu: Base64.encodeURI(generatedNonce) } : {}),
114
132
  }).encrypt(encPublicJwk);
115
133
 
116
134
  // Build the x-www-form-urlencoded form body
117
135
  const formBody = new URLSearchParams({
118
136
  response: encryptedResponse,
119
- state: requestObject.state,
137
+ ...(requestObject.state ? { state: requestObject.state } : {}),
120
138
  });
121
139
  return formBody.toString();
122
140
  };
@@ -129,7 +147,7 @@ export type SendAuthorizationResponse = (
129
147
  requestObject: Out<VerifyRequestObjectSignature>["requestObject"],
130
148
  presentationDefinitionId: string,
131
149
  jwkKeys: Out<FetchJwks>["keys"],
132
- remotePresentations: RemotePresentation[],
150
+ remotePresentation: RemotePresentation,
133
151
  context?: {
134
152
  appFetch?: GlobalFetch["fetch"];
135
153
  }
@@ -150,28 +168,25 @@ export const sendAuthorizationResponse: SendAuthorizationResponse = async (
150
168
  requestObject,
151
169
  presentationDefinitionId,
152
170
  jwkKeys,
153
- remotePresentations,
171
+ remotePresentation,
154
172
  { appFetch = fetch } = {}
155
173
  ): Promise<AuthorizationResponse> => {
174
+ const { generatedNonce, presentations } = remotePresentation;
156
175
  /**
157
176
  * 1. Prepare the VP token and presentation submission
158
177
  * If there is only one credential, `vpToken` is a single string.
159
178
  * If there are multiple credential, `vpToken` is an array of string.
160
179
  **/
161
180
  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
- );
181
+ presentations?.length === 1
182
+ ? presentations[0]?.vpToken
183
+ : presentations.map((presentation) => presentation.vpToken);
184
+
185
+ const descriptor_map = presentations.map((presentation, index) => ({
186
+ id: presentation.inputDescriptor.id,
187
+ path: presentations?.length === 1 ? `$` : `$[${index}]`,
188
+ format: presentation.format,
189
+ }));
175
190
 
176
191
  const presentation_submission = {
177
192
  id: uuid.v4(),
@@ -182,17 +197,22 @@ export const sendAuthorizationResponse: SendAuthorizationResponse = async (
182
197
  // 2. Choose the appropriate request body builder based on response mode
183
198
  const requestBody =
184
199
  requestObject.response_mode === "direct_post.jwt"
185
- ? await buildDirectPostJwtBody(jwkKeys, requestObject, {
186
- vp_token,
187
- presentation_submission,
188
- })
200
+ ? await buildDirectPostJwtBody(
201
+ jwkKeys,
202
+ requestObject,
203
+ {
204
+ vp_token,
205
+ presentation_submission,
206
+ },
207
+ generatedNonce
208
+ )
189
209
  : await buildDirectPostBody(requestObject, {
190
210
  vp_token,
191
211
  presentation_submission: presentation_submission,
192
212
  });
193
213
 
194
214
  // 3. Send the authorization response via HTTP POST and validate the response
195
- return await appFetch(requestObject.response_uri, {
215
+ const authResponse = await appFetch(requestObject.response_uri, {
196
216
  method: "POST",
197
217
  headers: {
198
218
  "Content-Type": "application/x-www-form-urlencoded",
@@ -201,7 +221,10 @@ export const sendAuthorizationResponse: SendAuthorizationResponse = async (
201
221
  })
202
222
  .then(hasStatusOrThrow(200))
203
223
  .then((res) => res.json())
204
- .then(AuthorizationResponse.parse);
224
+ .then(AuthorizationResponse.safeParse);
225
+
226
+ // Some Relying Parties may return an empty body.
227
+ return authResponse.success ? authResponse.data : {};
205
228
  };
206
229
 
207
230
  /**