Skip to main content

smart_id_rust_client/models/
common.rs

1use crate::error::Result;
2use crate::error::SmartIdClientError;
3use crate::models::api::authentication_session::{
4    AuthenticationDeviceLinkRequest, AuthenticationDeviceLinkSession,
5    AuthenticationNotificationRequest, AuthenticationNotificationSession,
6};
7use crate::models::api::certificate_choice_session::{
8    CertificateChoiceDeviceLinkRequest, CertificateChoiceDeviceLinkSession,
9    CertificateChoiceNotificationRequest, CertificateChoiceNotificationSession,
10};
11use crate::models::api::session_status::SessionStatusResponse;
12use crate::models::api::signature_session::{
13    SignatureDeviceLinkRequest, SignatureDeviceLinkSession, SignatureNotificationLinkedRequest,
14    SignatureNotificationLinkedSession, SignatureNotificationRequest, SignatureNotificationSession,
15};
16use crate::models::signature::{
17    ResponseSignature, SignatureAlgorithm, SignatureProtocol, SignatureProtocolParameters,
18};
19use base64::engine::general_purpose::STANDARD;
20use base64::prelude::BASE64_STANDARD;
21use base64::Engine;
22use chrono::{DateTime, Utc};
23use serde::{Deserialize, Serialize};
24use sha2::{Digest, Sha256};
25use std::cmp::Ordering;
26use strum_macros::{AsRefStr, Display, EnumString};
27use tracing::error;
28
29/// Request Properties
30///
31/// This struct represents the properties of a request to the Smart ID service.
32/// Currently, it only includes one property, `share_md_client_ip_address`.
33///
34/// # Properties
35///
36/// * `share_md_client_ip_address` - A boolean flag indicating whether the RP API server should share the user's mobile device IP address with the RP. By default, it is set to false. The RP must have proper privilege to use this property. See section IP sharing for details.
37#[non_exhaustive]
38#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct RequestProperties {
41    /// Whether the RP API server should share user mobile device IP address with the RP. By default it is set to false. The RP must have proper privilege to use this property. See section IP sharing for details.
42    pub share_md_client_ip_address: bool,
43}
44
45#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[allow(non_camel_case_types)]
47#[non_exhaustive]
48pub enum CertificateLevel {
49    #[default]
50    QUALIFIED,
51    ADVANCED,
52    QSCD,
53}
54
55impl CertificateLevel {
56    fn rank(&self) -> u8 {
57        match self {
58            CertificateLevel::ADVANCED => 0,
59            CertificateLevel::QUALIFIED => 1,
60            CertificateLevel::QSCD => 2,
61        }
62    }
63}
64
65impl PartialOrd for CertificateLevel {
66    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
67        Some(self.rank().cmp(&other.rank()))
68    }
69}
70impl Ord for CertificateLevel {
71    fn cmp(&self, other: &Self) -> Ordering {
72        self.rank().cmp(&other.rank())
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub enum SessionConfig {
78    AuthenticationDeviceLink {
79        // Response values
80        session_id: String,
81        session_secret: String,
82        session_token: String,
83        device_link_base: String,
84
85        // Request values
86        scheme_name: SchemeName,
87        relying_party_uuid: String,
88        relying_party_name: String,
89        initial_callback_url: Option<String>,
90        certificate_level: CertificateLevel,
91        signature_protocol: SignatureProtocol,
92        signature_protocol_parameters: SignatureProtocolParameters,
93        interactions: String,
94
95        // Calculated values
96        rp_challenge: String,
97        session_start_time: DateTime<Utc>,
98    },
99    AuthenticationNotification {
100        // Response values
101        session_id: String,
102
103        // Request values
104        scheme_name: SchemeName,
105        relying_party_uuid: String,
106        relying_party_name: String,
107        certificate_level: CertificateLevel,
108        signature_protocol: SignatureProtocol,
109        signature_protocol_parameters: SignatureProtocolParameters,
110        interactions: String,
111        vc_type: VCCodeType,
112
113        // Calculated values
114        rp_challenge: String,
115        session_start_time: DateTime<Utc>,
116    },
117    SignatureDeviceLink {
118        // Response values
119        session_id: String,
120        session_secret: String,
121        session_token: String,
122        device_link_base: String,
123
124        // Request values
125        scheme_name: SchemeName,
126        relying_party_uuid: String,
127        relying_party_name: String,
128        initial_callback_url: Option<String>,
129        certificate_level: CertificateLevel,
130        signature_protocol: SignatureProtocol,
131        signature_protocol_parameters: SignatureProtocolParameters,
132        interactions: String,
133
134        // Calculated values
135        digest: String,
136        session_start_time: DateTime<Utc>,
137    },
138    SignatureNotification {
139        // Response values
140        session_id: String,
141        vc: VCCode,
142
143        // Request values
144        scheme_name: SchemeName,
145        relying_party_uuid: String,
146        relying_party_name: String,
147        certificate_level: CertificateLevel,
148        signature_protocol: SignatureProtocol,
149        signature_protocol_parameters: SignatureProtocolParameters,
150        interactions: String,
151
152        // Calculated values
153        digest: String,
154        session_start_time: DateTime<Utc>,
155    },
156    SignatureNotificationLinked {
157        // Response values
158        session_id: String,
159
160        // Request values
161        scheme_name: SchemeName,
162        relying_party_uuid: String,
163        relying_party_name: String,
164        certificate_level: CertificateLevel,
165        signature_protocol: SignatureProtocol,
166        signature_protocol_parameters: SignatureProtocolParameters,
167        linked_session_id: String,
168        interactions: String,
169
170        // Calculated values
171        digest: String,
172        session_start_time: DateTime<Utc>,
173    },
174    CertificateChoiceDeviceLink {
175        // Response values
176        session_id: String,
177        session_token: String,
178        session_secret: String,
179        device_link_base: String,
180
181        // Request values
182        scheme_name: SchemeName,
183        relying_party_uuid: String,
184        relying_party_name: String,
185        initial_callback_url: Option<String>,
186        certificate_level: CertificateLevel,
187
188        // Calculated values
189        session_start_time: DateTime<Utc>,
190    },
191    CertificateChoiceNotification {
192        // Response values
193        session_id: String,
194
195        // Request values
196        scheme_name: SchemeName,
197        relying_party_uuid: String,
198        relying_party_name: String,
199        certificate_level: CertificateLevel,
200
201        // Calculated values
202        session_start_time: DateTime<Utc>,
203    },
204}
205
206impl SessionConfig {
207    pub fn session_id(&self) -> &String {
208        match self {
209            SessionConfig::AuthenticationDeviceLink { session_id, .. } => session_id,
210            SessionConfig::AuthenticationNotification { session_id, .. } => session_id,
211            SessionConfig::CertificateChoiceDeviceLink { session_id, .. } => session_id,
212            SessionConfig::CertificateChoiceNotification { session_id, .. } => session_id,
213            SessionConfig::SignatureDeviceLink { session_id, .. } => session_id,
214            SessionConfig::SignatureNotification { session_id, .. } => session_id,
215            SessionConfig::SignatureNotificationLinked { session_id, .. } => session_id,
216        }
217    }
218
219    pub(crate) fn requested_certificate_level(&self) -> &CertificateLevel {
220        match self {
221            SessionConfig::AuthenticationDeviceLink {
222                certificate_level, ..
223            } => certificate_level,
224            SessionConfig::AuthenticationNotification {
225                certificate_level, ..
226            } => certificate_level,
227            SessionConfig::CertificateChoiceDeviceLink {
228                certificate_level, ..
229            } => certificate_level,
230            SessionConfig::CertificateChoiceNotification {
231                certificate_level, ..
232            } => certificate_level,
233            SessionConfig::SignatureDeviceLink {
234                certificate_level, ..
235            } => certificate_level,
236            SessionConfig::SignatureNotification {
237                certificate_level, ..
238            } => certificate_level,
239            SessionConfig::SignatureNotificationLinked {
240                certificate_level, ..
241            } => certificate_level,
242        }
243    }
244
245    pub fn from_authentication_device_link_response(
246        authentication_response: AuthenticationDeviceLinkSession,
247        authentication_request: AuthenticationDeviceLinkRequest,
248        scheme_name: &SchemeName,
249    ) -> Result<SessionConfig> {
250        Ok(SessionConfig::AuthenticationDeviceLink {
251            scheme_name: scheme_name.clone(),
252            session_id: authentication_response.session_id,
253            session_secret: authentication_response.session_secret,
254            session_token: authentication_response.session_token,
255            device_link_base: authentication_response.device_link_base,
256            relying_party_uuid: authentication_request.relying_party_uuid,
257            relying_party_name: authentication_request.relying_party_name,
258            initial_callback_url: authentication_request.initial_callback_url,
259            certificate_level: authentication_request.certificate_level.into(),
260            signature_protocol: authentication_request.signature_protocol,
261            signature_protocol_parameters: authentication_request
262                .signature_protocol_parameters
263                .clone(),
264            interactions: authentication_request.interactions,
265            rp_challenge: authentication_request
266                .signature_protocol_parameters
267                .get_rp_challenge()
268                .ok_or(SmartIdClientError::InvalidSignatureProtocal(
269                    "RP challenge missing from authentication request",
270                ))?,
271            session_start_time: Utc::now(),
272        })
273    }
274
275    pub fn from_authentication_notification_response(
276        authentication_notification_response: AuthenticationNotificationSession,
277        authentication_request: AuthenticationNotificationRequest,
278        scheme_name: &SchemeName,
279    ) -> Result<SessionConfig> {
280        Ok(SessionConfig::AuthenticationNotification {
281            scheme_name: scheme_name.clone(),
282            session_id: authentication_notification_response.session_id,
283            relying_party_uuid: authentication_request.relying_party_uuid,
284            relying_party_name: authentication_request.relying_party_name,
285            certificate_level: authentication_request.certificate_level.into(),
286            signature_protocol: authentication_request.signature_protocol,
287            signature_protocol_parameters: authentication_request
288                .signature_protocol_parameters
289                .clone(),
290            interactions: authentication_request.interactions,
291            vc_type: authentication_request.vc_type,
292            rp_challenge: authentication_request
293                .signature_protocol_parameters
294                .get_rp_challenge()
295                .ok_or(SmartIdClientError::InvalidSignatureProtocal(
296                    "RP challenge missing from authentication request",
297                ))?,
298            session_start_time: Utc::now(),
299        })
300    }
301
302    pub fn from_signature_device_link_request_response(
303        signature_request_response: SignatureDeviceLinkSession,
304        signature_request: SignatureDeviceLinkRequest,
305        scheme_name: &SchemeName,
306    ) -> Result<SessionConfig> {
307        Ok(SessionConfig::SignatureDeviceLink {
308            scheme_name: scheme_name.clone(),
309            session_id: signature_request_response.session_id,
310            session_secret: signature_request_response.session_secret,
311            session_token: signature_request_response.session_token,
312            device_link_base: signature_request_response.device_link_base,
313            relying_party_uuid: signature_request.relying_party_uuid,
314            relying_party_name: signature_request.relying_party_name,
315            digest: signature_request
316                .signature_protocol_parameters
317                .get_digest()
318                .ok_or(SmartIdClientError::InvalidSignatureProtocal(
319                    "Digest missing from signature request",
320                ))?,
321            certificate_level: signature_request.certificate_level,
322            signature_protocol: signature_request.signature_protocol,
323            signature_protocol_parameters: signature_request.signature_protocol_parameters.clone(),
324            session_start_time: Utc::now(),
325            initial_callback_url: signature_request.initial_callback_url,
326            interactions: signature_request.interactions,
327        })
328    }
329
330    pub fn from_signature_notification_response(
331        signature_notification_response: SignatureNotificationSession,
332        signature_request: SignatureNotificationRequest,
333        scheme_name: &SchemeName,
334    ) -> Result<SessionConfig> {
335        Ok(SessionConfig::SignatureNotification {
336            scheme_name: scheme_name.clone(),
337            session_id: signature_notification_response.session_id,
338            relying_party_uuid: signature_request.relying_party_uuid,
339            relying_party_name: signature_request.relying_party_name,
340            digest: signature_request
341                .signature_protocol_parameters
342                .get_digest()
343                .ok_or(SmartIdClientError::InvalidSignatureProtocal(
344                    "Digest missing from signature request",
345                ))?,
346            certificate_level: signature_request.certificate_level,
347            signature_protocol: signature_request.signature_protocol,
348            signature_protocol_parameters: signature_request.signature_protocol_parameters.clone(),
349            session_start_time: Utc::now(),
350            interactions: signature_request.interactions,
351            vc: signature_notification_response.vc,
352        })
353    }
354
355    pub fn from_signature_notification_linked_response(
356        signature_notification_response: SignatureNotificationLinkedSession,
357        signature_request: SignatureNotificationLinkedRequest,
358        scheme_name: &SchemeName,
359    ) -> Result<SessionConfig> {
360        Ok(SessionConfig::SignatureNotificationLinked {
361            scheme_name: scheme_name.clone(),
362            session_id: signature_notification_response.session_id,
363            relying_party_uuid: signature_request.relying_party_uuid,
364            relying_party_name: signature_request.relying_party_name,
365            certificate_level: signature_request.certificate_level,
366            signature_protocol: signature_request.signature_protocol,
367            signature_protocol_parameters: signature_request.signature_protocol_parameters.clone(),
368            linked_session_id: signature_request.linked_session_id,
369            session_start_time: Utc::now(),
370            digest: signature_request
371                .signature_protocol_parameters
372                .get_digest()
373                .ok_or(SmartIdClientError::InvalidSignatureProtocal(
374                    "Digest missing from signature request",
375                ))?,
376            interactions: "".to_string(),
377        })
378    }
379
380    pub fn from_certificate_choice_device_link_response(
381        certificate_choice_response: CertificateChoiceDeviceLinkSession,
382        certificate_choice_request: CertificateChoiceDeviceLinkRequest,
383        scheme_name: &SchemeName,
384    ) -> SessionConfig {
385        SessionConfig::CertificateChoiceDeviceLink {
386            scheme_name: scheme_name.clone(),
387            session_id: certificate_choice_response.session_id,
388            session_token: certificate_choice_response.session_token,
389            session_secret: certificate_choice_response.session_secret,
390            device_link_base: certificate_choice_response.device_link_base,
391            relying_party_uuid: certificate_choice_request.relying_party_uuid,
392            relying_party_name: certificate_choice_request.relying_party_name,
393            initial_callback_url: certificate_choice_request.initial_callback_url,
394            certificate_level: certificate_choice_request.certificate_level,
395            session_start_time: Utc::now(),
396        }
397    }
398
399    pub fn from_certificate_choice_notification_response(
400        certificate_choice_response: CertificateChoiceNotificationSession,
401        certificate_choice_request: CertificateChoiceNotificationRequest,
402        _scheme_name: &SchemeName,
403    ) -> SessionConfig {
404        SessionConfig::CertificateChoiceNotification {
405            scheme_name: SchemeName::smart_id,
406            session_id: certificate_choice_response.session_id,
407            relying_party_uuid: certificate_choice_request.relying_party_uuid,
408            relying_party_name: certificate_choice_request.relying_party_name,
409            certificate_level: certificate_choice_request.certificate_level,
410            session_start_time: Utc::now(),
411        }
412    }
413
414    pub fn get_digest(&self, session_status: SessionStatusResponse) -> Option<String> {
415        match self {
416            SessionConfig::SignatureDeviceLink { digest, .. } => Some(digest.clone()),
417            SessionConfig::SignatureNotification { digest, .. } => Some(digest.clone()),
418            SessionConfig::SignatureNotificationLinked { digest, .. } => Some(digest.clone()),
419            SessionConfig::AuthenticationDeviceLink {
420                relying_party_name,
421
422                interactions,
423                rp_challenge,
424                scheme_name,
425                signature_protocol,
426                signature_protocol_parameters,
427                ..
428            } => {
429                // The authentication digest requires the challenge and protocol which are available before the session is started
430                // It also requires the server random which is only available after the session result is returned
431
432                let interaction_type_used = match session_status.interaction_type_used.clone() {
433                    Some(interaction) => interaction,
434                    None => {
435                        error!("Session status does not contain interaction type used, defaulting to DisplayTextAndPIN");
436                        return None;
437                    }
438                };
439
440                if let Some(ResponseSignature::ACSP_V2 {
441                    server_random,
442                    user_challenge,
443                    flow_type,
444                    ..
445                }) = session_status.signature
446                {
447                    let ascp_digest = SignatureAlgorithm::build_acsp_v2_digest(
448                        scheme_name.clone(),
449                        signature_protocol.clone(),
450                        &server_random,
451                        rp_challenge,
452                        &user_challenge,
453                        &BASE64_STANDARD.encode(relying_party_name),
454                        "",
455                        interactions,
456                        interaction_type_used,
457                        "",
458                        flow_type.clone(),
459                        signature_protocol_parameters.get_hashing_algorithm(),
460                    );
461
462                    Some(STANDARD.encode(ascp_digest))
463                } else {
464                    // Authentication device link can only be ACSP_V2, so this should never happen if the session is complete and successful
465                    None
466                }
467            }
468            SessionConfig::AuthenticationNotification {
469                scheme_name,
470                signature_protocol,
471                signature_protocol_parameters,
472                relying_party_name,
473                interactions,
474                rp_challenge,
475                ..
476            } => {
477                // The authentication digest requires the challenge and protocol which are available before the session is started
478                // It also requires the server random which is only available after the session result is returned
479                let interaction_type_used = match session_status.interaction_type_used.clone() {
480                    Some(interaction) => interaction,
481                    None => {
482                        error!("Session status does not contain interaction type used, defaulting to DisplayTextAndPIN");
483                        return None;
484                    }
485                };
486
487                if let Some(ResponseSignature::ACSP_V2 {
488                    server_random,
489                    user_challenge,
490                    flow_type,
491                    ..
492                }) = session_status.signature
493                {
494                    let ascp_digest = SignatureAlgorithm::build_acsp_v2_digest(
495                        scheme_name.clone(),
496                        signature_protocol.clone(),
497                        &server_random,
498                        rp_challenge,
499                        &user_challenge,
500                        &BASE64_STANDARD.encode(relying_party_name),
501                        "",
502                        interactions,
503                        interaction_type_used,
504                        "",
505                        flow_type.clone(),
506                        signature_protocol_parameters.get_hashing_algorithm(),
507                    );
508
509                    Some(STANDARD.encode(ascp_digest))
510                } else {
511                    // Authentication device link can only be ACSP_V2, so this should never happen if the session is complete and successful
512                    None
513                }
514            }
515            SessionConfig::CertificateChoiceDeviceLink { .. } => None, // Certificate choice does not have a digest
516            SessionConfig::CertificateChoiceNotification { .. } => None, // Certificate choice does not have a digest
517        }
518    }
519
520    // Calculate the VC code for notification-based authentication using the RP challenge.
521    // Based on documentation https://sk-eid.github.io/smart-id-documentation/rp-api/notification_based_flows.html
522    pub fn calculate_vc_code(&self) -> Result<VCCode> {
523        match self {
524            SessionConfig::AuthenticationNotification {
525                vc_type,
526                rp_challenge,
527                ..
528            } => {
529                let rp_challenge_bytes = base64::engine::general_purpose::STANDARD
530                    .decode(rp_challenge)
531                    .map_err(|_| {
532                        SmartIdClientError::InvalidSignatureProtocal("Invalid RP challenge")
533                    })?;
534                let sha256_hash = Sha256::digest(&rp_challenge_bytes);
535                let result = (((sha256_hash[30] as u16) << 8) + (sha256_hash[31] as u16)) % 10000;
536                let verification_code: String = format!("{:04}", result);
537
538                Ok(VCCode {
539                    vc_type: vc_type.clone(),
540                    value: verification_code,
541                })
542            }
543            _ => Err(SmartIdClientError::InvalidSignatureProtocal(
544                "VC code can only be calculated for AuthenticationNotification session",
545            )),
546        }
547    }
548}
549
550/// Represents a VC (Verification Code) used in the notification-based authentication session.
551/// This code is displayed to the user in their Smart ID app.
552///
553/// # Fields
554///
555/// * `vc_type` - The type of the VC code. Currently, the only allowed type is `alphaNumeric4`.
556/// * `value` - The value of the VC code.
557#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
558pub struct VCCode {
559    #[serde(rename = "type")]
560    pub vc_type: VCCodeType,
561    pub value: String,
562}
563
564/// Enum representing the type of the VC code.
565#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
566#[allow(non_camel_case_types)]
567#[non_exhaustive]
568pub enum VCCodeType {
569    numeric4,
570}
571
572/// Enum representing the scheme (environment) name.
573/// Refer to to the 'Environment' docs for more details https://sk-eid.github.io/smart-id-documentation/environments.html
574#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, AsRefStr, Display, EnumString)]
575#[strum(serialize_all = "kebab-case")]
576#[serde(rename_all = "kebab-case")]
577#[allow(non_camel_case_types)]
578#[non_exhaustive]
579pub enum SchemeName {
580    smart_id,
581    smart_id_demo,
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587    use crate::models::signature::{HashingAlgorithm, SignatureRequestAlgorithmParameters};
588
589    // Based on the example from the documentation, their example return value is incorrect though.
590    // https://sk-eid.github.io/smart-id-documentation/rp-api/notification_based_flows.html
591    #[test]
592    fn test_verification_code_calculation() {
593        let session_config = SessionConfig::AuthenticationNotification {
594            scheme_name: SchemeName::smart_id,
595            session_id: "test_session_id".to_string(),
596            relying_party_uuid: "test_relying_party_uuid".to_string(),
597            relying_party_name: "Test Relying Party".to_string(),
598            certificate_level: CertificateLevel::QUALIFIED,
599            signature_protocol: SignatureProtocol::ACSP_V2,
600            signature_protocol_parameters: SignatureProtocolParameters::ACSP_V2 {
601                rp_challenge: "GYS+yoah6emAcVDNIajwSs6UB/M95XrDxMzXBUkwQJ9YFDipXXzGpPc7raWcuc2+TEoRc7WvIZ/7dU/iRXenYg==".to_string(),
602                signature_algorithm: SignatureAlgorithm::RsassaPss,
603                signature_algorithm_parameters: SignatureRequestAlgorithmParameters {
604                    hash_algorithm: HashingAlgorithm::sha_256,
605                },
606            },
607            interactions: "Test interactions".to_string(),
608            vc_type: VCCodeType::numeric4,
609            rp_challenge: "dGVzdF9jaGFsbGVuZ2U=".to_string(), // Base64 encoded "test_challenge"
610            session_start_time: Default::default(),
611        };
612
613        let vc_code = session_config.calculate_vc_code().unwrap();
614        assert_eq!(vc_code.value, "9158"); // The example from the docs has an incorrect return value.
615        assert_eq!(vc_code.vc_type, VCCodeType::numeric4);
616    }
617}