@pagopa/io-react-native-wallet 1.2.2 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. package/lib/commonjs/credential/presentation/01-start-flow.js +12 -28
  2. package/lib/commonjs/credential/presentation/01-start-flow.js.map +1 -1
  3. package/lib/commonjs/credential/presentation/04-retrieve-rp-jwks.js +96 -24
  4. package/lib/commonjs/credential/presentation/04-retrieve-rp-jwks.js.map +1 -1
  5. package/lib/commonjs/credential/presentation/05-verify-request-object.js +7 -2
  6. package/lib/commonjs/credential/presentation/05-verify-request-object.js.map +1 -1
  7. package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js +9 -5
  8. package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
  9. package/lib/commonjs/credential/presentation/08-send-authorization-response.js +2 -2
  10. package/lib/commonjs/credential/presentation/08-send-authorization-response.js.map +1 -1
  11. package/lib/commonjs/credential/presentation/README.md +4 -4
  12. package/lib/commonjs/credential/presentation/errors.js +2 -19
  13. package/lib/commonjs/credential/presentation/errors.js.map +1 -1
  14. package/lib/commonjs/credential/presentation/types.js +7 -1
  15. package/lib/commonjs/credential/presentation/types.js.map +1 -1
  16. package/lib/commonjs/utils/crypto.js +41 -1
  17. package/lib/commonjs/utils/crypto.js.map +1 -1
  18. package/lib/module/credential/presentation/01-start-flow.js +12 -28
  19. package/lib/module/credential/presentation/01-start-flow.js.map +1 -1
  20. package/lib/module/credential/presentation/04-retrieve-rp-jwks.js +96 -24
  21. package/lib/module/credential/presentation/04-retrieve-rp-jwks.js.map +1 -1
  22. package/lib/module/credential/presentation/05-verify-request-object.js +7 -2
  23. package/lib/module/credential/presentation/05-verify-request-object.js.map +1 -1
  24. package/lib/module/credential/presentation/07-evaluate-input-descriptor.js +9 -5
  25. package/lib/module/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
  26. package/lib/module/credential/presentation/08-send-authorization-response.js +2 -2
  27. package/lib/module/credential/presentation/08-send-authorization-response.js.map +1 -1
  28. package/lib/module/credential/presentation/README.md +4 -4
  29. package/lib/module/credential/presentation/errors.js +0 -16
  30. package/lib/module/credential/presentation/errors.js.map +1 -1
  31. package/lib/module/credential/presentation/types.js +7 -1
  32. package/lib/module/credential/presentation/types.js.map +1 -1
  33. package/lib/module/utils/crypto.js +38 -0
  34. package/lib/module/utils/crypto.js.map +1 -1
  35. package/lib/typescript/credential/presentation/01-start-flow.d.ts +3 -3
  36. package/lib/typescript/credential/presentation/01-start-flow.d.ts.map +1 -1
  37. package/lib/typescript/credential/presentation/03-get-request-object.d.ts +1 -1
  38. package/lib/typescript/credential/presentation/04-retrieve-rp-jwks.d.ts +16 -9
  39. package/lib/typescript/credential/presentation/04-retrieve-rp-jwks.d.ts.map +1 -1
  40. package/lib/typescript/credential/presentation/05-verify-request-object.d.ts.map +1 -1
  41. package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts +3 -2
  42. package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts.map +1 -1
  43. package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts +1 -1
  44. package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts.map +1 -1
  45. package/lib/typescript/credential/presentation/errors.d.ts +0 -11
  46. package/lib/typescript/credential/presentation/errors.d.ts.map +1 -1
  47. package/lib/typescript/credential/presentation/types.d.ts +242 -3
  48. package/lib/typescript/credential/presentation/types.d.ts.map +1 -1
  49. package/lib/typescript/utils/crypto.d.ts +24 -0
  50. package/lib/typescript/utils/crypto.d.ts.map +1 -1
  51. package/package.json +3 -1
  52. package/src/credential/presentation/01-start-flow.ts +16 -32
  53. package/src/credential/presentation/03-get-request-object.ts +1 -1
  54. package/src/credential/presentation/04-retrieve-rp-jwks.ts +123 -35
  55. package/src/credential/presentation/05-verify-request-object.ts +4 -3
  56. package/src/credential/presentation/07-evaluate-input-descriptor.ts +20 -6
  57. package/src/credential/presentation/08-send-authorization-response.ts +2 -2
  58. package/src/credential/presentation/README.md +4 -4
  59. package/src/credential/presentation/errors.ts +0 -16
  60. package/src/credential/presentation/types.ts +8 -1
  61. package/src/utils/crypto.ts +43 -0
@@ -3,6 +3,12 @@ import { hasStatusOrThrow } from "../../utils/misc";
3
3
  import { RelyingPartyEntityConfiguration } from "../../entity/trust/types";
4
4
  import { decode as decodeJwt } from "@pagopa/io-react-native-jwt";
5
5
  import { NoSuitableKeysFoundInEntityConfiguration } from "./errors";
6
+ import { RequestObject } from "./types";
7
+ import {
8
+ convertCertToPem,
9
+ parsePublicKey,
10
+ getSigningJwk,
11
+ } from "../../utils/crypto";
6
12
 
7
13
  /**
8
14
  * Defines the signature for a function that retrieves JSON Web Key Sets (JWKS) from a client.
@@ -16,54 +22,136 @@ export type FetchJwks<T extends Array<unknown> = []> = (...args: T) => Promise<{
16
22
  }>;
17
23
 
18
24
  /**
19
- * Retrieves the JSON Web Key Set (JWKS) from the specified client's well-known endpoint.
20
- * It is formed using `{issUrl.base}/.well-known/jar-issuer${issUrl.pah}` as explained in SD-JWT VC issuer metadata section
25
+ * Fetches and parses JWKS from a given URI.
21
26
  *
22
- * @param requestObjectEncodedJwt - Request Object in JWT format.
23
- * @param options - Optional context containing a custom fetch implementation.
24
- * @param options.context - Optional context object.
25
- * @param options.context.appFetch - Optional custom fetch function to use instead of the global `fetch`.
26
- * @returns A promise resolving to an object containing an array of JWKs.
27
- * @throws Will throw an error if the JWKS retrieval fails.
27
+ * @param jwksUri - The JWKS URI.
28
+ * @param fetchFn - The fetch function to use.
29
+ * @returns An array of JWKs.
30
+ */
31
+ const fetchJwksFromUri = async (
32
+ jwksUri: string,
33
+ appFetch: GlobalFetch["fetch"]
34
+ ): Promise<JWK[]> => {
35
+ const jwks = await appFetch(jwksUri, {
36
+ method: "GET",
37
+ })
38
+ .then(hasStatusOrThrow(200))
39
+ .then((raw) => raw.json())
40
+ .then((json) => (json.jwks ? JWKS.parse(json.jwks) : JWKS.parse(json)));
41
+ return jwks.keys;
42
+ };
43
+
44
+ /**
45
+ * Retrieves JWKS when the client ID scheme includes x509 SAN DNS.
46
+ *
47
+ * @param decodedJwt - The decoded JWT.
48
+ * @param fetchFn - The fetch function to use.
49
+ * @returns An array of JWKs.
50
+ * @throws Will throw an error if no suitable keys are found.
51
+ */
52
+ const getJwksFromX509Cert = async (certChain: string[]): Promise<JWK[]> => {
53
+ if (!Array.isArray(certChain) || certChain.length === 0 || !certChain[0]) {
54
+ throw new NoSuitableKeysFoundInEntityConfiguration(
55
+ "No RP encrypt key found!"
56
+ );
57
+ }
58
+
59
+ const pemCert = convertCertToPem(certChain[0]);
60
+ const publicKey = parsePublicKey(pemCert);
61
+ if (!publicKey) {
62
+ throw new NoSuitableKeysFoundInEntityConfiguration(
63
+ "Unsupported public key type."
64
+ );
65
+ }
66
+ const signingJwk = getSigningJwk(publicKey);
67
+
68
+ return [signingJwk];
69
+ };
70
+
71
+ /**
72
+ * Constructs the well-known JWKS URL based on the issuer claim.
73
+ *
74
+ * @param issuer - The issuer URL.
75
+ * @returns The well-known JWKS URL.
76
+ */
77
+ const constructWellKnownJwksUrl = (issuer: string): string => {
78
+ const issuerUrl = new URL(issuer);
79
+ return new URL(
80
+ `/.well-known/jar-issuer${issuerUrl.pathname}`,
81
+ `${issuerUrl.protocol}//${issuerUrl.host}`
82
+ ).toString();
83
+ };
84
+
85
+ /**
86
+ * Fetches the JSON Web Key Set (JWKS) based on the provided Request Object encoded as a JWT.
87
+ * The retrieval process follows these steps in order:
88
+ *
89
+ * 1. **Direct JWK Retrieval**: If the JWT's protected header contains a `jwk` attribute, it uses this key directly.
90
+ * 2. **X.509 Certificate Retrieval**: If the protected header includes an `x5c` attribute, it extracts the JWKs from the provided X.509 certificate chain.
91
+ * 3. **Issuer's Well-Known Endpoint**: If neither `jwk` nor `x5c` are present, it constructs the JWKS URL using the issuer (`iss`) claim and fetches the keys from the issuer's well-known JWKS endpoint.
92
+ *
93
+ * The JWKS URL is constructed in the format `{issUrl.base}/.well-known/jar-issuer${issUrl.path}`,
94
+ * as detailed in the SD-JWT VC issuer metadata specification.
95
+ *
96
+ * @param requestObjectEncodedJwt - The Request Object encoded as a JWT.
97
+ * @param options - Optional parameters for fetching the JWKS.
98
+ * @param options.context - Optional context providing a custom fetch implementation.
99
+ * @param options.context.appFetch - A custom fetch function to replace the global `fetch` if provided.
100
+ * @returns A promise that resolves to an object containing an array of JSON Web Keys (JWKs).
101
+ * @throws {NoSuitableKeysFoundInEntityConfiguration} Throws an error if JWKS retrieval or key extraction fails.
28
102
  */
29
103
  export const fetchJwksFromRequestObject: FetchJwks<
30
- [string, { context?: { appFetch?: GlobalFetch["fetch"] } }]
104
+ [string, { context?: { appFetch?: GlobalFetch["fetch"] } }?]
31
105
  > = async (requestObjectEncodedJwt, { context = {} } = {}) => {
32
106
  const { appFetch = fetch } = context;
33
107
  const requestObjectJwt = decodeJwt(requestObjectEncodedJwt);
108
+ const jwks: JWK[] = [];
34
109
 
35
110
  // 1. check if request object jwt contains the 'jwk' attribute
36
111
  if (requestObjectJwt.protectedHeader?.jwk) {
37
- return {
38
- keys: [JWK.parse(requestObjectJwt.protectedHeader.jwk)],
39
- };
112
+ const keys = [JWK.parse(requestObjectJwt.protectedHeader.jwk)];
113
+ jwks.push(...keys);
114
+ }
115
+
116
+ // 2. check if request object jwt contains the 'x5c' attribute
117
+ if (requestObjectJwt.protectedHeader.x5c) {
118
+ const keys = await getJwksFromX509Cert(
119
+ requestObjectJwt.protectedHeader.x5c
120
+ );
121
+ jwks.push(...keys);
122
+ }
123
+
124
+ // 3. check if client_metadata contains the 'jwks' or 'jwks_uri' attribute
125
+ const requestObject = RequestObject.parse(requestObjectJwt.payload);
126
+ const { client_metadata } = requestObject;
127
+
128
+ if (client_metadata?.jwks_uri) {
129
+ const fetchedJwks = await fetchJwksFromUri(
130
+ new URL(client_metadata.jwks_uri).toString(),
131
+ appFetch
132
+ );
133
+ jwks.push(...fetchedJwks);
134
+ }
135
+
136
+ if (client_metadata?.jwks) {
137
+ jwks.push(...client_metadata.jwks.keys);
138
+ }
139
+
140
+ // 3. According to Potential profile, retrieve from RP endpoint using iss claim
141
+ const issuer = requestObjectJwt.payload?.iss;
142
+ if (jwks.length === 0 && typeof issuer === "string") {
143
+ const wellKnownJwksUrl = constructWellKnownJwksUrl(issuer);
144
+ const jwksKeys = await fetchJwksFromUri(wellKnownJwksUrl, appFetch);
145
+ jwks.push(...jwksKeys);
40
146
  }
41
147
 
42
- // 2. According to Potential profile, retrieve from RP endpoint using iss claim
43
- const issClaimValue = requestObjectJwt.payload?.iss as string;
44
- if (issClaimValue) {
45
- const issUrl = new URL(issClaimValue);
46
- const wellKnownUrl = new URL(
47
- `/.well-known/jar-issuer${issUrl.pathname}`,
48
- `${issUrl.protocol}//${issUrl.host}`
49
- ).toString();
50
-
51
- // Fetches the JWKS from a specific endpoint of the entity's well-known configuration
52
- const jwks = await appFetch(wellKnownUrl, {
53
- method: "GET",
54
- })
55
- .then(hasStatusOrThrow(200))
56
- .then((raw) => raw.json())
57
- .then((json) => JWKS.parse(json.jwks));
58
-
59
- return {
60
- keys: jwks.keys,
61
- };
148
+ if (jwks.length === 0) {
149
+ throw new NoSuitableKeysFoundInEntityConfiguration(
150
+ "Request Object signature verification"
151
+ );
62
152
  }
63
153
 
64
- throw new NoSuitableKeysFoundInEntityConfiguration(
65
- "Request Object signature verification"
66
- );
154
+ return { keys: jwks };
67
155
  };
68
156
 
69
157
  /**
@@ -15,9 +15,10 @@ export const verifyRequestObjectSignature: VerifyRequestObjectSignature =
15
15
  const requestObjectJwt = decodeJwt(requestObjectEncodedJwt);
16
16
 
17
17
  // verify token signature to ensure the request object is authentic
18
- const pubKey = jwkKeys?.find(
19
- ({ kid }) => kid === requestObjectJwt.protectedHeader.kid
20
- );
18
+ const pubKey =
19
+ jwkKeys?.find(
20
+ ({ kid }) => kid === requestObjectJwt.protectedHeader.kid
21
+ ) || jwkKeys?.find(({ use }) => use === "sig");
21
22
 
22
23
  if (!pubKey) {
23
24
  throw new UnverifiedEntityError("Request Object signature verification!");
@@ -9,6 +9,7 @@ const INDEX_CLAIM_NAME = 1;
9
9
  export type EvaluatedDisclosures = {
10
10
  requiredDisclosures: DisclosureWithEncoded[];
11
11
  optionalDisclosures: DisclosureWithEncoded[];
12
+ unrequestedDisclosures: DisclosureWithEncoded[];
12
13
  };
13
14
 
14
15
  export type EvaluateInputDescriptorSdJwt4VC = (
@@ -99,8 +100,8 @@ const extractClaimName = (path: string): string | undefined => {
99
100
  * - Validates whether required fields are present (unless marked optional)
100
101
  * and match any specified JSONPath.
101
102
  * - If a field includes a JSON Schema filter, validates the claim value against that schema.
102
- * - Enforces `limit_disclosure` rules by returning only disclosures matching the specified fields
103
- * if set to "required". Otherwise return the array of all disclosures.
103
+ * - Enforces `limit_disclosure` rules by returning only disclosures, required and optional, matching the specified fields
104
+ * if set to "required". Otherwise also return the array unrequestedDisclosures with disclosures which can be passed for a particular use case.
104
105
  * - Throws an error if a required field is invalid or missing.
105
106
  *
106
107
  * @param inputDescriptor - Describes constraints (fields, filters, etc.) that must be satisfied.
@@ -115,7 +116,8 @@ export const evaluateInputDescriptorForSdJwt4VC: EvaluateInputDescriptorSdJwt4VC
115
116
  // No validation, all field are optional
116
117
  return {
117
118
  requiredDisclosures: [],
118
- optionalDisclosures: disclosures,
119
+ optionalDisclosures: [],
120
+ unrequestedDisclosures: disclosures,
119
121
  };
120
122
  }
121
123
  const requiredClaimNames: string[] = [];
@@ -182,9 +184,6 @@ export const evaluateInputDescriptorForSdJwt4VC: EvaluateInputDescriptorSdJwt4VC
182
184
  }
183
185
 
184
186
  // Categorizes disclosures into required and optional based on claim names and disclosure constraints.
185
- const isNotLimitDisclosure = !(
186
- inputDescriptor.constraints.limit_disclosure === "required"
187
- );
188
187
 
189
188
  const requiredDisclosures = disclosures.filter((disclosure) =>
190
189
  requiredClaimNames.includes(disclosure.decoded[INDEX_CLAIM_NAME])
@@ -197,8 +196,23 @@ export const evaluateInputDescriptorForSdJwt4VC: EvaluateInputDescriptorSdJwt4VC
197
196
  !requiredClaimNames.includes(disclosure.decoded[INDEX_CLAIM_NAME]))
198
197
  );
199
198
 
199
+ const isNotLimitDisclosure = !(
200
+ inputDescriptor.constraints.limit_disclosure === "required"
201
+ );
202
+
203
+ const unrequestedDisclosures = isNotLimitDisclosure
204
+ ? disclosures.filter(
205
+ (disclosure) =>
206
+ !optionalClaimNames.includes(
207
+ disclosure.decoded[INDEX_CLAIM_NAME]
208
+ ) &&
209
+ !requiredClaimNames.includes(disclosure.decoded[INDEX_CLAIM_NAME])
210
+ )
211
+ : [];
212
+
200
213
  return {
201
214
  requiredDisclosures,
202
215
  optionalDisclosures,
216
+ unrequestedDisclosures,
203
217
  };
204
218
  };
@@ -192,7 +192,7 @@ export type SendAuthorizationResponse = (
192
192
  presentationDefinition: PresentationDefinition,
193
193
  jwkKeys: Out<FetchJwks>["keys"],
194
194
  presentation: Presentation, // TODO: [SIW-353] support multiple presentations
195
- context: {
195
+ context?: {
196
196
  appFetch?: GlobalFetch["fetch"];
197
197
  }
198
198
  ) => Promise<AuthorizationResponse>;
@@ -213,7 +213,7 @@ export const sendAuthorizationResponse: SendAuthorizationResponse = async (
213
213
  presentationDefinition,
214
214
  jwkKeys,
215
215
  presentation,
216
- { appFetch = fetch }
216
+ { appFetch = fetch } = {}
217
217
  ): Promise<AuthorizationResponse> => {
218
218
  // 1. Create the VP token and associated submission mapping
219
219
  const { vp_token, presentation_submission } = await prepareVpToken(
@@ -29,8 +29,8 @@ sequenceDiagram
29
29
  <summary>Remote Presentation flow</summary>
30
30
 
31
31
  ```ts
32
- // Scan e retrive qr-code
33
- const qrcode = ...
32
+ // Scan e retrive qr-code, decode it and get its parameters
33
+ const {requestUri, clientId} = ...
34
34
 
35
35
  // Retrieve the integrity key tag from the store and create its context
36
36
  const integrityKeyTag = "example"; // Let's assume this is the key tag used to create the wallet instance
@@ -55,7 +55,7 @@ const walletInstanceAttestation =
55
55
  });
56
56
 
57
57
  // Start the issuance flow
58
- const { requestURI, clientId } = Credential.Presentation.startFlowFromQR(qrcode);
58
+ const { requestURI, clientId } = Credential.Presentation.startFlowFromQR(requestUri, clientId);
59
59
 
60
60
  // If use trust federation: Evaluate issuer trust
61
61
  const { rpConf } = await Credential.Presentation.evaluateRelyingPartyTrust(clientId);
@@ -111,4 +111,4 @@ const { presentationDefinition } = await Credential.Presentation.fetchPresentDef
111
111
 
112
112
  ```
113
113
 
114
- </details>
114
+ </details>
@@ -40,22 +40,6 @@ export class NoSuitableKeysFoundInEntityConfiguration extends IoWalletError {
40
40
  }
41
41
  }
42
42
 
43
- /**
44
- * When a QR code is not valid.
45
- *
46
- */
47
- export class InvalidQRCodeError extends IoWalletError {
48
- code = "ERR_INVALID_QR_CODE";
49
-
50
- /**
51
- * @param detail A description of why the QR code is considered invalid.
52
- */
53
- constructor(detail: string) {
54
- const message = `QR code is not valid: ${detail}.`;
55
- super(message);
56
- }
57
- }
58
-
59
43
  /**
60
44
  * When the entity is unverified because the Relying Party is not trusted.
61
45
  *
@@ -1,6 +1,7 @@
1
1
  import type { CryptoContext } from "@pagopa/io-react-native-jwt";
2
2
  import { UnixTime } from "../../sd-jwt/types";
3
3
  import * as z from "zod";
4
+ import { JWKS } from "../../utils/jwk";
4
5
 
5
6
  /**
6
7
  * A pair that associate a tokenized Verified Credential with the claims presented or requested to present.
@@ -77,7 +78,13 @@ export const RequestObject = z.object({
77
78
  response_type: z.literal("vp_token"),
78
79
  response_mode: z.enum(["direct_post.jwt", "direct_post"]),
79
80
  client_id: z.string(),
80
- client_id_scheme: z.string(), // previous z.literal("entity_id"),
81
+ client_id_scheme: z.string().optional(), // previous z.literal("entity_id"),
82
+ client_metadata: z
83
+ .object({
84
+ jwks_uri: z.string().optional(),
85
+ jwks: JWKS.optional(),
86
+ })
87
+ .optional(), // previous z.literal("entity_id"),
81
88
  scope: z.string().optional(),
82
89
  presentation_definition: PresentationDefinition.optional(),
83
90
  });
@@ -7,6 +7,8 @@ import {
7
7
  import uuid from "react-native-uuid";
8
8
  import { thumbprint, type CryptoContext } from "@pagopa/io-react-native-jwt";
9
9
  import { fixBase64EncodingOnKey } from "./jwk";
10
+ import { X509, KEYUTIL, RSAKey, KJUR } from "jsrsasign";
11
+ import { JWK } from "./jwk";
10
12
 
11
13
  /**
12
14
  * Create a CryptoContext bound to a key pair.
@@ -63,3 +65,44 @@ export const withEphemeralKey = async <R>(
63
65
  const ephemeralContext = createCryptoContextFor(keytag);
64
66
  return fn(ephemeralContext).finally(() => deleteKey(keytag));
65
67
  };
68
+
69
+ /**
70
+ * Converts a certificate string to PEM format.
71
+ *
72
+ * @param certificate - The certificate string.
73
+ * @returns The PEM-formatted certificate.
74
+ */
75
+ export const convertCertToPem = (certificate: string): string =>
76
+ `-----BEGIN CERTIFICATE-----\n${certificate}\n-----END CERTIFICATE-----`;
77
+
78
+ /**
79
+ * Parses the public key from a PEM-formatted certificate.
80
+ *
81
+ * @param pemCert - The PEM-formatted certificate.
82
+ * @returns The public key object.
83
+ * @throws Will throw an error if the public key is unsupported.
84
+ */
85
+ export const parsePublicKey = (
86
+ pemCert: string
87
+ ): RSAKey | KJUR.crypto.ECDSA | undefined => {
88
+ const x509 = new X509();
89
+ x509.readCertPEM(pemCert);
90
+ const publicKey = x509.getPublicKey();
91
+
92
+ if (publicKey instanceof RSAKey || publicKey instanceof KJUR.crypto.ECDSA) {
93
+ return publicKey;
94
+ }
95
+
96
+ return undefined;
97
+ };
98
+
99
+ /**
100
+ * Retrieves the signing JWK from the public key.
101
+ *
102
+ * @param publicKey - The public key object.
103
+ * @returns The signing JWK.
104
+ */
105
+ export const getSigningJwk = (publicKey: RSAKey | KJUR.crypto.ECDSA): JWK => ({
106
+ ...JWK.parse(KEYUTIL.getJWKFromKey(publicKey)),
107
+ use: "sig",
108
+ });