@pagopa/io-react-native-wallet 0.2.2 → 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 (118) hide show
  1. package/lib/commonjs/pid/issuing.js +28 -0
  2. package/lib/commonjs/pid/issuing.js.map +1 -1
  3. package/lib/commonjs/pid/metadata.js +51 -0
  4. package/lib/commonjs/pid/metadata.js.map +1 -0
  5. package/lib/commonjs/pid/sd-jwt/index.js +2 -1
  6. package/lib/commonjs/pid/sd-jwt/index.js.map +1 -1
  7. package/lib/commonjs/rp/index.js +148 -3
  8. package/lib/commonjs/rp/index.js.map +1 -1
  9. package/lib/commonjs/rp/types.js +4 -0
  10. package/lib/commonjs/rp/types.js.map +1 -1
  11. package/lib/commonjs/sd-jwt/__test__/index.test.js +119 -0
  12. package/lib/commonjs/sd-jwt/__test__/index.test.js.map +1 -0
  13. package/lib/commonjs/sd-jwt/index.js +84 -4
  14. package/lib/commonjs/sd-jwt/index.js.map +1 -1
  15. package/lib/commonjs/sd-jwt/types.js +9 -0
  16. package/lib/commonjs/sd-jwt/types.js.map +1 -1
  17. package/lib/commonjs/sd-jwt/verifier.js +7 -5
  18. package/lib/commonjs/sd-jwt/verifier.js.map +1 -1
  19. package/lib/commonjs/utils/errors.js +76 -1
  20. package/lib/commonjs/utils/errors.js.map +1 -1
  21. package/lib/module/pid/issuing.js +30 -2
  22. package/lib/module/pid/issuing.js.map +1 -1
  23. package/lib/module/pid/metadata.js +43 -0
  24. package/lib/module/pid/metadata.js.map +1 -0
  25. package/lib/module/pid/sd-jwt/index.js +3 -3
  26. package/lib/module/pid/sd-jwt/index.js.map +1 -1
  27. package/lib/module/rp/index.js +150 -5
  28. package/lib/module/rp/index.js.map +1 -1
  29. package/lib/module/rp/types.js +4 -0
  30. package/lib/module/rp/types.js.map +1 -1
  31. package/lib/module/sd-jwt/__test__/index.test.js +118 -0
  32. package/lib/module/sd-jwt/__test__/index.test.js.map +1 -0
  33. package/lib/module/sd-jwt/index.js +83 -3
  34. package/lib/module/sd-jwt/index.js.map +1 -1
  35. package/lib/module/sd-jwt/types.js +10 -0
  36. package/lib/module/sd-jwt/types.js.map +1 -1
  37. package/lib/module/sd-jwt/verifier.js +8 -6
  38. package/lib/module/sd-jwt/verifier.js.map +1 -1
  39. package/lib/module/utils/errors.js +71 -0
  40. package/lib/module/utils/errors.js.map +1 -1
  41. package/lib/typescript/src/index.d.ts.map +1 -0
  42. package/lib/typescript/src/pid/index.d.ts.map +1 -0
  43. package/lib/typescript/{pid → src/pid}/issuing.d.ts +9 -0
  44. package/lib/typescript/src/pid/issuing.d.ts.map +1 -0
  45. package/lib/typescript/src/pid/metadata.d.ts +528 -0
  46. package/lib/typescript/src/pid/metadata.d.ts.map +1 -0
  47. package/lib/typescript/src/pid/sd-jwt/converters.d.ts.map +1 -0
  48. package/lib/typescript/src/pid/sd-jwt/index.d.ts.map +1 -0
  49. package/lib/typescript/src/pid/sd-jwt/types.d.ts.map +1 -0
  50. package/lib/typescript/src/rp/__test__/index.test.d.ts.map +1 -0
  51. package/lib/typescript/src/rp/index.d.ts +89 -0
  52. package/lib/typescript/src/rp/index.d.ts.map +1 -0
  53. package/lib/typescript/{rp → src/rp}/types.d.ts +54 -47
  54. package/lib/typescript/{rp → src/rp}/types.d.ts.map +1 -1
  55. package/lib/typescript/src/sd-jwt/__test__/converters.test.d.ts.map +1 -0
  56. package/lib/typescript/src/sd-jwt/__test__/index.test.d.ts +2 -0
  57. package/lib/typescript/src/sd-jwt/__test__/index.test.d.ts.map +1 -0
  58. package/lib/typescript/src/sd-jwt/__test__/types.test.d.ts.map +1 -0
  59. package/lib/typescript/src/sd-jwt/converters.d.ts.map +1 -0
  60. package/lib/typescript/{sd-jwt → src/sd-jwt}/index.d.ts +22 -2
  61. package/lib/typescript/src/sd-jwt/index.d.ts.map +1 -0
  62. package/lib/typescript/{sd-jwt → src/sd-jwt}/types.d.ts +12 -0
  63. package/lib/typescript/src/sd-jwt/types.d.ts.map +1 -0
  64. package/lib/typescript/src/sd-jwt/verifier.d.ts +3 -0
  65. package/lib/typescript/src/sd-jwt/verifier.d.ts.map +1 -0
  66. package/lib/typescript/src/utils/dpop.d.ts.map +1 -0
  67. package/lib/typescript/{utils → src/utils}/errors.d.ts +41 -0
  68. package/lib/typescript/src/utils/errors.d.ts.map +1 -0
  69. package/lib/typescript/src/utils/jwk.d.ts.map +1 -0
  70. package/lib/typescript/src/wallet-instance-attestation/index.d.ts.map +1 -0
  71. package/lib/typescript/src/wallet-instance-attestation/issuing.d.ts.map +1 -0
  72. package/lib/typescript/{wallet-instance-attestation → src/wallet-instance-attestation}/types.d.ts +8 -8
  73. package/lib/typescript/{wallet-instance-attestation → src/wallet-instance-attestation}/types.d.ts.map +1 -1
  74. package/package.json +4 -3
  75. package/src/pid/issuing.ts +38 -1
  76. package/src/pid/metadata.ts +46 -0
  77. package/src/pid/sd-jwt/index.ts +6 -3
  78. package/src/rp/index.ts +189 -5
  79. package/src/rp/types.ts +8 -0
  80. package/src/sd-jwt/__test__/index.test.ts +171 -0
  81. package/src/sd-jwt/index.ts +84 -7
  82. package/src/sd-jwt/types.ts +13 -0
  83. package/src/sd-jwt/verifier.ts +5 -7
  84. package/src/utils/errors.ts +81 -0
  85. package/lib/typescript/index.d.ts.map +0 -1
  86. package/lib/typescript/pid/index.d.ts.map +0 -1
  87. package/lib/typescript/pid/issuing.d.ts.map +0 -1
  88. package/lib/typescript/pid/sd-jwt/converters.d.ts.map +0 -1
  89. package/lib/typescript/pid/sd-jwt/index.d.ts.map +0 -1
  90. package/lib/typescript/pid/sd-jwt/types.d.ts.map +0 -1
  91. package/lib/typescript/rp/__test__/index.test.d.ts.map +0 -1
  92. package/lib/typescript/rp/index.d.ts +0 -43
  93. package/lib/typescript/rp/index.d.ts.map +0 -1
  94. package/lib/typescript/sd-jwt/__test__/converters.test.d.ts.map +0 -1
  95. package/lib/typescript/sd-jwt/__test__/types.test.d.ts.map +0 -1
  96. package/lib/typescript/sd-jwt/converters.d.ts.map +0 -1
  97. package/lib/typescript/sd-jwt/index.d.ts.map +0 -1
  98. package/lib/typescript/sd-jwt/types.d.ts.map +0 -1
  99. package/lib/typescript/sd-jwt/verifier.d.ts +0 -3
  100. package/lib/typescript/sd-jwt/verifier.d.ts.map +0 -1
  101. package/lib/typescript/utils/dpop.d.ts.map +0 -1
  102. package/lib/typescript/utils/errors.d.ts.map +0 -1
  103. package/lib/typescript/utils/jwk.d.ts.map +0 -1
  104. package/lib/typescript/wallet-instance-attestation/index.d.ts.map +0 -1
  105. package/lib/typescript/wallet-instance-attestation/issuing.d.ts.map +0 -1
  106. /package/lib/typescript/{index.d.ts → src/index.d.ts} +0 -0
  107. /package/lib/typescript/{pid → src/pid}/index.d.ts +0 -0
  108. /package/lib/typescript/{pid → src/pid}/sd-jwt/converters.d.ts +0 -0
  109. /package/lib/typescript/{pid → src/pid}/sd-jwt/index.d.ts +0 -0
  110. /package/lib/typescript/{pid → src/pid}/sd-jwt/types.d.ts +0 -0
  111. /package/lib/typescript/{rp → src/rp}/__test__/index.test.d.ts +0 -0
  112. /package/lib/typescript/{sd-jwt → src/sd-jwt}/__test__/converters.test.d.ts +0 -0
  113. /package/lib/typescript/{sd-jwt → src/sd-jwt}/__test__/types.test.d.ts +0 -0
  114. /package/lib/typescript/{sd-jwt → src/sd-jwt}/converters.d.ts +0 -0
  115. /package/lib/typescript/{utils → src/utils}/dpop.d.ts +0 -0
  116. /package/lib/typescript/{utils → src/utils}/jwk.d.ts +0 -0
  117. /package/lib/typescript/{wallet-instance-attestation → src/wallet-instance-attestation}/index.d.ts +0 -0
  118. /package/lib/typescript/{wallet-instance-attestation → src/wallet-instance-attestation}/issuing.d.ts +0 -0
@@ -1,14 +1,16 @@
1
1
  import {
2
2
  decode as decodeJwt,
3
+ verify as verifyJwt,
3
4
  sha256ToBase64,
4
5
  } from "@pagopa/io-react-native-jwt";
5
6
 
6
7
  import { SignJWT, thumbprint } from "@pagopa/io-react-native-jwt";
7
8
  import { JWK } from "../utils/jwk";
8
9
  import uuid from "react-native-uuid";
9
- import { PidIssuingError } from "../utils/errors";
10
+ import { PidIssuingError, PidMetadataError } from "../utils/errors";
10
11
  import { getUnsignedDPop } from "../utils/dpop";
11
12
  import { sign, generate, deleteKey } from "@pagopa/io-react-native-crypto";
13
+ import { PidIssuerEntityConfiguration } from "./metadata";
12
14
 
13
15
  // This is a temporary type that will be used for demo purposes only
14
16
  export type CieData = {
@@ -302,4 +304,39 @@ export class Issuing {
302
304
 
303
305
  throw new PidIssuingError(`Unable to obtain credential!`);
304
306
  }
307
+
308
+ /**
309
+ * Obtain the PID issuer metadata
310
+ *
311
+ * @function
312
+ * @returns PID issuer metadata
313
+ *
314
+ */
315
+ async getEntityConfiguration(): Promise<PidIssuerEntityConfiguration> {
316
+ const metadataUrl = new URL(
317
+ ".well-known/openid-federation",
318
+ this.pidProviderBaseUrl
319
+ ).href;
320
+
321
+ const response = await this.appFetch(metadataUrl);
322
+
323
+ if (response.status === 200) {
324
+ const jwtMetadata = await response.text();
325
+ const { payload } = decodeJwt(jwtMetadata);
326
+ const result = PidIssuerEntityConfiguration.safeParse(payload);
327
+ if (result.success) {
328
+ const parsedMetadata = result.data;
329
+ await verifyJwt(jwtMetadata, parsedMetadata.jwks.keys);
330
+ return parsedMetadata;
331
+ } else {
332
+ throw new PidMetadataError(result.error.message);
333
+ }
334
+ }
335
+
336
+ throw new PidMetadataError(
337
+ `Unable to obtain PID metadata. Response: ${await response.text()} with status: ${
338
+ response.status
339
+ }`
340
+ );
341
+ }
305
342
  }
@@ -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 };
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 { QRCodePayload, 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;
@@ -86,15 +98,18 @@ export class RelyingPartySolution {
86
98
 
87
99
  /**
88
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
89
102
  *
90
- * @function
103
+ * @async @function
91
104
  * @param signedWalletInstanceDPoP JWT of the Wallet Instance Attestation DPoP
92
105
  *
93
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
94
108
  *
95
109
  */
96
110
  async getRequestObject(
97
- signedWalletInstanceDPoP: string
111
+ signedWalletInstanceDPoP: string,
112
+ entity: RpEntityConfiguration
98
113
  ): Promise<RequestObject> {
99
114
  const decodedJwtDPop = await decodeJwt(signedWalletInstanceDPoP);
100
115
  const requestUri = decodedJwtDPop.payload.htu as string;
@@ -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
@@ -70,3 +70,11 @@ export const QRCodePayload = z.object({
70
70
  clientId: z.string(),
71
71
  requestURI: z.string(),
72
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
+ });
@@ -2,11 +2,21 @@ import { z } from "zod";
2
2
 
3
3
  import { decode as decodeJwt } from "@pagopa/io-react-native-jwt";
4
4
  import { verify as verifyJwt } from "@pagopa/io-react-native-jwt";
5
+ import { sha256ToBase64 } from "@pagopa/io-react-native-jwt";
5
6
 
6
7
  import { decodeBase64 } from "@pagopa/io-react-native-jwt";
7
- import { Disclosure } from "./types";
8
+ import { Disclosure, SdJwt4VC, type DisclosureWithEncoded } from "./types";
8
9
  import { verifyDisclosure } from "./verifier";
9
10
  import type { JWK } from "src/utils/jwk";
11
+ import {
12
+ ClaimsNotFoundBetweenDislosures,
13
+ ClaimsNotFoundInToken,
14
+ } from "../utils/errors";
15
+
16
+ const decodeDisclosure = (encoded: string): DisclosureWithEncoded => {
17
+ const decoded = Disclosure.parse(JSON.parse(decodeBase64(encoded)));
18
+ return { decoded, encoded };
19
+ };
10
20
 
11
21
  /**
12
22
  * Decode a given SD-JWT with Disclosures to get the parsed SD-JWT object they define.
@@ -25,7 +35,10 @@ import type { JWK } from "src/utils/jwk";
25
35
  export const decode = <S extends z.AnyZodObject>(
26
36
  token: string,
27
37
  schema: S
28
- ): { sdJwt: z.infer<S>; disclosures: Disclosure[] } => {
38
+ ): {
39
+ sdJwt: z.infer<S>;
40
+ disclosures: DisclosureWithEncoded[];
41
+ } => {
29
42
  // token are expected in the form "sd-jwt~disclosure0~disclosure1~...~disclosureN~"
30
43
  if (token.slice(-1) === "~") {
31
44
  token = token.slice(0, -1);
@@ -43,14 +56,75 @@ export const decode = <S extends z.AnyZodObject>(
43
56
  // get disclosures as list of triples
44
57
  // validate each triple
45
58
  // throw a validation error if at least one fails to parse
46
- const disclosures = rawDisclosures
47
- .map(decodeBase64)
48
- .map((e) => JSON.parse(e))
49
- .map((e) => Disclosure.parse(e));
59
+ const disclosures = rawDisclosures.map(decodeDisclosure);
50
60
 
51
61
  return { sdJwt, disclosures };
52
62
  };
53
63
 
64
+ /**
65
+ * Select disclosures from a given SD-JWT with Disclosures.
66
+ * Claims relate with disclosures by their name.
67
+ *
68
+ * @function
69
+ * @param token The encoded token that represents a valid sd-jwt for verifiable credentials
70
+ * @param claims The list of claims to be disclosed
71
+ *
72
+ * @throws {ClaimsNotFoundBetweenDislosures} When one or more claims does not relate to any discloure.
73
+ * @throws {ClaimsNotFoundInToken} When one or more claims are not contained in the SD-JWT token.
74
+ * @returns The encoded token with only the requested disclosures, along with the path each claim can be found on the SD-JWT token
75
+ *
76
+ */
77
+ export const disclose = async (
78
+ token: string,
79
+ claims: string[]
80
+ ): Promise<{ token: string; paths: { claim: string; path: string }[] }> => {
81
+ const [rawSdJwt, ...rawDisclosures] = token.split("~");
82
+ const { sdJwt, disclosures } = decode(token, SdJwt4VC);
83
+
84
+ // for each claim, return the path on which they are located in the SD-JWT token
85
+ const paths = await Promise.all(
86
+ claims.map(async (claim) => {
87
+ const disclosure = disclosures.find(
88
+ ({ decoded: [, name] }) => name === claim
89
+ );
90
+
91
+ // check every claim represents a known disclosure
92
+ if (!disclosure) {
93
+ throw new ClaimsNotFoundBetweenDislosures(claim);
94
+ }
95
+
96
+ const hash = await sha256ToBase64(disclosure.encoded);
97
+
98
+ // _sd is defined in verified_claims.claims and verified_claims.verification
99
+ // we must look into both
100
+ if (sdJwt.payload.verified_claims.claims._sd.includes(hash)) {
101
+ const index = sdJwt.payload.verified_claims.claims._sd.indexOf(hash);
102
+ return { claim, path: `verified_claims.claims._sd[${index}]` };
103
+ } else if (
104
+ sdJwt.payload.verified_claims.verification._sd.includes(hash)
105
+ ) {
106
+ const index =
107
+ sdJwt.payload.verified_claims.verification._sd.indexOf(hash);
108
+ return { claim, path: `verified_claims.verification._sd[${index}]` };
109
+ }
110
+
111
+ throw new ClaimsNotFoundInToken(claim);
112
+ })
113
+ );
114
+
115
+ const filteredDisclosures = rawDisclosures.filter((d) => {
116
+ const {
117
+ decoded: [, name],
118
+ } = decodeDisclosure(d);
119
+ return claims.includes(name);
120
+ });
121
+
122
+ // compose the final disclosed token
123
+ const disclosedToken = [rawSdJwt, ...filteredDisclosures].join("~");
124
+
125
+ return { token: disclosedToken, paths };
126
+ };
127
+
54
128
  /**
55
129
  * Verify a given SD-JWT with Disclosures
56
130
  * Same as {@link decode} plus:
@@ -91,5 +165,8 @@ export const verify = async <S extends z.AnyZodObject>(
91
165
  )
92
166
  );
93
167
 
94
- return decoded;
168
+ return {
169
+ sdJwt: decoded.sdJwt,
170
+ disclosures: decoded.disclosures.map((d) => d.decoded),
171
+ };
95
172
  };