cutil 0.1.0

A complete internal PKI toolkit for Rust
Documentation
use chrono::{DateTime, Utc};
use std::net::IpAddr;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CertSigAlgo {
    Ed25519,
    EcdsaP256,
    EcdsaP384,
    Rsa2048,
    Rsa3072,
    Rsa4096,
}

impl CertSigAlgo {
    pub fn to_rcgen(&self) -> &'static rcgen::SignatureAlgorithm {
        match self {
            CertSigAlgo::Ed25519 => &rcgen::PKCS_ED25519,
            CertSigAlgo::EcdsaP256 => &rcgen::PKCS_ECDSA_P256_SHA256,
            CertSigAlgo::EcdsaP384 => &rcgen::PKCS_ECDSA_P384_SHA384,
            CertSigAlgo::Rsa2048 | CertSigAlgo::Rsa3072 | CertSigAlgo::Rsa4096 => {
                &rcgen::PKCS_RSA_SHA256
            }
        }
    }

    pub fn key_pair(&self) -> Result<rcgen::KeyPair, crate::Error> {
        let alg = match self {
            CertSigAlgo::Ed25519 => &rcgen::PKCS_ED25519,
            CertSigAlgo::EcdsaP256 => &rcgen::PKCS_ECDSA_P256_SHA256,
            CertSigAlgo::EcdsaP384 => &rcgen::PKCS_ECDSA_P384_SHA384,
            CertSigAlgo::Rsa2048 | CertSigAlgo::Rsa3072 | CertSigAlgo::Rsa4096 => {
                &rcgen::PKCS_RSA_SHA256
            }
        };
        rcgen::KeyPair::generate(alg).map_err(|e| crate::Error::CertGen(e.to_string()))
    }

    pub fn name(&self) -> &'static str {
        match self {
            CertSigAlgo::Ed25519 => "Ed25519",
            CertSigAlgo::EcdsaP256 => "ECDSA P-256",
            CertSigAlgo::EcdsaP384 => "ECDSA P-384",
            CertSigAlgo::Rsa2048 => "RSA 2048",
            CertSigAlgo::Rsa3072 => "RSA 3072",
            CertSigAlgo::Rsa4096 => "RSA 4096",
        }
    }
}

impl std::str::FromStr for CertSigAlgo {
    type Err = crate::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "ed25519" => Ok(CertSigAlgo::Ed25519),
            "ecdsa-p256" | "ecdsap256" | "p256" => Ok(CertSigAlgo::EcdsaP256),
            "ecdsa-p384" | "ecdsap384" | "p384" => Ok(CertSigAlgo::EcdsaP384),
            "rsa2048" | "rsa-2048" => Ok(CertSigAlgo::Rsa2048),
            "rsa3072" | "rsa-3072" => Ok(CertSigAlgo::Rsa3072),
            "rsa4096" | "rsa-4096" | "rsa" => Ok(CertSigAlgo::Rsa4096),
            _ => Err(crate::Error::UnsupportedAlgorithm(s.to_string())),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CertType {
    RootCA,
    IntermediateCA,
    Server,
    Client,
    Both,
}

#[derive(Debug, Clone)]
pub struct DistinguishedName {
    pub common_name: String,
    pub organization: Option<String>,
    pub organizational_unit: Option<String>,
    pub country: Option<String>,
    pub state: Option<String>,
    pub locality: Option<String>,
}

impl DistinguishedName {
    pub fn new(cn: impl Into<String>) -> Self {
        Self {
            common_name: cn.into(),
            organization: None,
            organizational_unit: None,
            country: None,
            state: None,
            locality: None,
        }
    }

    pub fn with_organization(mut self, org: impl Into<String>) -> Self {
        self.organization = Some(org.into());
        self
    }

    pub fn with_organizational_unit(mut self, ou: impl Into<String>) -> Self {
        self.organizational_unit = Some(ou.into());
        self
    }

    pub fn with_country(mut self, country: impl Into<String>) -> Self {
        self.country = Some(country.into());
        self
    }

    pub fn with_state(mut self, state: impl Into<String>) -> Self {
        self.state = Some(state.into());
        self
    }

    pub fn with_locality(mut self, locality: impl Into<String>) -> Self {
        self.locality = Some(locality.into());
        self
    }

    pub fn to_rcgen(&self) -> rcgen::DistinguishedName {
        let mut dn = rcgen::DistinguishedName::new();
        dn.push(rcgen::DnType::CommonName, &self.common_name);

        if let Some(ref org) = self.organization {
            dn.push(rcgen::DnType::OrganizationName, org);
        }
        if let Some(ref ou) = self.organizational_unit {
            dn.push(rcgen::DnType::OrganizationalUnitName, ou);
        }
        if let Some(ref country) = self.country {
            dn.push(rcgen::DnType::CountryName, country);
        }
        if let Some(ref state) = self.state {
            dn.push(rcgen::DnType::StateOrProvinceName, state);
        }
        if let Some(ref locality) = self.locality {
            dn.push(rcgen::DnType::LocalityName, locality);
        }

        dn
    }
}

#[derive(Debug, Clone)]
pub struct SubjectAltName {
    pub dns_names: Vec<String>,
    pub ip_addresses: Vec<IpAddr>,
    pub email_addresses: Vec<String>,
}

impl SubjectAltName {
    pub fn new() -> Self {
        Self {
            dns_names: Vec::new(),
            ip_addresses: Vec::new(),
            email_addresses: Vec::new(),
        }
    }

    pub fn with_dns(mut self, dns: impl Into<String>) -> Self {
        self.dns_names.push(dns.into());
        self
    }

    pub fn with_ip(mut self, ip: IpAddr) -> Self {
        self.ip_addresses.push(ip);
        self
    }

    pub fn with_email(mut self, email: impl Into<String>) -> Self {
        self.email_addresses.push(email.into());
        self
    }

    pub fn is_empty(&self) -> bool {
        self.dns_names.is_empty() && self.ip_addresses.is_empty() && self.email_addresses.is_empty()
    }
}

impl Default for SubjectAltName {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Clone)]
pub struct CertificateRequest {
    pub subject: DistinguishedName,
    pub san: SubjectAltName,
    pub cert_type: CertType,
    pub validity_days: u32,
    pub key_usage: Vec<KeyUsage>,
    pub extended_key_usage: Vec<ExtendedKeyUsage>,
    pub crl_distribution_points: Vec<String>,
    pub ocsp_servers: Vec<String>,
}

impl CertificateRequest {
    pub fn new(cn: impl Into<String>, cert_type: CertType) -> Self {
        let (key_usage, extended_key_usage) = match cert_type {
            CertType::RootCA | CertType::IntermediateCA => (
                vec![
                    KeyUsage::DigitalSignature,
                    KeyUsage::KeyCertSign,
                    KeyUsage::CrlSign,
                ],
                vec![],
            ),
            CertType::Server => (
                vec![KeyUsage::DigitalSignature, KeyUsage::KeyEncipherment],
                vec![ExtendedKeyUsage::ServerAuth],
            ),
            CertType::Client => (
                vec![KeyUsage::DigitalSignature, KeyUsage::KeyEncipherment],
                vec![ExtendedKeyUsage::ClientAuth],
            ),
            CertType::Both => (
                vec![KeyUsage::DigitalSignature, KeyUsage::KeyEncipherment],
                vec![ExtendedKeyUsage::ServerAuth, ExtendedKeyUsage::ClientAuth],
            ),
        };

        Self {
            subject: DistinguishedName::new(cn),
            san: SubjectAltName::new(),
            cert_type,
            validity_days: 365,
            key_usage,
            extended_key_usage,
            crl_distribution_points: Vec::new(),
            ocsp_servers: Vec::new(),
        }
    }

    pub fn with_subject(mut self, subject: DistinguishedName) -> Self {
        self.subject = subject;
        self
    }

    pub fn with_san(mut self, san: SubjectAltName) -> Self {
        self.san = san;
        self
    }

    pub fn with_validity_days(mut self, days: u32) -> Self {
        self.validity_days = days;
        self
    }

    pub fn with_crl_dp(mut self, url: impl Into<String>) -> Self {
        self.crl_distribution_points.push(url.into());
        self
    }

    pub fn with_ocsp(mut self, url: impl Into<String>) -> Self {
        self.ocsp_servers.push(url.into());
        self
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyUsage {
    DigitalSignature,
    KeyEncipherment,
    KeyAgreement,
    KeyCertSign,
    CrlSign,
    DataEncipherment,
    NonRepudiation,
}

impl KeyUsage {
    pub fn to_rcgen(&self) -> rcgen::KeyUsagePurpose {
        match self {
            KeyUsage::DigitalSignature => rcgen::KeyUsagePurpose::DigitalSignature,
            KeyUsage::KeyEncipherment => rcgen::KeyUsagePurpose::KeyEncipherment,
            KeyUsage::KeyAgreement => rcgen::KeyUsagePurpose::KeyAgreement,
            KeyUsage::KeyCertSign => rcgen::KeyUsagePurpose::KeyCertSign,
            KeyUsage::CrlSign => rcgen::KeyUsagePurpose::CrlSign,
            KeyUsage::DataEncipherment => rcgen::KeyUsagePurpose::DataEncipherment,
            KeyUsage::NonRepudiation => rcgen::KeyUsagePurpose::ContentCommitment,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtendedKeyUsage {
    ServerAuth,
    ClientAuth,
    CodeSigning,
    EmailProtection,
    TimeStamping,
    OcspSigning,
}

impl ExtendedKeyUsage {
    pub fn to_rcgen(&self) -> rcgen::ExtendedKeyUsagePurpose {
        match self {
            ExtendedKeyUsage::ServerAuth => rcgen::ExtendedKeyUsagePurpose::ServerAuth,
            ExtendedKeyUsage::ClientAuth => rcgen::ExtendedKeyUsagePurpose::ClientAuth,
            ExtendedKeyUsage::CodeSigning => rcgen::ExtendedKeyUsagePurpose::CodeSigning,
            ExtendedKeyUsage::EmailProtection => rcgen::ExtendedKeyUsagePurpose::EmailProtection,
            ExtendedKeyUsage::TimeStamping => rcgen::ExtendedKeyUsagePurpose::TimeStamping,
            ExtendedKeyUsage::OcspSigning => rcgen::ExtendedKeyUsagePurpose::OcspSigning,
        }
    }
}

#[derive(Debug, Clone)]
pub struct RevokedCertificate {
    pub serial_number: Vec<u8>,
    pub revocation_time: DateTime<Utc>,
    pub reason: RevocationReason,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevocationReason {
    Unspecified,
    KeyCompromise,
    CACompromise,
    AffiliationChanged,
    Superseded,
    CessationOfOperation,
    CertificateHold,
    RemoveFromCRL,
    PrivilegeWithdrawn,
    AACompromise,
}

impl RevocationReason {
    pub fn to_code(&self) -> i32 {
        match self {
            RevocationReason::Unspecified => 0,
            RevocationReason::KeyCompromise => 1,
            RevocationReason::CACompromise => 2,
            RevocationReason::AffiliationChanged => 3,
            RevocationReason::Superseded => 4,
            RevocationReason::CessationOfOperation => 5,
            RevocationReason::CertificateHold => 6,
            RevocationReason::RemoveFromCRL => 8,
            RevocationReason::PrivilegeWithdrawn => 9,
            RevocationReason::AACompromise => 10,
        }
    }
}

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertificateInfo {
    pub subject: String,
    pub issuer: String,
    pub serial_number: String,
    pub not_before: String,
    pub not_after: String,
    pub signature_algorithm: String,
    pub public_key_algorithm: String,
    pub subject_alt_names: Vec<String>,
    pub is_ca: bool,
    pub key_usage: Vec<String>,
    pub extended_key_usage: Vec<String>,
}