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

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 (65) 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/credential/issuance/07-verify-and-parse-credential.js +6 -6
  6. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  7. package/lib/commonjs/index.js +10 -1
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/utils/errors.js +29 -1
  10. package/lib/commonjs/utils/errors.js.map +1 -1
  11. package/lib/commonjs/utils/integrity.js +2 -0
  12. package/lib/commonjs/utils/integrity.js.map +1 -0
  13. package/lib/commonjs/wallet-instance/index.js +29 -0
  14. package/lib/commonjs/wallet-instance/index.js.map +1 -0
  15. package/lib/commonjs/wallet-instance-attestation/issuing.js +48 -66
  16. package/lib/commonjs/wallet-instance-attestation/issuing.js.map +1 -1
  17. package/lib/commonjs/wallet-instance-attestation/types.js +1 -1
  18. package/lib/commonjs/wallet-instance-attestation/types.js.map +1 -1
  19. package/lib/module/client/generated/wallet-provider.js +105 -0
  20. package/lib/module/client/generated/wallet-provider.js.map +1 -0
  21. package/lib/module/client/index.js +34 -0
  22. package/lib/module/client/index.js.map +1 -0
  23. package/lib/module/credential/issuance/07-verify-and-parse-credential.js +6 -6
  24. package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  25. package/lib/module/index.js +3 -1
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/sd-jwt/verifier.js.map +1 -1
  28. package/lib/module/utils/errors.js +27 -0
  29. package/lib/module/utils/errors.js.map +1 -1
  30. package/lib/module/utils/integrity.js +2 -0
  31. package/lib/module/utils/integrity.js.map +1 -0
  32. package/lib/module/wallet-instance/index.js +23 -0
  33. package/lib/module/wallet-instance/index.js.map +1 -0
  34. package/lib/module/wallet-instance-attestation/issuing.js +48 -67
  35. package/lib/module/wallet-instance-attestation/issuing.js.map +1 -1
  36. package/lib/module/wallet-instance-attestation/types.js +1 -1
  37. package/lib/module/wallet-instance-attestation/types.js.map +1 -1
  38. package/lib/typescript/client/generated/wallet-provider.d.ts +242 -0
  39. package/lib/typescript/client/generated/wallet-provider.d.ts.map +1 -0
  40. package/lib/typescript/client/index.d.ts +7 -0
  41. package/lib/typescript/client/index.d.ts.map +1 -0
  42. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
  43. package/lib/typescript/index.d.ts +5 -1
  44. package/lib/typescript/index.d.ts.map +1 -1
  45. package/lib/typescript/utils/errors.d.ts +13 -0
  46. package/lib/typescript/utils/errors.d.ts.map +1 -1
  47. package/lib/typescript/utils/integrity.d.ts +21 -0
  48. package/lib/typescript/utils/integrity.d.ts.map +1 -0
  49. package/lib/typescript/wallet-instance/index.d.ts +7 -0
  50. package/lib/typescript/wallet-instance/index.d.ts.map +1 -0
  51. package/lib/typescript/wallet-instance-attestation/issuing.d.ts +15 -3
  52. package/lib/typescript/wallet-instance-attestation/issuing.d.ts.map +1 -1
  53. package/lib/typescript/wallet-instance-attestation/types.d.ts +5 -5
  54. package/package.json +9 -6
  55. package/src/client/generated/wallet-provider.ts +170 -0
  56. package/src/client/index.ts +58 -0
  57. package/src/credential/issuance/07-verify-and-parse-credential.ts +38 -34
  58. package/src/index.ts +7 -0
  59. package/src/sd-jwt/__test__/converters.test.js +24 -0
  60. package/src/sd-jwt/verifier.js +12 -0
  61. package/src/utils/errors.ts +28 -0
  62. package/src/utils/integrity.ts +23 -0
  63. package/src/wallet-instance/index.ts +29 -0
  64. package/src/wallet-instance-attestation/issuing.ts +68 -101
  65. 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
+ };
@@ -89,45 +89,49 @@ const parseCredentialSdJwt = (
89
89
 
90
90
  // attributes that are defined in the issuer configuration
91
91
  // and are present in the disclosure set
92
- const definedValues = attrDefinitions
93
- // retrieve the value from the disclosure set
94
- .map(
95
- ([attrKey, definition]) =>
96
- [
97
- attrKey,
98
- {
99
- ...definition,
100
- value: disclosures.find(
101
- (_) => _[1 /* name */] === attrKey
102
- )?.[2 /* value */],
103
- },
104
- ] as const
105
- )
106
- // add a human readable attribute name, with i18n, in the form { locale: name }
107
- // example: { "it-IT": "Nome", "en-EN": "Name", "es-ES": "Nombre" }
108
- .map(
109
- ([attrKey, { display, ...definition }]) =>
110
- [
111
- attrKey,
112
- {
113
- ...definition,
114
- name: display.reduce(
115
- (names, { locale, name }) => ({ ...names, [locale]: name }),
116
- {} as Record<string, string>
117
- ),
118
- },
119
- ] as const
120
- );
92
+ const definedValues = Object.fromEntries(
93
+ attrDefinitions
94
+ // retrieve the value from the disclosure set
95
+ .map(
96
+ ([attrKey, definition]) =>
97
+ [
98
+ attrKey,
99
+ {
100
+ ...definition,
101
+ value: disclosures.find(
102
+ (_) => _[1 /* name */] === attrKey
103
+ )?.[2 /* value */],
104
+ },
105
+ ] as const
106
+ )
107
+ // add a human readable attribute name, with i18n, in the form { locale: name }
108
+ // example: { "it-IT": "Nome", "en-EN": "Name", "es-ES": "Nombre" }
109
+ .map(
110
+ ([attrKey, { display, ...definition }]) =>
111
+ [
112
+ attrKey,
113
+ {
114
+ ...definition,
115
+ name: display.reduce(
116
+ (names, { locale, name }) => ({ ...names, [locale]: name }),
117
+ {} as Record<string, string>
118
+ ),
119
+ },
120
+ ] as const
121
+ )
122
+ );
121
123
 
122
124
  // attributes that are in the disclosure set
123
125
  // but are not defined in the issuer configuration
124
- const undefinedValues = disclosures
125
- .filter((_) => !Object.keys(definedValues).includes(_[1]))
126
- .map(([, key, value]) => [key, { value, mandatory: false, name: key }]);
126
+ const undefinedValues = Object.fromEntries(
127
+ disclosures
128
+ .filter((_) => !Object.keys(definedValues).includes(_[1]))
129
+ .map(([, key, value]) => [key, { value, mandatory: false, name: key }])
130
+ );
127
131
 
128
132
  return {
129
- ...Object.fromEntries(definedValues),
130
- ...Object.fromEntries(undefinedValues),
133
+ ...definedValues,
134
+ ...undefinedValues,
131
135
  };
132
136
  };
133
137
 
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(