acmex 0.8.0

AcmeX: High-performance, extensible ACME v2 (RFC 8555) client and server in Rust, supporting multiple DNS providers, storage backends, and crypto libraries.
Documentation
/// Common types and structures for the ACME protocol.
/// This module defines the core data structures used throughout the library,
/// including JWS headers, identifiers, and status enumerations.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Represents the header of a JSON Web Signature (JWS).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwsHeader {
    /// The signature algorithm (e.g., "EdDSA", "RS256").
    pub alg: String,
    /// The JSON Web Key (JWK) used for signing, typically included in the first request.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jwk: Option<serde_json::Value>,
    /// The Key ID (KID), used in subsequent requests after the account is registered.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kid: Option<String>,
    /// An anti-replay nonce provided by the ACME server.
    pub nonce: String,
    /// The URL of the resource being accessed.
    pub url: String,
}

/// A representation of a JSON Web Key (JWK).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Jwk {
    /// The key type (e.g., "RSA", "EC", "OKP").
    pub kty: String,
    /// The intended use of the key (typically "sig" for signing).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub use_: Option<String>,
    /// Allowed key operations (e.g., "sign", "verify").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub key_ops: Option<Vec<String>>,
    /// Additional key-specific parameters (e.g., "crv", "x" for Ed25519).
    #[serde(flatten)]
    pub params: HashMap<String, serde_json::Value>,
}

/// Detailed error information returned by the ACME server (RFC 7807).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcmeErrorDetail {
    /// A URI reference that identifies the problem type.
    #[serde(rename = "type")]
    pub error_type: String,
    /// A human-readable explanation specific to this occurrence of the problem.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    /// The HTTP status code generated by the origin server.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<u16>,
    /// A short, human-readable summary of the problem type.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// A URI reference that identifies the specific occurrence of the problem.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instance: Option<String>,
    /// Optional sub-problems providing more granular error details.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subproblems: Option<Vec<AcmeSubproblem>>,
}

/// A sub-problem providing additional context for an ACME error.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcmeSubproblem {
    /// The type of the sub-problem.
    #[serde(rename = "type")]
    pub error_type: String,
    /// Detailed explanation of the sub-problem.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    /// The identifier (e.g., domain) associated with this sub-problem.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub identifier: Option<Identifier>,
}

/// An identifier used in ACME authorizations (e.g., a DNS domain name).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identifier {
    /// The type of identifier (e.g., "dns" or "ip").
    #[serde(rename = "type")]
    pub id_type: String,
    /// The value of the identifier (e.g., "example.com").
    pub value: String,
}

impl Identifier {
    /// Creates a new DNS identifier for the given domain.
    pub fn dns(domain: impl Into<String>) -> Self {
        Self {
            id_type: "dns".to_string(),
            value: domain.into(),
        }
    }

    /// Creates a new IP identifier for the given IP address.
    pub fn ip(ip: impl Into<String>) -> Self {
        Self {
            id_type: "ip".to_string(),
            value: ip.into(),
        }
    }
}

/// Reasons for revoking a certificate (RFC 5280).
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[repr(u8)]
pub enum RevocationReason {
    /// No specific reason given.
    Unspecified = 0,
    /// The private key has been compromised.
    KeyCompromise = 1,
    /// The Certificate Authority has been compromised.
    CaCompromise = 2,
    /// The subject's affiliation has changed.
    AffiliationChanged = 3,
    /// The certificate has been superseded by a new one.
    Superseded = 4,
    /// The subject has ceased operations.
    CessationOfOperation = 5,
    /// The certificate is temporarily on hold.
    CertificateHold = 6,
    /// The certificate should be removed from the CRL.
    RemoveFromCRL = 8,
    /// The subject's privileges have been withdrawn.
    PrivilegeWithdrawn = 9,
    /// The Attribute Authority has been compromised.
    AACompromise = 10,
}

impl RevocationReason {
    /// Returns the numeric value of the revocation reason.
    pub fn as_u8(self) -> u8 {
        self as u8
    }
}

/// Contact information for an ACME account.
#[derive(Debug, Clone)]
pub struct Contact {
    /// An optional email address.
    pub email: Option<String>,
    /// An optional phone number.
    pub phone: Option<String>,
    /// An optional generic URL.
    pub url: Option<String>,
}

impl Contact {
    /// Creates a new contact with an email address.
    pub fn email(email: impl Into<String>) -> Self {
        Self {
            email: Some(email.into()),
            phone: None,
            url: None,
        }
    }

    /// Creates a new contact with a phone number.
    pub fn phone(phone: impl Into<String>) -> Self {
        Self {
            email: None,
            phone: Some(phone.into()),
            url: None,
        }
    }

    /// Creates a new contact with a generic URL.
    pub fn url(url: impl Into<String>) -> Self {
        Self {
            email: None,
            phone: None,
            url: Some(url.into()),
        }
    }

    /// Converts the contact information into an ACME-compatible URI string.
    pub fn to_uri(&self) -> String {
        if let Some(email) = &self.email {
            format!("mailto:{}", email)
        } else if let Some(phone) = &self.phone {
            format!("tel:{}", phone)
        } else if let Some(url) = &self.url {
            url.clone()
        } else {
            tracing::warn!("Attempted to convert an empty Contact to URI");
            String::new()
        }
    }
}

/// Supported ACME challenge types.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChallengeType {
    /// Validation via a file served over HTTP.
    Http01,
    /// Validation via a TXT record in DNS.
    Dns01,
    /// Validation via a specific TLS extension.
    TlsAlpn01,
}

impl ChallengeType {
    /// Returns the string representation of the challenge type.
    pub fn as_str(&self) -> &'static str {
        match self {
            ChallengeType::Http01 => "http-01",
            ChallengeType::Dns01 => "dns-01",
            ChallengeType::TlsAlpn01 => "tls-alpn-01",
        }
    }
}

impl std::str::FromStr for ChallengeType {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "http-01" => Ok(ChallengeType::Http01),
            "dns-01" => Ok(ChallengeType::Dns01),
            "tls-alpn-01" => Ok(ChallengeType::TlsAlpn01),
            _ => Err(format!("Unknown challenge type: {}", s)),
        }
    }
}

impl std::fmt::Display for ChallengeType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// The current status of a certificate order.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderStatus {
    /// The order is waiting for authorizations to be completed.
    Pending,
    /// All authorizations are valid; the order is ready for finalization.
    Ready,
    /// The server is processing the CSR and issuing the certificate.
    Processing,
    /// The certificate has been issued and is ready for download.
    Valid,
    /// The order has failed or been invalidated.
    Invalid,
    /// The order has expired.
    Expired,
    /// The order was deactivated by the user.
    Deactivated,
}

impl std::str::FromStr for OrderStatus {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "pending" => Ok(OrderStatus::Pending),
            "ready" => Ok(OrderStatus::Ready),
            "processing" => Ok(OrderStatus::Processing),
            "valid" => Ok(OrderStatus::Valid),
            "invalid" => Ok(OrderStatus::Invalid),
            "expired" => Ok(OrderStatus::Expired),
            "deactivated" => Ok(OrderStatus::Deactivated),
            _ => Err(format!("Unknown order status: {}", s)),
        }
    }
}

impl OrderStatus {
    /// Returns the string representation of the order status.
    pub fn as_str(&self) -> &'static str {
        match self {
            OrderStatus::Pending => "pending",
            OrderStatus::Ready => "ready",
            OrderStatus::Processing => "processing",
            OrderStatus::Valid => "valid",
            OrderStatus::Invalid => "invalid",
            OrderStatus::Expired => "expired",
            OrderStatus::Deactivated => "deactivated",
        }
    }
}

impl std::fmt::Display for OrderStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// The current status of an authorization.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorizationStatus {
    /// The authorization is waiting for challenges to be completed.
    Pending,
    /// The authorization has been successfully validated.
    Valid,
    /// The authorization has failed or been invalidated.
    Invalid,
    /// The authorization was deactivated by the user.
    Deactivated,
    /// The authorization has expired.
    Expired,
}

impl std::str::FromStr for AuthorizationStatus {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "pending" => Ok(AuthorizationStatus::Pending),
            "valid" => Ok(AuthorizationStatus::Valid),
            "invalid" => Ok(AuthorizationStatus::Invalid),
            "deactivated" => Ok(AuthorizationStatus::Deactivated),
            "expired" => Ok(AuthorizationStatus::Expired),
            _ => Err(format!("Unknown authorization status: {}", s)),
        }
    }
}

impl AuthorizationStatus {
    /// Returns the string representation of the authorization status.
    pub fn as_str(&self) -> &'static str {
        match self {
            AuthorizationStatus::Pending => "pending",
            AuthorizationStatus::Valid => "valid",
            AuthorizationStatus::Invalid => "invalid",
            AuthorizationStatus::Deactivated => "deactivated",
            AuthorizationStatus::Expired => "expired",
        }
    }
}

impl std::fmt::Display for AuthorizationStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

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

    #[test]
    fn test_identifier_dns() {
        let id = Identifier::dns("example.com");
        assert_eq!(id.id_type, "dns");
        assert_eq!(id.value, "example.com");
    }

    #[test]
    fn test_contact_email() {
        let contact = Contact::email("test@example.com");
        assert_eq!(contact.to_uri(), "mailto:test@example.com");
    }

    #[test]
    fn test_challenge_type() {
        assert_eq!(ChallengeType::Http01.as_str(), "http-01");
        assert_eq!("dns-01".parse::<ChallengeType>(), Ok(ChallengeType::Dns01));
    }

    #[test]
    fn test_order_status() {
        assert_eq!("pending".parse::<OrderStatus>(), Ok(OrderStatus::Pending));
        assert_eq!(OrderStatus::Valid.as_str(), "valid");
    }
}