@pagopa/io-react-native-wallet 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. package/lib/commonjs/client/generated/wallet-provider.js +126 -0
  2. package/lib/commonjs/client/generated/wallet-provider.js.map +1 -0
  3. package/lib/commonjs/client/index.js +41 -0
  4. package/lib/commonjs/client/index.js.map +1 -0
  5. package/lib/commonjs/index.js +10 -1
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/utils/errors.js +29 -1
  8. package/lib/commonjs/utils/errors.js.map +1 -1
  9. package/lib/commonjs/utils/integrity.js +2 -0
  10. package/lib/commonjs/utils/integrity.js.map +1 -0
  11. package/lib/commonjs/wallet-instance/index.js +29 -0
  12. package/lib/commonjs/wallet-instance/index.js.map +1 -0
  13. package/lib/commonjs/wallet-instance-attestation/issuing.js +48 -66
  14. package/lib/commonjs/wallet-instance-attestation/issuing.js.map +1 -1
  15. package/lib/commonjs/wallet-instance-attestation/types.js +1 -1
  16. package/lib/commonjs/wallet-instance-attestation/types.js.map +1 -1
  17. package/lib/module/client/generated/wallet-provider.js +105 -0
  18. package/lib/module/client/generated/wallet-provider.js.map +1 -0
  19. package/lib/module/client/index.js +34 -0
  20. package/lib/module/client/index.js.map +1 -0
  21. package/lib/module/index.js +3 -1
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/sd-jwt/verifier.js.map +1 -1
  24. package/lib/module/utils/errors.js +27 -0
  25. package/lib/module/utils/errors.js.map +1 -1
  26. package/lib/module/utils/integrity.js +2 -0
  27. package/lib/module/utils/integrity.js.map +1 -0
  28. package/lib/module/wallet-instance/index.js +23 -0
  29. package/lib/module/wallet-instance/index.js.map +1 -0
  30. package/lib/module/wallet-instance-attestation/issuing.js +48 -67
  31. package/lib/module/wallet-instance-attestation/issuing.js.map +1 -1
  32. package/lib/module/wallet-instance-attestation/types.js +1 -1
  33. package/lib/module/wallet-instance-attestation/types.js.map +1 -1
  34. package/lib/typescript/client/generated/wallet-provider.d.ts +242 -0
  35. package/lib/typescript/client/generated/wallet-provider.d.ts.map +1 -0
  36. package/lib/typescript/client/index.d.ts +7 -0
  37. package/lib/typescript/client/index.d.ts.map +1 -0
  38. package/lib/typescript/index.d.ts +5 -1
  39. package/lib/typescript/index.d.ts.map +1 -1
  40. package/lib/typescript/utils/errors.d.ts +13 -0
  41. package/lib/typescript/utils/errors.d.ts.map +1 -1
  42. package/lib/typescript/utils/integrity.d.ts +21 -0
  43. package/lib/typescript/utils/integrity.d.ts.map +1 -0
  44. package/lib/typescript/wallet-instance/index.d.ts +7 -0
  45. package/lib/typescript/wallet-instance/index.d.ts.map +1 -0
  46. package/lib/typescript/wallet-instance-attestation/issuing.d.ts +15 -3
  47. package/lib/typescript/wallet-instance-attestation/issuing.d.ts.map +1 -1
  48. package/lib/typescript/wallet-instance-attestation/types.d.ts +5 -5
  49. package/package.json +8 -5
  50. package/src/client/generated/wallet-provider.ts +170 -0
  51. package/src/client/index.ts +58 -0
  52. package/src/index.ts +7 -0
  53. package/src/sd-jwt/__test__/converters.test.js +24 -0
  54. package/src/sd-jwt/verifier.js +12 -0
  55. package/src/utils/errors.ts +28 -0
  56. package/src/utils/integrity.ts +23 -0
  57. package/src/wallet-instance/index.ts +29 -0
  58. package/src/wallet-instance-attestation/issuing.ts +68 -101
  59. package/src/wallet-instance-attestation/types.ts +1 -1
@@ -0,0 +1,58 @@
1
+ import { WalletProviderResponseError } from "../utils/errors";
2
+ import {
3
+ ProblemDetail,
4
+ createApiClient as createWalletProviderApiClient,
5
+ } from "./generated/wallet-provider";
6
+ import { ApiClient as WalletProviderApiClient } from "./generated/wallet-provider";
7
+
8
+ export type WalletProviderClient = WalletProviderApiClient;
9
+
10
+ const validateResponse = async (response: Response) => {
11
+ if (!response.ok) {
12
+ let problemDetail: ProblemDetail = {};
13
+ try {
14
+ problemDetail = ProblemDetail.parse(await response.json());
15
+ } catch {
16
+ problemDetail = {
17
+ title: "Invalid response from Wallet Provider",
18
+ };
19
+ }
20
+
21
+ let statusResponse = `Response status code: ${response.status}`;
22
+
23
+ throw new WalletProviderResponseError(
24
+ problemDetail.title
25
+ ? problemDetail.title
26
+ : "Invalid response from Wallet Provider",
27
+ problemDetail.type,
28
+ problemDetail.detail
29
+ ? statusResponse
30
+ : `${statusResponse} with detail: ${problemDetail.detail}`
31
+ );
32
+ }
33
+ return response;
34
+ };
35
+
36
+ export const getWalletProviderClient = (context: {
37
+ walletProviderBaseUrl: string;
38
+ appFetch?: GlobalFetch["fetch"];
39
+ }) => {
40
+ const { walletProviderBaseUrl, appFetch = fetch } = context;
41
+
42
+ return createWalletProviderApiClient(
43
+ (method, url, params) =>
44
+ appFetch(url, {
45
+ method,
46
+ body: params ? JSON.stringify(params.body) : undefined,
47
+ })
48
+ .then(validateResponse)
49
+ .then((res) => {
50
+ const contentType = res.headers.get("content-type");
51
+ if (contentType === "application/json") {
52
+ return res.json();
53
+ }
54
+ return res.text();
55
+ }),
56
+ walletProviderBaseUrl
57
+ );
58
+ };
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { fixBase64EncodingOnKey } from "./utils/jwk";
1
2
  // polyfill due to known bugs on URL implementation for react native
2
3
  // https://github.com/facebook/react-native/issues/24428
3
4
  import "react-native-url-polyfill/auto";
@@ -8,17 +9,23 @@ import * as SdJwt from "./sd-jwt";
8
9
  import * as Errors from "./utils/errors";
9
10
  import * as WalletInstanceAttestation from "./wallet-instance-attestation";
10
11
  import * as Trust from "./trust";
12
+ import * as WalletInstance from "./wallet-instance";
11
13
  import { AuthorizationDetail, AuthorizationDetails } from "./utils/par";
12
14
  import { createCryptoContextFor } from "./utils/crypto";
15
+ import type { IntegrityContext } from "./utils/integrity";
13
16
 
14
17
  export {
15
18
  SdJwt,
16
19
  PID,
17
20
  Credential,
18
21
  WalletInstanceAttestation,
22
+ WalletInstance,
19
23
  Errors,
20
24
  Trust,
21
25
  createCryptoContextFor,
22
26
  AuthorizationDetail,
23
27
  AuthorizationDetails,
28
+ fixBase64EncodingOnKey,
24
29
  };
30
+
31
+ export type { IntegrityContext };
@@ -0,0 +1,24 @@
1
+ import { getValueFromDisclosures } from "../converters";
2
+ const disclosures = [
3
+ ["6w1_soRXFgaHKfpYn3cvfQ", "given_name", "Mario"],
4
+ ["fuNp97Hf3wV6y48y-QZhIg", "birthdate", "1980-10-01"],
5
+ [
6
+ "p-9LzyWHZBVDvhXDWkN2xA",
7
+ "place_of_birth",
8
+ { country: "IT", locality: "Rome" },
9
+ ],
10
+ ];
11
+ describe("getValueFromDisclosures", () => {
12
+ it("should return correct value for given_name", () => {
13
+ const success = getValueFromDisclosures(disclosures, "given_name");
14
+ expect(success).toBe("Mario");
15
+ });
16
+ it("should return correct value for place_of_birth", () => {
17
+ const success = getValueFromDisclosures(disclosures, "place_of_birth");
18
+ expect(success).toEqual({ country: "IT", locality: "Rome" });
19
+ });
20
+ it("should fail", () => {
21
+ const success = getValueFromDisclosures(disclosures, "given_surname");
22
+ expect(success).toBeUndefined();
23
+ });
24
+ });
@@ -0,0 +1,12 @@
1
+ import { sha256ToBase64 } from "@pagopa/io-react-native-jwt";
2
+ import { ValidationFailed } from "../utils/errors";
3
+ export const verifyDisclosure = async ({ encoded, decoded }, claims) => {
4
+ let hash = await sha256ToBase64(encoded);
5
+ if (!claims.includes(hash)) {
6
+ throw new ValidationFailed(
7
+ "Validation of disclosure failed",
8
+ `${decoded}`,
9
+ "Disclosure hash not found in claims"
10
+ );
11
+ }
12
+ };
@@ -233,3 +233,31 @@ export class PidMetadataError extends Error {
233
233
  super(message);
234
234
  }
235
235
  }
236
+
237
+ /**
238
+ * An error subclass thrown when a Wallet Provider http request fail
239
+ *
240
+ */
241
+ export class WalletProviderResponseError extends IoWalletError {
242
+ static get code(): "ERR_IO_WALLET_PROVIDER_RESPONSE_FAILED" {
243
+ return "ERR_IO_WALLET_PROVIDER_RESPONSE_FAILED";
244
+ }
245
+
246
+ code = "ERR_IO_WALLET_PROVIDER_RESPONSE_FAILED";
247
+
248
+ /** The Claim for which the validation failed. */
249
+ claim: string;
250
+
251
+ /** Reason code for the validation failure. */
252
+ reason: string;
253
+
254
+ constructor(
255
+ message: string,
256
+ claim: string = "unspecified",
257
+ reason: string = "unspecified"
258
+ ) {
259
+ super(serializeAttrs({ message, claim, reason }));
260
+ this.claim = claim;
261
+ this.reason = reason;
262
+ }
263
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Interface for the integrity context which provides the necessary functions to interact with the integrity service.
3
+ * The functions are platform specific and must be implemented in the platform specific code.
4
+ * getHardwareKeyTag: returns the hardware key tag.
5
+ * getAttestation: requests the attestation from the integrity service.
6
+ * getHardwareSignatureWithAuthData: signs the clientData and returns the signature with the authenticator data.
7
+ */
8
+ export interface IntegrityContext {
9
+ getHardwareKeyTag: () => string;
10
+ getAttestation: (nonce: string) => Promise<string>;
11
+ getHardwareSignatureWithAuthData: (
12
+ clientData: string
13
+ ) => Promise<HardwareSignatureWithAuthData>;
14
+ }
15
+
16
+ /**
17
+ * Type returned by the getHardwareSignatureWithAuthData function of {@link IntegrityContext}.
18
+ * It contains the signature and the authenticator data.
19
+ */
20
+ export type HardwareSignatureWithAuthData = {
21
+ signature: string;
22
+ authenticatorData: string;
23
+ };
@@ -0,0 +1,29 @@
1
+ import { getWalletProviderClient } from "../client";
2
+ import type { IntegrityContext } from "..";
3
+
4
+ export async function createWalletInstance(context: {
5
+ integrityContext: IntegrityContext;
6
+ walletProviderBaseUrl: string;
7
+ appFetch?: GlobalFetch["fetch"];
8
+ }) {
9
+ const { integrityContext } = context;
10
+
11
+ const api = getWalletProviderClient(context);
12
+
13
+ //1. Obtain nonce
14
+ const challenge = await api.get("/nonce").then((response) => response.nonce);
15
+
16
+ const keyAttestation = await integrityContext.getAttestation(challenge);
17
+ const hardwareKeyTag = integrityContext.getHardwareKeyTag();
18
+
19
+ //2. Create Wallet Instance
20
+ await api.post("/wallet-instances", {
21
+ body: {
22
+ challenge,
23
+ key_attestation: keyAttestation,
24
+ hardware_key_tag: hardwareKeyTag,
25
+ },
26
+ });
27
+
28
+ return hardwareKeyTag;
29
+ }
@@ -1,77 +1,62 @@
1
- import {
2
- type CryptoContext,
3
- decode as decodeJwt,
4
- } from "@pagopa/io-react-native-jwt";
5
- import { verify as verifyJwt } from "@pagopa/io-react-native-jwt";
1
+ import { type CryptoContext } from "@pagopa/io-react-native-jwt";
6
2
  import { SignJWT, thumbprint } from "@pagopa/io-react-native-jwt";
7
3
  import { JWK, fixBase64EncodingOnKey } from "../utils/jwk";
8
- import { WalletInstanceAttestationRequestJwt } from "./types";
9
- import uuid from "react-native-uuid";
10
- import { WalletInstanceAttestationIssuingError } from "../utils/errors";
11
- import type { WalletProviderEntityConfiguration } from "../trust/types";
4
+ import { getWalletProviderClient } from "../client";
5
+ import type { IntegrityContext } from "..";
6
+ import { z } from "zod";
12
7
 
13
- async function getAttestationRequest(
8
+ /**
9
+ * Getter for an attestation request. The attestation request is a JWT that will be sent to the Wallet Provider to request a Wallet Instance Attestation.
10
+ *
11
+ * @param challenge - The nonce received from the Wallet Provider which is part of the signed clientData
12
+ * @param wiaCryptoContext - The key pair associated with the WIA. Will be use to prove the ownership of the attestation
13
+ * @param integrityContext - The integrity context which exposes a set of functions to interact with the device integrity service
14
+ * @param walletProviderBaseUrl - Base url for the Wallet Provider
15
+ * @returns A JWT containing the attestation request
16
+ */
17
+ export async function getAttestationRequest(
18
+ challenge: string,
14
19
  wiaCryptoContext: CryptoContext,
15
- walletProviderEntityConfiguration: WalletProviderEntityConfiguration
20
+ integrityContext: IntegrityContext,
21
+ walletProviderBaseUrl: string
16
22
  ): Promise<string> {
17
23
  const jwk = await wiaCryptoContext.getPublicKey();
18
24
  const parsedJwk = JWK.parse(jwk);
19
25
  const keyThumbprint = await thumbprint(parsedJwk);
20
26
  const publicKey = { ...parsedJwk, kid: keyThumbprint };
21
27
 
28
+ const clientData = {
29
+ challenge,
30
+ jwk_thumbprint: keyThumbprint,
31
+ };
32
+
33
+ const hardwareKeyTag = integrityContext.getHardwareKeyTag();
34
+ const { signature, authenticatorData } =
35
+ await integrityContext.getHardwareSignatureWithAuthData(
36
+ JSON.stringify(clientData)
37
+ );
38
+
22
39
  return new SignJWT(wiaCryptoContext)
23
40
  .setPayload({
24
41
  iss: keyThumbprint,
25
- aud: walletProviderEntityConfiguration.payload.iss,
26
- jti: `${uuid.v4()}`,
27
- nonce: `${uuid.v4()}`,
42
+ sub: walletProviderBaseUrl,
43
+ challenge,
44
+ hardware_signature: signature,
45
+ integrity_assertion: authenticatorData,
46
+ hardware_key_tag: hardwareKeyTag,
28
47
  cnf: {
29
48
  jwk: fixBase64EncodingOnKey(publicKey),
30
49
  },
31
50
  })
32
51
  .setProtectedHeader({
33
52
  kid: publicKey.kid,
34
- typ: "wiar+jwt",
53
+ typ: "war+jwt",
35
54
  })
36
55
  .setIssuedAt()
37
56
  .setExpirationTime("1h")
38
57
  .sign();
39
58
  }
40
59
 
41
- /**
42
- * Validate a Wallet Instance Attestation token.
43
- * Either return true or throw an exception.
44
- *
45
- * @param wia Signed Wallet Instance Attestation token
46
- * @param walletProviderEntityConfiguration Entity Configuration object for the issuing Wallet Provider
47
- * @returns The token is valid
48
- * @throws {WalletInstanceAttestationIssuingError} When the received token fails to validate. This can happen due to invalid signature, expired token or malformed JWT token.
49
- */
50
- async function verifyWalletInstanceAttestation(
51
- wia: string,
52
- walletProviderEntityConfiguration: WalletProviderEntityConfiguration
53
- ): Promise<true> {
54
- const {
55
- payload: {
56
- sub,
57
- metadata: {
58
- wallet_provider: {
59
- jwks: { keys },
60
- },
61
- },
62
- },
63
- } = walletProviderEntityConfiguration;
64
- return verifyJwt(wia, keys, { issuer: sub })
65
- .then((_) => true as const)
66
- .catch((ex) => {
67
- const reason = ex && ex instanceof Error ? ex.message : "unknown reason";
68
- throw new WalletInstanceAttestationIssuingError(
69
- "Unable to validate received wallet instance attestation",
70
- reason
71
- );
72
- });
73
- }
74
-
75
60
  /**
76
61
  * Request a Wallet Instance Attestation (WIA) to the Wallet provider
77
62
  *
@@ -80,60 +65,42 @@ async function verifyWalletInstanceAttestation(
80
65
  * @param walletProviderBaseUrl Base url for the Wallet Provider
81
66
  * @returns The retrieved Wallet Instance Attestation token
82
67
  */
83
- export const getAttestation =
84
- ({
85
- wiaCryptoContext,
86
- appFetch = fetch,
87
- }: {
88
- wiaCryptoContext: CryptoContext;
89
- appFetch?: GlobalFetch["fetch"];
90
- }) =>
91
- async (
92
- walletProviderEntityConfiguration: WalletProviderEntityConfiguration
93
- ): Promise<string> => {
94
- const signedAttestationRequest = await getAttestationRequest(
95
- wiaCryptoContext,
96
- walletProviderEntityConfiguration
97
- );
68
+ export const getAttestation = async ({
69
+ wiaCryptoContext,
70
+ integrityContext,
71
+ walletProviderBaseUrl,
72
+ appFetch = fetch,
73
+ }: {
74
+ wiaCryptoContext: CryptoContext;
75
+ integrityContext: IntegrityContext;
76
+ walletProviderBaseUrl: string;
77
+ appFetch?: GlobalFetch["fetch"];
78
+ }): Promise<string> => {
79
+ const api = getWalletProviderClient({
80
+ walletProviderBaseUrl,
81
+ appFetch,
82
+ });
98
83
 
99
- const decodedRequest = decodeJwt(signedAttestationRequest);
100
- const parsedRequest = WalletInstanceAttestationRequestJwt.parse({
101
- payload: decodedRequest.payload,
102
- header: decodedRequest.protectedHeader,
103
- });
104
- const publicKey = parsedRequest.payload.cnf.jwk;
84
+ // 1. Get nonce from backend
85
+ const challenge = await api.get("/nonce").then((response) => response.nonce);
105
86
 
106
- await verifyJwt(signedAttestationRequest, publicKey);
87
+ // 2. Get a signed attestation request
88
+ const signedAttestationRequest = await getAttestationRequest(
89
+ challenge,
90
+ wiaCryptoContext,
91
+ integrityContext,
92
+ walletProviderBaseUrl
93
+ );
107
94
 
108
- const tokenUrl =
109
- walletProviderEntityConfiguration.payload.metadata.wallet_provider
110
- .token_endpoint;
111
- const requestBody = {
112
- grant_type:
113
- "urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation",
114
- assertion: signedAttestationRequest,
115
- };
116
- const response = await appFetch(tokenUrl, {
117
- method: "POST",
118
- headers: {
119
- "Content-Type": "application/json",
95
+ // 3. Request WIA
96
+ const wia = await api
97
+ .post("/token", {
98
+ body: {
99
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
100
+ assertion: signedAttestationRequest,
120
101
  },
121
- body: JSON.stringify(requestBody),
122
- });
123
-
124
- if (response.status !== 201) {
125
- throw new WalletInstanceAttestationIssuingError(
126
- "Unable to obtain wallet instance attestation from wallet provider",
127
- `Response code: ${response.status}`
128
- );
129
- }
130
-
131
- const wia = await response.text();
132
-
133
- await verifyWalletInstanceAttestation(
134
- wia,
135
- walletProviderEntityConfiguration
136
- );
102
+ })
103
+ .then((result) => z.string().parse(result));
137
104
 
138
- return wia;
139
- };
105
+ return wia;
106
+ };
@@ -33,7 +33,7 @@ export const WalletInstanceAttestationRequestJwt = z.object({
33
33
  header: z.intersection(
34
34
  Jwt.shape.header,
35
35
  z.object({
36
- typ: z.literal("wiar+jwt"),
36
+ typ: z.literal("war+jwt"),
37
37
  })
38
38
  ),
39
39
  payload: z.intersection(