@pagopa/io-react-native-wallet 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/lib/commonjs/credential/issuance/06-obtain-credential.js +1 -5
  2. package/lib/commonjs/credential/issuance/06-obtain-credential.js.map +1 -1
  3. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js +33 -21
  4. package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  5. package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js +318 -24
  6. package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
  7. package/lib/commonjs/credential/presentation/08-send-authorization-response.js +47 -83
  8. package/lib/commonjs/credential/presentation/08-send-authorization-response.js.map +1 -1
  9. package/lib/commonjs/credential/presentation/errors.js +18 -1
  10. package/lib/commonjs/credential/presentation/errors.js.map +1 -1
  11. package/lib/commonjs/credential/presentation/index.js +8 -2
  12. package/lib/commonjs/credential/presentation/index.js.map +1 -1
  13. package/lib/commonjs/credential/presentation/types.js +6 -2
  14. package/lib/commonjs/credential/presentation/types.js.map +1 -1
  15. package/lib/commonjs/entity/trust/chain.js.map +1 -1
  16. package/lib/commonjs/mdoc/index.js +45 -13
  17. package/lib/commonjs/mdoc/index.js.map +1 -1
  18. package/lib/commonjs/sd-jwt/index.js +41 -1
  19. package/lib/commonjs/sd-jwt/index.js.map +1 -1
  20. package/lib/commonjs/utils/crypto.js +70 -4
  21. package/lib/commonjs/utils/crypto.js.map +1 -1
  22. package/lib/commonjs/utils/string.js +6 -7
  23. package/lib/commonjs/utils/string.js.map +1 -1
  24. package/lib/module/credential/issuance/06-obtain-credential.js +1 -5
  25. package/lib/module/credential/issuance/06-obtain-credential.js.map +1 -1
  26. package/lib/module/credential/issuance/07-verify-and-parse-credential.js +33 -21
  27. package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
  28. package/lib/module/credential/presentation/07-evaluate-input-descriptor.js +311 -23
  29. package/lib/module/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
  30. package/lib/module/credential/presentation/08-send-authorization-response.js +46 -81
  31. package/lib/module/credential/presentation/08-send-authorization-response.js.map +1 -1
  32. package/lib/module/credential/presentation/errors.js +16 -0
  33. package/lib/module/credential/presentation/errors.js.map +1 -1
  34. package/lib/module/credential/presentation/index.js +2 -2
  35. package/lib/module/credential/presentation/index.js.map +1 -1
  36. package/lib/module/credential/presentation/types.js +6 -2
  37. package/lib/module/credential/presentation/types.js.map +1 -1
  38. package/lib/module/entity/trust/chain.js.map +1 -1
  39. package/lib/module/mdoc/index.js +43 -12
  40. package/lib/module/mdoc/index.js.map +1 -1
  41. package/lib/module/sd-jwt/index.js +40 -1
  42. package/lib/module/sd-jwt/index.js.map +1 -1
  43. package/lib/module/utils/crypto.js +67 -2
  44. package/lib/module/utils/crypto.js.map +1 -1
  45. package/lib/module/utils/string.js +4 -6
  46. package/lib/module/utils/string.js.map +1 -1
  47. package/lib/typescript/credential/issuance/06-obtain-credential.d.ts.map +1 -1
  48. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts +1 -1
  49. package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
  50. package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts +106 -9
  51. package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts.map +1 -1
  52. package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts +4 -33
  53. package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts.map +1 -1
  54. package/lib/typescript/credential/presentation/errors.d.ts +11 -0
  55. package/lib/typescript/credential/presentation/errors.d.ts.map +1 -1
  56. package/lib/typescript/credential/presentation/index.d.ts +3 -3
  57. package/lib/typescript/credential/presentation/index.d.ts.map +1 -1
  58. package/lib/typescript/credential/presentation/types.d.ts +18 -6
  59. package/lib/typescript/credential/presentation/types.d.ts.map +1 -1
  60. package/lib/typescript/entity/trust/chain.d.ts.map +1 -1
  61. package/lib/typescript/mdoc/index.d.ts +6 -2
  62. package/lib/typescript/mdoc/index.d.ts.map +1 -1
  63. package/lib/typescript/sd-jwt/index.d.ts +19 -0
  64. package/lib/typescript/sd-jwt/index.d.ts.map +1 -1
  65. package/lib/typescript/utils/crypto.d.ts +8 -0
  66. package/lib/typescript/utils/crypto.d.ts.map +1 -1
  67. package/lib/typescript/utils/errors.d.ts.map +1 -1
  68. package/lib/typescript/utils/misc.d.ts.map +1 -1
  69. package/lib/typescript/utils/string.d.ts +3 -3
  70. package/lib/typescript/utils/string.d.ts.map +1 -1
  71. package/package.json +16 -14
  72. package/src/credential/issuance/06-obtain-credential.ts +1 -7
  73. package/src/credential/issuance/07-verify-and-parse-credential.ts +37 -16
  74. package/src/credential/presentation/07-evaluate-input-descriptor.ts +459 -49
  75. package/src/credential/presentation/08-send-authorization-response.ts +57 -101
  76. package/src/credential/presentation/errors.ts +16 -0
  77. package/src/credential/presentation/index.ts +8 -4
  78. package/src/credential/presentation/types.ts +16 -3
  79. package/src/entity/trust/chain.ts +14 -10
  80. package/src/mdoc/index.ts +72 -15
  81. package/src/sd-jwt/index.ts +49 -1
  82. package/src/utils/crypto.ts +61 -2
  83. package/src/utils/errors.ts +2 -2
  84. package/src/utils/misc.ts +2 -2
  85. package/src/utils/string.ts +4 -6
@@ -1,22 +1,17 @@
1
- import {
2
- EncryptJwe,
3
- SignJWT,
4
- sha256ToBase64,
5
- } from "@pagopa/io-react-native-jwt";
1
+ import { EncryptJwe } from "@pagopa/io-react-native-jwt";
6
2
  import uuid from "react-native-uuid";
7
3
  import type { FetchJwks } from "./04-retrieve-rp-jwks";
8
4
  import type { VerifyRequestObjectSignature } from "./05-verify-request-object";
9
5
  import { NoSuitableKeysFoundInEntityConfiguration } from "./errors";
10
6
  import { hasStatusOrThrow, type Out } from "../../utils/misc";
11
- import { disclose } from "../../sd-jwt";
12
7
  import {
13
8
  DirectAuthorizationBodyPayload,
14
9
  ErrorResponse,
15
- PresentationDefinition,
16
- type Presentation,
10
+ type RemotePresentation,
17
11
  } from "./types";
18
12
  import * as z from "zod";
19
13
  import type { JWK } from "../../utils/jwk";
14
+ import { Base64 } from "js-base64";
20
15
 
21
16
  export type AuthorizationResponse = z.infer<typeof AuthorizationResponse>;
22
17
  export const AuthorizationResponse = z.object({
@@ -54,78 +49,6 @@ export const choosePublicKeyToEncrypt = (
54
49
  );
55
50
  };
56
51
 
57
- /**
58
- * Prepares a Verified Presentation (VP) token to be sent as part of an
59
- * authorization response in an OpenID 4 Verifiable Presentations flow.
60
- *
61
- * @param requestObject - The request object containing the nonce, response URI, and other necessary info.
62
- * @param presentationTuple - A tuple containing a verifiable credential, the claims to disclose,
63
- * and a cryptographic context for signing.
64
- * @returns An object containing the signed VP token (`vp_token`) and a `presentation_submission` object.
65
- * @param presentationDefinition - Definition outlining presentation requirements.
66
- * @param presentationTuple - Tuple containing:
67
- * - A verifiable credential.
68
- * - Claims that should be disclosed.
69
- * - Cryptographic context for signing.
70
- * @returns An object with:
71
- * - `vp_token`: The signed VP token.
72
- * - `presentation_submission`: Object mapping disclosed credentials to the request.
73
- *
74
- * @remarks
75
- * 1. The `disclose()` function is used to produce a token with only the requested claims.
76
- * 2. A new JWT is then signed, including the VP, `jti`, `iss`, `nonce`, audience, and expiration.
77
- * 3. The `presentation_submission` object follows the OpenID 4 VP specification for describing
78
- * how the disclosed credentials map to the request.
79
- *
80
- * @todo [SIW-353] Support multiple verifiable credentials in a single request.
81
- */
82
- export const prepareVpToken = async (
83
- requestObject: Out<VerifyRequestObjectSignature>["requestObject"],
84
- presentationDefinition: PresentationDefinition,
85
- [verifiableCredential, requestedClaims, cryptoContext]: Presentation
86
- ): Promise<{
87
- vp_token: string;
88
- presentation_submission: Record<string, unknown>;
89
- }> => {
90
- // Produce a VP token with only requested claims from the verifiable credential
91
- const { token: vp } = await disclose(verifiableCredential, requestedClaims);
92
-
93
- // <Issuer-signed JWT>~<Disclosure 1>~<Disclosure N>~
94
- const sd_hash = await sha256ToBase64(`${vp}~`);
95
-
96
- const kbJwt = await new SignJWT(cryptoContext)
97
- .setProtectedHeader({
98
- typ: "kb+jwt",
99
- alg: "ES256",
100
- })
101
- .setPayload({
102
- sd_hash,
103
- nonce: requestObject.nonce,
104
- })
105
- .setAudience(requestObject.client_id)
106
- .setIssuedAt()
107
- .sign();
108
-
109
- // <Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure N>~<KB-JWT>
110
- const vp_token = [vp, kbJwt].join("~");
111
-
112
- // Determine the descriptor ID to use for mapping. Fallback to first input descriptor ID if not specified
113
- // We support only one credential for now, so we get first input_descriptor and create just one descriptor_map
114
- const presentation_submission = {
115
- id: uuid.v4(),
116
- definition_id: presentationDefinition.id,
117
- descriptor_map: [
118
- {
119
- id: presentationDefinition?.input_descriptors[0]?.id,
120
- path: `$`,
121
- format: "vc+sd-jwt",
122
- },
123
- ],
124
- };
125
-
126
- return { vp_token, presentation_submission };
127
- };
128
-
129
52
  /**
130
53
  * Builds a URL-encoded form body for a direct POST response without encryption.
131
54
  *
@@ -138,10 +61,15 @@ export const buildDirectPostBody = async (
138
61
  payload: DirectAuthorizationBodyPayload
139
62
  ): Promise<string> => {
140
63
  const formUrlEncodedBody = new URLSearchParams({
141
- state: requestObject.state,
64
+ ...(requestObject.state ? { state: requestObject.state } : {}),
142
65
  ...Object.fromEntries(
143
66
  Object.entries(payload).map(([key, value]) => {
144
- return [key, typeof value === "object" ? JSON.stringify(value) : value];
67
+ return [
68
+ key,
69
+ Array.isArray(value) || typeof value === "object"
70
+ ? JSON.stringify(value)
71
+ : value,
72
+ ];
145
73
  })
146
74
  ),
147
75
  });
@@ -155,13 +83,15 @@ export const buildDirectPostBody = async (
155
83
  * @param jwkKeys - Array of JWKs from the Relying Party for encryption.
156
84
  * @param requestObject - Contains state, nonce, and other relevant info.
157
85
  * @param payload - Object that contains either the VP token to encrypt and the mapping of the credential disclosures or the error code
86
+ * @param generatedNonce - Optional nonce for the `apu` claim in the JWE header, it is used during ISO 18013-7.
158
87
  * @returns A URL-encoded string for an `application/x-www-form-urlencoded` POST body,
159
88
  * where `response` contains the encrypted JWE.
160
89
  */
161
90
  export const buildDirectPostJwtBody = async (
162
91
  jwkKeys: Out<FetchJwks>["keys"],
163
92
  requestObject: Out<VerifyRequestObjectSignature>["requestObject"],
164
- payload: DirectAuthorizationBodyPayload
93
+ payload: DirectAuthorizationBodyPayload,
94
+ generatedNonce?: string
165
95
  ): Promise<string> => {
166
96
  // Prepare the authorization response payload to be encrypted
167
97
  const authzResponsePayload = JSON.stringify({
@@ -171,7 +101,6 @@ export const buildDirectPostJwtBody = async (
171
101
 
172
102
  // Choose a suitable RSA public key for encryption
173
103
  const encPublicJwk = choosePublicKeyToEncrypt(jwkKeys);
174
-
175
104
  // Encrypt the authorization payload
176
105
  const { client_metadata } = requestObject;
177
106
  const encryptedResponse = await new EncryptJwe(authzResponsePayload, {
@@ -184,12 +113,15 @@ export const buildDirectPostJwtBody = async (
184
113
  | "A256CBC-HS512"
185
114
  | "A128CBC-HS256") || "A256CBC-HS512",
186
115
  kid: encPublicJwk.kid,
116
+ /* ISO 18013-7 */
117
+ apv: Base64.encodeURI(requestObject.nonce),
118
+ ...(generatedNonce ? { apu: Base64.encodeURI(generatedNonce) } : {}),
187
119
  }).encrypt(encPublicJwk);
188
120
 
189
121
  // Build the x-www-form-urlencoded form body
190
122
  const formBody = new URLSearchParams({
191
123
  response: encryptedResponse,
192
- state: requestObject.state,
124
+ ...(requestObject.state ? { state: requestObject.state } : {}),
193
125
  });
194
126
  return formBody.toString();
195
127
  };
@@ -200,9 +132,9 @@ export const buildDirectPostJwtBody = async (
200
132
  */
201
133
  export type SendAuthorizationResponse = (
202
134
  requestObject: Out<VerifyRequestObjectSignature>["requestObject"],
203
- presentationDefinition: PresentationDefinition,
135
+ presentationDefinitionId: string,
204
136
  jwkKeys: Out<FetchJwks>["keys"],
205
- presentation: Presentation, // TODO: [SIW-353] support multiple presentations
137
+ remotePresentation: RemotePresentation,
206
138
  context?: {
207
139
  appFetch?: GlobalFetch["fetch"];
208
140
  }
@@ -221,32 +153,53 @@ export type SendAuthorizationResponse = (
221
153
  */
222
154
  export const sendAuthorizationResponse: SendAuthorizationResponse = async (
223
155
  requestObject,
224
- presentationDefinition,
156
+ presentationDefinitionId,
225
157
  jwkKeys,
226
- presentation,
158
+ remotePresentation,
227
159
  { appFetch = fetch } = {}
228
160
  ): Promise<AuthorizationResponse> => {
229
- // 1. Create the VP token and associated submission mapping
230
- const { vp_token, presentation_submission } = await prepareVpToken(
231
- requestObject,
232
- presentationDefinition,
233
- presentation
234
- );
161
+ const { generatedNonce, presentations } = remotePresentation;
162
+ /**
163
+ * 1. Prepare the VP token and presentation submission
164
+ * If there is only one credential, `vpToken` is a single string.
165
+ * If there are multiple credential, `vpToken` is an array of string.
166
+ **/
167
+ const vp_token =
168
+ presentations?.length === 1
169
+ ? presentations[0]?.vpToken
170
+ : presentations.map((presentation) => presentation.vpToken);
171
+
172
+ const descriptor_map = presentations.map((presentation, index) => ({
173
+ id: presentation.inputDescriptor.id,
174
+ path: presentations?.length === 1 ? `$` : `$[${index}]`,
175
+ format: presentation.format,
176
+ }));
177
+
178
+ const presentation_submission = {
179
+ id: uuid.v4(),
180
+ definition_id: presentationDefinitionId,
181
+ descriptor_map,
182
+ };
235
183
 
236
184
  // 2. Choose the appropriate request body builder based on response mode
237
185
  const requestBody =
238
186
  requestObject.response_mode === "direct_post.jwt"
239
- ? await buildDirectPostJwtBody(jwkKeys, requestObject, {
240
- vp_token,
241
- presentation_submission,
242
- })
187
+ ? await buildDirectPostJwtBody(
188
+ jwkKeys,
189
+ requestObject,
190
+ {
191
+ vp_token,
192
+ presentation_submission,
193
+ },
194
+ generatedNonce
195
+ )
243
196
  : await buildDirectPostBody(requestObject, {
244
197
  vp_token,
245
198
  presentation_submission: presentation_submission,
246
199
  });
247
200
 
248
201
  // 3. Send the authorization response via HTTP POST and validate the response
249
- return await appFetch(requestObject.response_uri, {
202
+ const authResponse = await appFetch(requestObject.response_uri, {
250
203
  method: "POST",
251
204
  headers: {
252
205
  "Content-Type": "application/x-www-form-urlencoded",
@@ -255,7 +208,10 @@ export const sendAuthorizationResponse: SendAuthorizationResponse = async (
255
208
  })
256
209
  .then(hasStatusOrThrow(200))
257
210
  .then((res) => res.json())
258
- .then(AuthorizationResponse.parse);
211
+ .then(AuthorizationResponse.safeParse);
212
+
213
+ // Some Relying Parties may return an empty body.
214
+ return authResponse.success ? authResponse.data : {};
259
215
  };
260
216
 
261
217
  /**
@@ -71,3 +71,19 @@ export class MissingDataError extends IoWalletError {
71
71
  super(message);
72
72
  }
73
73
  }
74
+
75
+ /**
76
+ * When a credential is not found in the wallet.
77
+ *
78
+ */
79
+ export class CredentialNotFoundError extends IoWalletError {
80
+ code = "ERR_CREDENTIAL_NOT_FOUND";
81
+
82
+ /**
83
+ * @param credentialId The ID of the credential that was not found.
84
+ */
85
+ constructor(credentialId: string) {
86
+ const message = `Credential not found: ${credentialId}.`;
87
+ super(message);
88
+ }
89
+ }
@@ -21,8 +21,10 @@ import {
21
21
  type FetchPresentationDefinition,
22
22
  } from "./06-fetch-presentation-definition";
23
23
  import {
24
- evaluateInputDescriptorForSdJwt4VC,
25
- type EvaluateInputDescriptorSdJwt4VC,
24
+ evaluateInputDescriptors,
25
+ prepareRemotePresentations,
26
+ type EvaluateInputDescriptors,
27
+ type PrepareRemotePresentations,
26
28
  } from "./07-evaluate-input-descriptor";
27
29
  import {
28
30
  sendAuthorizationResponse,
@@ -40,9 +42,10 @@ export {
40
42
  fetchJwksFromConfig,
41
43
  verifyRequestObjectSignature,
42
44
  fetchPresentDefinition,
43
- evaluateInputDescriptorForSdJwt4VC,
45
+ evaluateInputDescriptors,
44
46
  sendAuthorizationResponse,
45
47
  sendAuthorizationErrorResponse,
48
+ prepareRemotePresentations,
46
49
  Errors,
47
50
  };
48
51
  export type {
@@ -52,7 +55,8 @@ export type {
52
55
  FetchJwks,
53
56
  VerifyRequestObjectSignature,
54
57
  FetchPresentationDefinition,
55
- EvaluateInputDescriptorSdJwt4VC,
58
+ EvaluateInputDescriptors,
59
+ PrepareRemotePresentations,
56
60
  SendAuthorizationResponse,
57
61
  SendAuthorizationErrorResponse,
58
62
  };
@@ -9,9 +9,22 @@ import { JWKS } from "../../utils/jwk";
9
9
  export type Presentation = [
10
10
  /* verified credential token */ string,
11
11
  /* claims */ string[],
12
- /* the context for the key associated to the credential */ CryptoContext
12
+ /* the context for the key associated to the credential */ CryptoContext,
13
13
  ];
14
14
 
15
+ /**
16
+ * A object that associate the information needed to multiple remote presentation
17
+ */
18
+ export type RemotePresentation = {
19
+ presentations: {
20
+ requestedClaims: string[];
21
+ inputDescriptor: InputDescriptor;
22
+ format: string;
23
+ vpToken: string;
24
+ }[];
25
+ generatedNonce?: string /* nonce generated by app, used in mdoc presentation */;
26
+ };
27
+
15
28
  const Fields = z.object({
16
29
  path: z.array(z.string().min(1)), // Array of JSONPath string expressions
17
30
  id: z.string().optional(), // Unique string ID
@@ -72,7 +85,7 @@ export const RequestObject = z.object({
72
85
  iss: z.string().optional(), //optional by RFC 7519, mandatory for Potential
73
86
  iat: UnixTime.optional(),
74
87
  exp: UnixTime.optional(),
75
- state: z.string(),
88
+ state: z.string().optional(),
76
89
  nonce: z.string(),
77
90
  response_uri: z.string(),
78
91
  response_type: z.literal("vp_token"),
@@ -111,7 +124,7 @@ export type DirectAuthorizationBodyPayload = z.infer<
111
124
  >;
112
125
  export const DirectAuthorizationBodyPayload = z.union([
113
126
  z.object({
114
- vp_token: z.string(),
127
+ vp_token: z.union([z.string(), z.array(z.string())]).optional(),
115
128
  presentation_submission: z.record(z.string(), z.unknown()),
116
129
  }),
117
130
  z.object({ error: ErrorResponse }),
@@ -70,8 +70,8 @@ export async function validateTrustChain(
70
70
  elementIndex === 0
71
71
  ? FirstElementShape
72
72
  : elementIndex === chain.length - 1
73
- ? LastElementShape
74
- : MiddleElementShape;
73
+ ? LastElementShape
74
+ : MiddleElementShape;
75
75
 
76
76
  // select the kid from the current index
77
77
  const selectKid = (currentIndex: number): string => {
@@ -136,15 +136,19 @@ export function renewTrustChain(
136
136
  ec.success
137
137
  ? getSignedEntityConfiguration(ec.data.payload.iss, { appFetch })
138
138
  : es.success
139
- ? getSignedEntityStatement(es.data.payload.iss, es.data.payload.sub, {
140
- appFetch,
141
- })
142
- : // if the element fail to parse in both EntityStatement and EntityConfiguration, raise an error
143
- Promise.reject(
144
- new IoWalletError(
145
- `Cannot renew trust chain because the element #${i} failed to be parsed.`
139
+ ? getSignedEntityStatement(
140
+ es.data.payload.iss,
141
+ es.data.payload.sub,
142
+ {
143
+ appFetch,
144
+ }
145
+ )
146
+ : // if the element fail to parse in both EntityStatement and EntityConfiguration, raise an error
147
+ Promise.reject(
148
+ new IoWalletError(
149
+ `Cannot renew trust chain because the element #${i} failed to be parsed.`
150
+ )
146
151
  )
147
- )
148
152
  )
149
153
  );
150
154
  }
package/src/mdoc/index.ts CHANGED
@@ -1,28 +1,85 @@
1
- import { CBOR } from "@pagopa/io-react-native-cbor";
1
+ import { CBOR, COSE, ISO18013 } from "@pagopa/io-react-native-cbor";
2
2
  import type { JWK } from "../utils/jwk";
3
+ import type { PublicKey } from "@pagopa/io-react-native-crypto";
4
+ import { b64utob64 } from "jsrsasign";
5
+ import {
6
+ convertCertToPem,
7
+ getSigningJwk,
8
+ parsePublicKey,
9
+ } from "../utils/crypto";
10
+ import { type Presentation } from "../credential/presentation/types";
11
+ import { base64ToBase64Url } from "../utils/string";
3
12
 
4
13
  export const verify = async (
5
14
  token: string,
6
- publicKey: JWK | JWK[]
7
- ): Promise<{ mDoc: CBOR.MDOC }> => {
15
+ _: JWK | JWK[]
16
+ ): Promise<{ issuerSigned: CBOR.IssuerSigned }> => {
8
17
  // get decoded data
9
- const documents = await CBOR.decodeDocuments(token);
10
- if (!documents || documents.documents.length === 0) {
11
- throw new Error("Invalid mDoc");
12
- }
13
- const mDoc = documents.documents[0];
14
- if (!mDoc) {
18
+ const issuerSigned = await CBOR.decodeIssuerSigned(token);
19
+ if (!issuerSigned) {
15
20
  throw new Error("Invalid mDoc");
16
21
  }
17
22
 
18
- const sigKey = Array.isArray(publicKey)
19
- ? publicKey.find((k) => k.use === "sig")
20
- : publicKey;
21
- sigKey;
23
+ const cert = issuerSigned.issuerAuth.unprotectedHeader[0]?.keyId;
24
+ if (!cert) throw new Error("Certificate not present in credential");
25
+
26
+ const pemcert = convertCertToPem(b64utob64(cert));
27
+ const publickey = parsePublicKey(pemcert);
28
+ if (!publickey) throw new Error("Certificate not present in credential");
29
+
30
+ const jwk = getSigningJwk(publickey);
31
+
32
+ jwk.x = b64utob64(jwk.x!);
33
+ jwk.y = b64utob64(jwk.y!);
34
+
35
+ const signatureCorrect = await COSE.verify(
36
+ b64utob64(issuerSigned.issuerAuth.rawValue!),
37
+ jwk as PublicKey
38
+ ).catch(() => false);
39
+ if (!signatureCorrect) throw new Error("Invalid mDoc signature");
40
+
41
+ return { issuerSigned };
42
+ };
43
+
44
+ export const prepareVpTokenMdoc = async (
45
+ requestNonce: string,
46
+ generatedNonce: string,
47
+ clientId: string,
48
+ responseUri: string,
49
+ docType: string,
50
+ keyTag: string,
51
+ [verifiableCredential, requestedClaims, _]: Presentation
52
+ ): Promise<{
53
+ vp_token: string;
54
+ }> => {
55
+ /* verifiableCredential is a IssuerSigned structure */
56
+ const documents = [
57
+ {
58
+ issuerSignedContent: verifiableCredential,
59
+ alias: keyTag,
60
+ docType,
61
+ },
62
+ ];
63
+
64
+ /* we map each requested claim as for ex. { "org.iso.18013.5.1.mDL" { <claim-name>: true, ... }} for selective disclosure */
65
+ const fieldRequestedAndAccepted = JSON.stringify({
66
+ [docType]: requestedClaims.reduce((acc, item) => {
67
+ return { ...acc, [item]: true };
68
+ }, {}),
69
+ });
22
70
 
23
- //await COSE.verify(mDoc.issuerSigned.issuerAuth, sigKey as PublicKey);
71
+ /* clientId,responseUri,requestNonce are retrieved by Auth Request Object */
72
+ /* create DeviceResponse as { documents: { docType, issuerSigned, deviceSigned }, version, status } */
73
+ const vp_token = await ISO18013.generateOID4VPDeviceResponse(
74
+ clientId,
75
+ responseUri,
76
+ requestNonce,
77
+ generatedNonce,
78
+ documents,
79
+ fieldRequestedAndAccepted
80
+ );
24
81
 
25
82
  return {
26
- mDoc,
83
+ vp_token: base64ToBase64Url(vp_token),
27
84
  };
28
85
  };
@@ -2,12 +2,13 @@ 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
+ import { SignJWT, sha256ToBase64 } from "@pagopa/io-react-native-jwt";
6
6
  import { Disclosure, SdJwt4VC, type DisclosureWithEncoded } from "./types";
7
7
  import { verifyDisclosure } from "./verifier";
8
8
  import type { JWK } from "../utils/jwk";
9
9
  import * as Errors from "./errors";
10
10
  import { Base64 } from "js-base64";
11
+ import { type Presentation } from "../credential/presentation/types";
11
12
 
12
13
  const decodeDisclosure = (encoded: string): DisclosureWithEncoded => {
13
14
  const utf8String = Base64.decode(encoded); // Decode Base64 into UTF-8 string
@@ -163,4 +164,51 @@ export const verify = async <S extends z.ZodType<SdJwt4VC>>(
163
164
  };
164
165
  };
165
166
 
167
+ /**
168
+ * Prepares a Verified Presentation (VP) token to be sent as part of an
169
+ * authorization response in an OpenID 4 Verifiable Presentations flow.
170
+ *
171
+ * @param nonce - The nonce provided by the relying party.
172
+ * @param client_id - The client identifier of the relying party.
173
+ * @param presentation - An object containing the verifiable credential, the claims to disclose,
174
+ * and the cryptographic context for signing.
175
+ * @returns An object containing the signed VP token (`vp_token`).
176
+ *
177
+ * @remarks
178
+ * 1. The `disclose()` function is used to produce a token with only the requested claims.
179
+ * 2. A KB-JWT is then signed, including sd_hash and `nonce`.
180
+ * 3. The `vp_token` is composed of the disclosed VP and the KB-JWT.
181
+ */
182
+ export const prepareVpToken = async (
183
+ nonce: string,
184
+ client_id: string,
185
+ [verifiableCredential, requestedClaims, cryptoContext]: Presentation
186
+ ): Promise<{
187
+ vp_token: string;
188
+ }> => {
189
+ // Produce a VP token with only requested claims from the verifiable credential
190
+ const { token: vp } = await disclose(verifiableCredential, requestedClaims);
191
+
192
+ // <Issuer-signed JWT>~<Disclosure 1>~<Disclosure N>~
193
+ const sd_hash = await sha256ToBase64(`${vp}~`);
194
+
195
+ const kbJwt = await new SignJWT(cryptoContext)
196
+ .setProtectedHeader({
197
+ typ: "kb+jwt",
198
+ alg: "ES256",
199
+ })
200
+ .setPayload({
201
+ sd_hash,
202
+ nonce: nonce,
203
+ })
204
+ .setAudience(client_id)
205
+ .setIssuedAt()
206
+ .sign();
207
+
208
+ // <Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure N>~<KB-JWT>
209
+ const vp_token = [vp, kbJwt].join("~");
210
+
211
+ return { vp_token };
212
+ };
213
+
166
214
  export { SdJwt4VC, Errors };
@@ -3,12 +3,14 @@ import {
3
3
  sign,
4
4
  generate,
5
5
  deleteKey,
6
+ type PublicKey,
6
7
  } from "@pagopa/io-react-native-crypto";
7
8
  import uuid from "react-native-uuid";
8
9
  import { thumbprint, type CryptoContext } from "@pagopa/io-react-native-jwt";
9
- import { fixBase64EncodingOnKey } from "./jwk";
10
10
  import { X509, KEYUTIL, RSAKey, KJUR } from "jsrsasign";
11
11
  import { JWK } from "./jwk";
12
+ import { removePadding } from "@pagopa/io-react-native-jwt";
13
+ import { Buffer } from "buffer";
12
14
 
13
15
  /**
14
16
  * Create a CryptoContext bound to a key pair.
@@ -26,7 +28,7 @@ export const createCryptoContextFor = (keytag: string): CryptoContext => {
26
28
  */
27
29
  async getPublicKey() {
28
30
  return getPublicKey(keytag)
29
- .then(fixBase64EncodingOnKey)
31
+ .then(fixBase64WithLeadingZero)
30
32
  .then(async (jwk) => ({
31
33
  ...jwk,
32
34
  // Keys in the TEE are not stored with their KID, which is supposed to be assigned when they are included in JWK sets.
@@ -48,6 +50,45 @@ export const createCryptoContextFor = (keytag: string): CryptoContext => {
48
50
  };
49
51
  };
50
52
 
53
+ /**
54
+ * This function takes a JSON Web Key (JWK) and returns a new JWK with its base64-url properties (x, y, e, n) processed.
55
+ * Each property is passed through the `removeLeadingZeroAndParseb64u` function if it exists, which fixes any unwanted leading zeros.
56
+ *
57
+ * @param key - The input JSON Web Key that may contain properties with potential leading zero issues.
58
+ * @returns A new JSON Web Key with the processed properties.
59
+ */
60
+ const fixBase64WithLeadingZero = (key: JWK): JWK => {
61
+ const { x, y, e, n, ...pk } = key;
62
+
63
+ return {
64
+ ...pk,
65
+ ...(x ? { x: removeLeadingZeroAndParseb64u(x) } : {}),
66
+ ...(y ? { y: removeLeadingZeroAndParseb64u(y) } : {}),
67
+ ...(e ? { e: removeLeadingZeroAndParseb64u(e) } : {}),
68
+ ...(n ? { n: removeLeadingZeroAndParseb64u(n) } : {}),
69
+ };
70
+ };
71
+
72
+ /**
73
+ * This function processes a base64-encoded string to remove any unwanted leading zeros.
74
+ * It converts the input base64 string into a buffer, then to a hex string, checks for a leading "00",
75
+ * and removes it if present. The result is then converted back to a base64-url.
76
+ *
77
+ * @param input - The base64 encoded string to process.
78
+ * @returns A new base64-url encoded string with any leading zero removed.
79
+ */
80
+ const removeLeadingZeroAndParseb64u = (input: string): string => {
81
+ // Decode base64 input into a Buffer
82
+ const buffer = Buffer.from(input, "base64");
83
+ const hex = buffer.toString("hex");
84
+ // If the hex string starts with "00", remove the first two characters
85
+ const fixedHex = hex.startsWith("00") ? hex.slice(2) : hex;
86
+ const newBuffer = Buffer.from(fixedHex, "hex");
87
+
88
+ // removePadding convert base64 string to base64-url
89
+ return removePadding(newBuffer.toString("base64"));
90
+ };
91
+
51
92
  /**
52
93
  * Executes the input function injecting an ephemeral crypto context.
53
94
  * An ephemeral crypto context is a context which is bound to a key
@@ -106,3 +147,21 @@ export const getSigningJwk = (publicKey: RSAKey | KJUR.crypto.ECDSA): JWK => ({
106
147
  ...JWK.parse(KEYUTIL.getJWKFromKey(publicKey)),
107
148
  use: "sig",
108
149
  });
150
+
151
+ /**
152
+ * This function takes two {@link PublicKey} and evaluates and compares their thumbprints
153
+ * @param key1 The first key
154
+ * @param key2 The second key
155
+ * @returns true if the keys' thumbprints are equal, false otherwise
156
+ */
157
+ export const compareKeysByThumbprint = async (
158
+ key1: PublicKey,
159
+ key2: PublicKey
160
+ ) => {
161
+ //Parallel for optimization
162
+ const [thumbprint1, thumbprint2] = await Promise.all([
163
+ thumbprint(key1),
164
+ thumbprint(key2),
165
+ ]);
166
+ return thumbprint1 === thumbprint2;
167
+ };
@@ -189,8 +189,8 @@ export class ResponseErrorBuilder<T extends typeof UnexpectedStatusCodeError> {
189
189
  type ErrorCodeMap<T> = T extends typeof IssuerResponseError
190
190
  ? IssuerResponseErrorCode
191
191
  : T extends typeof WalletProviderResponseError
192
- ? WalletProviderResponseErrorCode
193
- : never;
192
+ ? WalletProviderResponseErrorCode
193
+ : never;
194
194
 
195
195
  type ErrorCase<T> = {
196
196
  code: ErrorCodeMap<T>;
package/src/utils/misc.ts CHANGED
@@ -37,8 +37,8 @@ export const parseRawHttpResponse = <T extends Record<string, unknown>>(
37
37
  export type Out<FN> = FN extends (...args: any[]) => Promise<any>
38
38
  ? Awaited<ReturnType<FN>>
39
39
  : FN extends (...args: any[]) => any
40
- ? ReturnType<FN>
41
- : never;
40
+ ? ReturnType<FN>
41
+ : never;
42
42
 
43
43
  /**
44
44
  * TODO [SIW-1310]: replace this function with a cryptographically secure one.