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
use serde::{Deserialize, Serialize};
use thiserror::Error;

/// Result type for ACME operations, using AcmeError as the error type.
pub type Result<T> = std::result::Result<T, AcmeError>;

/// Comprehensive error types for ACME operations.
/// This enum covers protocol, account, order, challenge, and transport errors.
#[derive(Error, Debug)]
pub enum AcmeError {
    /// Protocol-level error returned from the ACME server.
    #[error("Protocol error: {0}")]
    Protocol(String),

    /// Errors related to account registration, update, or key rollover.
    #[error("Account error: {0}")]
    Account(String),

    /// Errors occurring during order creation or processing.
    #[error("Order error: {status}, detail: {detail}")]
    Order {
        /// The status of the order when the error occurred.
        status: String,
        /// Detailed error message from the server.
        detail: String,
    },

    /// Errors occurring during challenge verification.
    #[error("Challenge failed: {challenge_type}, error: {error}")]
    Challenge {
        /// The type of challenge (e.g., "http-01", "dns-01").
        challenge_type: String,
        /// The error message describing why the challenge failed.
        error: String,
    },

    /// Errors related to certificate generation, parsing, or verification.
    #[error("Certificate error: {0}")]
    Certificate(String),

    /// Errors from cryptographic operations (e.g., key generation, signing).
    #[error("Crypto error: {0}")]
    Crypto(String),

    /// Errors related to storage/persistence backends.
    #[error("Storage error: {0}")]
    Storage(String),

    /// Errors occurring during HTTP transport.
    #[error("Transport error: {0}")]
    Transport(String),

    /// Standard I/O errors.
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// JSON serialization or deserialization errors.
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    /// Errors caused by invalid user input or parameters.
    #[error("Invalid input: {0}")]
    InvalidInput(String),

    /// Errors indicating that an operation has timed out.
    #[error("Timeout: {0}")]
    Timeout(String),

    /// Errors indicating that a requested resource was not found.
    #[error("Not found: {0}")]
    NotFound(String),

    /// Errors related to system or client configuration.
    #[error("Configuration error: {0}")]
    Configuration(String),

    /// Errors occurring during PEM encoding or decoding.
    #[error("PEM error: {0}")]
    Pem(String),

    /// Errors indicating that the client has been rate limited by the server.
    #[error("Rate limited, retry after: {0:?}")]
    RateLimited(Option<std::time::Duration>),
}

/// RFC 7807 Problem Details for HTTP APIs.
/// Used to provide machine-readable error responses in a standardized format.
#[derive(Debug, Serialize, Deserialize)]
pub struct ProblemDetails {
    /// A URI reference that identifies the problem type.
    #[serde(rename = "type")]
    pub problem_type: String,
    /// A short, human-readable summary of the problem type.
    pub title: String,
    /// The HTTP status code generated by the origin server for this occurrence of the problem.
    pub status: u16,
    /// A human-readable explanation specific to this occurrence of the problem.
    pub detail: String,
    /// A URI reference that identifies the specific occurrence of the problem.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instance: Option<String>,
}

impl AcmeError {
    /// Creates a new Protocol error.
    pub fn protocol<S: Into<String>>(msg: S) -> Self {
        Self::Protocol(msg.into())
    }

    /// Creates a new Account error.
    pub fn account<S: Into<String>>(msg: S) -> Self {
        Self::Account(msg.into())
    }

    /// Creates a new Order error with status and detail.
    pub fn order<S: Into<String>>(status: S, detail: S) -> Self {
        Self::Order {
            status: status.into(),
            detail: detail.into(),
        }
    }

    /// Creates a new Challenge error with type and error message.
    pub fn challenge<S: Into<String>>(challenge_type: S, error: S) -> Self {
        Self::Challenge {
            challenge_type: challenge_type.into(),
            error: error.into(),
        }
    }

    /// Creates a new Certificate error.
    pub fn certificate<S: Into<String>>(msg: S) -> Self {
        Self::Certificate(msg.into())
    }

    /// Creates a new Crypto error.
    pub fn crypto<S: Into<String>>(msg: S) -> Self {
        Self::Crypto(msg.into())
    }

    /// Creates a new Storage error.
    pub fn storage<S: Into<String>>(msg: S) -> Self {
        Self::Storage(msg.into())
    }

    /// Creates a new Transport error.
    pub fn transport<S: Into<String>>(msg: S) -> Self {
        Self::Transport(msg.into())
    }

    /// Creates a new InvalidInput error.
    pub fn invalid_input<S: Into<String>>(msg: S) -> Self {
        Self::InvalidInput(msg.into())
    }

    /// Creates a new Timeout error.
    pub fn timeout<S: Into<String>>(msg: S) -> Self {
        Self::Timeout(msg.into())
    }

    /// Creates a new NotFound error.
    pub fn not_found<S: Into<String>>(msg: S) -> Self {
        Self::NotFound(msg.into())
    }

    /// Creates a new Configuration error.
    pub fn configuration<S: Into<String>>(msg: S) -> Self {
        Self::Configuration(msg.into())
    }

    /// Creates a new PEM error.
    pub fn pem<S: Into<String>>(msg: S) -> Self {
        Self::Pem(msg.into())
    }

    /// Converts the AcmeError into an RFC 7807 ProblemDetails structure.
    /// This is useful for returning standardized error responses in an API.
    pub fn to_problem_details(&self) -> ProblemDetails {
        match self {
            Self::Protocol(d) => ProblemDetails {
                problem_type: "https://acmex.sh/errors/protocol".into(),
                title: "ACME Protocol Error".into(),
                status: 400,
                detail: d.clone(),
                instance: None,
            },
            Self::Account(d) => ProblemDetails {
                problem_type: "https://acmex.sh/errors/account".into(),
                title: "Account Operation Failed".into(),
                status: 403,
                detail: d.clone(),
                instance: None,
            },
            Self::Order { status, detail } => ProblemDetails {
                problem_type: "https://acmex.sh/errors/order".into(),
                title: format!("Order Failed (Status: {})", status),
                status: 400,
                detail: detail.clone(),
                instance: None,
            },
            Self::Storage(d) => ProblemDetails {
                problem_type: "https://acmex.sh/errors/storage".into(),
                title: "Storage Error".into(),
                status: 500,
                detail: d.clone(),
                instance: None,
            },
            Self::Transport(d) => ProblemDetails {
                problem_type: "https://acmex.sh/errors/transport".into(),
                title: "Network Transport Error".into(),
                status: 502,
                detail: d.clone(),
                instance: None,
            },
            _ => ProblemDetails {
                problem_type: "https://acmex.sh/errors/internal".into(),
                title: "Internal Server Error".into(),
                status: 500,
                detail: self.to_string(),
                instance: None,
            },
        }
    }
}

impl From<std::time::SystemTimeError> for AcmeError {
    fn from(err: std::time::SystemTimeError) -> Self {
        Self::Protocol(format!("SystemTime error: {}", err))
    }
}

#[cfg(feature = "dns-route53")]
impl From<aws_sdk_route53::error::BuildError> for AcmeError {
    fn from(err: aws_sdk_route53::error::BuildError) -> Self {
        Self::Configuration(format!("Route53 build error: {}", err))
    }
}