@pagopa/io-react-native-wallet 2.0.0-next.5 → 2.0.0-next.7

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 (72) hide show
  1. package/lib/commonjs/credential/issuance/02-evaluate-issuer-trust.js +6 -1
  2. package/lib/commonjs/credential/issuance/02-evaluate-issuer-trust.js.map +1 -1
  3. package/lib/commonjs/credential/issuance/06-obtain-credential.js.map +1 -1
  4. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js +186 -9
  5. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  6. package/lib/commonjs/credential/issuance/README.md +7 -2
  7. package/lib/commonjs/mdoc/const.js +9 -0
  8. package/lib/commonjs/mdoc/const.js.map +1 -0
  9. package/lib/commonjs/mdoc/converter.js +26 -0
  10. package/lib/commonjs/mdoc/converter.js.map +1 -0
  11. package/lib/commonjs/mdoc/index.js +74 -0
  12. package/lib/commonjs/mdoc/index.js.map +1 -0
  13. package/lib/commonjs/mdoc/utils.js +14 -0
  14. package/lib/commonjs/mdoc/utils.js.map +1 -0
  15. package/lib/commonjs/trust/types.js +2 -1
  16. package/lib/commonjs/trust/types.js.map +1 -1
  17. package/lib/commonjs/utils/crypto.js +35 -1
  18. package/lib/commonjs/utils/crypto.js.map +1 -1
  19. package/lib/module/credential/issuance/02-evaluate-issuer-trust.js +6 -1
  20. package/lib/module/credential/issuance/02-evaluate-issuer-trust.js.map +1 -1
  21. package/lib/module/credential/issuance/06-obtain-credential.js.map +1 -1
  22. package/lib/module/credential/issuance/07-verify-and-parse-credential.js +187 -10
  23. package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  24. package/lib/module/credential/issuance/README.md +7 -2
  25. package/lib/module/mdoc/const.js +2 -0
  26. package/lib/module/mdoc/const.js.map +1 -0
  27. package/lib/module/mdoc/converter.js +20 -0
  28. package/lib/module/mdoc/converter.js.map +1 -0
  29. package/lib/module/mdoc/index.js +67 -0
  30. package/lib/module/mdoc/index.js.map +1 -0
  31. package/lib/module/mdoc/utils.js +7 -0
  32. package/lib/module/mdoc/utils.js.map +1 -0
  33. package/lib/module/trust/types.js +2 -1
  34. package/lib/module/trust/types.js.map +1 -1
  35. package/lib/module/utils/crypto.js +32 -0
  36. package/lib/module/utils/crypto.js.map +1 -1
  37. package/lib/typescript/credential/issuance/02-evaluate-issuer-trust.d.ts.map +1 -1
  38. package/lib/typescript/credential/issuance/06-obtain-credential.d.ts +2 -1
  39. package/lib/typescript/credential/issuance/06-obtain-credential.d.ts.map +1 -1
  40. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts +8 -9
  41. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
  42. package/lib/typescript/mdoc/const.d.ts +2 -0
  43. package/lib/typescript/mdoc/const.d.ts.map +1 -0
  44. package/lib/typescript/mdoc/converter.d.ts +8 -0
  45. package/lib/typescript/mdoc/converter.d.ts.map +1 -0
  46. package/lib/typescript/mdoc/index.d.ts +5 -0
  47. package/lib/typescript/mdoc/index.d.ts.map +1 -0
  48. package/lib/typescript/mdoc/utils.d.ts +7 -0
  49. package/lib/typescript/mdoc/utils.d.ts.map +1 -0
  50. package/lib/typescript/trust/build-chain.d.ts +2 -2
  51. package/lib/typescript/trust/types.d.ts +161 -26
  52. package/lib/typescript/trust/types.d.ts.map +1 -1
  53. package/lib/typescript/utils/crypto.d.ts +16 -0
  54. package/lib/typescript/utils/crypto.d.ts.map +1 -1
  55. package/package.json +13 -11
  56. package/src/credential/issuance/02-evaluate-issuer-trust.ts +2 -1
  57. package/src/credential/issuance/06-obtain-credential.ts +2 -1
  58. package/src/credential/issuance/07-verify-and-parse-credential.ts +257 -22
  59. package/src/credential/issuance/README.md +7 -2
  60. package/src/mdoc/const.ts +1 -0
  61. package/src/mdoc/converter.ts +26 -0
  62. package/src/mdoc/index.ts +93 -0
  63. package/src/mdoc/utils.ts +7 -0
  64. package/src/trust/types.ts +5 -1
  65. package/src/utils/crypto.ts +39 -1
  66. package/lib/commonjs/credential/issuance/const.js +0 -14
  67. package/lib/commonjs/credential/issuance/const.js.map +0 -1
  68. package/lib/module/credential/issuance/const.js +0 -4
  69. package/lib/module/credential/issuance/const.js.map +0 -1
  70. package/lib/typescript/credential/issuance/const.d.ts +0 -5
  71. package/lib/typescript/credential/issuance/const.d.ts.map +0 -1
  72. package/src/credential/issuance/const.ts +0 -11
@@ -1,4 +1,5 @@
1
1
  import { type CryptoContext } from "@pagopa/io-react-native-jwt";
2
+ import { JWK } from "./jwk";
2
3
  /**
3
4
  * Create a CryptoContext bound to a key pair.
4
5
  * Key pair is supposed to exist already in the device's keychain.
@@ -16,4 +17,19 @@ export declare const createCryptoContextFor: (keytag: string) => CryptoContext;
16
17
  * @returns The returned value of the input procedure.
17
18
  */
18
19
  export declare const withEphemeralKey: <R>(fn: (ephemeralContext: CryptoContext) => Promise<R>) => Promise<R>;
20
+ /**
21
+ * Converts a base64-encoded DER certificate to PEM format.
22
+ *
23
+ * @param certificate - The base64-encoded DER certificate.
24
+ * @returns The PEM-formatted certificate.
25
+ */
26
+ export declare const convertBase64DerToPem: (certificate: string) => string;
27
+ /**
28
+ * Retrieves the signing JWK from a PEM-formatted certificate.
29
+ *
30
+ * @param pemCert - The PEM-formatted certificate.
31
+ * @returns The signing JWK.
32
+ * @throws Will throw an error if the public key is unsupported.
33
+ */
34
+ export declare const getSigninJwkFromCert: (pemCert: string) => JWK;
19
35
  //# sourceMappingURL=crypto.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAE7E;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,WAAY,MAAM,KAAG,aAsBvD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,6BACJ,aAAa,8BAOrC,CAAC"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAOA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAI5B;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,WAAY,MAAM,KAAG,aAsBvD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,6BACJ,aAAa,8BAOrC,CAAC;AACF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,gBAAiB,MAAM,KAAG,MACc,CAAC;AAE3E;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,YAAa,MAAM,KAAG,GAkBtD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagopa/io-react-native-wallet",
3
- "version": "2.0.0-next.5",
3
+ "version": "2.0.0-next.7",
4
4
  "description": "Provide data structures, helpers and API for IO Wallet",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -30,7 +30,7 @@
30
30
  "test": "jest",
31
31
  "tsc": "tsc --noEmit",
32
32
  "lint": "eslint . -c .eslintrc.js --ext .ts,.tsx",
33
- "prepack": "bob build",
33
+ "prepack": "yarn generate && bob build",
34
34
  "release": "release-it",
35
35
  "example": "yarn --cwd example",
36
36
  "bootstrap": "yarn example && yarn install",
@@ -53,12 +53,15 @@
53
53
  "registry": "https://registry.npmjs.org/"
54
54
  },
55
55
  "devDependencies": {
56
- "@pagopa/io-react-native-crypto": "^1.2.2",
56
+ "@pagopa/io-react-native-crypto": "^1.2.3",
57
+ "@pagopa/io-react-native-iso18013": "^0.3.0",
57
58
  "@pagopa/io-react-native-jwt": "^2.1.0",
59
+ "@react-native/babel-preset": "0.78.3",
58
60
  "@react-native/eslint-config": "^0.75.5",
59
61
  "@rushstack/eslint-patch": "^1.3.2",
60
- "@types/jest": "^28.1.2",
61
- "@types/react": "^18.2.6",
62
+ "@types/jest": "^29.5.13",
63
+ "@types/jsrsasign": "^10.5.15",
64
+ "@types/react": "^19.0.0",
62
65
  "@types/react-native": "0.70.0",
63
66
  "@types/url-parse": "^1.4.11",
64
67
  "del-cli": "^5.0.0",
@@ -67,16 +70,14 @@
67
70
  "jest": "^28.1.1",
68
71
  "pod-install": "^0.1.0",
69
72
  "prettier": "^3.5.3",
70
- "react": "18.3.1",
71
- "react-native": "0.75.5",
73
+ "react": "19.0.0",
74
+ "react-native": "0.78.3",
72
75
  "react-native-builder-bob": "^0.20.0",
73
76
  "typed-openapi": "^0.4.1",
74
77
  "typescript": "5.0.4"
75
78
  },
76
- "resolutions": {
77
- "@types/react": "^18.2.6"
78
- },
79
79
  "peerDependencies": {
80
+ "@pagopa/io-react-native-iso18013": "*",
80
81
  "@pagopa/io-react-native-crypto": "*",
81
82
  "@pagopa/io-react-native-jwt": "*",
82
83
  "react": "*",
@@ -93,7 +94,7 @@
93
94
  "<rootDir>/lib/"
94
95
  ],
95
96
  "transformIgnorePatterns": [
96
- "node_modules/(?!(jest-)?@react-native|react-native|uuid)"
97
+ "node_modules/(?!(jest-)?@react-native|react-native|uuid|@pagopa/io-react-native-iso18013)"
97
98
  ],
98
99
  "setupFiles": [
99
100
  "<rootDir>/jestSetup.js"
@@ -118,6 +119,7 @@
118
119
  "js-base64": "^3.7.7",
119
120
  "js-sha256": "^0.9.0",
120
121
  "jsonpath-plus": "^10.2.0",
122
+ "jsrsasign": "^11.1.0",
121
123
  "parse-url": "^9.2.0",
122
124
  "react-native-url-polyfill": "^2.0.0",
123
125
  "react-native-uuid": "^2.0.1",
@@ -27,6 +27,7 @@ export const evaluateIssuerTrust: EvaluateIssuerTrust = async (
27
27
  ) => {
28
28
  const issuerConf = await getCredentialIssuerEntityConfiguration(issuerUrl, {
29
29
  appFetch: context.appFetch,
30
- }).then((_) => _.payload.metadata);
30
+ }).then(({ payload }) => payload.metadata);
31
+
31
32
  return { issuerConf };
32
33
  };
@@ -18,6 +18,7 @@ import { CredentialResponse, NonceResponse } from "./types";
18
18
  import { createDPopToken } from "../../utils/dpop";
19
19
  import { v4 as uuidv4 } from "uuid";
20
20
  import { LogLevel, Logger } from "../../utils/logging";
21
+ import type { SupportedCredentialFormat } from "../../trust/types";
21
22
 
22
23
  export type ObtainCredential = (
23
24
  issuerConf: Out<EvaluateIssuerTrust>["issuerConf"],
@@ -35,7 +36,7 @@ export type ObtainCredential = (
35
36
  operationType?: "reissuing"
36
37
  ) => Promise<{
37
38
  credential: string;
38
- format: string;
39
+ format: SupportedCredentialFormat;
39
40
  }>;
40
41
 
41
42
  export const createNonceProof = async (
@@ -6,12 +6,22 @@ import { SdJwt4VC, verify as verifySdJwt } from "../../sd-jwt";
6
6
  import { getValueFromDisclosures } from "../../sd-jwt/converters";
7
7
  import { isSameThumbprint, type JWK } from "../../utils/jwk";
8
8
  import type { ObtainCredential } from "./06-obtain-credential";
9
- import { Logger, LogLevel } from "../../utils/logging";
9
+ import { verify as verifyMdoc } from "../../mdoc";
10
+ import { MDOC_DEFAULT_NAMESPACE } from "../../mdoc/const";
11
+ import { getParsedCredentialClaimKey } from "../../mdoc/utils";
12
+ import { LogLevel, Logger } from "../../utils/logging";
13
+ import { extractElementValueAsDate } from "../../mdoc/converter";
14
+ import type { CBOR } from "@pagopa/io-react-native-iso18013";
15
+ import type { PublicKey } from "@pagopa/io-react-native-crypto";
10
16
 
11
17
  type IssuerConf = Out<EvaluateIssuerTrust>["issuerConf"];
12
18
  type CredentialConf =
13
19
  IssuerConf["openid_credential_issuer"]["credential_configurations_supported"][string];
14
20
 
21
+ type DecodedMDocCredential = Out<typeof verifyMdoc> & {
22
+ issuerSigned: CBOR.IssuerSigned;
23
+ };
24
+
15
25
  export type VerifyAndParseCredential = (
16
26
  issuerConf: IssuerConf,
17
27
  credential: Out<ObtainCredential>["credential"],
@@ -26,7 +36,8 @@ export type VerifyAndParseCredential = (
26
36
  * Include attributes that are not explicitly mapped in the issuer configuration.
27
37
  */
28
38
  includeUndefinedAttributes?: boolean;
29
- }
39
+ },
40
+ x509CertRoot?: string
30
41
  ) => Promise<{
31
42
  parsedCredential: ParsedCredential;
32
43
  expiration: Date;
@@ -34,11 +45,9 @@ export type VerifyAndParseCredential = (
34
45
  }>;
35
46
 
36
47
  // The credential as a collection of attributes in plain value
37
- type ParsedCredential = Record<
48
+ type ParsedCredential = {
38
49
  /** Attribute key */
39
- string,
40
- {
41
- /** Human-readable name of the attribute */
50
+ [claim: string]: {
42
51
  name:
43
52
  | /* if i18n is provided */ Record<
44
53
  string /* locale */,
@@ -46,10 +55,9 @@ type ParsedCredential = Record<
46
55
  >
47
56
  | /* if no i18n is provided */ string
48
57
  | undefined; // Add undefined as a possible value for the name property
49
- /** The actual value of the attribute */
50
58
  value: unknown;
51
- }
52
- >;
59
+ };
60
+ };
53
61
 
54
62
  // handy alias
55
63
  type DecodedSdJwtCredential = Out<typeof verifySdJwt> & {
@@ -139,7 +147,123 @@ const parseCredentialSdJwt = (
139
147
 
140
148
  return definedValues;
141
149
  };
150
+ const parseCredentialMDoc = (
151
+ // the list of supported credentials, as defined in the issuer configuration
152
+ credentialConfig: CredentialConf,
153
+ // credential_type: string,
154
+ { issuerSigned }: DecodedMDocCredential,
155
+ ignoreMissingAttributes: boolean = false,
156
+ includeUndefinedAttributes: boolean = false
157
+ ): ParsedCredential => {
158
+ if (!credentialConfig) {
159
+ throw new IoWalletError("Credential type not supported by the issuer");
160
+ }
161
+
162
+ if (!credentialConfig.claims) {
163
+ throw new IoWalletError("Missing claims in the credential subject");
164
+ }
165
+
166
+ const attrDefinitions = credentialConfig.claims.map<
167
+ [string, string, { name: string; locale: string }[]]
168
+ >(({ path: [namespace, attribute], display }) => [
169
+ namespace as string,
170
+ attribute as string,
171
+ display,
172
+ ]);
173
+
174
+ if (!issuerSigned.nameSpaces) {
175
+ throw new IoWalletError("Missing claims in the credential");
176
+ }
177
+
178
+ const flatNamespaces = Object.entries(issuerSigned.nameSpaces).flatMap(
179
+ ([namespace, values]) =>
180
+ values.map<[string, string, string]>((v) => [
181
+ namespace,
182
+ v.elementIdentifier,
183
+ v.elementValue,
184
+ ])
185
+ );
186
+
187
+ // Check that all mandatory attributes defined in the issuer configuration are present in the disclosure set
188
+ // and filter the non present ones
189
+ const attrsNotInDisclosures = attrDefinitions.filter(
190
+ ([attrDefNamespace, attrKey]) =>
191
+ flatNamespaces.some(
192
+ ([namespace, claim]) =>
193
+ attrDefNamespace === namespace && attrKey === claim
194
+ )
195
+ );
196
+ if (attrsNotInDisclosures.length > 0) {
197
+ const missing = attrsNotInDisclosures
198
+ .map(([, attrKey]) => attrKey)
199
+ .join(", ");
200
+ const received = Object.keys(Object.values(flatNamespaces));
201
+
202
+ if (!ignoreMissingAttributes) {
203
+ throw new IoWalletError(
204
+ `Some attributes are missing in the credential. Missing: [${missing}], received: [${received}]`
205
+ );
206
+ }
207
+ }
208
+
209
+ // Attributes defined in the issuer configuration and present in the disclosure set
210
+ const definedValues = attrDefinitions
211
+ // Retrieve the value from the corresponding disclosure
212
+ .map(
213
+ ([attrDefNamespace, attrKey, display]) =>
214
+ [
215
+ attrDefNamespace,
216
+ attrKey,
217
+ {
218
+ display,
219
+ value: flatNamespaces.find(
220
+ ([namespace, name]) =>
221
+ attrDefNamespace === namespace && name === attrKey
222
+ )?.[2],
223
+ },
224
+ ] as const
225
+ )
226
+ //filter the not found elements
227
+ .filter(([_, __, definition]) => definition.value !== undefined)
228
+ // Add a human-readable attribute name, with i18n, in the form { locale: name }
229
+ // Example: { "it-IT": "Nome", "en-EN": "Name", "es-ES": "Nombre" }
230
+ .reduce<ParsedCredential>(
231
+ (acc, [attrDefNamespace, attrKey, { display, value }]) => ({
232
+ ...acc,
233
+ [getParsedCredentialClaimKey(attrDefNamespace, attrKey)]: {
234
+ value,
235
+ name: display.reduce(
236
+ (names, { locale, name }) => ({
237
+ ...names,
238
+ [locale]: name,
239
+ }),
240
+ {}
241
+ ),
242
+ },
243
+ }),
244
+ {}
245
+ );
246
+
247
+ if (includeUndefinedAttributes) {
248
+ const undefinedValues: ParsedCredential = Object.fromEntries(
249
+ Object.values(flatNamespaces)
250
+ .filter(
251
+ ([namespace, key]) =>
252
+ !definedValues[getParsedCredentialClaimKey(namespace, key)]
253
+ )
254
+ .map(([namespace, key, value]) => [
255
+ getParsedCredentialClaimKey(namespace, key),
256
+ { value, name: key },
257
+ ])
258
+ );
259
+ return {
260
+ ...definedValues,
261
+ ...undefinedValues,
262
+ };
263
+ }
142
264
 
265
+ return definedValues;
266
+ };
143
267
  /**
144
268
  * Given a credential, verify it's in the supported format
145
269
  * and the credential is correctly signed
@@ -176,6 +300,48 @@ async function verifyCredentialSdJwt(
176
300
 
177
301
  return decodedCredential;
178
302
  }
303
+ /**
304
+ * Given a credential, verify it's in the supported format
305
+ * and the credential is correctly signed
306
+ * and it's bound to the given key
307
+ *
308
+ * @param rawCredential The received credential
309
+ * @param issuerKeys The set of public keys of the issuer,
310
+ * which will be used to verify the signature
311
+ * @param holderBindingContext The access to the holder's key
312
+ *
313
+ * @throws If the signature verification fails
314
+ * @throws If the credential is not in the SdJwt4VC format
315
+ * @throws If the holder binding is not properly configured
316
+ *
317
+ */
318
+ async function verifyCredentialMDoc(
319
+ rawCredential: string,
320
+ x509CertRoot: string,
321
+ holderBindingContext: CryptoContext
322
+ ): Promise<DecodedMDocCredential> {
323
+ const [decodedCredential, holderBindingKey] =
324
+ // parallel for optimization
325
+ await Promise.all([
326
+ verifyMdoc(rawCredential, x509CertRoot),
327
+ holderBindingContext.getPublicKey(),
328
+ ]);
329
+
330
+ if (!decodedCredential) {
331
+ throw new IoWalletError("No MDOC credentials found!");
332
+ }
333
+
334
+ const key =
335
+ decodedCredential.issuerSigned.issuerAuth.payload.deviceKeyInfo.deviceKey;
336
+
337
+ if (!(await isSameThumbprint(key, holderBindingKey as PublicKey))) {
338
+ throw new IoWalletError(
339
+ `Failed to verify holder binding, holder binding key and mDoc deviceKey don't match`
340
+ );
341
+ }
342
+
343
+ return decodedCredential;
344
+ }
179
345
 
180
346
  const verifyAndParseCredentialSdJwt: VerifyAndParseCredential = async (
181
347
  issuerConf,
@@ -231,6 +397,60 @@ const verifyAndParseCredentialSdJwt: VerifyAndParseCredential = async (
231
397
  };
232
398
  };
233
399
 
400
+ const verifyAndParseCredentialMDoc: VerifyAndParseCredential = async (
401
+ issuerConf,
402
+ credential,
403
+ credentialConfigurationId,
404
+ { credentialCryptoContext, ignoreMissingAttributes },
405
+ x509CertRoot
406
+ ) => {
407
+ if (!x509CertRoot) {
408
+ throw new IoWalletError("Missing x509CertRoot");
409
+ }
410
+
411
+ const decoded = await verifyCredentialMDoc(
412
+ credential,
413
+ x509CertRoot,
414
+ credentialCryptoContext
415
+ );
416
+
417
+ const credentialConfig =
418
+ issuerConf.openid_credential_issuer.credential_configurations_supported[
419
+ credentialConfigurationId
420
+ ]!;
421
+ const parsedCredential = parseCredentialMDoc(
422
+ credentialConfig,
423
+ decoded,
424
+ ignoreMissingAttributes,
425
+ ignoreMissingAttributes
426
+ );
427
+
428
+ const expirationDate = extractElementValueAsDate(
429
+ parsedCredential?.[
430
+ getParsedCredentialClaimKey(MDOC_DEFAULT_NAMESPACE, "expiry_date")
431
+ ]?.value as string
432
+ );
433
+ if (!expirationDate) {
434
+ throw new IoWalletError(`expirationDate must be present!!`);
435
+ }
436
+ expirationDate.setDate(expirationDate.getDate() + 1);
437
+
438
+ const maybeIssuedAt = extractElementValueAsDate(
439
+ parsedCredential?.[
440
+ getParsedCredentialClaimKey(MDOC_DEFAULT_NAMESPACE, "issue_date")
441
+ ]?.value as string
442
+ );
443
+ maybeIssuedAt?.setDate(maybeIssuedAt.getDate() + 1);
444
+
445
+ return {
446
+ parsedCredential,
447
+ credential,
448
+ credentialConfigurationId,
449
+ expiration: expirationDate,
450
+ issuedAt: maybeIssuedAt ?? undefined,
451
+ };
452
+ };
453
+
234
454
  /**
235
455
  * Verify and parse an encoded credential.
236
456
  * @param issuerConf The Issuer configuration returned by {@link evaluateIssuerTrust}
@@ -248,24 +468,39 @@ export const verifyAndParseCredential: VerifyAndParseCredential = async (
248
468
  issuerConf,
249
469
  credential,
250
470
  credentialConfigurationId,
251
- context
471
+ context,
472
+ x509CertRoot
252
473
  ) => {
253
474
  const format =
254
475
  issuerConf.openid_credential_issuer.credential_configurations_supported[
255
476
  credentialConfigurationId
256
477
  ]?.format;
257
478
 
258
- if (format === "dc+sd-jwt") {
259
- Logger.log(LogLevel.DEBUG, "Parsing credential in dc+sd-jwt format");
260
- return verifyAndParseCredentialSdJwt(
261
- issuerConf,
262
- credential,
263
- credentialConfigurationId,
264
- context
265
- );
266
- }
479
+ switch (format) {
480
+ case "dc+sd-jwt": {
481
+ Logger.log(LogLevel.DEBUG, "Parsing credential in dc+sd-jwt format");
482
+ return verifyAndParseCredentialSdJwt(
483
+ issuerConf,
484
+ credential,
485
+ credentialConfigurationId,
486
+ context
487
+ );
488
+ }
489
+ case "mso_mdoc": {
490
+ Logger.log(LogLevel.DEBUG, "Parsing credential in mso_mdoc format");
491
+ return verifyAndParseCredentialMDoc(
492
+ issuerConf,
493
+ credential,
494
+ credentialConfigurationId,
495
+ context,
496
+ x509CertRoot
497
+ );
498
+ }
267
499
 
268
- const message = `Unsupported credential format: ${format}`;
269
- Logger.log(LogLevel.ERROR, message);
270
- throw new IoWalletError(message);
500
+ default: {
501
+ const message = `Unsupported credential format: ${format}`;
502
+ Logger.log(LogLevel.ERROR, message);
503
+ throw new IoWalletError(message);
504
+ }
505
+ }
271
506
  };
@@ -171,7 +171,7 @@ const { credential_configuration_id, credential_identifiers } =
171
171
  accessToken.authorization_details[0]!;
172
172
 
173
173
  // Obtain the credential
174
- const { credential } = await Credential.Issuance.obtainCredential(
174
+ const { credential, format } = await Credential.Issuance.obtainCredential(
175
175
  issuerConf,
176
176
  accessToken,
177
177
  clientId,
@@ -186,6 +186,10 @@ const { credential } = await Credential.Issuance.obtainCredential(
186
186
  }
187
187
  );
188
188
 
189
+ // The certificate below is required to perform the `x5chain` validation of credentials in `mdoc` format.
190
+ // In a real-world scenario, it must be obtained from the appropriate endpoint exposed by the Trust Anchor
191
+ const mockX509CertRoot = format === "mso_mdoc" ? "base64encodedX509CertRoot" : undefined
192
+
189
193
  /*
190
194
  * Parse and verify the credential. The ignoreMissingAttributes flag must be set to false or omitted in production.
191
195
  * WARNING: includeUndefinedAttributes should not be set to true in production in order to get only claims explicitly declared by the issuer.
@@ -199,7 +203,8 @@ const { parsedCredential } =
199
203
  credentialCryptoContext,
200
204
  ignoreMissingAttributes: true,
201
205
  includeUndefinedAttributes: false
202
- }
206
+ },
207
+ mockX509CertRoot
203
208
  );
204
209
 
205
210
  const credentialType =
@@ -0,0 +1 @@
1
+ export const MDOC_DEFAULT_NAMESPACE = "org.iso.18013.5.1";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Extracts the date value of a given elementIdentifier from an MDOC object.
3
+ * Searches through the issuerSigned namespaces and attempts to parse the value as a Date.
4
+ * The expected date format is "YYYY-MM-DD".
5
+ * Returns the Date object if found, otherwise returns null.
6
+ */
7
+ export function extractElementValueAsDate(elementValue: string): Date | null {
8
+ if (typeof elementValue === "string") {
9
+ const dateParts = elementValue.split("-");
10
+ if (dateParts.length === 3) {
11
+ const [year, month, day] = dateParts.map(Number);
12
+ if (
13
+ day !== undefined &&
14
+ month !== undefined &&
15
+ year !== undefined &&
16
+ !isNaN(day) &&
17
+ !isNaN(month) &&
18
+ !isNaN(year)
19
+ ) {
20
+ return new Date(year, month - 1, day); // Month is zero-based in JS Date
21
+ }
22
+ }
23
+ }
24
+
25
+ return null; // Return null if no matching element is found or it's not a valid date
26
+ }
@@ -0,0 +1,93 @@
1
+ import { CBOR, COSE } from "@pagopa/io-react-native-iso18013";
2
+ import { b64utob64 } from "jsrsasign";
3
+ import {
4
+ verifyCertificateChain,
5
+ type CertificateValidationResult,
6
+ type PublicKey,
7
+ type X509CertificateOptions,
8
+ } from "@pagopa/io-react-native-crypto";
9
+ import { MissingX509CertsError, X509ValidationError } from "../trust/errors";
10
+ import { IoWalletError } from "../utils/errors";
11
+ import { convertBase64DerToPem, getSigninJwkFromCert } from "../utils/crypto";
12
+
13
+ export const verify = async (
14
+ token: string,
15
+ x509CertRoot: string
16
+ ): Promise<{ issuerSigned: CBOR.IssuerSigned }> => {
17
+ // get decoded data
18
+ const issuerSigned = await CBOR.decodeIssuerSigned(token);
19
+
20
+ if (!issuerSigned) {
21
+ throw new IoWalletError("Invalid mDoc");
22
+ }
23
+
24
+ if (
25
+ !issuerSigned.issuerAuth.unprotectedHeader?.x5chain &&
26
+ (!Array.isArray(issuerSigned.issuerAuth.unprotectedHeader.x5chain) ||
27
+ issuerSigned.issuerAuth.unprotectedHeader.x5chain.length === 0)
28
+ ) {
29
+ throw new MissingX509CertsError("Missing x509 certificates");
30
+ }
31
+ const x5chain =
32
+ issuerSigned.issuerAuth.unprotectedHeader.x5chain.map(b64utob64);
33
+ // Verify the x5chain
34
+ await verifyX5chain(x5chain, x509CertRoot);
35
+
36
+ const coseSign1 = issuerSigned.issuerAuth.rawValue;
37
+
38
+ if (!coseSign1) {
39
+ throw new IoWalletError("Missing coseSign1");
40
+ }
41
+ // Once the x5chain is verified, the signatures verification can be performed
42
+ await verifyMdocSignature(coseSign1, x5chain[0]!);
43
+
44
+ return { issuerSigned };
45
+ };
46
+
47
+ /**
48
+ * This function checks whether the x509 certificate chain is valid against a specified Certificate Authority (CA)
49
+ *
50
+ * @param x5chain The mdoc's x509 certificate chain
51
+ * @param x509CertRoot The Trust Anchor CA
52
+ * @param options Options for certificate validation
53
+ */
54
+ const verifyX5chain = async (
55
+ x5chain: string[],
56
+ x509CertRoot: string,
57
+ options: X509CertificateOptions = {
58
+ connectTimeout: 10000,
59
+ readTimeout: 10000,
60
+ requireCrl: true,
61
+ }
62
+ ) => {
63
+ const x509ValidationResult: CertificateValidationResult =
64
+ await verifyCertificateChain(x5chain, x509CertRoot, options);
65
+
66
+ if (!x509ValidationResult.isValid) {
67
+ throw new X509ValidationError(
68
+ `X.509 certificate chain validation failed. Status: ${x509ValidationResult.validationStatus}. Error: ${x509ValidationResult.errorMessage}`,
69
+ {
70
+ x509ValidationStatus: x509ValidationResult.validationStatus,
71
+ x509ErrorMessage: x509ValidationResult.errorMessage,
72
+ }
73
+ );
74
+ }
75
+ };
76
+ /**
77
+ * This function verifies that the signature is valid for the given certificate.
78
+ * If not, it throws an error
79
+ *
80
+ * @param coseSign1 The COSE-Sign1 object encoded in base64 or base64url
81
+ * @param cert The `x5chain`'s leaf certificate
82
+ */
83
+ const verifyMdocSignature = async (coseSign1: string, cert: string) => {
84
+ const pemcert = convertBase64DerToPem(cert);
85
+ const jwk = getSigninJwkFromCert(pemcert);
86
+
87
+ jwk.x = b64utob64(jwk.x!);
88
+ jwk.y = b64utob64(jwk.y!);
89
+
90
+ const signatureCorrect = await COSE.verify(coseSign1, jwk as PublicKey);
91
+
92
+ if (!signatureCorrect) throw new Error("Invalid mDoc signature");
93
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @param namespace The mdoc credential `namespace`
3
+ * @param key The claim attribute key
4
+ * @returns A string consisting of the concatenation of the namespace and the claim key, separated by a colon
5
+ */
6
+ export const getParsedCredentialClaimKey = (namespace: string, key: string) =>
7
+ `${namespace}:${key}`;
@@ -38,7 +38,7 @@ const CredentialIssuerDisplayMetadata = z.object({
38
38
 
39
39
  type ClaimsMetadata = z.infer<typeof ClaimsMetadata>;
40
40
  const ClaimsMetadata = z.object({
41
- path: z.array(z.string()),
41
+ path: z.array(z.union([z.string(), z.number(), z.null()])), // https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-15.html#name-claims-path-pointer
42
42
  display: z.array(CredentialDisplayMetadata),
43
43
  });
44
44
 
@@ -71,6 +71,10 @@ const SupportedCredentialMetadata = z.intersection(
71
71
  })
72
72
  );
73
73
 
74
+ export type SupportedCredentialFormat = z.infer<
75
+ typeof SupportedCredentialMetadata
76
+ >["format"];
77
+
74
78
  export type EntityStatement = z.infer<typeof EntityStatement>;
75
79
  export const EntityStatement = z.object({
76
80
  header: z.object({