use std::sync::Arc;
use tracing::{debug, error, info, warn};
use crate::acme_issuer::CertIssuer;
use crate::async_jobs::{JobQueue, RetryConfig, do_with_retry};
use crate::cache::CertCache;
use crate::certificates::{Certificate, subject_qualifies_for_cert};
use crate::crypto::{
KeyType, decode_private_key_pem, encode_private_key_pem, generate_csr, generate_private_key,
};
use crate::error::{Error, Result, StorageError};
use crate::handshake::{CertResolver, OnDemandConfig};
use crate::ocsp::{OcspConfig, OcspStatus, staple_ocsp};
use crate::storage::{
CertificateResource, Storage, load_certificate, site_cert_key, site_meta_key, site_private_key,
store_certificate,
};
type EventCallback = Arc<dyn Fn(&str, &serde_json::Value) -> Result<()> + Send + Sync>;
type SubjectTransformer = Arc<dyn Fn(&str) -> Result<String> + Send + Sync>;
#[derive(Debug, Clone, Copy, Default)]
pub enum IssuerPolicy {
#[default]
UseFirstIssuer,
UseFirstRandomIssuer,
}
pub trait CertificateSelector: Send + Sync {
fn select_certificate(
&self,
hello: &rustls::server::ClientHello<'_>,
choices: &[&Certificate],
) -> Option<usize>;
}
pub const EVENT_CERT_OBTAINING: &str = "cert_obtaining";
pub const EVENT_CERT_OBTAINED: &str = "cert_obtained";
pub const EVENT_CERT_RENEWED: &str = "cert_renewed";
pub const EVENT_CERT_REVOKED: &str = "cert_revoked";
pub const EVENT_CERT_FAILED: &str = "cert_failed";
pub const EVENT_CACHED_MANAGED_CERT: &str = "cached_managed_cert";
const CERT_ISSUE_LOCK_OP: &str = "issue_cert";
pub struct Config {
pub renewal_window_ratio: f64,
pub on_demand: Option<Arc<OnDemandConfig>>,
pub issuers: Vec<Arc<dyn CertIssuer>>,
pub storage: Arc<dyn Storage>,
pub key_type: KeyType,
pub ocsp: OcspConfig,
pub cache: Arc<CertCache>,
pub on_event: Option<EventCallback>,
pub interactive: bool,
pub must_staple: bool,
pub reuse_private_keys: bool,
pub subject_transformer: Option<SubjectTransformer>,
pub default_server_name: Option<String>,
pub fallback_server_name: Option<String>,
pub disable_storage_check: bool,
pub issuer_policy: IssuerPolicy,
pub disable_ari: bool,
pub cert_selection: Option<Arc<dyn CertificateSelector>>,
job_queue: Arc<JobQueue>,
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("renewal_window_ratio", &self.renewal_window_ratio)
.field("key_type", &self.key_type)
.field("ocsp", &self.ocsp)
.field("interactive", &self.interactive)
.field("must_staple", &self.must_staple)
.field("reuse_private_keys", &self.reuse_private_keys)
.field("default_server_name", &self.default_server_name)
.field("fallback_server_name", &self.fallback_server_name)
.field("disable_storage_check", &self.disable_storage_check)
.field("issuer_policy", &self.issuer_policy)
.field("disable_ari", &self.disable_ari)
.field("issuers_count", &self.issuers.len())
.field("on_demand", &self.on_demand.as_ref().map(|_| "..."))
.field("on_event", &self.on_event.as_ref().map(|_| "..."))
.field(
"subject_transformer",
&self.subject_transformer.as_ref().map(|_| "..."),
)
.field(
"cert_selection",
&self.cert_selection.as_ref().map(|_| "..."),
)
.finish_non_exhaustive()
}
}
pub struct ConfigBuilder {
renewal_window_ratio: f64,
on_demand: Option<Arc<OnDemandConfig>>,
issuers: Option<Vec<Arc<dyn CertIssuer>>>,
storage: Option<Arc<dyn Storage>>,
key_type: KeyType,
ocsp: OcspConfig,
cache: Option<Arc<CertCache>>,
on_event: Option<EventCallback>,
interactive: bool,
must_staple: bool,
reuse_private_keys: bool,
subject_transformer: Option<SubjectTransformer>,
default_server_name: Option<String>,
fallback_server_name: Option<String>,
disable_storage_check: bool,
issuer_policy: IssuerPolicy,
disable_ari: bool,
cert_selection: Option<Arc<dyn CertificateSelector>>,
}
impl ConfigBuilder {
pub fn renewal_window_ratio(mut self, ratio: f64) -> Self {
self.renewal_window_ratio = ratio;
self
}
pub fn on_demand(mut self, on_demand: Arc<OnDemandConfig>) -> Self {
self.on_demand = Some(on_demand);
self
}
pub fn issuers(mut self, issuers: Vec<Arc<dyn CertIssuer>>) -> Self {
self.issuers = Some(issuers);
self
}
pub fn storage(mut self, storage: Arc<dyn Storage>) -> Self {
self.storage = Some(storage);
self
}
pub fn key_type(mut self, key_type: KeyType) -> Self {
self.key_type = key_type;
self
}
pub fn ocsp(mut self, ocsp: OcspConfig) -> Self {
self.ocsp = ocsp;
self
}
pub fn cache(mut self, cache: Arc<CertCache>) -> Self {
self.cache = Some(cache);
self
}
pub fn on_event(mut self, on_event: EventCallback) -> Self {
self.on_event = Some(on_event);
self
}
pub fn interactive(mut self, interactive: bool) -> Self {
self.interactive = interactive;
self
}
pub fn must_staple(mut self, must_staple: bool) -> Self {
self.must_staple = must_staple;
self
}
pub fn reuse_private_keys(mut self, reuse: bool) -> Self {
self.reuse_private_keys = reuse;
self
}
pub fn subject_transformer(mut self, f: SubjectTransformer) -> Self {
self.subject_transformer = Some(f);
self
}
pub fn default_server_name(mut self, name: impl Into<String>) -> Self {
self.default_server_name = Some(name.into());
self
}
pub fn fallback_server_name(mut self, name: impl Into<String>) -> Self {
self.fallback_server_name = Some(name.into());
self
}
pub fn disable_storage_check(mut self, disable: bool) -> Self {
self.disable_storage_check = disable;
self
}
pub fn issuer_policy(mut self, policy: IssuerPolicy) -> Self {
self.issuer_policy = policy;
self
}
pub fn disable_ari(mut self, disable: bool) -> Self {
self.disable_ari = disable;
self
}
pub fn cert_selection(mut self, selector: Arc<dyn CertificateSelector>) -> Self {
self.cert_selection = Some(selector);
self
}
pub fn build(self) -> Config {
use crate::cache::CacheOptions;
let storage = self
.storage
.expect("Config requires a Storage implementation -- call .storage() on the builder");
let cache = self
.cache
.unwrap_or_else(|| CertCache::new(CacheOptions::default()));
let issuers = self.issuers.unwrap_or_default();
Config {
renewal_window_ratio: self.renewal_window_ratio,
on_demand: self.on_demand,
issuers,
storage,
key_type: self.key_type,
ocsp: self.ocsp,
cache,
on_event: self.on_event,
interactive: self.interactive,
must_staple: self.must_staple,
reuse_private_keys: self.reuse_private_keys,
subject_transformer: self.subject_transformer,
default_server_name: self.default_server_name,
fallback_server_name: self.fallback_server_name,
disable_storage_check: self.disable_storage_check,
issuer_policy: self.issuer_policy,
disable_ari: self.disable_ari,
cert_selection: self.cert_selection,
job_queue: Arc::new(JobQueue::new("cert_management")),
}
}
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder {
renewal_window_ratio: crate::certificates::DEFAULT_RENEWAL_WINDOW_RATIO,
on_demand: None,
issuers: None,
storage: None,
key_type: KeyType::default(),
ocsp: OcspConfig::default(),
cache: None,
on_event: None,
interactive: false,
must_staple: false,
reuse_private_keys: false,
subject_transformer: None,
default_server_name: None,
fallback_server_name: None,
disable_storage_check: true,
issuer_policy: IssuerPolicy::default(),
disable_ari: false,
cert_selection: None,
}
}
}
impl Config {
fn emit(&self, event_name: &str, data: &serde_json::Value) -> Result<()> {
if let Some(ref on_event) = self.on_event {
on_event(event_name, data)?;
}
Ok(())
}
}
impl Config {
fn lock_key(op: &str, domain: &str) -> String {
format!("{op}_{domain}")
}
async fn storage_has_cert_resources(
storage: &dyn Storage,
issuer: &dyn CertIssuer,
domain: &str,
) -> bool {
let issuer_key = issuer.issuer_key();
let cert_key = site_cert_key(&issuer_key, domain);
let key_key = site_private_key(&issuer_key, domain);
let meta_key = site_meta_key(&issuer_key, domain);
let cert_ok = storage.exists(&cert_key).await.unwrap_or(false);
let key_ok = storage.exists(&key_key).await.unwrap_or(false);
let meta_ok = storage.exists(&meta_key).await.unwrap_or(false);
cert_ok && key_ok && meta_ok
}
async fn storage_has_cert_resources_any_issuer(&self, domain: &str) -> bool {
for issuer in &self.issuers {
if Self::storage_has_cert_resources(self.storage.as_ref(), issuer.as_ref(), domain)
.await
{
return true;
}
}
false
}
async fn load_cert_resource_any_issuer(&self, domain: &str) -> Result<CertificateResource> {
let mut last_err = None;
for issuer in &self.issuers {
match load_certificate(self.storage.as_ref(), &issuer.issuer_key(), domain).await {
Ok(cert_res) => return Ok(cert_res),
Err(e) => {
last_err = Some(e);
}
}
}
Err(last_err.unwrap_or_else(|| {
Error::Storage(StorageError::NotFound(format!(
"no certificate resource found for '{domain}' from any configured issuer"
)))
}))
}
async fn delete_site_assets(&self, issuer_key: &str, domain: &str) -> Result<()> {
self.storage
.delete(&site_cert_key(issuer_key, domain))
.await?;
self.storage
.delete(&site_private_key(issuer_key, domain))
.await?;
self.storage
.delete(&site_meta_key(issuer_key, domain))
.await?;
Ok(())
}
}
impl Config {
pub async fn make_certificate_with_ocsp(
&self,
cert_res: &CertificateResource,
) -> Result<Certificate> {
let mut cert = Certificate::from_pem(&cert_res.certificate_pem, &cert_res.private_key_pem)?;
if !self.ocsp.disable_stapling {
match staple_ocsp(self.storage.as_ref(), &mut cert, &self.ocsp).await {
Ok(_stapled) => {
debug!(
names = ?cert.names,
"OCSP staple applied"
);
}
Err(e) => {
warn!(
names = ?cert.names,
error = %e,
"failed to staple OCSP"
);
}
}
}
Ok(cert)
}
}
impl Config {
pub async fn manage_sync(&self, domains: &[String]) -> Result<()> {
self.manage_all(domains, false).await
}
pub async fn manage_async(&self, domains: &[String]) -> Result<()> {
for domain in domains {
let domain = domain.to_lowercase();
let cached_certs = self.cache.all_matching_certificates(&domain).await;
let already_managed = cached_certs.iter().any(|c| c.managed);
if already_managed {
continue;
}
let job_name = format!("manage_{domain}");
let storage = Arc::clone(&self.storage);
let cache = Arc::clone(&self.cache);
let issuers = self.issuers.clone();
let key_type = self.key_type;
let ocsp = self.ocsp.clone();
let renewal_ratio = self.renewal_window_ratio;
let on_event = self.on_event.clone();
let on_demand = self.on_demand.clone();
let interactive = self.interactive;
let must_staple = self.must_staple;
let reuse_private_keys = self.reuse_private_keys;
let default_server_name = self.default_server_name.clone();
let fallback_server_name = self.fallback_server_name.clone();
let disable_storage_check = self.disable_storage_check;
let subject_transformer = self.subject_transformer.clone();
let issuer_policy = self.issuer_policy;
let disable_ari = self.disable_ari;
let cert_selection = self.cert_selection.clone();
let domain_owned = domain.clone();
self.job_queue
.submit(job_name, move || async move {
let cfg = Config {
renewal_window_ratio: renewal_ratio,
on_demand,
issuers,
storage,
key_type,
ocsp,
cache,
on_event,
interactive,
must_staple,
reuse_private_keys,
subject_transformer,
default_server_name,
fallback_server_name,
disable_storage_check,
issuer_policy,
disable_ari,
cert_selection,
job_queue: Arc::new(JobQueue::new("bg_manage")),
};
if let Err(e) = cfg.manage_one(&domain_owned, true).await {
error!(
domain = %domain_owned,
error = %e,
"background certificate management failed"
);
}
})
.await;
}
Ok(())
}
async fn manage_all(&self, domains: &[String], r#async: bool) -> Result<()> {
for domain in domains {
let domain = domain.to_lowercase();
self.manage_one(&domain, r#async).await?;
}
Ok(())
}
async fn manage_one(&self, domain: &str, r#async: bool) -> Result<()> {
let cached_certs = self.cache.all_matching_certificates(domain).await;
for cert in &cached_certs {
if cert.managed {
return Ok(());
}
}
match self.cache_managed_certificate(domain).await {
Ok(cert) => {
let mut needs_action = cert.needs_renewal(self.renewal_window_ratio);
if !needs_action && let Some(OcspStatus::Revoked) = cert.ocsp_status {
warn!(
domain = %domain,
"certificate OCSP status is Revoked; triggering renewal"
);
needs_action = true;
}
if needs_action {
if r#async {
self.renew_cert_async(domain, false).await?;
} else {
self.renew_cert_sync(domain, false).await?;
}
let _ = self.cache_managed_certificate(domain).await;
}
Ok(())
}
Err(e) => {
let is_not_found = matches!(&e, Error::Storage(StorageError::NotFound(_)));
if !is_not_found {
return Err(Error::Other(format!("{domain}: caching certificate: {e}")));
}
if r#async {
self.obtain_cert_async(domain).await
} else {
self.obtain_cert_sync(domain).await
}
}
}
}
pub async fn cache_managed_certificate(&self, domain: &str) -> Result<Certificate> {
let cert = self.load_managed_certificate(domain).await?;
self.cache.add(cert.clone()).await;
let _ = self.emit(
EVENT_CACHED_MANAGED_CERT,
&serde_json::json!({"sans": cert.names}),
);
Ok(cert)
}
async fn load_managed_certificate(&self, domain: &str) -> Result<Certificate> {
let cert_res = self.load_cert_resource_any_issuer(domain).await?;
let mut cert = self.make_certificate_with_ocsp(&cert_res).await?;
cert.managed = true;
cert.issuer_key = cert_res.issuer_key.clone();
Ok(cert)
}
pub async fn load_cert_from_storage(&self, domain: &str) -> Result<Option<Certificate>> {
match self.load_managed_certificate(domain).await {
Ok(cert) => Ok(Some(cert)),
Err(Error::Storage(StorageError::NotFound(_))) => Ok(None),
Err(e) => Err(e),
}
}
}
impl Config {
pub async fn obtain_cert_sync(&self, domain: &str) -> Result<()> {
self.obtain_cert(domain, true).await
}
pub async fn obtain_cert_async(&self, domain: &str) -> Result<()> {
self.obtain_cert(domain, false).await
}
async fn obtain_cert(&self, domain: &str, interactive: bool) -> Result<()> {
if self.issuers.is_empty() {
return Err(Error::Config(
"no issuers configured; cannot obtain certificate".into(),
));
}
if !subject_qualifies_for_cert(domain) {
return Err(Error::Config(format!(
"domain '{domain}' does not qualify for a certificate"
)));
}
if self.storage_has_cert_resources_any_issuer(domain).await {
debug!(domain = %domain, "certificate already exists in storage; skipping obtain");
return Ok(());
}
info!(domain = %domain, "acquiring lock for certificate obtain");
let lock_key = Self::lock_key(CERT_ISSUE_LOCK_OP, domain);
self.storage.lock(&lock_key).await?;
let result = if interactive {
self.do_obtain(domain).await
} else {
let storage = Arc::clone(&self.storage);
let res = do_with_retry(&RetryConfig::default(), |_| self.do_obtain(domain)).await;
drop(storage);
res
};
info!(domain = %domain, "releasing lock for certificate obtain");
if let Err(unlock_err) = self.storage.unlock(&lock_key).await {
error!(
domain = %domain,
error = %unlock_err,
"failed to release lock after certificate obtain"
);
}
result
}
async fn do_obtain(&self, domain: &str) -> Result<()> {
let domain = if let Some(ref transformer) = self.subject_transformer {
let transformed = transformer(domain)?;
debug!(
original = %domain,
transformed = %transformed,
"subject transformer applied"
);
transformed
} else {
domain.to_string()
};
let domain = domain.as_str();
if self.storage_has_cert_resources_any_issuer(domain).await {
info!(domain = %domain, "certificate already exists in storage (obtained by another instance)");
return Ok(());
}
info!(domain = %domain, "obtaining certificate");
self.emit(
EVENT_CERT_OBTAINING,
&serde_json::json!({"identifier": domain, "renewal": false}),
)?;
let (private_key, private_key_pem) = if self.reuse_private_keys {
self.load_or_generate_private_key(domain).await?
} else {
let pk = generate_private_key(self.key_type)?;
let pem = encode_private_key_pem(&pk)?;
(pk, pem)
};
let domains = vec![domain.to_string()];
let csr_der = generate_csr(&private_key, &domains, self.must_staple)?;
let mut issuers: Vec<Arc<dyn CertIssuer>> = self.issuers.clone();
if matches!(self.issuer_policy, IssuerPolicy::UseFirstRandomIssuer) {
use rand::seq::SliceRandom;
issuers.shuffle(&mut rand::rng());
}
let mut last_err: Option<Error> = None;
let mut issuer_keys = Vec::new();
for (i, issuer) in issuers.iter().enumerate() {
let ik = issuer.issuer_key();
issuer_keys.push(ik.clone());
debug!(
domain = %domain,
issuer = %ik,
attempt = i + 1,
total = issuers.len(),
"trying issuer"
);
match issuer.issue(&csr_der, &domains).await {
Ok(issued) => {
let cert_res = CertificateResource {
sans: domains.clone(),
certificate_pem: issued.certificate_pem,
private_key_pem: private_key_pem.as_bytes().to_vec(),
issuer_data: Some(issued.metadata),
issuer_key: ik.clone(),
};
store_certificate(self.storage.as_ref(), &ik, &cert_res).await?;
info!(
domain = %domain,
issuer = %ik,
"certificate obtained successfully"
);
let _ = self.emit(
EVENT_CERT_OBTAINED,
&serde_json::json!({
"identifier": domain,
"issuer": ik,
"renewal": false,
}),
);
return Ok(());
}
Err(e) => {
error!(
domain = %domain,
issuer = %ik,
error = %e,
"could not get certificate from issuer"
);
last_err = Some(e);
}
}
}
let _ = self.emit(
EVENT_CERT_FAILED,
&serde_json::json!({
"identifier": domain,
"renewal": false,
"issuers": issuer_keys,
}),
);
Err(last_err
.unwrap_or_else(|| Error::Config(format!("[{domain}] obtain: all issuers failed"))))
}
async fn load_or_generate_private_key(
&self,
domain: &str,
) -> Result<(crate::crypto::PrivateKey, String)> {
for issuer in &self.issuers {
let key_path = site_private_key(&issuer.issuer_key(), domain);
match self.storage.load(&key_path).await {
Ok(pem_bytes) => {
if let Ok(pem_str) = std::str::from_utf8(&pem_bytes)
&& let Ok(pk) = decode_private_key_pem(pem_str)
{
debug!(
domain = %domain,
issuer = %issuer.issuer_key(),
"reusing existing private key from storage"
);
return Ok((pk, pem_str.to_string()));
}
}
Err(_) => continue,
}
}
let pk = generate_private_key(self.key_type)?;
let pem = encode_private_key_pem(&pk)?;
Ok((pk, pem))
}
}
impl Config {
pub async fn renew_cert_sync(&self, domain: &str, force: bool) -> Result<()> {
self.renew_cert(domain, force, true).await
}
pub async fn renew_cert_async(&self, domain: &str, force: bool) -> Result<()> {
self.renew_cert(domain, force, false).await
}
async fn renew_cert(&self, domain: &str, force: bool, interactive: bool) -> Result<()> {
if self.issuers.is_empty() {
return Err(Error::Config(
"no issuers configured; cannot renew certificate".into(),
));
}
info!(domain = %domain, "acquiring lock for certificate renewal");
let lock_key = Self::lock_key(CERT_ISSUE_LOCK_OP, domain);
self.storage.lock(&lock_key).await?;
let result = if interactive {
self.do_renew(domain, force).await
} else {
do_with_retry(&RetryConfig::default(), |_| self.do_renew(domain, force)).await
};
info!(domain = %domain, "releasing lock for certificate renewal");
if let Err(unlock_err) = self.storage.unlock(&lock_key).await {
error!(
domain = %domain,
error = %unlock_err,
"failed to release lock after certificate renewal"
);
}
result
}
async fn do_renew(&self, domain: &str, force: bool) -> Result<()> {
let cert_res = self.load_cert_resource_any_issuer(domain).await?;
let cert = self.make_certificate_with_ocsp(&cert_res).await?;
let needs_renewal = cert.needs_renewal(self.renewal_window_ratio);
if !needs_renewal && !force {
info!(
domain = %domain,
"certificate does not need renewal (may have been renewed by another instance); reloading into cache"
);
let mut fresh = cert;
fresh.managed = true;
fresh.issuer_key = cert_res.issuer_key.clone();
self.cache.add(fresh).await;
return Ok(());
}
if !needs_renewal && force {
info!(
domain = %domain,
"certificate does not need renewal, but renewal is being forced"
);
}
info!(domain = %domain, "renewing certificate");
self.emit(
EVENT_CERT_OBTAINING,
&serde_json::json!({
"identifier": domain,
"renewal": true,
"forced": force,
}),
)?;
let (private_key, private_key_pem) = if self.reuse_private_keys {
self.load_or_generate_private_key(domain).await?
} else {
let pk = generate_private_key(self.key_type)?;
let pem = encode_private_key_pem(&pk)?;
(pk, pem)
};
let domains = vec![domain.to_string()];
let csr_der = generate_csr(&private_key, &domains, self.must_staple)?;
let mut issuers: Vec<Arc<dyn CertIssuer>> = self.issuers.clone();
if matches!(self.issuer_policy, IssuerPolicy::UseFirstRandomIssuer) {
use rand::seq::SliceRandom;
issuers.shuffle(&mut rand::rng());
}
let mut last_err: Option<Error> = None;
let mut issuer_keys = Vec::new();
for issuer in &issuers {
let ik = issuer.issuer_key();
issuer_keys.push(ik.clone());
match issuer.issue(&csr_der, &domains).await {
Ok(issued) => {
let new_cert_res = CertificateResource {
sans: domains.clone(),
certificate_pem: issued.certificate_pem,
private_key_pem: private_key_pem.as_bytes().to_vec(),
issuer_data: Some(issued.metadata),
issuer_key: ik.clone(),
};
store_certificate(self.storage.as_ref(), &ik, &new_cert_res).await?;
info!(
domain = %domain,
issuer = %ik,
"certificate renewed successfully"
);
let _ = self.emit(
EVENT_CERT_RENEWED,
&serde_json::json!({
"identifier": domain,
"issuer": ik,
}),
);
return Ok(());
}
Err(e) => {
error!(
domain = %domain,
issuer = %ik,
error = %e,
"could not renew certificate from issuer"
);
last_err = Some(e);
}
}
}
let _ = self.emit(
EVENT_CERT_FAILED,
&serde_json::json!({
"identifier": domain,
"renewal": true,
"issuers": issuer_keys,
}),
);
Err(last_err
.unwrap_or_else(|| Error::Config(format!("[{domain}] renew: all issuers failed"))))
}
}
impl Config {
pub async fn revoke_cert(&self, domain: &str, reason: Option<u8>) -> Result<()> {
for (i, issuer) in self.issuers.iter().enumerate() {
let ik = issuer.issuer_key();
let cert_res = match load_certificate(self.storage.as_ref(), &ik, domain).await {
Ok(res) => res,
Err(_) => continue,
};
let pk_key = site_private_key(&ik, domain);
let pk_exists = self.storage.exists(&pk_key).await.unwrap_or(false);
if !pk_exists {
return Err(Error::Config(format!(
"private key not found for '{domain}' (issuer {i}: {ik})"
)));
}
info!(
domain = %domain,
issuer = %ik,
"revoking certificate"
);
if let Some(revoker) = issuer.as_revoker() {
revoker.revoke(&cert_res.certificate_pem, reason).await?;
info!(
domain = %domain,
issuer = %ik,
"certificate revoked via ACME"
);
} else {
warn!(
domain = %domain,
issuer = %ik,
"issuer does not support revocation; skipping ACME revoke call"
);
}
self.delete_site_assets(&ik, domain).await?;
info!(
domain = %domain,
issuer = %ik,
"certificate assets deleted after revocation"
);
let _ = self.emit(
EVENT_CERT_REVOKED,
&serde_json::json!({
"identifier": domain,
"issuer": ik,
}),
);
let cached_certs = self.cache.all_matching_certificates(domain).await;
for cert in &cached_certs {
if cert.issuer_key == ik {
self.cache.remove(&cert.hash).await;
}
}
return Ok(());
}
Err(Error::Config(format!(
"no certificate found in storage for '{domain}' from any configured issuer"
)))
}
}
impl Config {
pub fn tls_config(&self) -> rustls::ServerConfig {
let mut resolver = CertResolver::new(self.cache.clone());
if self.default_server_name.is_some() {
resolver.set_default_server_name(self.default_server_name.clone());
}
if self.fallback_server_name.is_some() {
resolver.set_fallback_server_name(self.fallback_server_name.clone());
}
let mut tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(Arc::new(resolver));
tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
tls_config
}
}
impl Config {
pub async fn client_credentials(
&self,
domain: &str,
) -> Result<(
Vec<rustls::pki_types::CertificateDer<'static>>,
rustls::pki_types::PrivateKeyDer<'static>,
)> {
let cert = match self.cache.get_by_name(domain).await {
Some(c) => c,
None => {
let mut found = None;
for issuer in &self.issuers {
let ik = issuer.issuer_key();
let cert_key = crate::storage::site_cert_key(&ik, domain);
let key_key = crate::storage::site_private_key(&ik, domain);
if let (Ok(cert_pem), Ok(key_pem)) = (
self.storage.load(&cert_key).await,
self.storage.load(&key_key).await,
) && let Ok(c) = Certificate::from_pem(&cert_pem, &key_pem)
{
found = Some(c);
break;
}
}
match found {
Some(c) => c,
None => {
return Err(Error::Config(format!(
"no certificate found for domain '{domain}' in cache or storage"
)));
}
}
}
};
let cert_chain = cert.cert_chain.clone();
let pk_bytes = cert.private_key_der.as_ref().ok_or_else(|| {
Error::Config(format!("certificate for '{domain}' has no private key"))
})?;
if pk_bytes.is_empty() {
return Err(Error::Config(format!(
"certificate for '{domain}' has empty private key bytes"
)));
}
use rustls::pki_types::{
PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer,
};
use crate::certificates::PrivateKeyKind;
let pk_der = match cert.private_key_kind {
PrivateKeyKind::Pkcs8 => {
PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pk_bytes.clone()))
}
PrivateKeyKind::Pkcs1 => {
PrivateKeyDer::Pkcs1(PrivatePkcs1KeyDer::from(pk_bytes.clone()))
}
PrivateKeyKind::Sec1 => PrivateKeyDer::Sec1(PrivateSec1KeyDer::from(pk_bytes.clone())),
PrivateKeyKind::None => {
return Err(Error::Config(format!(
"certificate for '{domain}' has unknown private key kind"
)));
}
};
Ok((cert_chain, pk_der))
}
pub async fn client_tls_config(&self, domain: &str) -> Result<rustls::ClientConfig> {
let (cert_chain, pk_der) = self.client_credentials(domain).await?;
let config = rustls::ClientConfig::builder()
.with_root_certificates(rustls::RootCertStore::empty())
.with_client_auth_cert(cert_chain, pk_der)
.map_err(|e| Error::Config(format!("failed to build client TLS config: {e}")))?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use chrono::Utc;
use tokio::sync::RwLock;
use super::*;
use crate::cache::CacheOptions;
use crate::storage::KeyInfo;
struct MemoryStorage {
data: RwLock<HashMap<String, Vec<u8>>>,
}
impl MemoryStorage {
fn new() -> Self {
Self {
data: RwLock::new(HashMap::new()),
}
}
}
#[async_trait::async_trait]
impl Storage for MemoryStorage {
async fn store(&self, key: &str, value: &[u8]) -> Result<()> {
let mut data = self.data.write().await;
data.insert(key.to_owned(), value.to_vec());
Ok(())
}
async fn load(&self, key: &str) -> Result<Vec<u8>> {
let data = self.data.read().await;
data.get(key)
.cloned()
.ok_or_else(|| Error::Storage(StorageError::NotFound(key.to_owned())))
}
async fn delete(&self, key: &str) -> Result<()> {
let mut data = self.data.write().await;
data.remove(key);
Ok(())
}
async fn exists(&self, key: &str) -> Result<bool> {
let data = self.data.read().await;
Ok(data.contains_key(key))
}
async fn list(&self, path: &str, _recursive: bool) -> Result<Vec<String>> {
let data = self.data.read().await;
let keys: Vec<String> = data
.keys()
.filter(|k| k.starts_with(path))
.cloned()
.collect();
Ok(keys)
}
async fn stat(&self, key: &str) -> Result<KeyInfo> {
let data = self.data.read().await;
match data.get(key) {
Some(v) => Ok(KeyInfo {
key: key.to_owned(),
modified: Utc::now(),
size: v.len() as u64,
is_terminal: true,
}),
None => Err(Error::Storage(StorageError::NotFound(key.to_owned()))),
}
}
async fn lock(&self, _name: &str) -> Result<()> {
Ok(())
}
async fn unlock(&self, _name: &str) -> Result<()> {
Ok(())
}
}
#[test]
fn test_config_builder_defaults() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
assert!((config.renewal_window_ratio - 1.0 / 3.0).abs() < f64::EPSILON);
assert_eq!(config.key_type, KeyType::EcdsaP256);
assert!(!config.interactive);
assert!(config.issuers.is_empty());
assert!(config.on_demand.is_none());
assert!(config.on_event.is_none());
}
#[test]
#[should_panic(expected = "Config requires a Storage")]
fn test_config_builder_panics_without_storage() {
let _config = Config::builder().build();
}
#[test]
fn test_config_builder_custom_values() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let cache = CertCache::new(CacheOptions::default());
let config = Config::builder()
.storage(storage)
.cache(cache)
.key_type(KeyType::EcdsaP384)
.renewal_window_ratio(0.5)
.interactive(true)
.build();
assert_eq!(config.key_type, KeyType::EcdsaP384);
assert!((config.renewal_window_ratio - 0.5).abs() < f64::EPSILON);
assert!(config.interactive);
}
#[test]
fn test_config_debug() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("Config"));
assert!(debug_str.contains("renewal_window_ratio"));
}
#[test]
fn test_lock_key_format() {
let key = Config::lock_key("issue_cert", "example.com");
assert_eq!(key, "issue_cert_example.com");
}
#[test]
fn test_event_constants() {
assert_eq!(EVENT_CERT_OBTAINING, "cert_obtaining");
assert_eq!(EVENT_CERT_OBTAINED, "cert_obtained");
assert_eq!(EVENT_CERT_RENEWED, "cert_renewed");
assert_eq!(EVENT_CERT_REVOKED, "cert_revoked");
assert_eq!(EVENT_CERT_FAILED, "cert_failed");
assert_eq!(EVENT_CACHED_MANAGED_CERT, "cached_managed_cert");
}
#[tokio::test]
async fn test_storage_has_no_cert_resources() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
let has = config
.storage_has_cert_resources_any_issuer("example.com")
.await;
assert!(!has);
}
#[tokio::test]
async fn test_load_cert_from_storage_not_found() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
let result = config.load_cert_from_storage("example.com").await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_emit_with_callback() {
use std::sync::atomic::{AtomicUsize, Ordering};
let call_count = Arc::new(AtomicUsize::new(0));
let count_clone = Arc::clone(&call_count);
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder()
.storage(storage)
.on_event(Arc::new(move |event, _data| {
assert_eq!(event, "test_event");
count_clone.fetch_add(1, Ordering::SeqCst);
Ok(())
}))
.build();
config.emit("test_event", &serde_json::json!({})).unwrap();
assert_eq!(call_count.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_emit_without_callback() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
config.emit("test_event", &serde_json::json!({})).unwrap();
}
#[tokio::test]
async fn test_obtain_cert_no_issuers() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
let result = config.obtain_cert_sync("example.com").await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("no issuers configured"));
}
#[tokio::test]
async fn test_obtain_cert_invalid_domain() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
let result = config.obtain_cert(".invalid.com", true).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_renew_cert_no_issuers() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
let result = config.renew_cert_sync("example.com", false).await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("no issuers configured"));
}
#[tokio::test]
async fn test_manage_sync_no_issuers_no_storage() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
let result = config.manage_sync(&["example.com".to_string()]).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_revoke_cert_nothing_in_storage() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let config = Config::builder().storage(storage).build();
let result = config.revoke_cert("example.com", None).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_delete_site_assets() {
let storage = Arc::new(MemoryStorage::new());
let storage_trait: Arc<dyn Storage> = Arc::clone(&storage) as _;
storage
.store("certificates/test/example.com/example.com.crt", b"cert")
.await
.unwrap();
storage
.store("certificates/test/example.com/example.com.key", b"key")
.await
.unwrap();
storage
.store("certificates/test/example.com/example.com.json", b"meta")
.await
.unwrap();
let config = Config::builder().storage(storage_trait).build();
let _result = config.delete_site_assets("test", "example.com").await;
}
}