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