use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "lowercase")]
pub enum CertificateAuthority {
#[serde(rename = "letsencrypt")]
#[default]
LetsEncrypt,
#[cfg(feature = "google-ca")]
#[serde(rename = "google")]
Google,
#[cfg(feature = "zerossl-ca")]
#[serde(rename = "zerossl")]
ZeroSSL,
#[serde(rename = "custom")]
Custom,
}
impl fmt::Display for CertificateAuthority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CertificateAuthority::LetsEncrypt => write!(f, "Let's Encrypt"),
#[cfg(feature = "google-ca")]
CertificateAuthority::Google => write!(f, "Google Trust Services"),
#[cfg(feature = "zerossl-ca")]
CertificateAuthority::ZeroSSL => write!(f, "ZeroSSL"),
CertificateAuthority::Custom => write!(f, "Custom CA"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CAConfig {
pub ca: CertificateAuthority,
#[serde(default)]
pub environment: Environment,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contact_email: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum Environment {
#[serde(rename = "production")]
#[default]
Production,
#[serde(rename = "staging")]
Staging,
}
impl CAConfig {
pub fn new(ca: CertificateAuthority, environment: Environment) -> Self {
tracing::debug!("Creating new CAConfig for {} ({:?})", ca, environment);
Self {
ca,
environment,
custom_url: None,
contact_email: None,
}
}
pub fn with_custom_url(mut self, url: String) -> Self {
self.custom_url = Some(url);
self
}
pub fn with_contact_email(mut self, email: String) -> Self {
self.contact_email = Some(email);
self
}
pub fn directory_url(&self) -> Result<String, String> {
let url = match self.ca {
CertificateAuthority::LetsEncrypt => match self.environment {
Environment::Production => {
"https://acme-v02.api.letsencrypt.org/directory".to_string()
}
Environment::Staging => {
"https://acme-staging-v02.api.letsencrypt.org/directory".to_string()
}
},
#[cfg(feature = "google-ca")]
CertificateAuthority::Google => "https://dv.google.com/acme/directory".to_string(),
#[cfg(feature = "zerossl-ca")]
CertificateAuthority::ZeroSSL => "https://acme.zerossl.com/v2/DV90".to_string(),
CertificateAuthority::Custom => self
.custom_url
.clone()
.ok_or_else(|| "Custom CA requires custom_url to be set".to_string())?,
};
tracing::debug!("Resolved directory URL: {}", url);
Ok(url)
}
pub fn validate(&self) -> Result<(), String> {
if self.ca == CertificateAuthority::Custom && self.custom_url.is_none() {
tracing::error!("Validation failed: Custom CA selected but no URL provided");
return Err("Custom CA requires custom_url to be set".to_string());
}
Ok(())
}
}
impl fmt::Display for CAConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({:?})", self.ca, self.environment)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_ca() {
let config = CAConfig::default();
assert_eq!(config.ca, CertificateAuthority::LetsEncrypt);
assert_eq!(config.environment, Environment::Production);
}
#[test]
fn test_letsencrypt_urls() {
let prod = CAConfig::new(CertificateAuthority::LetsEncrypt, Environment::Production);
assert_eq!(
prod.directory_url().unwrap(),
"https://acme-v02.api.letsencrypt.org/directory"
);
let staging = CAConfig::new(CertificateAuthority::LetsEncrypt, Environment::Staging);
assert_eq!(
staging.directory_url().unwrap(),
"https://acme-staging-v02.api.letsencrypt.org/directory"
);
}
#[test]
fn test_custom_ca() {
let config = CAConfig::new(CertificateAuthority::Custom, Environment::Production)
.with_custom_url("https://ca.example.com/acme/directory".to_string());
assert_eq!(
config.directory_url().unwrap(),
"https://ca.example.com/acme/directory"
);
}
#[test]
fn test_custom_ca_validation() {
let config = CAConfig::new(CertificateAuthority::Custom, Environment::Production);
assert!(config.validate().is_err());
let config = config.with_custom_url("https://ca.example.com/acme/directory".to_string());
assert!(config.validate().is_ok());
}
#[test]
fn test_ca_display() {
let config = CAConfig::default();
assert_eq!(config.to_string(), "Let's Encrypt (Production)");
}
#[cfg(feature = "google-ca")]
#[test]
fn test_google_ca_url() {
let config = CAConfig::new(CertificateAuthority::Google, Environment::Production);
assert_eq!(
config.directory_url().unwrap(),
"https://dv.google.com/acme/directory"
);
}
#[cfg(feature = "zerossl-ca")]
#[test]
fn test_zerossl_ca_url() {
let config = CAConfig::new(CertificateAuthority::ZeroSSL, Environment::Production);
assert_eq!(
config.directory_url().unwrap(),
"https://acme.zerossl.com/v2/DV90"
);
}
}