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#[non_exhaustive]
38#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct RequestProperties {
41 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 session_id: String,
81 session_secret: String,
82 session_token: String,
83 device_link_base: String,
84
85 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 rp_challenge: String,
97 session_start_time: DateTime<Utc>,
98 },
99 AuthenticationNotification {
100 session_id: String,
102
103 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 rp_challenge: String,
115 session_start_time: DateTime<Utc>,
116 },
117 SignatureDeviceLink {
118 session_id: String,
120 session_secret: String,
121 session_token: String,
122 device_link_base: String,
123
124 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 digest: String,
136 session_start_time: DateTime<Utc>,
137 },
138 SignatureNotification {
139 session_id: String,
141 vc: VCCode,
142
143 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 digest: String,
154 session_start_time: DateTime<Utc>,
155 },
156 SignatureNotificationLinked {
157 session_id: String,
159
160 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 digest: String,
172 session_start_time: DateTime<Utc>,
173 },
174 CertificateChoiceDeviceLink {
175 session_id: String,
177 session_token: String,
178 session_secret: String,
179 device_link_base: String,
180
181 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 session_start_time: DateTime<Utc>,
190 },
191 CertificateChoiceNotification {
192 session_id: String,
194
195 scheme_name: SchemeName,
197 relying_party_uuid: String,
198 relying_party_name: String,
199 certificate_level: CertificateLevel,
200
201 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 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 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 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 None
513 }
514 }
515 SessionConfig::CertificateChoiceDeviceLink { .. } => None, SessionConfig::CertificateChoiceNotification { .. } => None, }
518 }
519
520 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#[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#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
566#[allow(non_camel_case_types)]
567#[non_exhaustive]
568pub enum VCCodeType {
569 numeric4,
570}
571
572#[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 #[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(), session_start_time: Default::default(),
611 };
612
613 let vc_code = session_config.calculate_vc_code().unwrap();
614 assert_eq!(vc_code.value, "9158"); assert_eq!(vc_code.vc_type, VCCodeType::numeric4);
616 }
617}