@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.
- package/lib/commonjs/credential/issuance/06-obtain-credential.js +5 -1
- package/lib/commonjs/credential/issuance/06-obtain-credential.js.map +1 -1
- 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 +45 -18
- 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/06-obtain-credential.js +5 -1
- package/lib/module/credential/issuance/06-obtain-credential.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 +45 -18
- 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/06-obtain-credential.d.ts.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 +6 -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/06-obtain-credential.ts +3 -1
- 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 +50 -27
- 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({
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
163
|
-
?
|
164
|
-
:
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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(
|
186
|
-
|
187
|
-
|
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
|
-
|
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.
|
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
|
/**
|