@pagopa/io-react-native-wallet 0.2.1 → 0.2.3

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 (128) hide show
  1. package/lib/commonjs/index.js +9 -1
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/pid/issuing.js +28 -0
  4. package/lib/commonjs/pid/issuing.js.map +1 -1
  5. package/lib/commonjs/pid/metadata.js +51 -0
  6. package/lib/commonjs/pid/metadata.js.map +1 -0
  7. package/lib/commonjs/pid/sd-jwt/index.js +2 -1
  8. package/lib/commonjs/pid/sd-jwt/index.js.map +1 -1
  9. package/lib/commonjs/rp/__test__/index.test.js +3 -5
  10. package/lib/commonjs/rp/__test__/index.test.js.map +1 -1
  11. package/lib/commonjs/rp/index.js +165 -15
  12. package/lib/commonjs/rp/index.js.map +1 -1
  13. package/lib/commonjs/rp/types.js +13 -1
  14. package/lib/commonjs/rp/types.js.map +1 -1
  15. package/lib/commonjs/sd-jwt/__test__/index.test.js +119 -0
  16. package/lib/commonjs/sd-jwt/__test__/index.test.js.map +1 -0
  17. package/lib/commonjs/sd-jwt/index.js +84 -4
  18. package/lib/commonjs/sd-jwt/index.js.map +1 -1
  19. package/lib/commonjs/sd-jwt/types.js +9 -0
  20. package/lib/commonjs/sd-jwt/types.js.map +1 -1
  21. package/lib/commonjs/sd-jwt/verifier.js +7 -5
  22. package/lib/commonjs/sd-jwt/verifier.js.map +1 -1
  23. package/lib/commonjs/utils/errors.js +76 -1
  24. package/lib/commonjs/utils/errors.js.map +1 -1
  25. package/lib/module/index.js +5 -1
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/pid/issuing.js +30 -2
  28. package/lib/module/pid/issuing.js.map +1 -1
  29. package/lib/module/pid/metadata.js +43 -0
  30. package/lib/module/pid/metadata.js.map +1 -0
  31. package/lib/module/pid/sd-jwt/index.js +3 -3
  32. package/lib/module/pid/sd-jwt/index.js.map +1 -1
  33. package/lib/module/rp/__test__/index.test.js +3 -5
  34. package/lib/module/rp/__test__/index.test.js.map +1 -1
  35. package/lib/module/rp/index.js +168 -18
  36. package/lib/module/rp/index.js.map +1 -1
  37. package/lib/module/rp/types.js +11 -0
  38. package/lib/module/rp/types.js.map +1 -1
  39. package/lib/module/sd-jwt/__test__/index.test.js +118 -0
  40. package/lib/module/sd-jwt/__test__/index.test.js.map +1 -0
  41. package/lib/module/sd-jwt/index.js +83 -3
  42. package/lib/module/sd-jwt/index.js.map +1 -1
  43. package/lib/module/sd-jwt/types.js +10 -0
  44. package/lib/module/sd-jwt/types.js.map +1 -1
  45. package/lib/module/sd-jwt/verifier.js +8 -6
  46. package/lib/module/sd-jwt/verifier.js.map +1 -1
  47. package/lib/module/utils/errors.js +71 -0
  48. package/lib/module/utils/errors.js.map +1 -1
  49. package/lib/typescript/{index.d.ts → src/index.d.ts} +3 -1
  50. package/lib/typescript/src/index.d.ts.map +1 -0
  51. package/lib/typescript/src/pid/index.d.ts.map +1 -0
  52. package/lib/typescript/{pid → src/pid}/issuing.d.ts +9 -0
  53. package/lib/typescript/src/pid/issuing.d.ts.map +1 -0
  54. package/lib/typescript/src/pid/metadata.d.ts +528 -0
  55. package/lib/typescript/src/pid/metadata.d.ts.map +1 -0
  56. package/lib/typescript/src/pid/sd-jwt/converters.d.ts.map +1 -0
  57. package/lib/typescript/{pid → src/pid}/sd-jwt/index.d.ts +1 -1
  58. package/lib/typescript/src/pid/sd-jwt/index.d.ts.map +1 -0
  59. package/lib/typescript/src/pid/sd-jwt/types.d.ts.map +1 -0
  60. package/lib/typescript/src/rp/__test__/index.test.d.ts.map +1 -0
  61. package/lib/typescript/src/rp/index.d.ts +89 -0
  62. package/lib/typescript/src/rp/index.d.ts.map +1 -0
  63. package/lib/typescript/{rp → src/rp}/types.d.ts +71 -47
  64. package/lib/typescript/{rp → src/rp}/types.d.ts.map +1 -1
  65. package/lib/typescript/src/sd-jwt/__test__/converters.test.d.ts.map +1 -0
  66. package/lib/typescript/src/sd-jwt/__test__/index.test.d.ts +2 -0
  67. package/lib/typescript/src/sd-jwt/__test__/index.test.d.ts.map +1 -0
  68. package/lib/typescript/src/sd-jwt/__test__/types.test.d.ts.map +1 -0
  69. package/lib/typescript/src/sd-jwt/converters.d.ts.map +1 -0
  70. package/lib/typescript/{sd-jwt → src/sd-jwt}/index.d.ts +22 -2
  71. package/lib/typescript/src/sd-jwt/index.d.ts.map +1 -0
  72. package/lib/typescript/{sd-jwt → src/sd-jwt}/types.d.ts +12 -0
  73. package/lib/typescript/src/sd-jwt/types.d.ts.map +1 -0
  74. package/lib/typescript/src/sd-jwt/verifier.d.ts +3 -0
  75. package/lib/typescript/src/sd-jwt/verifier.d.ts.map +1 -0
  76. package/lib/typescript/src/utils/dpop.d.ts.map +1 -0
  77. package/lib/typescript/{utils → src/utils}/errors.d.ts +41 -0
  78. package/lib/typescript/src/utils/errors.d.ts.map +1 -0
  79. package/lib/typescript/src/utils/jwk.d.ts.map +1 -0
  80. package/lib/typescript/src/wallet-instance-attestation/index.d.ts.map +1 -0
  81. package/lib/typescript/src/wallet-instance-attestation/issuing.d.ts.map +1 -0
  82. package/lib/typescript/{wallet-instance-attestation → src/wallet-instance-attestation}/types.d.ts +8 -8
  83. package/lib/typescript/{wallet-instance-attestation → src/wallet-instance-attestation}/types.d.ts.map +1 -1
  84. package/package.json +7 -5
  85. package/src/index.ts +13 -1
  86. package/src/pid/issuing.ts +38 -1
  87. package/src/pid/metadata.ts +46 -0
  88. package/src/pid/sd-jwt/index.ts +7 -4
  89. package/src/rp/__test__/index.test.ts +5 -9
  90. package/src/rp/index.ts +208 -24
  91. package/src/rp/types.ts +16 -0
  92. package/src/sd-jwt/__test__/index.test.ts +171 -0
  93. package/src/sd-jwt/index.ts +84 -7
  94. package/src/sd-jwt/types.ts +13 -0
  95. package/src/sd-jwt/verifier.ts +5 -7
  96. package/src/utils/errors.ts +81 -0
  97. package/lib/typescript/index.d.ts.map +0 -1
  98. package/lib/typescript/pid/index.d.ts.map +0 -1
  99. package/lib/typescript/pid/issuing.d.ts.map +0 -1
  100. package/lib/typescript/pid/sd-jwt/converters.d.ts.map +0 -1
  101. package/lib/typescript/pid/sd-jwt/index.d.ts.map +0 -1
  102. package/lib/typescript/pid/sd-jwt/types.d.ts.map +0 -1
  103. package/lib/typescript/rp/__test__/index.test.d.ts.map +0 -1
  104. package/lib/typescript/rp/index.d.ts +0 -43
  105. package/lib/typescript/rp/index.d.ts.map +0 -1
  106. package/lib/typescript/sd-jwt/__test__/converters.test.d.ts.map +0 -1
  107. package/lib/typescript/sd-jwt/__test__/types.test.d.ts.map +0 -1
  108. package/lib/typescript/sd-jwt/converters.d.ts.map +0 -1
  109. package/lib/typescript/sd-jwt/index.d.ts.map +0 -1
  110. package/lib/typescript/sd-jwt/types.d.ts.map +0 -1
  111. package/lib/typescript/sd-jwt/verifier.d.ts +0 -3
  112. package/lib/typescript/sd-jwt/verifier.d.ts.map +0 -1
  113. package/lib/typescript/utils/dpop.d.ts.map +0 -1
  114. package/lib/typescript/utils/errors.d.ts.map +0 -1
  115. package/lib/typescript/utils/jwk.d.ts.map +0 -1
  116. package/lib/typescript/wallet-instance-attestation/index.d.ts.map +0 -1
  117. package/lib/typescript/wallet-instance-attestation/issuing.d.ts.map +0 -1
  118. /package/lib/typescript/{pid → src/pid}/index.d.ts +0 -0
  119. /package/lib/typescript/{pid → src/pid}/sd-jwt/converters.d.ts +0 -0
  120. /package/lib/typescript/{pid → src/pid}/sd-jwt/types.d.ts +0 -0
  121. /package/lib/typescript/{rp → src/rp}/__test__/index.test.d.ts +0 -0
  122. /package/lib/typescript/{sd-jwt → src/sd-jwt}/__test__/converters.test.d.ts +0 -0
  123. /package/lib/typescript/{sd-jwt → src/sd-jwt}/__test__/types.test.d.ts +0 -0
  124. /package/lib/typescript/{sd-jwt → src/sd-jwt}/converters.d.ts +0 -0
  125. /package/lib/typescript/{utils → src/utils}/dpop.d.ts +0 -0
  126. /package/lib/typescript/{utils → src/utils}/jwk.d.ts +0 -0
  127. /package/lib/typescript/{wallet-instance-attestation → src/wallet-instance-attestation}/index.d.ts +0 -0
  128. /package/lib/typescript/{wallet-instance-attestation → src/wallet-instance-attestation}/issuing.d.ts +0 -0
@@ -0,0 +1,46 @@
1
+ import { JWK } from "../utils/jwk";
2
+ import { z } from "zod";
3
+
4
+ export type PidDisplayMetadata = z.infer<typeof PidDisplayMetadata>;
5
+ export const PidDisplayMetadata = z.object({
6
+ name: z.string(),
7
+ locale: z.string(),
8
+ logo: z.object({
9
+ url: z.string(),
10
+ alt_text: z.string(),
11
+ }),
12
+ background_color: z.string(),
13
+ text_color: z.string(),
14
+ });
15
+
16
+ export type PidIssuerEntityConfiguration = z.infer<
17
+ typeof PidIssuerEntityConfiguration
18
+ >;
19
+ export const PidIssuerEntityConfiguration = z.object({
20
+ jwks: z.object({ keys: z.array(JWK) }),
21
+ metadata: z.object({
22
+ openid_credential_issuer: z.object({
23
+ credential_issuer: z.string(),
24
+ authorization_endpoint: z.string(),
25
+ token_endpoint: z.string(),
26
+ pushed_authorization_request_endpoint: z.string(),
27
+ dpop_signing_alg_values_supported: z.array(z.string()),
28
+ credential_endpoint: z.string(),
29
+ credentials_supported: z.object({
30
+ "eu.eudiw.pid.it": z.object({
31
+ format: z.literal("vc+sd-jwt"),
32
+ cryptographic_binding_methods_supported: z.array(z.string()),
33
+ cryptographic_suites_supported: z.array(z.string()),
34
+ display: z.array(PidDisplayMetadata),
35
+ }),
36
+ }),
37
+ }),
38
+ federation_entity: z.object({
39
+ organization_name: z.string(),
40
+ homepage_uri: z.string(),
41
+ policy_uri: z.string(),
42
+ tos_uri: z.string(),
43
+ logo_uri: z.string(),
44
+ }),
45
+ }),
46
+ });
@@ -1,5 +1,4 @@
1
- import { decode as decodeJwt } from "../../sd-jwt";
2
- import { verify as verifyJwt } from "../../sd-jwt";
1
+ import { decode as decodeJwt, verify as verifyJwt } from "../../sd-jwt";
3
2
  import { PID } from "./types";
4
3
  import { pidFromToken } from "./converters";
5
4
  import { Disclosure, SdJwt4VC } from "../../sd-jwt/types";
@@ -20,7 +19,11 @@ import { Disclosure, SdJwt4VC } from "../../sd-jwt/types";
20
19
  *
21
20
  */
22
21
  export function decode(token: string): PidWithToken {
23
- let { sdJwt, disclosures } = decodeJwt(token, SdJwt4VC);
22
+ let { sdJwt, disclosures: disclosuresWithOriginal } = decodeJwt(
23
+ token,
24
+ SdJwt4VC
25
+ );
26
+ const disclosures = disclosuresWithOriginal.map((d) => d.decoded);
24
27
  const pid = pidFromToken(sdJwt, disclosures);
25
28
 
26
29
  return { pid, sdJwt, disclosures };
@@ -54,7 +57,7 @@ export async function verify(token: string): Promise<VerifyResult> {
54
57
  return decoded;
55
58
  }
56
59
 
57
- type PidWithToken = {
60
+ export type PidWithToken = {
58
61
  // The object with the parsed data for PID
59
62
  pid: PID;
60
63
  // The object with the parsed SD-JWT token that shipped the PID. It will be needed to present PID data.
@@ -1,22 +1,18 @@
1
1
  import { RelyingPartySolution } from "..";
2
2
  import { AuthRequestDecodeError } from "../../utils/errors";
3
3
 
4
- const walletInstanceAttestation =
5
- "eyJhbGciOiJFUzI1NiIsImtpZCI6IjV0NVlZcEJoTi1FZ0lFRUk1aVV6cjZyME1SMDJMblZRME9tZWttTktjalkiLCJ0cnVzdF9jaGFpbiI6WyJleUpoYkdjaU9pSkZVei4uLjZTMEEiLCJleUpoYkdjaU9pSkZVei4uLmpKTEEiLCJleUpoYkdjaU9pSkZVei4uLkg5Z3ciXSwidHlwIjoidmErand0IiwieDVjIjpbIk1JSUJqRENDIC4uLiBYRmVoZ0tRQT09Il19.eyJpc3MiOiJodHRwczovL3dhbGxldC1wcm92aWRlci5leGFtcGxlLm9yZyIsInN1YiI6InZiZVhKa3NNNDV4cGh0QU5uQ2lHNm1DeXVVNGpmR056b3BHdUt2b2dnOWMiLCJ0eXBlIjoiV2FsbGV0SW5zdGFuY2VBdHRlc3RhdGlvbiIsInBvbGljeV91cmkiOiJodHRwczovL3dhbGxldC1wcm92aWRlci5leGFtcGxlLm9yZy9wcml2YWN5X3BvbGljeSIsInRvc191cmkiOiJodHRwczovL3dhbGxldC1wcm92aWRlci5leGFtcGxlLm9yZy9pbmZvX3BvbGljeSIsImxvZ29fdXJpIjoiaHR0cHM6Ly93YWxsZXQtcHJvdmlkZXIuZXhhbXBsZS5vcmcvbG9nby5zdmciLCJhc2MiOiJodHRwczovL3dhbGxldC1wcm92aWRlci5leGFtcGxlLm9yZy9Mb0EvYmFzaWMiLCJjbmYiOnsiandrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiNEhOcHRJLXhyMnBqeVJKS0dNbno0V21kblFEX3VKU3E0Ujk1Tmo5OGI0NCIsInkiOiJMSVpuU0IzOXZGSmhZZ1MzazdqWEU0cjMtQ29HRlF3WnRQQklScXBObHJnIiwia2lkIjoidmJlWEprc000NXhwaHRBTm5DaUc2bUN5dVU0amZHTnpvcEd1S3ZvZ2c5YyJ9fSwiYXV0aG9yaXphdGlvbl9lbmRwb2ludCI6ImV1ZGl3OiIsInJlc3BvbnNlX3R5cGVzX3N1cHBvcnRlZCI6WyJ2cF90b2tlbiJdLCJ2cF9mb3JtYXRzX3N1cHBvcnRlZCI6eyJqd3RfdnBfanNvbiI6eyJhbGdfdmFsdWVzX3N1cHBvcnRlZCI6WyJFUzI1NiJdfSwiand0X3ZjX2pzb24iOnsiYWxnX3ZhbHVlc19zdXBwb3J0ZWQiOlsiRVMyNTYiXX19LCJyZXF1ZXN0X29iamVjdF9zaWduaW5nX2FsZ192YWx1ZXNfc3VwcG9ydGVkIjpbIkVTMjU2Il0sInByZXNlbnRhdGlvbl9kZWZpbml0aW9uX3VyaV9zdXBwb3J0ZWQiOmZhbHNlLCJpYXQiOjE2ODcyODExOTUsImV4cCI6MTY4NzI4ODM5NX0.OTuPik6p3o9j6VOx-uCyxRvHwoh1pDiiZcBQFNQt2uE3dK-8izGNflJVETi_uhGSZOf25Enkq-UvEin9NrbJNw";
6
- const rp = new RelyingPartySolution(
7
- "http://rp.example",
8
- walletInstanceAttestation
9
- );
10
4
  describe("decodeAuthRequestQR", () => {
11
5
  it("should return authentication request URL", async () => {
12
6
  const qrcode =
13
7
  "ZXVkaXc6Ly9hdXRob3JpemU/Y2xpZW50X2lkPWh0dHBzOi8vdmVyaWZpZXIuZXhhbXBsZS5vcmcmcmVxdWVzdF91cmk9aHR0cHM6Ly92ZXJpZmllci5leGFtcGxlLm9yZy9yZXF1ZXN0X3VyaQ==";
14
- const result = rp.decodeAuthRequestQR(qrcode);
15
- expect(result).toEqual("https://verifier.example.org/request_uri");
8
+ const result = RelyingPartySolution.decodeAuthRequestQR(qrcode);
9
+ expect(result.requestURI).toEqual(
10
+ "https://verifier.example.org/request_uri"
11
+ );
16
12
  });
17
13
  it("should throw exception with invalid QR", async () => {
18
14
  const qrcode = "aHR0cDovL2dvb2dsZS5pdA==";
19
- expect(() => rp.decodeAuthRequestQR(qrcode)).toThrowError(
15
+ expect(() => RelyingPartySolution.decodeAuthRequestQR(qrcode)).toThrowError(
20
16
  AuthRequestDecodeError
21
17
  );
22
18
  });
package/src/rp/index.ts CHANGED
@@ -1,14 +1,26 @@
1
- import { AuthRequestDecodeError, IoWalletError } from "../utils/errors";
1
+ import {
2
+ AuthRequestDecodeError,
3
+ IoWalletError,
4
+ NoSuitableKeysFoundInEntityConfiguration,
5
+ } from "../utils/errors";
2
6
  import {
3
7
  decode as decodeJwt,
4
8
  decodeBase64,
5
9
  sha256ToBase64,
6
10
  SignJWT,
11
+ EncryptJwe,
12
+ verify,
7
13
  } from "@pagopa/io-react-native-jwt";
8
- import { RequestObject, RpEntityConfiguration } from "./types";
14
+ import {
15
+ QRCodePayload,
16
+ RequestObject,
17
+ RpEntityConfiguration,
18
+ type Presentation,
19
+ } from "./types";
9
20
 
10
21
  import uuid from "react-native-uuid";
11
22
  import type { JWK } from "@pagopa/io-react-native-jwt/lib/typescript/types";
23
+ import { disclose } from "../sd-jwt";
12
24
 
13
25
  export class RelyingPartySolution {
14
26
  relyingPartyBaseUrl: string;
@@ -33,24 +45,25 @@ export class RelyingPartySolution {
33
45
  * @returns The authentication request url
34
46
  *
35
47
  */
36
- decodeAuthRequestQR(qrcode: string): string {
37
- try {
38
- const decoded = decodeBase64(qrcode);
39
- const decodedUrl = new URL(decoded);
40
- const requestUri = decodedUrl.searchParams.get("request_uri");
41
- if (requestUri) {
42
- return requestUri;
43
- } else {
44
- throw new AuthRequestDecodeError(
45
- "Unable to obtain request_uri from QR code",
46
- `${decodedUrl}`
47
- );
48
- }
49
- } catch {
50
- throw new AuthRequestDecodeError(
51
- "Unable to decode QR code authentication request url",
52
- qrcode
53
- );
48
+ static decodeAuthRequestQR(qrcode: string): QRCodePayload {
49
+ const decoded = decodeBase64(qrcode);
50
+ const decodedUrl = new URL(decoded);
51
+ const protocol = decodedUrl.protocol;
52
+ const resource = decodedUrl.hostname;
53
+ const requestURI = decodedUrl.searchParams.get("request_uri");
54
+ const clientId = decodedUrl.searchParams.get("client_id");
55
+
56
+ const result = QRCodePayload.safeParse({
57
+ protocol,
58
+ resource,
59
+ requestURI,
60
+ clientId,
61
+ });
62
+
63
+ if (result.success) {
64
+ return result.data;
65
+ } else {
66
+ throw new AuthRequestDecodeError(result.error.message, `${decodedUrl}`);
54
67
  }
55
68
  }
56
69
  /**
@@ -85,19 +98,21 @@ export class RelyingPartySolution {
85
98
 
86
99
  /**
87
100
  * Obtain the Request Object for RP authentication
101
+ * @see https://italia.github.io/eudi-wallet-it-docs/versione-corrente/en/relying-party-solution.html
88
102
  *
89
- * @function
103
+ * @async @function
90
104
  * @param signedWalletInstanceDPoP JWT of the Wallet Instance Attestation DPoP
91
105
  *
92
106
  * @returns The Request Object JWT
107
+ * @throws {NoSuitableKeysFoundInEntityConfiguration} When the Request Object is signed with a key not listed in RP's entity configuration
93
108
  *
94
109
  */
95
110
  async getRequestObject(
96
- signedWalletInstanceDPoP: string
111
+ signedWalletInstanceDPoP: string,
112
+ entity: RpEntityConfiguration
97
113
  ): Promise<RequestObject> {
98
114
  const decodedJwtDPop = await decodeJwt(signedWalletInstanceDPoP);
99
115
  const requestUri = decodedJwtDPop.payload.htu as string;
100
-
101
116
  const response = await this.appFetch(requestUri, {
102
117
  method: "GET",
103
118
  headers: {
@@ -108,11 +123,28 @@ export class RelyingPartySolution {
108
123
 
109
124
  if (response.status === 200) {
110
125
  const responseText = await response.text();
111
- const responseJwt = await decodeJwt(responseText);
126
+ const responseJwt = decodeJwt(responseText);
127
+
128
+ // verify token signature according to RP's entity configuration
129
+ // to ensure the request object is authentic
130
+ {
131
+ const pubKey = entity.payload.jwks.keys.find(
132
+ ({ kid }) => kid === responseJwt.protectedHeader.kid
133
+ );
134
+ if (!pubKey) {
135
+ throw new NoSuitableKeysFoundInEntityConfiguration(
136
+ "Request Object signature verification"
137
+ );
138
+ }
139
+ await verify(responseText, pubKey);
140
+ }
141
+
142
+ // parse request object it has the expected shape by specification
112
143
  const requestObj = RequestObject.parse({
113
144
  header: responseJwt.protectedHeader,
114
145
  payload: responseJwt.payload,
115
146
  });
147
+
116
148
  return requestObj;
117
149
  }
118
150
 
@@ -121,6 +153,158 @@ export class RelyingPartySolution {
121
153
  );
122
154
  }
123
155
 
156
+ /**
157
+ * Prepare the Verified Presentation token for a received request object in the context of an authorization request flow.
158
+ * The presentation is prepared by disclosing data from provided credentials, according to requested claims
159
+ * Each Verified Credential come along with the claims the user accepts to disclose from it.
160
+ *
161
+ * The returned token is unsigned (sign should be apply by the caller).
162
+ *
163
+ * @todo accept more than a Verified Credential
164
+ *
165
+ * @param requestObj The incoming request object, which the requirements for the requested authorization
166
+ * @param presentation The Verified Credential containing user data along with the list of claims to be disclosed.
167
+ * @returns The unsigned Verified Presentation token
168
+ * @throws {ClaimsNotFoundBetweenDislosures} If the Verified Credential does not contain one or more requested claims.
169
+ *
170
+ */
171
+ async prepareVpToken(
172
+ requestObj: RequestObject,
173
+ [vc, claims]: Presentation // TODO: [SIW-353] support multiple presentations
174
+ ): Promise<{
175
+ vp_token: string;
176
+ presentation_submission: Record<string, unknown>;
177
+ }> {
178
+ // this throws if vc cannot satisfy all the requested claims
179
+ const { token: vp, paths } = await disclose(vc, claims);
180
+
181
+ // TODO: [SIW-359] check all requeste claims of the requestedObj are satisfied
182
+
183
+ const vp_token = new SignJWT({ vp })
184
+ .setAudience(requestObj.payload.response_uri)
185
+ .setExpirationTime("1h")
186
+ .setProtectedHeader({
187
+ typ: "JWT",
188
+ alg: "ES256",
189
+ })
190
+ .toSign();
191
+
192
+ const [definition_id, vc_scope] = requestObj.payload.scope;
193
+ const presentation_submission = {
194
+ definition_id,
195
+ id: `${uuid.v4()}`,
196
+ descriptor_map: paths.map((p) => ({
197
+ id: vc_scope,
198
+ path: `$.vp_token.${p.path}`,
199
+ format: "vc+sd-jwt",
200
+ })),
201
+ };
202
+
203
+ return { vp_token, presentation_submission };
204
+ }
205
+
206
+ /**
207
+ * Compose and send an Authorization Response in the context of an authorization request flow.
208
+ *
209
+ * @todo MUST add presentation_submission
210
+ *
211
+ * @param requestObj The incoming request object, which the requirements for the requested authorization
212
+ * @param vp_token The signed Verified Presentation token with data to send.
213
+ * @param presentation_submission
214
+ * @param entity The RP entity configuration
215
+ * @returns The response from the RP
216
+ * @throws {IoWalletError} if the submission fails.
217
+ * @throws {NoSuitableKeysFoundInEntityConfiguration} If entity do not contain any public key
218
+ *
219
+ */
220
+ async sendAuthorizationResponse(
221
+ requestObj: RequestObject,
222
+ vp_token: string,
223
+ presentation_submission: Record<string, unknown>,
224
+ entity: RpEntityConfiguration
225
+ ): Promise<string> {
226
+ // the request is an unsigned jws without iss, aud, exp
227
+ // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-signed-and-encrypted-respon
228
+ const jwk = this.choosePublicKeyToEncrypt(entity);
229
+ const enc = this.getEncryptionAlgByJwk(jwk);
230
+
231
+ const authzResponsePayload = JSON.stringify({
232
+ state: requestObj.payload.state,
233
+ presentation_submission,
234
+ vp_token,
235
+ });
236
+ const encrypted = await new EncryptJwe(authzResponsePayload, {
237
+ alg: jwk.alg,
238
+ enc,
239
+ }).encrypt(jwk);
240
+
241
+ const formBody = new URLSearchParams({ response: encrypted });
242
+ const response = await this.appFetch(requestObj.payload.response_uri, {
243
+ method: "POST",
244
+ headers: {
245
+ "Content-Type": "application/x-www-form-urlencoded",
246
+ },
247
+ body: formBody.toString(),
248
+ });
249
+
250
+ if (response.status === 200) {
251
+ return response.text();
252
+ }
253
+
254
+ throw new IoWalletError(
255
+ `Unable to send Authorization Response. Response code: ${response.status}`
256
+ );
257
+ }
258
+
259
+ /**
260
+ * Select a public key from those provided by the RP.
261
+ * Keys with algorithm "RSA-OAEP-256" or "RSA-OAEP" are expected, the firsts to be preferred.
262
+ *
263
+ * @param entity The RP entity configuration
264
+ * @returns A suitable public key with its compatible encryption algorithm
265
+ * @throws {NoSuitableKeysFoundInEntityConfiguration} If entity do not contain any public key suitable for encrypting
266
+ */
267
+ private choosePublicKeyToEncrypt(
268
+ entity: RpEntityConfiguration
269
+ ): (JWK & { alg: "RSA-OAEP-256" }) | (JWK & { alg: "RSA-OAEP" }) {
270
+ // Look for keys using "RSA-OAEP-256", and pick a random one
271
+ const [usingRsa256] = entity.payload.jwks.keys.filter(
272
+ <T>(k: T & { alg?: string }): k is T & { alg: "RSA-OAEP-256" } =>
273
+ typeof k.alg === "string" && k.alg === "RSA-OAEP-256"
274
+ );
275
+
276
+ if (usingRsa256) {
277
+ return usingRsa256;
278
+ }
279
+
280
+ // Look for keys using "RSA-OAEP", and pick a random one
281
+ const [usingRsa] = entity.payload.jwks.keys.filter(
282
+ <T>(k: T & { alg?: string }): k is T & { alg: "RSA-OAEP" } =>
283
+ typeof k.alg === "string" && k.alg === "RSA-OAEP"
284
+ );
285
+
286
+ if (usingRsa) {
287
+ return usingRsa;
288
+ }
289
+
290
+ // No suitable key has been found
291
+ throw new NoSuitableKeysFoundInEntityConfiguration(
292
+ "Encrypt with RP public key"
293
+ );
294
+ }
295
+
296
+ private getEncryptionAlgByJwk({
297
+ alg,
298
+ }: (JWK & { alg: "RSA-OAEP-256" }) | (JWK & { alg: "RSA-OAEP" })):
299
+ | "A128CBC-HS256"
300
+ | "A256CBC-HS512" {
301
+ if (alg === "RSA-OAEP-256") return "A256CBC-HS512";
302
+ if (alg === "RSA-OAEP") return "A128CBC-HS256";
303
+
304
+ const _: never = alg;
305
+ throw new Error(`Invalid jwk algorithm: ${_}`);
306
+ }
307
+
124
308
  /**
125
309
  * Obtain the relying party entity configuration.
126
310
  */
package/src/rp/types.ts CHANGED
@@ -62,3 +62,19 @@ export const RpEntityConfiguration = z.object({
62
62
  authority_hints: z.array(z.string()),
63
63
  }),
64
64
  });
65
+
66
+ export type QRCodePayload = z.infer<typeof QRCodePayload>;
67
+ export const QRCodePayload = z.object({
68
+ protocol: z.literal("eudiw:"),
69
+ resource: z.string(), // TODO: refine to known paths using literals
70
+ clientId: z.string(),
71
+ requestURI: z.string(),
72
+ });
73
+
74
+ /**
75
+ * A pair that associate a tokenized Verified Credential with the claims presented or requested to present.
76
+ */
77
+ export type Presentation = [
78
+ /* verified credential token */ string,
79
+ /* claims */ string[]
80
+ ];
@@ -0,0 +1,171 @@
1
+ import { decode, disclose } from "../index";
2
+
3
+ import { encodeBase64, decodeBase64 } from "@pagopa/io-react-native-jwt";
4
+ import { SdJwt4VC } from "../types";
5
+
6
+ // Examples from https://www.ietf.org/id/draft-terbu-sd-jwt-vc-02.html#name-example-4
7
+ // but adapted to adhere to format declared in https://italia.github.io/eudi-wallet-it-docs/versione-corrente/en/pid-eaa-data-model.html#id2
8
+ // In short, the token is a Frankenstein composed as follows:
9
+ // - the header is taken from the italian specification, with kid and alg valued according to the signing keys
10
+ // - disclosures are taken from the SD-JWT-4-VC standard
11
+ // - payload is taken from the italian specification, but _sd are compiled with:
12
+ // - "address" is used as verification._sd
13
+ // - all others disclosures are in claims._sd
14
+ const token =
15
+ "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImIxODZlYTBjMTkyNTc5MzA5N2JmMDFiOGEyODlhNDVmIiwidHJ1c3RfY2hhaW4iOlsiTkVoUmRFUnBZbmxIWTNNNVdsZFdUV1oyYVVobSAuLi4iLCJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2IC4uLiIsIklrSllkbVp5Ykc1b1FVMTFTRkl3TjJGcVZXMUIgLi4uIl19.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsInN1YiI6Ik56YkxzWGg4dURDY2Q3bm9XWEZaQWZIa3hac1JHQzlYcy4uLiIsImp0aSI6InVybjp1dWlkOjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlhdCI6MTU0MTQ5MzcyNCwiZXhwIjoxNTQxNDkzNzI0LCJzdGF0dXMiOiJodHRwczovL2V4YW1wbGUuY29tL3N0YXR1cyIsImNuZiI6eyJqd2siOnsia3R5IjoiUlNBIiwidXNlIjoic2lnIiwibiI6IjFUYS1zRSIsImUiOiJBUUFCIiwia2lkIjoiWWhORlMzWW5DOXRqaUNhaXZoV0xWVUozQXh3R0d6Xzk4dVJGYXFNRUVzIn19LCJ0eXBlIjoiUGVyc29uSWRlbnRpZmljYXRpb25EYXRhIiwidmVyaWZpZWRfY2xhaW1zIjp7InZlcmlmaWNhdGlvbiI6eyJfc2QiOlsiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSJdLCJ0cnVzdF9mcmFtZXdvcmsiOiJlaWRhcyIsImFzc3VyYW5jZV9sZXZlbCI6ImhpZ2gifSwiY2xhaW1zIjp7Il9zZCI6WyIwOXZLckpNT2x5VFdNMHNqcHVfcGRPQlZCUTJNMXkzS2hwSDUxNW5Ya3BZIiwiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9kYXcxc1g3NnBvVWxnQ3diSSIsIkVrTzhkaFcwZEhFSmJ2VUhsRV9WQ2V1Qzl1UkVMT2llTFpoaDdYYlVUdEEiLCJJbER6SUtlaVpkRHdwcXBLNlpmYnlwaEZ2ejVGZ25XYS1zTjZ3cVFYQ2l3IiwiUG9yRmJwS3VWdTZ4eW1KYWd2a0ZzRlhBYlJvYzJKR2xBVUEyQkE0bzdjSSIsIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCJqZHJURThZY2JZNEVpZnVnaWhpQWVfQlBla3hKUVpJQ2VpVVF3WTlRcXhJIiwianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdfX0sIl9zZF9hbGciOiJzaGEtMjU2In0.8wwSHCd47wCgzRYXvvPTTRXGS-hk9V8jRzy7WSjRBTZxSHxJkGOSWwBVAA-kpJ-IvQS7699aLWxIMqAvr34sOA~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImlzX292ZXJfMTgiLCB0cnVlXQ~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImlzX292ZXJfMjEiLCB0cnVlXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImlzX292ZXJfNjUiLCB0cnVlXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0";
16
+
17
+ const unsigned =
18
+ "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImIxODZlYTBjMTkyNTc5MzA5N2JmMDFiOGEyODlhNDVmIiwidHJ1c3RfY2hhaW4iOlsiTkVoUmRFUnBZbmxIWTNNNVdsZFdUV1oyYVVobSAuLi4iLCJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2IC4uLiIsIklrSllkbVp5Ykc1b1FVMTFTRkl3TjJGcVZXMUIgLi4uIl19.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsInN1YiI6Ik56YkxzWGg4dURDY2Q3bm9XWEZaQWZIa3hac1JHQzlYcy4uLiIsImp0aSI6InVybjp1dWlkOjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlhdCI6MTU0MTQ5MzcyNCwiZXhwIjoxNTQxNDkzNzI0LCJzdGF0dXMiOiJodHRwczovL2V4YW1wbGUuY29tL3N0YXR1cyIsImNuZiI6eyJqd2siOnsia3R5IjoiUlNBIiwidXNlIjoic2lnIiwibiI6IjFUYS1zRSIsImUiOiJBUUFCIiwia2lkIjoiWWhORlMzWW5DOXRqaUNhaXZoV0xWVUozQXh3R0d6Xzk4dVJGYXFNRUVzIn19LCJ0eXBlIjoiUGVyc29uSWRlbnRpZmljYXRpb25EYXRhIiwidmVyaWZpZWRfY2xhaW1zIjp7InZlcmlmaWNhdGlvbiI6eyJfc2QiOlsiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSJdLCJ0cnVzdF9mcmFtZXdvcmsiOiJlaWRhcyIsImFzc3VyYW5jZV9sZXZlbCI6ImhpZ2gifSwiY2xhaW1zIjp7Il9zZCI6WyIwOXZLckpNT2x5VFdNMHNqcHVfcGRPQlZCUTJNMXkzS2hwSDUxNW5Ya3BZIiwiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9kYXcxc1g3NnBvVWxnQ3diSSIsIkVrTzhkaFcwZEhFSmJ2VUhsRV9WQ2V1Qzl1UkVMT2llTFpoaDdYYlVUdEEiLCJJbER6SUtlaVpkRHdwcXBLNlpmYnlwaEZ2ejVGZ25XYS1zTjZ3cVFYQ2l3IiwiUG9yRmJwS3VWdTZ4eW1KYWd2a0ZzRlhBYlJvYzJKR2xBVUEyQkE0bzdjSSIsIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCJqZHJURThZY2JZNEVpZnVnaWhpQWVfQlBla3hKUVpJQ2VpVVF3WTlRcXhJIiwianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdfX0sIl9zZF9hbGciOiJzaGEtMjU2In0";
19
+
20
+ const signature =
21
+ "8wwSHCd47wCgzRYXvvPTTRXGS-hk9V8jRzy7WSjRBTZxSHxJkGOSWwBVAA-kpJ-IvQS7699aLWxIMqAvr34sOA";
22
+
23
+ const signed = `${unsigned}.${signature}`;
24
+
25
+ const tokenizedDisclosures = [
26
+ "WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd",
27
+ "WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd",
28
+ "WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ",
29
+ "WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ",
30
+ "WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0",
31
+ "WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImlzX292ZXJfMTgiLCB0cnVlXQ",
32
+ "WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImlzX292ZXJfMjEiLCB0cnVlXQ",
33
+ "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImlzX292ZXJfNjUiLCB0cnVlXQ",
34
+ "WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0",
35
+ ];
36
+
37
+ const sdJwt = {
38
+ header: {
39
+ typ: "vc+sd-jwt",
40
+ alg: "ES256",
41
+ kid: "b186ea0c1925793097bf01b8a289a45f",
42
+ trust_chain: [
43
+ "NEhRdERpYnlHY3M5WldWTWZ2aUhm ...",
44
+ "eyJhbGciOiJSUzI1NiIsImtpZCI6 ...",
45
+ "IkJYdmZybG5oQU11SFIwN2FqVW1B ...",
46
+ ],
47
+ },
48
+ payload: {
49
+ iss: "https://example.com/issuer",
50
+ sub: "NzbLsXh8uDCcd7noWXFZAfHkxZsRGC9Xs...",
51
+ jti: "urn:uuid:6c5c0a49-b589-431d-bae7-219122a9ec2c",
52
+ iat: 1541493724,
53
+ exp: 1541493724,
54
+ status: "https://example.com/status",
55
+ cnf: {
56
+ jwk: {
57
+ kty: "RSA",
58
+ use: "sig",
59
+ n: "1Ta-sE",
60
+ e: "AQAB",
61
+ kid: "YhNFS3YnC9tjiCaivhWLVUJ3AxwGGz_98uRFaqMEEs",
62
+ },
63
+ },
64
+ type: "PersonIdentificationData",
65
+ verified_claims: {
66
+ verification: {
67
+ _sd: ["JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE"],
68
+ trust_framework: "eidas",
69
+ assurance_level: "high",
70
+ },
71
+ claims: {
72
+ _sd: [
73
+ "09vKrJMOlyTWM0sjpu_pdOBVBQ2M1y3KhpH515nXkpY",
74
+ "2rsjGbaC0ky8mT0pJrPioWTq0_daw1sX76poUlgCwbI",
75
+ "EkO8dhW0dHEJbvUHlE_VCeuC9uRELOieLZhh7XbUTtA",
76
+ "IlDzIKeiZdDwpqpK6ZfbyphFvz5FgnWa-sN6wqQXCiw",
77
+ "PorFbpKuVu6xymJagvkFsFXAbRoc2JGlAUA2BA4o7cI",
78
+ "TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo",
79
+ "jdrTE8YcbY4EifugihiAe_BPekxJQZICeiUQwY9QqxI",
80
+ "jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4",
81
+ ],
82
+ },
83
+ },
84
+ _sd_alg: "sha-256",
85
+ },
86
+ };
87
+
88
+ // In the very same order than tokenizedDisclosures
89
+ const disclosures = [
90
+ ["2GLC42sKQveCfGfryNRN9w", "given_name", "John"],
91
+ ["eluV5Og3gSNII8EYnsxA_A", "family_name", "Doe"],
92
+ ["6Ij7tM-a5iVPGboS5tmvVA", "email", "johndoe@example.com"],
93
+ ["eI8ZWm9QnKPpNPeNenHdhQ", "phone_number", "+1-202-555-0101"],
94
+ ["AJx-095VPrpTtN4QMOqROA", "birthdate", "1940-01-01"],
95
+ ["Pc33JM2LchcU_lHggv_ufQ", "is_over_18", true],
96
+ ["G02NSrQfjFXQ7Io09syajA", "is_over_21", true],
97
+ ["lklxF5jMYlGTPUovMNIvCA", "is_over_65", true],
98
+ [
99
+ "Qg_O64zqAxe412a108iroA",
100
+ "address",
101
+ {
102
+ street_address: "123 Main St",
103
+ locality: "Anytown",
104
+ region: "Anystate",
105
+ country: "US",
106
+ },
107
+ ],
108
+ ];
109
+ it("Ensures example data correctness", () => {
110
+ expect(
111
+ JSON.parse(decodeBase64(encodeBase64(JSON.stringify(sdJwt.header))))
112
+ ).toEqual(sdJwt.header);
113
+ expect([signed, ...tokenizedDisclosures].join("~")).toBe(token);
114
+ });
115
+
116
+ describe("decode", () => {
117
+ it("should decode a valid token", () => {
118
+ const result = decode(token, SdJwt4VC);
119
+ expect(result).toEqual({
120
+ sdJwt,
121
+ disclosures: disclosures.map((decoded, i) => ({
122
+ decoded,
123
+ encoded: tokenizedDisclosures[i],
124
+ })),
125
+ });
126
+ });
127
+ });
128
+
129
+ describe("disclose", () => {
130
+ it("should encode a valid sdjwt (one claim)", async () => {
131
+ const result = await disclose(token, ["given_name"]);
132
+ const expected = {
133
+ token: `${signed}~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd`,
134
+ paths: [{ claim: "given_name", path: "verified_claims.claims._sd[7]" }],
135
+ };
136
+
137
+ expect(result).toEqual(expected);
138
+ });
139
+
140
+ it("should encode a valid sdjwt (no claims)", async () => {
141
+ const result = await disclose(token, []);
142
+ const expected = { token: `${signed}`, paths: [] };
143
+
144
+ expect(result).toEqual(expected);
145
+ });
146
+
147
+ it("should encode a valid sdjwt (multiple claims)", async () => {
148
+ const result = await disclose(token, ["given_name", "email"]);
149
+ const expected = {
150
+ token: `${signed}~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ`,
151
+ paths: [
152
+ {
153
+ claim: "given_name",
154
+ path: "verified_claims.claims._sd[7]",
155
+ },
156
+ {
157
+ claim: "email",
158
+ path: "verified_claims.verification._sd[0]",
159
+ },
160
+ ],
161
+ };
162
+
163
+ expect(result).toEqual(expected);
164
+ });
165
+
166
+ it("should fail on unknown claim", async () => {
167
+ const fn = async () => disclose(token, ["unknown"]);
168
+
169
+ await expect(fn()).rejects.toEqual(expect.any(Error));
170
+ });
171
+ });