@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.
- package/lib/commonjs/credential/issuance/06-obtain-credential.js +1 -5
- package/lib/commonjs/credential/issuance/06-obtain-credential.js.map +1 -1
- package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js +33 -21
- package/lib/commonjs/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
- package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js +318 -24
- package/lib/commonjs/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
- package/lib/commonjs/credential/presentation/08-send-authorization-response.js +47 -83
- package/lib/commonjs/credential/presentation/08-send-authorization-response.js.map +1 -1
- package/lib/commonjs/credential/presentation/errors.js +18 -1
- package/lib/commonjs/credential/presentation/errors.js.map +1 -1
- package/lib/commonjs/credential/presentation/index.js +8 -2
- package/lib/commonjs/credential/presentation/index.js.map +1 -1
- package/lib/commonjs/credential/presentation/types.js +6 -2
- package/lib/commonjs/credential/presentation/types.js.map +1 -1
- package/lib/commonjs/entity/trust/chain.js.map +1 -1
- package/lib/commonjs/mdoc/index.js +45 -13
- package/lib/commonjs/mdoc/index.js.map +1 -1
- package/lib/commonjs/sd-jwt/index.js +41 -1
- package/lib/commonjs/sd-jwt/index.js.map +1 -1
- package/lib/commonjs/utils/crypto.js +70 -4
- package/lib/commonjs/utils/crypto.js.map +1 -1
- package/lib/commonjs/utils/string.js +6 -7
- package/lib/commonjs/utils/string.js.map +1 -1
- package/lib/module/credential/issuance/06-obtain-credential.js +1 -5
- package/lib/module/credential/issuance/06-obtain-credential.js.map +1 -1
- package/lib/module/credential/issuance/07-verify-and-parse-credential.js +33 -21
- package/lib/module/credential/issuance/07-verify-and-parse-credential.js.map +1 -1
- package/lib/module/credential/presentation/07-evaluate-input-descriptor.js +311 -23
- package/lib/module/credential/presentation/07-evaluate-input-descriptor.js.map +1 -1
- package/lib/module/credential/presentation/08-send-authorization-response.js +46 -81
- package/lib/module/credential/presentation/08-send-authorization-response.js.map +1 -1
- package/lib/module/credential/presentation/errors.js +16 -0
- package/lib/module/credential/presentation/errors.js.map +1 -1
- package/lib/module/credential/presentation/index.js +2 -2
- package/lib/module/credential/presentation/index.js.map +1 -1
- package/lib/module/credential/presentation/types.js +6 -2
- package/lib/module/credential/presentation/types.js.map +1 -1
- package/lib/module/entity/trust/chain.js.map +1 -1
- package/lib/module/mdoc/index.js +43 -12
- package/lib/module/mdoc/index.js.map +1 -1
- package/lib/module/sd-jwt/index.js +40 -1
- package/lib/module/sd-jwt/index.js.map +1 -1
- package/lib/module/utils/crypto.js +67 -2
- package/lib/module/utils/crypto.js.map +1 -1
- package/lib/module/utils/string.js +4 -6
- package/lib/module/utils/string.js.map +1 -1
- package/lib/typescript/credential/issuance/06-obtain-credential.d.ts.map +1 -1
- package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts +1 -1
- package/lib/typescript/credential/issuance/07-verify-and-parse-credential.d.ts.map +1 -1
- package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts +106 -9
- package/lib/typescript/credential/presentation/07-evaluate-input-descriptor.d.ts.map +1 -1
- package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts +4 -33
- package/lib/typescript/credential/presentation/08-send-authorization-response.d.ts.map +1 -1
- package/lib/typescript/credential/presentation/errors.d.ts +11 -0
- package/lib/typescript/credential/presentation/errors.d.ts.map +1 -1
- package/lib/typescript/credential/presentation/index.d.ts +3 -3
- package/lib/typescript/credential/presentation/index.d.ts.map +1 -1
- package/lib/typescript/credential/presentation/types.d.ts +18 -6
- package/lib/typescript/credential/presentation/types.d.ts.map +1 -1
- package/lib/typescript/entity/trust/chain.d.ts.map +1 -1
- package/lib/typescript/mdoc/index.d.ts +6 -2
- package/lib/typescript/mdoc/index.d.ts.map +1 -1
- package/lib/typescript/sd-jwt/index.d.ts +19 -0
- package/lib/typescript/sd-jwt/index.d.ts.map +1 -1
- package/lib/typescript/utils/crypto.d.ts +8 -0
- package/lib/typescript/utils/crypto.d.ts.map +1 -1
- package/lib/typescript/utils/errors.d.ts.map +1 -1
- package/lib/typescript/utils/misc.d.ts.map +1 -1
- package/lib/typescript/utils/string.d.ts +3 -3
- package/lib/typescript/utils/string.d.ts.map +1 -1
- package/package.json +16 -14
- package/src/credential/issuance/06-obtain-credential.ts +1 -7
- package/src/credential/issuance/07-verify-and-parse-credential.ts +37 -16
- package/src/credential/presentation/07-evaluate-input-descriptor.ts +459 -49
- package/src/credential/presentation/08-send-authorization-response.ts +57 -101
- package/src/credential/presentation/errors.ts +16 -0
- package/src/credential/presentation/index.ts +8 -4
- package/src/credential/presentation/types.ts +16 -3
- package/src/entity/trust/chain.ts +14 -10
- package/src/mdoc/index.ts +72 -15
- package/src/sd-jwt/index.ts +49 -1
- package/src/utils/crypto.ts +61 -2
- package/src/utils/errors.ts +2 -2
- package/src/utils/misc.ts +2 -2
- 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
|
-
|
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 [
|
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
|
-
|
135
|
+
presentationDefinitionId: string,
|
204
136
|
jwkKeys: Out<FetchJwks>["keys"],
|
205
|
-
|
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
|
-
|
156
|
+
presentationDefinitionId,
|
225
157
|
jwkKeys,
|
226
|
-
|
158
|
+
remotePresentation,
|
227
159
|
{ appFetch = fetch } = {}
|
228
160
|
): Promise<AuthorizationResponse> => {
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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(
|
240
|
-
|
241
|
-
|
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
|
-
|
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.
|
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
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
7
|
-
): Promise<{
|
15
|
+
_: JWK | JWK[]
|
16
|
+
): Promise<{ issuerSigned: CBOR.IssuerSigned }> => {
|
8
17
|
// get decoded data
|
9
|
-
const
|
10
|
-
if (!
|
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
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
83
|
+
vp_token: base64ToBase64Url(vp_token),
|
27
84
|
};
|
28
85
|
};
|
package/src/sd-jwt/index.ts
CHANGED
@@ -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 };
|
package/src/utils/crypto.ts
CHANGED
@@ -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(
|
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
|
+
};
|
package/src/utils/errors.ts
CHANGED
@@ -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
|
-
|
193
|
-
|
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
|
-
|
41
|
-
|
40
|
+
? ReturnType<FN>
|
41
|
+
: never;
|
42
42
|
|
43
43
|
/**
|
44
44
|
* TODO [SIW-1310]: replace this function with a cryptographically secure one.
|