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

Sign up to get free protection for your applications and to get access to all the features.
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
+ });