@pagopa/io-react-native-wallet 2.0.0-next.0 → 2.0.0-next.1
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/trust/README.md +147 -0
- package/lib/commonjs/trust/chain.js +47 -10
- package/lib/commonjs/trust/chain.js.map +1 -1
- package/lib/commonjs/trust/errors.js +24 -1
- package/lib/commonjs/trust/errors.js.map +1 -1
- package/lib/commonjs/trust/index.js +11 -5
- package/lib/commonjs/trust/index.js.map +1 -1
- package/lib/commonjs/trust/utils.js +30 -1
- package/lib/commonjs/trust/utils.js.map +1 -1
- package/lib/module/trust/README.md +147 -0
- package/lib/module/trust/chain.js +49 -12
- package/lib/module/trust/chain.js.map +1 -1
- package/lib/module/trust/errors.js +23 -2
- package/lib/module/trust/errors.js.map +1 -1
- package/lib/module/trust/index.js +11 -5
- package/lib/module/trust/index.js.map +1 -1
- package/lib/module/trust/utils.js +27 -0
- package/lib/module/trust/utils.js.map +1 -1
- package/lib/typescript/client/generated/wallet-provider.d.ts +12 -12
- package/lib/typescript/credential/presentation/types.d.ts +4 -4
- package/lib/typescript/credential/status/types.d.ts +6 -6
- package/lib/typescript/sd-jwt/index.d.ts +12 -12
- package/lib/typescript/sd-jwt/types.d.ts +6 -6
- package/lib/typescript/trust/chain.d.ts +8 -6
- package/lib/typescript/trust/chain.d.ts.map +1 -1
- package/lib/typescript/trust/errors.d.ts +22 -0
- package/lib/typescript/trust/errors.d.ts.map +1 -1
- package/lib/typescript/trust/index.d.ts +208 -206
- package/lib/typescript/trust/index.d.ts.map +1 -1
- package/lib/typescript/trust/types.d.ts +559 -559
- package/lib/typescript/trust/utils.d.ts +10 -0
- package/lib/typescript/trust/utils.d.ts.map +1 -1
- package/lib/typescript/wallet-instance-attestation/types.d.ts +25 -25
- package/package.json +2 -2
- package/src/trust/README.md +147 -0
- package/src/trust/chain.ts +91 -15
- package/src/trust/errors.ts +32 -1
- package/src/trust/index.ts +11 -4
- package/src/trust/utils.ts +35 -0
@@ -1,4 +1,5 @@
|
|
1
1
|
import type { JWK, JWTDecodeResult } from "../utils/jwk";
|
2
|
+
import type { TrustAnchorEntityConfiguration } from "./types";
|
2
3
|
export type ParsedToken = {
|
3
4
|
header: JWTDecodeResult["protectedHeader"];
|
4
5
|
payload: JWTDecodeResult["payload"];
|
@@ -9,4 +10,13 @@ export declare const verify: (token: string, kid: string, jwks: JWK[]) => Promis
|
|
9
10
|
* It seems like typescript can't correctly infer the return type of the function.
|
10
11
|
*/
|
11
12
|
export declare const decode: (token: string) => ParsedToken;
|
13
|
+
/**
|
14
|
+
* Extracts the X.509 Trust Anchor certificate (Base64 encoded) from the
|
15
|
+
* Trust Anchor's Entity Configuration.
|
16
|
+
*
|
17
|
+
* @param trustAnchorEntity The entity configuration of the known trust anchor.
|
18
|
+
* @returns The Base64 encoded X.509 certificate string.
|
19
|
+
* @throws {FederationError} If the certificate cannot be derived.
|
20
|
+
*/
|
21
|
+
export declare function getTrustAnchorX509Certificate(trustAnchorEntity: TrustAnchorEntityConfiguration): string;
|
12
22
|
//# sourceMappingURL=utils.d.ts.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/trust/utils.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;IAC3C,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;CACrC,CAAC;AAIF,eAAO,MAAM,MAAM,UACV,MAAM,OACR,MAAM,QACL,GAAG,EAAE,KACV,QAAQ,WAAW,CAOrB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,UAAW,MAAM,KAAG,WAGtC,CAAC"}
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/trust/utils.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,SAAS,CAAC;AAE9D,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;IAC3C,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;CACrC,CAAC;AAIF,eAAO,MAAM,MAAM,UACV,MAAM,OACR,MAAM,QACL,GAAG,EAAE,KACV,QAAQ,WAAW,CAOrB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,UAAW,MAAM,KAAG,WAGtC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,iBAAiB,EAAE,8BAA8B,GAChD,MAAM,CAqBR"}
|
@@ -246,6 +246,15 @@ export declare const WalletInstanceAttestationRequestJwt: z.ZodObject<{
|
|
246
246
|
integrity_assertion: string;
|
247
247
|
}>>;
|
248
248
|
}, "strip", z.ZodTypeAny, {
|
249
|
+
header: {
|
250
|
+
alg: string;
|
251
|
+
kid: string;
|
252
|
+
typ: string;
|
253
|
+
x5c?: string[] | undefined;
|
254
|
+
trust_chain?: string[] | undefined;
|
255
|
+
} & {
|
256
|
+
typ: "wp-war+jwt";
|
257
|
+
};
|
249
258
|
payload: {
|
250
259
|
iss: string;
|
251
260
|
iat: number;
|
@@ -285,6 +294,7 @@ export declare const WalletInstanceAttestationRequestJwt: z.ZodObject<{
|
|
285
294
|
hardware_signature: string;
|
286
295
|
integrity_assertion: string;
|
287
296
|
};
|
297
|
+
}, {
|
288
298
|
header: {
|
289
299
|
alg: string;
|
290
300
|
kid: string;
|
@@ -294,7 +304,6 @@ export declare const WalletInstanceAttestationRequestJwt: z.ZodObject<{
|
|
294
304
|
} & {
|
295
305
|
typ: "wp-war+jwt";
|
296
306
|
};
|
297
|
-
}, {
|
298
307
|
payload: {
|
299
308
|
iss: string;
|
300
309
|
iat: number;
|
@@ -334,15 +343,6 @@ export declare const WalletInstanceAttestationRequestJwt: z.ZodObject<{
|
|
334
343
|
hardware_signature: string;
|
335
344
|
integrity_assertion: string;
|
336
345
|
};
|
337
|
-
header: {
|
338
|
-
alg: string;
|
339
|
-
kid: string;
|
340
|
-
typ: string;
|
341
|
-
x5c?: string[] | undefined;
|
342
|
-
trust_chain?: string[] | undefined;
|
343
|
-
} & {
|
344
|
-
typ: "wp-war+jwt";
|
345
|
-
};
|
346
346
|
}>;
|
347
347
|
export type WalletInstanceAttestationJwt = z.infer<typeof WalletInstanceAttestationJwt>;
|
348
348
|
export declare const WalletInstanceAttestationJwt: z.ZodObject<{
|
@@ -591,6 +591,16 @@ export declare const WalletInstanceAttestationJwt: z.ZodObject<{
|
|
591
591
|
wallet_name?: string | undefined;
|
592
592
|
}>>;
|
593
593
|
}, "strip", z.ZodTypeAny, {
|
594
|
+
header: {
|
595
|
+
alg: string;
|
596
|
+
kid: string;
|
597
|
+
typ: string;
|
598
|
+
x5c?: string[] | undefined;
|
599
|
+
trust_chain?: string[] | undefined;
|
600
|
+
} & {
|
601
|
+
typ: "oauth-client-attestation+jwt";
|
602
|
+
trust_chain: string[];
|
603
|
+
};
|
594
604
|
payload: {
|
595
605
|
iss: string;
|
596
606
|
iat: number;
|
@@ -629,6 +639,7 @@ export declare const WalletInstanceAttestationJwt: z.ZodObject<{
|
|
629
639
|
wallet_link?: string | undefined;
|
630
640
|
wallet_name?: string | undefined;
|
631
641
|
};
|
642
|
+
}, {
|
632
643
|
header: {
|
633
644
|
alg: string;
|
634
645
|
kid: string;
|
@@ -639,7 +650,6 @@ export declare const WalletInstanceAttestationJwt: z.ZodObject<{
|
|
639
650
|
typ: "oauth-client-attestation+jwt";
|
640
651
|
trust_chain: string[];
|
641
652
|
};
|
642
|
-
}, {
|
643
653
|
payload: {
|
644
654
|
iss: string;
|
645
655
|
iat: number;
|
@@ -678,16 +688,6 @@ export declare const WalletInstanceAttestationJwt: z.ZodObject<{
|
|
678
688
|
wallet_link?: string | undefined;
|
679
689
|
wallet_name?: string | undefined;
|
680
690
|
};
|
681
|
-
header: {
|
682
|
-
alg: string;
|
683
|
-
kid: string;
|
684
|
-
typ: string;
|
685
|
-
x5c?: string[] | undefined;
|
686
|
-
trust_chain?: string[] | undefined;
|
687
|
-
} & {
|
688
|
-
typ: "oauth-client-attestation+jwt";
|
689
|
-
trust_chain: string[];
|
690
|
-
};
|
691
691
|
}>;
|
692
692
|
export type WalletAttestationResponse = z.infer<typeof WalletAttestationResponse>;
|
693
693
|
export declare const WalletAttestationResponse: z.ZodObject<{
|
@@ -695,21 +695,21 @@ export declare const WalletAttestationResponse: z.ZodObject<{
|
|
695
695
|
wallet_attestation: z.ZodString;
|
696
696
|
format: z.ZodEnum<["jwt", "dc+sd-jwt", "mso_mdoc"]>;
|
697
697
|
}, "strip", z.ZodTypeAny, {
|
698
|
-
format: "jwt" | "dc+sd-jwt" | "mso_mdoc";
|
699
698
|
wallet_attestation: string;
|
700
|
-
}, {
|
701
699
|
format: "jwt" | "dc+sd-jwt" | "mso_mdoc";
|
700
|
+
}, {
|
702
701
|
wallet_attestation: string;
|
702
|
+
format: "jwt" | "dc+sd-jwt" | "mso_mdoc";
|
703
703
|
}>, "many">;
|
704
704
|
}, "strip", z.ZodTypeAny, {
|
705
705
|
wallet_attestations: {
|
706
|
-
format: "jwt" | "dc+sd-jwt" | "mso_mdoc";
|
707
706
|
wallet_attestation: string;
|
707
|
+
format: "jwt" | "dc+sd-jwt" | "mso_mdoc";
|
708
708
|
}[];
|
709
709
|
}, {
|
710
710
|
wallet_attestations: {
|
711
|
-
format: "jwt" | "dc+sd-jwt" | "mso_mdoc";
|
712
711
|
wallet_attestation: string;
|
712
|
+
format: "jwt" | "dc+sd-jwt" | "mso_mdoc";
|
713
713
|
}[];
|
714
714
|
}>;
|
715
715
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@pagopa/io-react-native-wallet",
|
3
|
-
"version": "2.0.0-next.
|
3
|
+
"version": "2.0.0-next.1",
|
4
4
|
"description": "Provide data structures, helpers and API for IO Wallet",
|
5
5
|
"main": "lib/commonjs/index",
|
6
6
|
"module": "lib/module/index",
|
@@ -53,7 +53,7 @@
|
|
53
53
|
"registry": "https://registry.npmjs.org/"
|
54
54
|
},
|
55
55
|
"devDependencies": {
|
56
|
-
"@pagopa/io-react-native-crypto": "^
|
56
|
+
"@pagopa/io-react-native-crypto": "^1.2.2",
|
57
57
|
"@pagopa/io-react-native-jwt": "^2.1.0",
|
58
58
|
"@react-native/eslint-config": "^0.75.5",
|
59
59
|
"@rushstack/eslint-patch": "^1.3.2",
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# Trust Chain Validation
|
2
|
+
|
3
|
+
This module implements **Trust Chain validation** for Entity Configurations and Entity Statements in line with the [IT Wallet Federation Specifications](https://italia.github.io/eid-wallet-it-docs/). It ensures that an entity's metadata is trusted by validating a chain of signed JWTs up to a known Trust Anchor.
|
4
|
+
|
5
|
+
The validation covers:
|
6
|
+
|
7
|
+
* JWT signature verification (using the next entity's JWKS)
|
8
|
+
* Trust chain ordering (leaf → parent → Trust Anchor)
|
9
|
+
* Optional X.509 CRL-based certificate validation
|
10
|
+
|
11
|
+
## Sequence Diagram
|
12
|
+
|
13
|
+
```mermaid
|
14
|
+
sequenceDiagram
|
15
|
+
autonumber
|
16
|
+
participant A as Leaf Entity
|
17
|
+
participant B as Intermediate (Federation Authority)
|
18
|
+
participant C as Trust Anchor
|
19
|
+
|
20
|
+
A->>A: Self-issued Entity Configuration (JWT)
|
21
|
+
B->>A: Signed Entity Statement (JWT)
|
22
|
+
C->>B: Signed Entity Statement (JWT or self-issued EC)
|
23
|
+
|
24
|
+
Note over A,C: Each JWT is validated with the next issuer's public keys
|
25
|
+
```
|
26
|
+
|
27
|
+
## Errors
|
28
|
+
|
29
|
+
| Error | Description |
|
30
|
+
| ----------------------------- | ------------------------------------------------------------------ |
|
31
|
+
| `TrustChainEmptyError` | The input chain is empty. |
|
32
|
+
| `TrustChainTokenMissingError` | One of the JWTs in the chain is missing. |
|
33
|
+
| `X509ValidationError` | X.509 certificate validation failed (e.g. revocation, expiration). |
|
34
|
+
| `FederationError` | Generic federation processing error. |
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
### Validate a trust chain
|
39
|
+
|
40
|
+
```ts
|
41
|
+
import { validateTrustChain } from "./trust";
|
42
|
+
import { trustAnchorEntityConfiguration } from "./your-data";
|
43
|
+
import { chain } from "./your-data"; // array of JWTs, starting from leaf
|
44
|
+
|
45
|
+
const result = await validateTrustChain(trustAnchorEntityConfiguration, chain, {
|
46
|
+
connectTimeout: 3000,
|
47
|
+
readTimeout: 3000,
|
48
|
+
requireCrl: false,
|
49
|
+
});
|
50
|
+
```
|
51
|
+
|
52
|
+
* The `chain` must be an array of signed JWT strings.
|
53
|
+
* The first JWT must be a self-issued `EntityConfiguration`.
|
54
|
+
* The last JWT must be an `EntityStatement` or a self-issued Trust Anchor `EntityConfiguration`.
|
55
|
+
|
56
|
+
### Renew a trust chain
|
57
|
+
|
58
|
+
```ts
|
59
|
+
import { renewTrustChain } from "./trust";
|
60
|
+
|
61
|
+
const newChain = await renewTrustChain(chain);
|
62
|
+
```
|
63
|
+
|
64
|
+
This will fetch updated JWTs from each authority in the chain.
|
65
|
+
|
66
|
+
### Build a trust chain
|
67
|
+
|
68
|
+
```ts
|
69
|
+
import { buildTrustChain } from "./trust";
|
70
|
+
|
71
|
+
const chain = await buildTrustChain({
|
72
|
+
leaf: "https://example-leaf",
|
73
|
+
trustAnchor: trustAnchorEntityConfiguration,
|
74
|
+
});
|
75
|
+
```
|
76
|
+
|
77
|
+
* **leaf**: the entity URL of the subject to be trusted.
|
78
|
+
* **trustAnchor**: the known trust anchor configuration.
|
79
|
+
* Returns a list of JWT strings ordered from leaf to trust anchor.
|
80
|
+
|
81
|
+
|
82
|
+
## Trust Chain Structure
|
83
|
+
|
84
|
+
| Position | JWT Type | Requirements |
|
85
|
+
| -------- | ----------------------------------- |-------------------------------|
|
86
|
+
| First | Entity Configuration | `iss === sub` (self-issued) |
|
87
|
+
| Middle | Entity Statement | `iss ≠ sub`, signed by parent |
|
88
|
+
| Last | Entity Statement or Trust Anchor EC | Trust Anchor must be known |
|
89
|
+
|
90
|
+
### Build and Validate Example
|
91
|
+
|
92
|
+
```ts
|
93
|
+
import {
|
94
|
+
buildTrustChain,
|
95
|
+
validateTrustChain,
|
96
|
+
} from "./trust";
|
97
|
+
import { trustAnchorEntityConfiguration } from "./your-data";
|
98
|
+
|
99
|
+
const chain = await buildTrustChain({
|
100
|
+
leaf: "https://example-leaf",
|
101
|
+
trustAnchor: trustAnchorEntityConfiguration,
|
102
|
+
});
|
103
|
+
|
104
|
+
const result = await validateTrustChain(trustAnchorEntityConfiguration, chain, {
|
105
|
+
connectTimeout: 3000,
|
106
|
+
readTimeout: 3000,
|
107
|
+
requireCrl: true,
|
108
|
+
});
|
109
|
+
```
|
110
|
+
|
111
|
+
* This example fetches and builds the full trust chain dynamically, then validates it end-to-end.
|
112
|
+
|
113
|
+
## Example Trust Chain
|
114
|
+
|
115
|
+
```ts
|
116
|
+
[
|
117
|
+
{
|
118
|
+
header: { alg: "ES256", kid: "leaf-kid" },
|
119
|
+
payload: { iss: "https://leaf", sub: "https://leaf", jwks: { keys: [...] } }
|
120
|
+
},
|
121
|
+
{
|
122
|
+
header: { alg: "ES256", kid: "intermediate-kid" },
|
123
|
+
payload: { iss: "https://intermediate", sub: "https://leaf", jwks: { keys: [...] } }
|
124
|
+
},
|
125
|
+
{
|
126
|
+
header: { alg: "ES256", kid: "ta-kid" },
|
127
|
+
payload: { iss: "https://ta", sub: "https://ta", jwks: { keys: [...] } }
|
128
|
+
}
|
129
|
+
]
|
130
|
+
```
|
131
|
+
|
132
|
+
## Mocking in Tests
|
133
|
+
|
134
|
+
If you're testing in Node (not in React Native), you need to mock X.509 and crypto-native dependencies:
|
135
|
+
|
136
|
+
```ts
|
137
|
+
jest.mock("@pagopa/io-react-native-crypto", () => ({
|
138
|
+
verifyCertificateChain: jest.fn().mockResolvedValue({
|
139
|
+
isValid: true,
|
140
|
+
validationStatus: "VALID",
|
141
|
+
errorMessage: undefined,
|
142
|
+
}),
|
143
|
+
generate: jest.fn().mockResolvedValue({ ... }),
|
144
|
+
}));
|
145
|
+
```
|
146
|
+
|
147
|
+
Ensure mocked `JWK`s contain an `x5c` array to trigger certificate validation logic during tests.
|
package/src/trust/chain.ts
CHANGED
@@ -6,13 +6,26 @@ import {
|
|
6
6
|
import { JWK } from "../utils/jwk";
|
7
7
|
import * as z from "zod";
|
8
8
|
import { getSignedEntityConfiguration, getSignedEntityStatement } from ".";
|
9
|
-
import { decode, type ParsedToken, verify } from "./utils";
|
10
9
|
import {
|
10
|
+
decode,
|
11
|
+
getTrustAnchorX509Certificate,
|
12
|
+
type ParsedToken,
|
13
|
+
verify,
|
14
|
+
} from "./utils";
|
15
|
+
import {
|
16
|
+
FederationError,
|
11
17
|
MissingFederationFetchEndpointError,
|
18
|
+
MissingX509CertsError,
|
12
19
|
TrustChainEmptyError,
|
13
20
|
TrustChainRenewalError,
|
14
21
|
TrustChainTokenMissingError,
|
22
|
+
X509ValidationError,
|
15
23
|
} from "./errors";
|
24
|
+
import {
|
25
|
+
type CertificateValidationResult,
|
26
|
+
verifyCertificateChain,
|
27
|
+
type X509CertificateOptions,
|
28
|
+
} from "@pagopa/io-react-native-crypto";
|
16
29
|
|
17
30
|
// The first element of the chain is supposed to be the Entity Configuration for the document issuer
|
18
31
|
const FirstElementShape = EntityConfiguration;
|
@@ -26,16 +39,18 @@ const LastElementShape = z.union([
|
|
26
39
|
]);
|
27
40
|
|
28
41
|
/**
|
29
|
-
* Validates a provided trust chain against a known trust
|
42
|
+
* Validates a provided trust chain against a known trust anchor, including X.509 certificate checks.
|
30
43
|
*
|
31
|
-
* @param trustAnchorEntity The entity configuration of the known trust anchor
|
32
|
-
* @param chain The chain of statements to be validated
|
33
|
-
* @
|
34
|
-
* @
|
44
|
+
* @param trustAnchorEntity The entity configuration of the known trust anchor (for JWT validation).
|
45
|
+
* @param chain The chain of statements to be validated.
|
46
|
+
* @param x509Options Options for X.509 certificate validation.
|
47
|
+
* @returns The list of parsed tokens representing the chain.
|
48
|
+
* @throws {FederationError} If the chain is not valid (JWT or X.509). Specific errors like TrustChainEmptyError, X509ValidationError may be thrown.
|
35
49
|
*/
|
36
50
|
export async function validateTrustChain(
|
37
51
|
trustAnchorEntity: TrustAnchorEntityConfiguration,
|
38
|
-
chain: string[]
|
52
|
+
chain: string[],
|
53
|
+
x509Options: X509CertificateOptions
|
39
54
|
): Promise<ParsedToken[]> {
|
40
55
|
// If the chain is empty, fail
|
41
56
|
if (chain.length === 0) {
|
@@ -50,7 +65,7 @@ export async function validateTrustChain(
|
|
50
65
|
? LastElementShape
|
51
66
|
: MiddleElementShape;
|
52
67
|
|
53
|
-
//
|
68
|
+
// Select the kid from the current index
|
54
69
|
const selectKid = (currentIndex: number): string => {
|
55
70
|
const token = chain[currentIndex];
|
56
71
|
if (!token) {
|
@@ -63,8 +78,8 @@ export async function validateTrustChain(
|
|
63
78
|
return shape.parse(decode(token)).header.kid;
|
64
79
|
};
|
65
80
|
|
66
|
-
//
|
67
|
-
//
|
81
|
+
// Select keys from the next token
|
82
|
+
// If the current token is the last, keys from trust anchor will be used
|
68
83
|
const selectKeys = (currentIndex: number): JWK[] => {
|
69
84
|
if (currentIndex === chain.length - 1) {
|
70
85
|
return trustAnchorEntity.payload.jwks.keys;
|
@@ -82,13 +97,74 @@ export async function validateTrustChain(
|
|
82
97
|
return shape.parse(decode(nextToken)).payload.jwks.keys;
|
83
98
|
};
|
84
99
|
|
100
|
+
const x509TrustAnchorCertBase64 =
|
101
|
+
getTrustAnchorX509Certificate(trustAnchorEntity);
|
102
|
+
|
85
103
|
// Iterate the chain and validate each element's signature against the public keys of its next
|
86
104
|
// If there is no next, hence it's the end of the chain, and it must be verified by the Trust Anchor
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
105
|
+
const validationPromises = chain.map(async (tokenString, i) => {
|
106
|
+
const kidFromTokenHeader = selectKid(i);
|
107
|
+
const signerJwks = selectKeys(i);
|
108
|
+
|
109
|
+
// Step 1: Verify JWT signature
|
110
|
+
const parsedToken = await verify(
|
111
|
+
tokenString,
|
112
|
+
kidFromTokenHeader,
|
113
|
+
signerJwks
|
114
|
+
);
|
115
|
+
|
116
|
+
// Step 2: X.509 Certificate Chain Validation
|
117
|
+
const jwkUsedForVerification = signerJwks.find(
|
118
|
+
(k) => k.kid === kidFromTokenHeader
|
119
|
+
);
|
120
|
+
|
121
|
+
if (!jwkUsedForVerification) {
|
122
|
+
throw new FederationError(
|
123
|
+
`JWK with kid '${kidFromTokenHeader}' was not found in signer's JWKS for token at index ${i}, though JWT verification passed.`,
|
124
|
+
{ tokenIndex: i, kid: kidFromTokenHeader }
|
125
|
+
);
|
126
|
+
}
|
127
|
+
|
128
|
+
if (
|
129
|
+
!jwkUsedForVerification.x5c ||
|
130
|
+
jwkUsedForVerification.x5c.length === 0
|
131
|
+
) {
|
132
|
+
throw new MissingX509CertsError(
|
133
|
+
`JWK with kid '${kidFromTokenHeader}' does not contain an X.509 certificate chain (x5c) for token at index ${i}.`
|
134
|
+
);
|
135
|
+
}
|
136
|
+
|
137
|
+
// If the chain has more than one certificate AND
|
138
|
+
// the last certificate in the x5c chain is the same as the trust anchor,
|
139
|
+
// remove the anchor from the chain being passed, as it's supplied separately.
|
140
|
+
const certChainBase64 =
|
141
|
+
jwkUsedForVerification.x5c.length > 1 &&
|
142
|
+
jwkUsedForVerification.x5c.at(-1) === x509TrustAnchorCertBase64
|
143
|
+
? jwkUsedForVerification.x5c.slice(0, -1)
|
144
|
+
: jwkUsedForVerification.x5c;
|
145
|
+
|
146
|
+
const x509ValidationResult: CertificateValidationResult =
|
147
|
+
await verifyCertificateChain(
|
148
|
+
certChainBase64,
|
149
|
+
x509TrustAnchorCertBase64,
|
150
|
+
x509Options
|
151
|
+
);
|
152
|
+
|
153
|
+
if (!x509ValidationResult.isValid) {
|
154
|
+
throw new X509ValidationError(
|
155
|
+
`X.509 certificate chain validation failed for token at index ${i} (kid: ${kidFromTokenHeader}). Status: ${x509ValidationResult.validationStatus}. Error: ${x509ValidationResult.errorMessage}`,
|
156
|
+
{
|
157
|
+
tokenIndex: i,
|
158
|
+
kid: kidFromTokenHeader,
|
159
|
+
x509ValidationStatus: x509ValidationResult.validationStatus,
|
160
|
+
x509ErrorMessage: x509ValidationResult.errorMessage,
|
161
|
+
}
|
162
|
+
);
|
163
|
+
}
|
164
|
+
return parsedToken;
|
165
|
+
});
|
166
|
+
|
167
|
+
return Promise.all(validationPromises);
|
92
168
|
}
|
93
169
|
|
94
170
|
/**
|
package/src/trust/errors.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
import { IoWalletError, serializeAttrs } from "../utils/errors";
|
1
|
+
import { IoWalletError, serializeAttrs } from "../utils/errors";
|
2
|
+
import type { CertificateValidationStatus } from "@pagopa/io-react-native-crypto"; // Ensure this path is correct
|
2
3
|
|
3
4
|
/**
|
4
5
|
* Base class for all federation-specific errors.
|
@@ -103,3 +104,33 @@ export class MissingFederationFetchEndpointError extends FederationError {
|
|
103
104
|
super(message, details);
|
104
105
|
}
|
105
106
|
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Error thrown when the X.509 certificate chain is missing in an entity's configuration.
|
110
|
+
*/
|
111
|
+
export class MissingX509CertsError extends FederationError {
|
112
|
+
code = "ERR_FED_MISSING_X509_CERTS";
|
113
|
+
constructor(message: string) {
|
114
|
+
super(message, undefined);
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
/**
|
119
|
+
* Error thrown when an X.509 certificate validation fails.
|
120
|
+
* This is used to indicate issues with the certificate chain or signature verification.
|
121
|
+
*/
|
122
|
+
export class X509ValidationError extends FederationError {
|
123
|
+
code = "ERR_FED_X509_VALIDATION_FAILED";
|
124
|
+
constructor(
|
125
|
+
message: string,
|
126
|
+
details?: {
|
127
|
+
tokenIndex?: number;
|
128
|
+
kid?: string;
|
129
|
+
x509ValidationStatus?: CertificateValidationStatus;
|
130
|
+
x509ErrorMessage?: string;
|
131
|
+
[key: string]: unknown;
|
132
|
+
}
|
133
|
+
) {
|
134
|
+
super(message, details);
|
135
|
+
}
|
136
|
+
}
|
package/src/trust/index.ts
CHANGED
@@ -19,6 +19,7 @@ import {
|
|
19
19
|
RelyingPartyNotAuthorizedError,
|
20
20
|
TrustAnchorKidMissingError,
|
21
21
|
} from "./errors";
|
22
|
+
import type { X509CertificateOptions } from "@pagopa/io-react-native-crypto";
|
22
23
|
|
23
24
|
export type {
|
24
25
|
WalletProviderEntityConfiguration,
|
@@ -35,25 +36,31 @@ export type {
|
|
35
36
|
*
|
36
37
|
* @param trustAnchorEntity The entity configuration of the known trust anchor
|
37
38
|
* @param chain The chain of statements to be validated
|
38
|
-
* @param
|
39
|
-
* @param appFetch
|
39
|
+
* @param x509Options Options for the verification process
|
40
|
+
* @param appFetch (optional) fetch api implementation
|
41
|
+
* @param renewOnFail Whether to attempt to renew the trust chain if the initial validation fails
|
40
42
|
* @returns The result of the chain validation
|
41
43
|
* @throws {FederationError} If the chain is not valid
|
42
44
|
*/
|
43
45
|
export async function verifyTrustChain(
|
44
46
|
trustAnchorEntity: TrustAnchorEntityConfiguration,
|
45
47
|
chain: string[],
|
48
|
+
x509Options: X509CertificateOptions = {
|
49
|
+
connectTimeout: 10000,
|
50
|
+
readTimeout: 10000,
|
51
|
+
requireCrl: true,
|
52
|
+
},
|
46
53
|
{
|
47
54
|
appFetch = fetch,
|
48
55
|
renewOnFail = true,
|
49
56
|
}: { appFetch?: GlobalFetch["fetch"]; renewOnFail?: boolean } = {}
|
50
57
|
): Promise<ReturnType<typeof validateTrustChain>> {
|
51
58
|
try {
|
52
|
-
return validateTrustChain(trustAnchorEntity, chain);
|
59
|
+
return validateTrustChain(trustAnchorEntity, chain, x509Options);
|
53
60
|
} catch (error) {
|
54
61
|
if (renewOnFail) {
|
55
62
|
const renewedChain = await renewTrustChain(chain, appFetch);
|
56
|
-
return validateTrustChain(trustAnchorEntity, renewedChain);
|
63
|
+
return validateTrustChain(trustAnchorEntity, renewedChain, x509Options);
|
57
64
|
} else {
|
58
65
|
throw error;
|
59
66
|
}
|
package/src/trust/utils.ts
CHANGED
@@ -4,6 +4,8 @@ import {
|
|
4
4
|
} from "@pagopa/io-react-native-jwt";
|
5
5
|
|
6
6
|
import type { JWK, JWTDecodeResult } from "../utils/jwk";
|
7
|
+
import { FederationError } from "./errors";
|
8
|
+
import type { TrustAnchorEntityConfiguration } from "./types";
|
7
9
|
|
8
10
|
export type ParsedToken = {
|
9
11
|
header: JWTDecodeResult["protectedHeader"];
|
@@ -33,3 +35,36 @@ export const decode = (token: string): ParsedToken => {
|
|
33
35
|
const { protectedHeader: header, payload } = decodeJwt(token);
|
34
36
|
return { header, payload };
|
35
37
|
};
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Extracts the X.509 Trust Anchor certificate (Base64 encoded) from the
|
41
|
+
* Trust Anchor's Entity Configuration.
|
42
|
+
*
|
43
|
+
* @param trustAnchorEntity The entity configuration of the known trust anchor.
|
44
|
+
* @returns The Base64 encoded X.509 certificate string.
|
45
|
+
* @throws {FederationError} If the certificate cannot be derived.
|
46
|
+
*/
|
47
|
+
export function getTrustAnchorX509Certificate(
|
48
|
+
trustAnchorEntity: TrustAnchorEntityConfiguration
|
49
|
+
): string {
|
50
|
+
const taHeaderKid = trustAnchorEntity.header.kid;
|
51
|
+
const taSigningJwk = trustAnchorEntity.payload.jwks.keys.find(
|
52
|
+
(key) => key.kid === taHeaderKid
|
53
|
+
);
|
54
|
+
|
55
|
+
if (!taSigningJwk) {
|
56
|
+
throw new FederationError(
|
57
|
+
`Cannot derive X.509 Trust Anchor certificate: JWK with kid '${taHeaderKid}' not found in Trust Anchor's JWKS.`,
|
58
|
+
{ trustAnchorKid: taHeaderKid, reason: "JWK not found for header kid" }
|
59
|
+
);
|
60
|
+
}
|
61
|
+
|
62
|
+
if (taSigningJwk.x5c && taSigningJwk.x5c.length > 0 && taSigningJwk.x5c[0]) {
|
63
|
+
return taSigningJwk.x5c[0];
|
64
|
+
}
|
65
|
+
|
66
|
+
throw new FederationError(
|
67
|
+
`Cannot derive X.509 Trust Anchor certificate: JWK with kid '${taHeaderKid}' does not contain a valid 'x5c' certificate array.`,
|
68
|
+
{ trustAnchorKid: taHeaderKid, reason: "Missing or empty x5c in JWK" }
|
69
|
+
);
|
70
|
+
}
|