Skip to content

Commit a981cac

Browse files
mastro993ale-mazz
andauthored
feat(IT-Wallet): [SIW-3045] Add L2+ PID issuance flow with MRTD PoP (#7647)
## Short description This PR introduces the L2+ flow for IT-Wallet. L2+ adds an extra step after the primary SPID or CieID identification, requiring the user to complete an MRTD (ID card) proof-of-possession check. The verification is performed through an NFC scan of the ID card. This PR also includes a major refactoring of the CIE scan logic, making it reusable and shareable across both the CIE+PIN and MRTD flows. ## List of changes proposed in this pull request ### Credential Issuance Flow & Proof Handling * Added support for selecting proof type (`mrtd-pop` or `none`) during credential issuance, controlled by the new `withMRTDPoP` parameter in `startAuthFlow`, and updated related function signatures and logic in `itwIssuanceUtils.ts`. [[1]](diffhunk://#diff-2cd6d88d7a0a5ffab205730094379c0e1c542e964874a0edf5a5d60416e295c3R28) [[2]](diffhunk://#diff-2cd6d88d7a0a5ffab205730094379c0e1c542e964874a0edf5a5d60416e295c3R39-L50) [[3]](diffhunk://#diff-2cd6d88d7a0a5ffab205730094379c0e1c542e964874a0edf5a5d60416e295c3R67-R69) * Improved validation in `getCredentialIdentifierFromAccessToken` to throw an error for unsupported authorization detail types, increasing robustness. ### MRTD PoP Challenge Utilities * Introduced the `mrtd.ts` utility with functions for initializing and validating MRTD PoP challenges, encapsulating cryptographic context management and challenge handling. ### NFC Feature Detection & L3 Flow * Simplified L3 feature detection by checking for `level === "l3"` directly instead of using a helper, and updated related tests and screens to reflect this logic. [[1]](diffhunk://#diff-aac82824191e7fddf4b8b79ddee9b8b13896340cea331ff4f4f2e6ea90335170L31-R30) [[2]](diffhunk://#diff-ad0b45dc86f8c2a0249744d4413aef7b7d8667579124c2c7d156067e8c474304L21-R35) * Updated the discovery component to use wallet lifecycle validity for determining whether to start in "upgrade" or "issuance" mode, improving flow control. ### Localization & Dependency Updates * Updated Italian localization strings for credential issuance modes to clarify durations and hints. * Upgraded the `@pagopa/io-react-native-wallet` dependency to version 2.4.0 for access to new features and bug fixes. ### Error Handling & Analytics * Refactored the CIE card read failure content to improve error tracking, analytics integration, and retry handling, making the component more robust and customizable. [[1]](diffhunk://#diff-9c4c2ef6526484c9cf6023ef5875cd7d05a72c0a0bc331bb30305c47f1fafee8L2-R65) [[2]](diffhunk://#diff-4f759b936530a9db89be53e8df2c20c5e69713067abb07ce762fce6d0dcc64ddL227-R229) ## How to test - With the IT-Wallet whitelist flag **disabled**: - Verify that all identification methods (CIE+PIN, SPID, CieID) behave as before and no regressions are introduced. - With the IT-Wallet whitelist flag **enabled**: - Verify that the CIE+PIN identification flow still works correctly and without regressions. - Verify that it is possible to obtain an L3 PID using SPID or CieID through the MRTD PoP flow. ## Demo | SPID + CIE | CieID + CIE | | --- | --- | | <video src="https://github.com/user-attachments/assets/83e7bf7b-7c60-4b21-9d7c-01175c00c395" /> | <video src="https://github.com/user-attachments/assets/f72e8343-0905-4f0b-bff4-99cdb880bc37" /> | --------- Co-authored-by: Alessandro Mazzon <[email protected]>
1 parent 052410d commit a981cac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1827
-1534
lines changed

locales/it/index.json

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4749,27 +4749,24 @@
47494749
"mode": {
47504750
"ciePin": {
47514751
"title": "Continua con CIE + PIN",
4752-
"subtitle": {
4753-
"default": "Dovrai usare la Carta di Identità Elettronica (CIE) e inserire il suo PIN di 8 cifre.",
4754-
"l3-next": "Ha una durata di 12 mesi"
4755-
},
4752+
"subtitle": "Ha una durata di 12 mesi",
47564753
"badge": "scelta consigliata"
47574754
},
47584755
"cieId": {
47594756
"title": "Continua con l’app CieID",
47604757
"subtitle": {
47614758
"default": "Dovrai usare credenziali e app CieID",
4762-
"l3-next": "Ha una durata di 12 mesi"
4759+
"l3": "Ha una durata di 12 mesi"
47634760
}
47644761
},
47654762
"spid": {
47664763
"title": {
47674764
"default": "Continua con SPID",
4768-
"l3-next": "Continua con SPID + CIE"
4765+
"l3": "Continua con SPID + CIE"
47694766
},
47704767
"subtitle": {
47714768
"default": "Dovrai usare credenziali e app (o SMS)",
4772-
"l3-next": "Ha una durata di 90 giorni"
4769+
"l3": "Ha una durata di 90 giorni"
47734770
}
47744771
}
47754772
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"@pagopa/io-react-native-jwt": "^2.1.0",
7070
"@pagopa/io-react-native-login-utils": "^1.1.0",
7171
"@pagopa/io-react-native-secure-storage": "^0.2.1",
72-
"@pagopa/io-react-native-wallet": "^2.3.0",
72+
"@pagopa/io-react-native-wallet": "^2.4.0",
7373
"@pagopa/io-react-native-wallet-legacy": "npm:@pagopa/[email protected]",
7474
"@pagopa/io-react-native-zendesk": "^0.3.30",
7575
"@pagopa/react-native-cie": "^1.4.3",

ts/features/common/components/cie/CieCardReadContent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ const ContentAndroid = (props: CieCardReadContentProps) => (
232232
);
233233

234234
/**
235-
* Renders the read progress screen content based on the platform
235+
* Renders the read progress screen content based on the platform.
236+
* It is fully customizable via props and it is used as base component to display the
237+
* reading, failure and success states for the CIE manager flow.
236238
*/
237239
export const CieCardReadContent = platformSelect({
238240
ios: ContentIos,

ts/features/common/utils/cie/__tests__/index.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

ts/features/itwallet/common/utils/itwCredentialIssuanceUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const requestCredential = async ({
5959
await Credential.Issuance.startUserAuthorization(
6060
issuerConf,
6161
credentialIds,
62+
{ proofType: "none" },
6263
{
6364
walletInstanceAttestation,
6465
redirectUri: env.ISSUANCE_REDIRECT_URI,

ts/features/itwallet/common/utils/itwCredentialUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,10 @@ export const isItwCredential = ({
147147
return pipe(
148148
O.tryCatch(getVerificationByFormat[format as CredentialFormat]),
149149
O.chain(O.fromNullable),
150-
O.chainNullableK(({ assurance_level }) => assurance_level === "high"),
150+
O.chainNullableK(
151+
({ assurance_level, trust_framework }) =>
152+
assurance_level === "high" || trust_framework === "it_l2+document_proof"
153+
),
151154
O.getOrElse(() => false)
152155
);
153156
};

ts/features/itwallet/common/utils/itwIssuanceUtils.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type StartAuthFlowParams = {
2525
env: Env;
2626
walletAttestation: string;
2727
identification: IdentificationContext;
28+
withMRTDPoP: boolean;
2829
};
2930

3031
/**
@@ -35,19 +36,20 @@ type StartAuthFlowParams = {
3536
* @param env - The environment to use for the wallet provider base URL
3637
* @param walletAttestation - The wallet attestation.
3738
* @param identification - The identification context.
39+
* @param withMRTDPoP - Whether to use MRTD PoP proof or not.
3840
* @returns Authentication params to use when completing the flow.
3941
*/
4042
const startAuthFlow = async ({
4143
env,
4244
walletAttestation,
43-
identification
45+
identification,
46+
withMRTDPoP
4447
}: StartAuthFlowParams) => {
4548
const startFlow: Credential.Issuance.StartFlow = () => ({
4649
issuerUrl: env.WALLET_PID_PROVIDER_BASE_URL,
4750
credentialId: "dc_sd_jwt_PersonIdentificationData"
4851
});
4952

50-
// When issuing an L3 PID, we should not provide an IDP hint
5153
const idpHint = getIdpHint(identification, env);
5254

5355
const { issuerUrl, credentialId } = startFlow();
@@ -62,6 +64,9 @@ const startAuthFlow = async ({
6264
await Credential.Issuance.startUserAuthorization(
6365
issuerConf,
6466
[credentialId],
67+
withMRTDPoP
68+
? { proofType: "mrtd-pop", idpHinting: idpHint }
69+
: { proofType: "none" },
6570
{
6671
walletInstanceAttestation: walletAttestation,
6772
redirectUri: env.ISSUANCE_REDIRECT_URI,
@@ -218,6 +223,12 @@ function getCredentialIdentifierFromAccessToken(
218223
accessToken: CredentialAccessToken,
219224
authorizationDetail: AuthorizationDetail
220225
) {
226+
if (authorizationDetail.type !== "openid_credential") {
227+
throw new Error(
228+
`Unsupported authorization detail type: ${authorizationDetail.type}`
229+
);
230+
}
231+
221232
const accessTokenAuthDetail = accessToken.authorization_details.find(
222233
authDetails =>
223234
authDetails.credential_configuration_id ===
@@ -275,11 +286,6 @@ const SPID_IDP_HINTS: { [key: string]: string } = {
275286
* @param isL3 flag that indicates that we need to issue an L3 PID
276287
*/
277288
export const getIdpHint = (idCtx: IdentificationContext, env: Env) => {
278-
if (idCtx.level === "L3") {
279-
// When issuing an L3 PID, we should not provide an IDP hint
280-
return undefined;
281-
}
282-
283289
const isSpidMode = idCtx.mode === "spid";
284290

285291
if (env.type === "pre") {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {
2+
createCryptoContextFor,
3+
Credential
4+
} from "@pagopa/io-react-native-wallet";
5+
import { WIA_KEYTAG } from "./itwCryptoContextUtils";
6+
import { IssuerConfiguration } from "./itwTypesUtils";
7+
8+
export type InitMrtdPoPChallengeParams = {
9+
issuerConf: IssuerConfiguration;
10+
walletInstanceAttestation: string;
11+
authRedirectUrl: string;
12+
};
13+
14+
export type ValidateMrtdPoPChallengeParams = {
15+
issuerConf: IssuerConfiguration;
16+
walletInstanceAttestation: string;
17+
validationUrl: string;
18+
mrtd_auth_session: string;
19+
mrtd_pop_nonce: string;
20+
mrtd: Credential.Issuance.MRTDPoP.MrtdPayload;
21+
ias: Credential.Issuance.MRTDPoP.IasPayload;
22+
};
23+
24+
export const initMrtdPoPChallenge = async ({
25+
authRedirectUrl,
26+
issuerConf,
27+
walletInstanceAttestation
28+
}: InitMrtdPoPChallengeParams) => {
29+
const wiaCryptoContext = createCryptoContextFor(WIA_KEYTAG);
30+
31+
const { challenge_info } =
32+
await Credential.Issuance.continueUserAuthorizationWithMRTDPoPChallenge(
33+
authRedirectUrl
34+
);
35+
36+
const {
37+
htu: initUrl,
38+
mrtd_auth_session,
39+
mrtd_pop_jwt_nonce
40+
} = await Credential.Issuance.MRTDPoP.verifyAndParseChallengeInfo(
41+
issuerConf,
42+
challenge_info,
43+
{ wiaCryptoContext }
44+
);
45+
46+
const {
47+
htu: validationUrl,
48+
challenge,
49+
mrtd_pop_nonce
50+
} = await Credential.Issuance.MRTDPoP.initChallenge(
51+
issuerConf,
52+
initUrl,
53+
mrtd_auth_session,
54+
mrtd_pop_jwt_nonce,
55+
{
56+
walletInstanceAttestation,
57+
wiaCryptoContext
58+
}
59+
);
60+
61+
return {
62+
challenge,
63+
mrtd_auth_session,
64+
mrtd_pop_nonce,
65+
validationUrl
66+
};
67+
};
68+
69+
export const validateMrtdPoPChallenge = async ({
70+
validationUrl,
71+
mrtd_auth_session,
72+
mrtd_pop_nonce,
73+
issuerConf,
74+
walletInstanceAttestation,
75+
ias,
76+
mrtd
77+
}: ValidateMrtdPoPChallengeParams) => {
78+
const wiaCryptoContext = createCryptoContextFor(WIA_KEYTAG);
79+
80+
const { mrtd_val_pop_nonce, redirect_uri } =
81+
await Credential.Issuance.MRTDPoP.validateChallenge(
82+
issuerConf,
83+
validationUrl,
84+
mrtd_auth_session,
85+
mrtd_pop_nonce,
86+
mrtd,
87+
ias,
88+
{
89+
walletInstanceAttestation,
90+
wiaCryptoContext
91+
}
92+
);
93+
94+
const { callbackUrl } =
95+
await Credential.Issuance.MRTDPoP.buildChallengeCallbackUrl(
96+
redirect_uri,
97+
mrtd_val_pop_nonce,
98+
mrtd_auth_session
99+
);
100+
101+
return {
102+
callbackUrl
103+
};
104+
};

ts/features/itwallet/discovery/components/ItwDiscoveryInfoComponent.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
import { useItwDismissalDialog } from "../../common/hooks/useItwDismissalDialog.tsx";
4747
import { itwIsActivationDisabledSelector } from "../../common/store/selectors/remoteConfig.ts";
4848
import { generateItwIOMarkdownRules } from "../../common/utils/markdown.tsx";
49+
import { itwLifecycleIsValidSelector } from "../../lifecycle/store/selectors/index.ts";
4950
import { ItwEidIssuanceMachineContext } from "../../machine/eid/provider.tsx";
5051
import { selectIsLoading } from "../../machine/eid/selectors.ts";
5152

@@ -63,16 +64,17 @@ export const ItwDiscoveryInfoComponent = () => {
6364
const isLoading = ItwEidIssuanceMachineContext.useSelector(selectIsLoading);
6465
const itwActivationDisabled = useIOSelector(itwIsActivationDisabledSelector);
6566
const { tos_url } = useIOSelector(tosConfigSelector);
67+
const isWalletValid = useIOSelector(itwLifecycleIsValidSelector);
6668
const toast = useIOToast();
6769

6870
useOnFirstRender(
6971
useCallback(() => {
7072
machineRef.send({
7173
type: "start",
72-
mode: "issuance",
74+
mode: isWalletValid ? "upgrade" : "issuance",
7375
level: "l3"
7476
});
75-
}, [machineRef])
77+
}, [machineRef, isWalletValid])
7678
);
7779

7880
const dismissalDialog = useItwDismissalDialog({

ts/features/itwallet/discovery/screens/ItwDiscoveryInfoScreen.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppPa
22
import { useIOSelector } from "../../../../store/hooks.ts";
33
import { itwHasNfcFeatureSelector } from "../../identification/common/store/selectors/index.ts";
44
import { EidIssuanceLevel } from "../../machine/eid/context.ts";
5-
import { isL3IssuanceFeaturesEnabled } from "../../machine/eid/utils.ts";
65
import { ItwParamsList } from "../../navigation/ItwParamsList.ts";
76
import { ItwDiscoveryInfoComponent } from "../components/ItwDiscoveryInfoComponent.tsx";
87
import { ItwDiscoveryInfoFallbackComponent } from "../components/ItwDiscoveryInfoFallbackComponent.tsx";
@@ -28,7 +27,7 @@ export const ItwDiscoveryInfoScreen = ({
2827
const { level = "l2" } = route.params ?? {};
2928
const hasNfcFeature = useIOSelector(itwHasNfcFeatureSelector);
3029

31-
if (isL3IssuanceFeaturesEnabled(level)) {
30+
if (level === "l3") {
3231
if (!hasNfcFeature) {
3332
// L3 requires NFC, show not supported screen
3433
return <ItwNfcNotSupportedComponent />;

0 commit comments

Comments
 (0)