smart-id-rust-client 0.3.3

Smart ID Rust Client
Documentation
use crate::error::Result;
use crate::error::SmartIdClientError;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use oid_registry::asn1_rs::FromDer;
use oid_registry::OidRegistry;
use serde::{Deserialize, Serialize};
use x509_parser::certificate::X509Certificate;

/// User Identity
///
/// This struct represents the identity of a user, including their given name, surname, and identity code.
/// This is used to validate the identity of a user against the identity provided in the response certificates.
///
/// # Properties
///
/// * `given_name` - The given name of the user.
/// * `surname` - The surname of the user.
/// * `identity_code` - The identity code of the user.
///
/// # Example
///
/// ```rust
/// use smart_id_rust_client::models::user_identity::UserIdentity;
///
/// // Create a user identity from stored information
/// let user_identity = UserIdentity {
///     given_name: "Joey".to_string(),
///     surname: "de l'Arago".to_string(),
///     identity_code: "PNOBE-{ETSI_NUMBER}".to_string(),
/// };
///
/// // Create a user identity from a certificate
/// let certificate = "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";
/// let user_identity = UserIdentity::from_certificate(certificate.to_string()).unwrap();
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserIdentity {
    pub given_name: String,
    pub surname: String,
    pub identity_code: String,
}

impl UserIdentity {
    /// Creates a `UserIdentity` from a base64 encoded certificate.
    ///
    /// # Arguments
    ///
    /// * `certificate` - A base64 encoded string representing the user's certificate.
    ///
    /// # Errors
    ///
    /// Returns an error if the certificate cannot be decoded from base64 or if the certificate cannot be parsed.
    ///
    /// # Example
    ///
    /// ```rust
    /// use smart_id_rust_client::models::user_identity::UserIdentity;
    /// use smart_id_rust_client::error::Result;
    ///
    /// let certificate = "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";
    /// let user_identity = UserIdentity::from_certificate(certificate.to_string()).unwrap();
    /// ```
    pub fn from_certificate(certificate: String) -> Result<Self> {
        let decoded_cert = BASE64_STANDARD.decode(&certificate).map_err(|e| {
            SmartIdClientError::FailedToValidateSessionResponseCertificate(format!(
                "Could not decode base64 certificate: {:?}",
                e
            ))
        })?;

        let (_, parsed_cert) = X509Certificate::from_der(decoded_cert.as_slice()).map_err(|e| {
            SmartIdClientError::FailedToValidateSessionResponseCertificate(format!(
                "Failed to parse certificate: {:?}",
                e
            ))
        })?;

        let given_name = UserIdentity::get_attribute_value(&parsed_cert, "givenName")?;
        let surname = UserIdentity::get_attribute_value(&parsed_cert, "surname")?;
        let identity_code = UserIdentity::get_attribute_value(&parsed_cert, "serialNumber")?;

        Ok(UserIdentity {
            given_name,
            surname,
            identity_code,
        })
    }

    pub(crate) fn identity_matches_certificate(&self, certificate: String) -> Result<()> {
        let certificate_identity = UserIdentity::from_certificate(certificate)?;

        if self.given_name.to_uppercase() != certificate_identity.given_name.to_uppercase() {
            return Err(
                SmartIdClientError::FailedToValidateSessionResponseCertificate(format!(
                    "Given name provided in identity does not match certificate: {:?} != {:?}",
                    self.given_name.to_uppercase(),
                    certificate_identity.given_name.to_uppercase()
                )),
            );
        }

        if self.surname.to_uppercase() != certificate_identity.surname.to_uppercase() {
            return Err(
                SmartIdClientError::FailedToValidateSessionResponseCertificate(format!(
                    "Surname provided in identity does not match certificate: {:?} != {:?}",
                    self.surname.to_uppercase(),
                    certificate_identity.surname.to_uppercase()
                )),
            );
        }

        if self.identity_code.to_uppercase() != certificate_identity.identity_code.to_uppercase() {
            return Err(
                SmartIdClientError::FailedToValidateSessionResponseCertificate(format!(
                    "Identity code provided in identity does not match certificate: {:?} != {:?}",
                    self.identity_code.to_uppercase(),
                    certificate_identity.identity_code.to_uppercase()
                )),
            );
        }

        Ok(())
    }

    fn get_attribute_value(certificate: &X509Certificate, oid_simple_name: &str) -> Result<String> {
        let registry = OidRegistry::default().with_x509();
        let oid = registry
            .iter_by_sn(oid_simple_name)
            .next()
            .map(|(oid, _)| oid)
            .ok_or(
                SmartIdClientError::FailedToValidateSessionResponseCertificate(format!(
                    "OID not found in registry: {:?}",
                    oid_simple_name
                )),
            )?;

        let attribute = certificate
            .subject()
            .iter_attributes()
            .find(|a| a.attr_type() == oid)
            .ok_or(
                SmartIdClientError::FailedToValidateSessionResponseCertificate(format!(
                    "OID not found in certificate: {:?}",
                    oid
                )),
            )?;

        attribute
            .attr_value()
            .as_string()
            .or_else(|_e| {
                attribute
                    .attr_value()
                    .as_printablestring()
                    .map(|g| g.string())
            })
            .map_err(|e| {
                SmartIdClientError::FailedToValidateSessionResponseCertificate(
                    format!("Certificate does not match provided user identity, attribute missing from cert: {:?}", e)
                )
            })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const CERT: &str = "";

    #[test]
    #[ignore]
    fn test_check_identity_against_certificate() {
        let user_identity = UserIdentity {
            given_name: "Joey".to_string(),
            surname: "de l'Arago".to_string(),
            identity_code: "PNOBE-{ETSI_NUMBER}".to_string(),
        };

        let result = user_identity.identity_matches_certificate(CERT.to_string());

        println!("Result: {:?}", result);
        assert!(result.is_ok());
    }

    #[test]
    #[ignore]
    fn test_check_identity_case_insensitive() {
        let user_identity = UserIdentity {
            given_name: "joey".to_string(),
            surname: "DE L'ARAGO".to_string(),
            identity_code: "pnobe-{ETSI_NUMBER}".to_string(),
        };

        let result = user_identity.identity_matches_certificate(CERT.to_string());

        assert!(result.is_ok());
    }

    #[test]
    #[ignore]
    fn test_check_identity_incorrect_given_name() {
        let user_identity = UserIdentity {
            given_name: "Incorrect".to_string(),
            surname: "de l'Arago".to_string(),
            identity_code: "PNOBE-{ETSI_NUMBER}".to_string(),
        };

        let result = user_identity.identity_matches_certificate(CERT.to_string());

        assert!(result.is_err());
    }

    #[test]
    #[ignore]
    fn test_check_identity_incorrect_surname() {
        let user_identity = UserIdentity {
            given_name: "Joey".to_string(),
            surname: "Incorrect".to_string(),
            identity_code: "PNOBE-{ETSI_NUMBER}".to_string(),
        };

        let result = user_identity.identity_matches_certificate(CERT.to_string());

        assert!(result.is_err());
    }

    #[test]
    #[ignore]
    fn test_check_identity_incorrect_identity_code() {
        let user_identity = UserIdentity {
            given_name: "Joey".to_string(),
            surname: "de l'Arago".to_string(),
            identity_code: "Incorrect".to_string(),
        };

        let result = user_identity.identity_matches_certificate(CERT.to_string());

        assert!(result.is_err());
    }

}