Skip to main content

rusmes_acme/
lib.rs

1//! ACME v2 protocol client for automatic TLS certificate management
2//!
3//! This crate implements the [ACME v2](https://www.rfc-editor.org/rfc/rfc8555) protocol
4//! (Automatic Certificate Management Environment) to obtain and renew TLS certificates
5//! from [Let's Encrypt](https://letsencrypt.org/) or any other ACME-compatible CA.
6//!
7//! # Key Features
8//!
9//! - **HTTP-01 challenge** — serves the challenge token over HTTP on
10//!   `/.well-known/acme-challenge/<token>` via an in-memory map that can be mounted
11//!   on any HTTP server (see [`Http01Handler`]).
12//! - **DNS-01 challenge** — creates `_acme-challenge.<domain>` TXT records through a
13//!   pluggable [`DnsProvider`] trait; includes a [`MockDnsProvider`] for testing
14//!   (see [`Dns01Handler`]).
15//! - **Automatic renewal** — [`RenewalManager`] runs a background Tokio task that
16//!   checks the certificate expiry at a configurable interval and renews proactively
17//!   (default: 30 days before expiry).
18//! - **CSR generation** — [`CsrGenerator`] uses `rcgen` to produce ECDSA P-256 or RSA
19//!   CSRs without any C/Fortran dependencies.
20//! - **Certificate storage** — `CertificateStorage` manages per-domain `*.crt`,
21//!   `*.key`, and `*.chain` files with correct Unix permissions (0o600 for keys,
22//!   0o644 for certs).
23//! - **Staging / Production** — the [`AcmeConfig`] builder exposes a `.staging()`
24//!   method that switches to the Let's Encrypt staging environment.
25//!
26//! # Usage
27//!
28//! ```rust,no_run
29//! use rusmes_acme::{AcmeClient, AcmeConfig, ChallengeType, Http01Handler, RenewalManager};
30//!
31//! # async fn example() -> rusmes_acme::Result<()> {
32//! // Build configuration
33//! let config = AcmeConfig::new(
34//!     "admin@example.com".to_string(),
35//!     vec!["example.com".to_string(), "www.example.com".to_string()],
36//! )
37//! .challenge_type(ChallengeType::Http01)
38//! .renewal(30, 3600);
39//!
40//! // Create ACME client and attach an HTTP-01 handler
41//! let http_handler = Http01Handler::new();
42//! let client = AcmeClient::new(config.clone())?
43//!     .with_http01_handler(http_handler);
44//!
45//! // Request a certificate (blocks until ACME challenge completes)
46//! let cert = client.request_certificate().await?;
47//! cert.save(&config.cert_path, &config.key_path).await?;
48//!
49//! // Start automatic renewal in the background
50//! let manager = RenewalManager::new(client, config);
51//! manager.start().await?;
52//! # Ok(())
53//! # }
54//! ```
55//!
56//! # Error Handling
57//!
58//! All fallible operations return [`Result<T>`][crate::Result], which aliases
59//! `std::result::Result<T, AcmeError>`. The [`AcmeError`] enum covers ACME protocol
60//! failures, challenge failures, validation errors, I/O errors, HTTP client errors,
61//! and JSON serialisation errors.
62//!
63//! # Relevant Standards
64//!
65//! - ACME v2: [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555)
66//! - HTTP-01 challenge: [RFC 8555 §8.3](https://www.rfc-editor.org/rfc/rfc8555#section-8.3)
67//! - DNS-01 challenge: [RFC 8555 §8.4](https://www.rfc-editor.org/rfc/rfc8555#section-8.4)
68//! - TLS certificate format: [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280) (X.509 v3)
69//! - CSR format: [RFC 2986](https://www.rfc-editor.org/rfc/rfc2986) (PKCS #10)
70
71pub mod cert;
72pub mod challenge;
73pub mod client;
74pub mod config;
75pub mod dns01;
76pub mod http01;
77pub mod renewal;
78pub mod storage;
79
80pub use cert::{Certificate, CsrGenerator, KeyType};
81pub use client::AcmeClient;
82pub use config::{AcmeConfig, ChallengeType};
83pub use dns01::{Dns01Handler, DnsProvider, MockDnsProvider};
84pub use http01::Http01Handler;
85pub use renewal::RenewalManager;
86
87use thiserror::Error;
88
89#[derive(Debug, Error)]
90pub enum AcmeError {
91    #[error("ACME protocol error: {0}")]
92    Protocol(String),
93
94    #[error("Challenge failed: {0}")]
95    ChallengeFailed(String),
96
97    #[error("Certificate validation failed: {0}")]
98    ValidationFailed(String),
99
100    #[error("Storage error: {0}")]
101    Storage(String),
102
103    #[error("HTTP error: {0}")]
104    Http(#[from] reqwest::Error),
105
106    #[error("I/O error: {0}")]
107    Io(#[from] std::io::Error),
108
109    #[error("JSON error: {0}")]
110    Json(#[from] serde_json::Error),
111
112    #[error("Other error: {0}")]
113    Other(String),
114}
115
116pub type Result<T> = std::result::Result<T, AcmeError>;
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_error_display() {
124        let err = AcmeError::Protocol("test".to_string());
125        assert_eq!(err.to_string(), "ACME protocol error: test");
126    }
127
128    #[test]
129    fn test_error_from_io() {
130        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
131        let err: AcmeError = io_err.into();
132        assert!(matches!(err, AcmeError::Io(_)));
133    }
134}