use rand::RngExt;
use std::collections::HashSet;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use crate::cache::{Cache, CacheOptions};
use crate::certificate::{Certificate, DEFAULT_RENEWAL_WINDOW_RATIO};
use crate::crypto::StandardKeyGenerator;
use crate::error::{ConfigError, Error, Result};
use crate::events::{OnEventFn, ShouldEmitFn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IssuerPolicy {
#[default]
UseFirstIssuer,
UseFirstRandomIssuer,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OcspConfig {
pub disable_stapling: bool,
pub replace_revoked: bool,
pub responder_overrides: std::collections::HashMap<String, String>,
}
impl Default for OcspConfig {
fn default() -> Self {
Self {
disable_stapling: false,
replace_revoked: true,
responder_overrides: std::collections::HashMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Policy {
pub renewal_window_ratio: f64,
pub key_type: crate::crypto::KeyType,
pub must_staple: bool,
pub reuse_private_keys: bool,
pub issuer_policy: IssuerPolicy,
pub ocsp: OcspConfig,
pub disable_ari: bool,
pub default_server_name: String,
pub fallback_server_name: String,
pub disable_storage_check: bool,
}
impl Default for Policy {
fn default() -> Self {
Self {
renewal_window_ratio: 0.0,
key_type: crate::crypto::KeyType::default(),
must_staple: false,
reuse_private_keys: false,
issuer_policy: IssuerPolicy::default(),
ocsp: OcspConfig::default(),
disable_ari: false,
default_server_name: String::new(),
fallback_server_name: String::new(),
disable_storage_check: false,
}
}
}
impl Policy {
#[must_use]
pub fn with_renewal_window_ratio(mut self, ratio: f64) -> Self {
self.renewal_window_ratio = ratio;
self
}
#[must_use]
pub fn with_key_type(mut self, key_type: crate::crypto::KeyType) -> Self {
self.key_type = key_type;
self
}
#[must_use]
pub fn with_must_staple(mut self, must_staple: bool) -> Self {
self.must_staple = must_staple;
self
}
#[must_use]
pub fn with_reuse_private_keys(mut self, reuse: bool) -> Self {
self.reuse_private_keys = reuse;
self
}
#[must_use]
pub fn with_issuer_policy(mut self, issuer_policy: IssuerPolicy) -> Self {
self.issuer_policy = issuer_policy;
self
}
#[must_use]
pub fn with_ocsp(mut self, ocsp: OcspConfig) -> Self {
self.ocsp = ocsp;
self
}
#[must_use]
pub fn with_disable_ari(mut self, disable_ari: bool) -> Self {
self.disable_ari = disable_ari;
self
}
#[must_use]
pub fn with_default_server_name(mut self, name: impl Into<String>) -> Self {
self.default_server_name = name.into();
self
}
#[must_use]
pub fn with_fallback_server_name(mut self, name: impl Into<String>) -> Self {
self.fallback_server_name = name.into();
self
}
#[must_use]
pub fn with_disable_storage_check(mut self, disable: bool) -> Self {
self.disable_storage_check = disable;
self
}
pub fn apply_to(&self, options: &mut ConfigOptions) {
options.renewal_window_ratio = self.renewal_window_ratio;
options.key_source = Some(Arc::new(crate::crypto::StandardKeyGenerator {
key_type: self.key_type,
}));
options.must_staple = self.must_staple;
options.reuse_private_keys = self.reuse_private_keys;
options.issuer_policy = self.issuer_policy;
options.ocsp = self.ocsp.clone();
options.disable_ari = self.disable_ari;
options.default_server_name = self.default_server_name.clone();
options.fallback_server_name = self.fallback_server_name.clone();
options.disable_storage_check = self.disable_storage_check;
}
}
#[derive(Default)]
pub struct OnDemandConfig {
pub decision_func: Option<DecisionFn>, pub host_allowlist: Option<HashSet<String>>,
pub rate_limit: Option<Arc<crate::ratelimiter::RingBufferRateLimiter>>,
pub managers: Vec<Arc<dyn Manager>>,
pub(crate) allowlist: std::sync::RwLock<Option<std::collections::HashSet<String>>>,
}
impl Clone for OnDemandConfig {
fn clone(&self) -> Self {
Self {
decision_func: self.decision_func.clone(),
host_allowlist: self.host_allowlist.clone(),
rate_limit: self.rate_limit.clone(),
managers: self.managers.clone(),
allowlist: std::sync::RwLock::new(self.allowlist.read().ok().and_then(|g| g.clone())),
}
}
}
impl From<Arc<OnDemandConfig>> for OnDemandConfig {
fn from(config: Arc<OnDemandConfig>) -> Self {
(*config).clone()
}
}
impl OnDemandConfig {
#[must_use]
pub fn with_decision_func(mut self, decision: DecisionFn) -> Self {
self.decision_func = Some(decision);
self
}
#[must_use]
pub fn with_sync_decision<F>(self, decision: F) -> Self
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
self.with_decision_func(Arc::new(move |_ct, name| {
let allowed = decision(&name);
Box::pin(async move {
if allowed {
Ok(())
} else {
Err(crate::error::Error::Certificate(
crate::error::CertificateError::NotAllowed(name),
))
}
})
}))
}
#[must_use]
pub fn with_sync_decision_func<F>(self, decision: F) -> Self
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
self.with_sync_decision(decision)
}
#[must_use]
pub fn with_host_allowlist<I, S>(mut self, names: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.host_allowlist = Some(
names
.into_iter()
.map(|name| crate::certificate::normalized_name(name.as_ref()))
.collect(),
);
self
}
#[must_use]
pub fn with_rate_limit(
mut self,
rate_limit: Arc<crate::ratelimiter::RingBufferRateLimiter>,
) -> Self {
self.rate_limit = Some(rate_limit);
self
}
#[must_use]
pub fn with_managers(mut self, managers: Vec<Arc<dyn Manager>>) -> Self {
self.managers = managers;
self
}
}
impl std::fmt::Debug for OnDemandConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OnDemandConfig")
.field("has_decision_func", &self.decision_func.is_some())
.field("host_allowlist", &self.host_allowlist)
.field("has_rate_limit", &self.rate_limit.is_some())
.field("managers", &self.managers.len())
.finish_non_exhaustive()
}
}
pub type DecisionFn = Arc<
dyn Fn(
CancellationToken,
String,
) -> futures::future::BoxFuture<'static, crate::error::Result<()>>
+ Send
+ Sync,
>;
#[async_trait::async_trait]
pub trait Manager: Send + Sync + std::fmt::Debug {
async fn get_certificate(
&self,
ct: &CancellationToken,
hello: &crate::handshake::ClientHelloInfo,
) -> Result<Option<Certificate>>;
}
pub trait CertificateSelector: Send + Sync + std::fmt::Debug {
fn select_certificate(
&self,
hello: &crate::handshake::ClientHelloInfo,
choices: &[Certificate],
) -> Result<Certificate>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultCertificateSelector;
impl CertificateSelector for DefaultCertificateSelector {
fn select_certificate(
&self,
_hello: &crate::handshake::ClientHelloInfo,
choices: &[Certificate],
) -> Result<Certificate> {
if choices.len() == 1 {
return Ok(choices[0].clone());
}
let now = time::OffsetDateTime::now_utc();
let mut best: Option<&Certificate> = None;
for cert in choices {
if best.is_none() {
best = Some(cert);
}
let unexpired = match &cert.info {
Some(info) => now >= info.not_before && now < info.not_after,
None => true, };
if unexpired {
return Ok(cert.clone());
}
}
best.cloned()
.ok_or(Error::Certificate(crate::error::CertificateError::NoNames))
}
}
pub type SubjectTransformerFn = Arc<
dyn Fn(CancellationToken, String) -> futures::future::BoxFuture<'static, String> + Send + Sync,
>;
#[derive(Clone)]
pub struct ConfigOptions {
pub renewal_window_ratio: f64,
pub on_event: Option<OnEventFn>,
pub should_emit: Option<ShouldEmitFn>,
pub default_server_name: String,
pub fallback_server_name: String,
pub on_demand: Option<OnDemandConfig>,
pub must_staple: bool,
pub issuers: Vec<Arc<dyn crate::issuer::Issuer>>,
pub issuer_policy: IssuerPolicy,
pub reuse_private_keys: bool,
pub key_source: Option<Arc<dyn crate::crypto::KeyGenerator>>,
pub cert_selection: Option<Arc<dyn CertificateSelector>>,
pub ocsp: OcspConfig,
pub storage: Option<Arc<dyn crate::storage::Storage>>,
pub cert_store: Option<Arc<dyn crate::cert_store::CertStore>>,
pub local_cache: Option<Arc<dyn crate::storage::Storage>>,
pub disable_storage_check: bool,
pub subject_transformer: Option<SubjectTransformerFn>,
pub disable_ari: bool,
}
impl Default for ConfigOptions {
fn default() -> Self {
Self {
renewal_window_ratio: 0.0,
on_event: None,
should_emit: None,
default_server_name: String::new(),
fallback_server_name: String::new(),
on_demand: None,
must_staple: false,
issuers: vec![Arc::new(crate::acme::AcmeIssuer::lets_encrypt())],
issuer_policy: IssuerPolicy::default(),
reuse_private_keys: false,
key_source: None,
cert_selection: None,
ocsp: OcspConfig::default(),
storage: None,
cert_store: None,
local_cache: None,
disable_storage_check: false,
subject_transformer: None,
disable_ari: false,
}
}
}
impl ConfigOptions {
#[must_use]
pub fn policy(&self) -> Policy {
Policy {
renewal_window_ratio: self.renewal_window_ratio,
key_type: self
.key_source
.as_ref()
.and_then(|source| source.key_type())
.unwrap_or_default(),
must_staple: self.must_staple,
reuse_private_keys: self.reuse_private_keys,
issuer_policy: self.issuer_policy,
ocsp: self.ocsp.clone(),
disable_ari: self.disable_ari,
default_server_name: self.default_server_name.clone(),
fallback_server_name: self.fallback_server_name.clone(),
disable_storage_check: self.disable_storage_check,
}
}
#[must_use]
pub fn with_policy(mut self, policy: Policy) -> Self {
policy.apply_to(&mut self);
self
}
}
#[derive(Clone, Debug, Default)]
pub struct ConfigBuilder {
options: ConfigOptions,
cache_options: CacheOptions,
cache: Option<Arc<Cache>>,
maintenance: Option<crate::maintain::MaintenanceConfig>,
}
impl ConfigBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn options(mut self, options: ConfigOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub fn policy(mut self, policy: Policy) -> Self {
policy.apply_to(&mut self.options);
self
}
#[must_use]
pub fn cache_options(mut self, options: CacheOptions) -> Self {
self.cache_options = options;
self
}
#[must_use]
pub fn maintenance(mut self, maintenance: crate::maintain::MaintenanceConfig) -> Self {
maintenance.apply_to_cache_options(&mut self.cache_options);
maintenance.apply_to_config_options(&mut self.options);
self.maintenance = Some(maintenance);
self
}
#[must_use]
pub fn maintenance_config(self, maintenance: crate::maintain::MaintenanceConfig) -> Self {
self.maintenance(maintenance)
}
#[must_use]
pub fn cache(mut self, cache: Arc<Cache>) -> Self {
self.cache = Some(cache);
self
}
#[must_use]
pub fn storage(mut self, storage: Arc<dyn crate::storage::Storage>) -> Self {
self.options.storage = Some(storage);
self
}
#[must_use]
pub fn cert_store(mut self, store: Arc<dyn crate::cert_store::CertStore>) -> Self {
self.options.cert_store = Some(store);
self
}
#[must_use]
pub fn issuers(mut self, issuers: Vec<Arc<dyn crate::issuer::Issuer>>) -> Self {
self.options.issuers = issuers;
self
}
#[must_use]
pub fn on_demand<T>(mut self, on_demand: T) -> Self
where
T: Into<OnDemandConfig>,
{
self.options.on_demand = Some(on_demand.into());
self
}
#[must_use]
pub fn on_event(mut self, on_event: OnEventFn) -> Self {
self.options.on_event = Some(on_event);
self
}
#[must_use]
pub fn should_emit(mut self, should_emit: ShouldEmitFn) -> Self {
self.options.should_emit = Some(should_emit);
self
}
#[must_use]
pub fn cert_selection(mut self, selector: Arc<dyn CertificateSelector>) -> Self {
self.options.cert_selection = Some(selector);
self
}
#[must_use]
pub fn subject_transformer(mut self, transformer: SubjectTransformerFn) -> Self {
self.options.subject_transformer = Some(transformer);
self
}
#[must_use]
pub fn reuse_private_keys(mut self, reuse: bool) -> Self {
self.options.reuse_private_keys = reuse;
self
}
#[must_use]
pub fn ocsp(mut self, ocsp: OcspConfig) -> Self {
self.options.ocsp = ocsp;
self
}
#[must_use]
pub fn disable_ari(mut self, disable: bool) -> Self {
self.options.disable_ari = disable;
self
}
#[must_use]
pub fn renewal_window_ratio(mut self, ratio: f64) -> Self {
self.options.renewal_window_ratio = ratio;
self
}
#[must_use]
pub fn key_type(mut self, key_type: crate::crypto::KeyType) -> Self {
self.options.key_source = Some(Arc::new(StandardKeyGenerator { key_type }));
self
}
#[must_use]
pub fn default_server_name(mut self, name: impl Into<String>) -> Self {
self.options.default_server_name = name.into();
self
}
#[must_use]
pub fn fallback_server_name(mut self, name: impl Into<String>) -> Self {
self.options.fallback_server_name = name.into();
self
}
#[must_use]
pub fn disable_storage_check(mut self, disable: bool) -> Self {
self.options.disable_storage_check = disable;
self
}
#[must_use]
pub fn issuer_policy(mut self, policy: IssuerPolicy) -> Self {
self.options.issuer_policy = policy;
self
}
#[must_use]
pub fn must_staple(mut self, must_staple: bool) -> Self {
self.options.must_staple = must_staple;
self
}
pub fn build(self) -> Result<Arc<Config>> {
let starts_maintenance = self.cache.is_none();
let cache = match self.cache {
Some(cache) => {
if let Some(maintenance) = &self.maintenance {
let mut options = cache.options();
maintenance.apply_to_cache_options(&mut options);
cache.set_options(options);
}
cache
}
None => Cache::new_without_maintenance(self.cache_options)?,
};
let config = Config::new(Arc::clone(&cache), self.options)?;
if starts_maintenance {
cache.start_maintenance()?;
}
Ok(config)
}
}
impl std::fmt::Debug for ConfigOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigOptions")
.field("renewal_window_ratio", &self.renewal_window_ratio)
.field("default_server_name", &self.default_server_name)
.field("on_demand", &self.on_demand)
.field("must_staple", &self.must_staple)
.field("issuers", &self.issuers.len())
.field("custom_cert_store", &self.cert_store.is_some())
.field("issuer_policy", &self.issuer_policy)
.field("reuse_private_keys", &self.reuse_private_keys)
.field("disable_ari", &self.disable_ari)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct Config {
pub options: Arc<ConfigOptions>,
pub(crate) cert_cache: Arc<Cache>,
pub(crate) cert_store: Arc<dyn crate::cert_store::CertStore>,
pub(crate) load_flights: Arc<crate::singleflight::SingleFlight<crate::handshake::FlightCert>>,
pub(crate) obtain_flights: Arc<crate::singleflight::SingleFlight<Result<(), Arc<Error>>>>,
}
pub(crate) struct CachedConfig {
options: Arc<ConfigOptions>,
cache: std::sync::Weak<Cache>,
cert_store: Arc<dyn crate::cert_store::CertStore>,
load_flights: Arc<crate::singleflight::SingleFlight<crate::handshake::FlightCert>>,
obtain_flights: Arc<crate::singleflight::SingleFlight<Result<(), Arc<Error>>>>,
}
impl CachedConfig {
fn new(config: &Config) -> Self {
Self {
options: Arc::clone(&config.options),
cache: Arc::downgrade(&config.cert_cache),
cert_store: Arc::clone(&config.cert_store),
load_flights: Arc::clone(&config.load_flights),
obtain_flights: Arc::clone(&config.obtain_flights),
}
}
pub(crate) fn upgrade(&self) -> Option<Arc<Config>> {
Some(Arc::new(Config {
options: Arc::clone(&self.options),
cert_cache: self.cache.upgrade()?,
cert_store: Arc::clone(&self.cert_store),
load_flights: Arc::clone(&self.load_flights),
obtain_flights: Arc::clone(&self.obtain_flights),
}))
}
}
impl Config {
#[must_use]
pub fn builder() -> ConfigBuilder {
ConfigBuilder::default()
}
pub fn new(cert_cache: Arc<Cache>, mut options: ConfigOptions) -> Result<Arc<Self>> {
if !options.renewal_window_ratio.is_finite()
|| !(0.0..1.0).contains(&options.renewal_window_ratio)
{
return Err(Error::Config(ConfigError::Invalid(
"renewal_window_ratio must be finite and in [0, 1) (0 selects the default)".into(),
)));
}
#[cfg(not(feature = "file-storage"))]
if options.storage.is_none() && crate::storage::STORAGE_DEFAULT.get().is_none() {
return Err(Error::Config(ConfigError::Missing(
"no storage backend configured; set ConfigOptions::storage, install \
storage::STORAGE_DEFAULT, or enable the `file-storage` feature"
.into(),
)));
}
if let Some(on_demand) = options.on_demand.as_mut()
&& let Some(allowlist) = on_demand.host_allowlist.as_mut()
{
*allowlist = allowlist.iter().map(|name| normalized_name(name)).collect();
}
let cert_store = options.cert_store.clone().unwrap_or_else(|| {
Arc::new(crate::cert_store::KeyValueCertStore::new(
options
.storage
.clone()
.unwrap_or_else(default_file_storage_arc),
)) as Arc<dyn crate::cert_store::CertStore>
});
let issuer_storage = options
.storage
.clone()
.or_else(|| Some(default_file_storage_arc()));
if let Some(storage) = issuer_storage {
for issuer in &mut options.issuers {
if let Some(acme) = issuer.as_any().downcast_ref::<crate::acme::AcmeIssuer>()
&& acme.storage.is_none()
{
let mut bound = acme.clone();
bound.storage = Some(Arc::clone(&storage));
*issuer = Arc::new(bound);
}
#[cfg(feature = "zerossl")]
if let Some(zerossl) = issuer
.as_any()
.downcast_ref::<crate::zerossl::ZeroSslIssuer>()
&& zerossl.inner.storage.is_none()
{
let mut bound = zerossl.clone();
bound.inner.storage = Some(Arc::clone(&storage));
*issuer = Arc::new(bound);
}
}
}
let cfg = Arc::new(Self {
options: Arc::new(options),
cert_cache,
cert_store,
load_flights: Arc::new(crate::singleflight::SingleFlight::default()),
obtain_flights: Arc::new(crate::singleflight::SingleFlight::default()),
});
if let Ok(mut owner) = cfg.cert_cache.owner.write()
&& owner.is_none()
{
*owner = Some(CachedConfig::new(&cfg));
}
Ok(cfg)
}
pub fn new_default() -> Result<Arc<Self>> {
Self::new(default_cache(), ConfigOptions::default())
}
#[must_use]
pub fn cache(&self) -> &Arc<Cache> {
&self.cert_cache
}
#[must_use]
pub fn policy(&self) -> Policy {
self.options.policy()
}
#[must_use]
pub fn cert_store(&self) -> &Arc<dyn crate::cert_store::CertStore> {
&self.cert_store
}
#[must_use]
pub fn renewal_window_ratio(&self) -> f64 {
if self.options.renewal_window_ratio <= 0.0 {
DEFAULT_RENEWAL_WINDOW_RATIO
} else {
self.options.renewal_window_ratio
}
}
#[must_use]
pub fn storage(&self) -> Arc<dyn crate::storage::Storage> {
#[cfg(feature = "local-cache")]
if let Some(local) = &self.options.local_cache {
return local.clone();
}
self.options
.storage
.clone()
.unwrap_or_else(default_file_storage_arc)
}
#[must_use]
pub fn ground_truth_storage(&self) -> Arc<dyn crate::storage::Storage> {
self.options
.storage
.clone()
.unwrap_or_else(default_file_storage_arc)
}
#[must_use]
pub fn key_source(&self) -> Arc<dyn crate::crypto::KeyGenerator> {
self.options
.key_source
.clone()
.unwrap_or_else(|| Arc::new(StandardKeyGenerator::default()))
}
#[must_use]
pub fn cert_selection(&self) -> Arc<dyn CertificateSelector> {
self.options
.cert_selection
.clone()
.unwrap_or_else(|| Arc::new(DefaultCertificateSelector))
}
}
impl AsRef<Config> for Config {
fn as_ref(&self) -> &Config {
self
}
}
static DEFAULT_CACHE: std::sync::OnceLock<Arc<Cache>> = std::sync::OnceLock::new();
#[must_use]
pub fn default_cache() -> Arc<Cache> {
DEFAULT_CACHE
.get_or_init(|| Cache::new(CacheOptions::default()).expect("default cache"))
.clone()
}
fn default_file_storage_arc() -> Arc<dyn crate::storage::Storage> {
#[cfg(feature = "file-storage")]
{
crate::storage::STORAGE_DEFAULT
.get_or_init(|| {
crate::storage::FileStorage::default_storage() as Arc<dyn crate::storage::Storage>
})
.clone()
}
#[cfg(not(feature = "file-storage"))]
{
crate::storage::STORAGE_DEFAULT.get().cloned().expect(
"no Storage configured: set ConfigOptions.storage (or storage::STORAGE_DEFAULT) \
or enable the `file-storage` feature",
)
}
}
pub use crate::certificate::normalized_name as normalize_name;
use crate::certificate::{
RenewalDecision, make_certificate, normalized_name, subject_qualifies_for_cert,
};
use crate::crypto::{key_pair_from_pkcs8, pem_decode_private_key, pem_encode_private_key};
use crate::error::{IssuerError, StorageError};
use crate::events::{
CertFailedData, CertObtainedData, CertObtainingData, CertRenewedData, CertRevokedData,
EventKind, emit,
};
use crate::issuer::{CertificateResource, IssuedCertificate};
use crate::runtime::do_with_retry;
use crate::storage::{LockGuard, acquire_lock};
impl Config {
pub async fn manage_sync(&self, ct: &CancellationToken, domain_names: &[String]) -> Result<()> {
self.manage_all(ct, domain_names, false).await
}
pub async fn manage(&self, ct: &CancellationToken, domain_names: &[String]) -> Result<()> {
self.manage_sync(ct, domain_names).await
}
pub async fn manage_domains(&self, domain_names: &[String]) -> Result<()> {
let ct = CancellationToken::new();
self.manage_sync(&ct, domain_names).await
}
pub async fn manage_domains_in_background(&self, domain_names: &[String]) -> Result<()> {
let ct = CancellationToken::new();
self.manage_async(&ct, domain_names).await
}
pub async fn manage_async(
&self,
ct: &CancellationToken,
domain_names: &[String],
) -> Result<()> {
self.manage_all(ct, domain_names, true).await
}
async fn manage_all(
&self,
ct: &CancellationToken,
domain_names: &[String],
async_mode: bool,
) -> Result<()> {
if let Some(on_demand) = &self.options.on_demand {
let mut allowlist = on_demand
.allowlist
.write()
.map_err(|_| Error::Internal("allowlist poisoned".into()))?;
if allowlist.is_none() {
*allowlist = Some(domain_names.iter().map(|n| normalized_name(n)).collect());
}
}
for domain_name in domain_names {
let name = normalized_name(domain_name);
if async_mode {
let job = format!("manage_{name}");
let this = self.clone();
let ct2 = ct.clone();
let name2 = name.clone();
crate::runtime::global_job_manager().submit(&job, move || {
Box::pin(async move {
if let Err(err) = this.manage_one_inner(&ct2, &name2, false).await {
tracing::error!(domain = %name2, error = %err, "async management failed");
}
Ok(())
})
})?;
} else {
self.manage_one(ct, &name).await?;
}
}
Ok(())
}
pub async fn manage_one(&self, ct: &CancellationToken, domain_name: &str) -> Result<()> {
self.manage_one_inner(ct, domain_name, true).await
}
async fn manage_one_inner(
&self,
ct: &CancellationToken,
domain_name: &str,
interactive: bool,
) -> Result<()> {
let name = normalized_name(domain_name);
if self
.options
.on_demand
.as_ref()
.map(|od| {
od.allowlist
.read()
.ok()
.and_then(|g| g.as_ref().map(|set| !set.contains(&name)))
.unwrap_or(false)
})
.unwrap_or(false)
{
return Ok(());
}
match self.cache_managed_certificate(ct, &name).await {
Ok(_) => {}
Err(Error::Storage(StorageError::NotFound(_))) => {
self.obtain_cert(ct, domain_name, interactive).await?;
self.cache_managed_certificate(ct, &name).await?;
}
Err(err) => return Err(err),
}
if let Some(cert) = self
.cert_cache
.get_all_matching_certs(&name)
.into_iter()
.find(|c| c.names.contains(&name) && c.managed())
&& self.cert_needs_renewal(&cert)
{
self.renew_cert(ct, domain_name, false, interactive).await?;
}
Ok(())
}
pub async fn obtain_cert_sync(&self, ct: &CancellationToken, name: &str) -> Result<String> {
self.obtain_cert(ct, name, true).await
}
pub async fn obtain(&self, ct: &CancellationToken, name: &str) -> Result<String> {
self.obtain_cert_sync(ct, name).await
}
pub async fn obtain_cert_non_interactive(
&self,
ct: &CancellationToken,
name: &str,
) -> Result<String> {
self.obtain_cert(ct, name, false).await
}
pub async fn obtain_cert(
&self,
ct: &CancellationToken,
name: &str,
interactive: bool,
) -> Result<String> {
let name = self.transform_subject(ct, name).await;
if !subject_qualifies_for_cert(&name) {
return Err(Error::Certificate(
crate::error::CertificateError::NotAllowed(name),
));
}
if self.storage_has_cert_resources_any_issuer(&name).await? {
tracing::debug!(domain = %name, "certificate already in storage; will not obtain");
return Ok(String::new());
}
if !self.options.disable_storage_check {
self.check_storage().await?;
}
let lock_key = format!("issue_cert_{name}");
let guard = acquire_lock(ct, &self.storage(), &lock_key).await?;
self.cert_store.validate_write_guard(&guard)?;
if self.storage_has_cert_resources_any_issuer(&name).await? {
tracing::debug!(domain = %name, "certificate obtained while waiting for lock");
return Ok(String::new());
}
if interactive {
self.obtain_cert_inner(ct, &name, 0, true, &guard).await
} else {
do_with_retry(ct, |attempt| {
self.obtain_cert_inner(ct, &name, attempt, false, &guard)
})
.await
}
}
async fn obtain_cert_inner(
&self,
ct: &CancellationToken,
name: &str,
attempt: u32,
interactive: bool,
guard: &LockGuard,
) -> Result<String> {
require_live_lock(guard)?;
emit(
self.options.on_event.as_ref(),
self.options.should_emit.as_ref(),
ct,
EventKind::CertObtaining(CertObtainingData {
identifier: name.to_owned(),
renewal: false,
forced: false,
remaining: None,
issuer: None,
}),
)
.await?;
require_live_lock(guard)?;
let issuance = self
.issue_for_name(ct, name, attempt, interactive, None)
.await;
require_live_lock(guard)?;
match issuance {
Ok(issued) => {
let resource = self
.save_cert_resource(
&issued.issuer_key,
name,
&issued.issued,
&issued.private_key_pem,
guard,
)
.await?;
emit(
self.options.on_event.as_ref(),
self.options.should_emit.as_ref(),
ct,
EventKind::CertObtained(CertObtainedData {
identifier: name.to_owned(),
renewal: false,
storage_path: Some(resource.names_key()),
private_key_path: None,
metadata_path: None,
csr_pem: None,
}),
)
.await?;
Ok(resource.names_key())
}
Err(err) => {
let _ = emit(
self.options.on_event.as_ref(),
self.options.should_emit.as_ref(),
ct,
EventKind::CertFailed(CertFailedData {
identifier: name.to_owned(),
issuers: self
.options
.issuers
.iter()
.map(|i| i.issuer_key())
.collect(),
error: err.to_string(),
}),
)
.await;
Err(err)
}
}
}
async fn issue_for_name(
&self,
ct: &CancellationToken,
name: &str,
attempt: u32,
interactive: bool,
replaces: Option<&str>,
) -> Result<IssuedWithKey> {
let key_pem = if self.options.reuse_private_keys {
self.load_existing_private_key(name).await
} else {
None
};
let key_pem = match &key_pem {
Some(pem) => {
let key_der = pem_decode_private_key(pem)?;
let _ = key_pair_from_pkcs8(key_der.secret_der())?;
pem.clone()
}
None => {
let key_der = self.key_source().generate_key()?;
pem_encode_private_key(&key_der)?
}
};
let kp = key_pair_from_pkcs8(pem_decode_private_key(&key_pem)?.secret_der())?;
let csr_der = crate::crypto::generate_csr(
&kp,
&crate::crypto::CsrOptions {
dns_names: vec![name.to_owned()],
ip_addresses: vec![],
must_staple: self.options.must_staple,
},
)?;
let csr = crate::issuer::Csr {
der: csr_der,
dns_names: vec![name.to_owned()],
ip_addresses: vec![],
};
let mut issuers = self.options.issuers.clone();
if issuers.is_empty() {
return Err(Error::Config(ConfigError::Missing(
"no issuers configured".into(),
)));
}
if self.options.issuer_policy == IssuerPolicy::UseFirstRandomIssuer {
use rand::seq::SliceRandom as _;
issuers.shuffle(&mut rand::rng());
}
let mut last_err: Option<Error> = None;
for issuer in &issuers {
if let Err(err) = issuer.pre_check(ct, &[name.to_owned()], interactive).await {
tracing::warn!(issuer = %issuer.issuer_key(), error = %err, "issuer pre-check failed");
last_err = Some(err);
continue;
}
match issuer
.issue_with_replaces(ct, &csr, attempt, replaces)
.await
{
Ok(issued) => {
return Ok(IssuedWithKey {
issuer_key: issuer.issuer_key(),
issued: IssuedCertificate {
certificate: issued.certificate,
metadata: issued.metadata,
},
private_key_pem: key_pem,
});
}
Err(err) => {
tracing::warn!(issuer = %issuer.issuer_key(), error = %err, "issuer failed");
last_err = Some(err);
}
}
}
Err(last_err.unwrap_or(Error::Issuer(IssuerError::AllIssuersFailed)))
}
pub async fn renew_cert_sync(
&self,
ct: &CancellationToken,
name: &str,
force: bool,
) -> Result<()> {
self.renew_cert(ct, name, force, true).await
}
pub async fn renew_cert(
&self,
ct: &CancellationToken,
name: &str,
force: bool,
interactive: bool,
) -> Result<()> {
self.renew_cert_inner(ct, name, force, interactive, false)
.await
}
pub async fn renew_cert_non_interactive(
&self,
ct: &CancellationToken,
name: &str,
force: bool,
) -> Result<()> {
self.renew_cert(ct, name, force, false).await
}
pub async fn renew_cert_compromised(
&self,
ct: &CancellationToken,
name: &str,
interactive: bool,
) -> Result<()> {
self.renew_cert_inner(ct, name, true, interactive, true)
.await
}
async fn renew_cert_inner(
&self,
ct: &CancellationToken,
name: &str,
force: bool,
interactive: bool,
compromised: bool,
) -> Result<()> {
let requested_name = name.to_owned();
let name = self.transform_subject(ct, name).await;
let lock_key = format!("issue_cert_{name}");
let guard = acquire_lock(ct, &self.storage(), &lock_key).await?;
self.cert_store.validate_write_guard(&guard)?;
let (issuer_key, old_resource, old_cert) =
match self.load_cert_resource_any_issuer(&name).await {
Ok(found) => found,
Err(Error::Storage(StorageError::NotFound(_))) => {
drop(guard);
self.obtain_cert(ct, &requested_name, interactive).await?;
return Ok(());
}
Err(err) => return Err(err),
};
if !force {
let stored =
make_certificate(&old_resource.certificate_pem, &old_resource.private_key_pem)?;
if !self.cert_needs_renewal(&stored) {
tracing::debug!(domain = %name, "renewal check: certificate does not need renewal");
return Ok(());
}
}
let remaining = old_cert
.lifetime_remaining(time::OffsetDateTime::now_utc())
.unwrap_or_default();
let replaces = old_cert.ari_replaces_id();
if compromised {
require_live_lock(&guard)?;
self.move_compromised_private_key_locked(&issuer_key, &name, &guard)
.await?;
}
if interactive {
self.renew_cert_locked(
ct,
RenewalContext {
guard: &guard,
name: &name,
force,
issuer_key: &issuer_key,
remaining,
attempt: 0,
interactive: true,
replaces: replaces.as_deref(),
},
)
.await
} else {
do_with_retry(ct, |attempt| {
self.renew_cert_locked(
ct,
RenewalContext {
guard: &guard,
name: &name,
force,
issuer_key: &issuer_key,
remaining,
attempt,
interactive: false,
replaces: replaces.as_deref(),
},
)
})
.await
}
}
async fn move_compromised_private_key_locked(
&self,
issuer_key: &str,
domain: &str,
guard: &LockGuard,
) -> Result<String> {
let key = format!(
"compromised/{}/{}/{}.key",
time::OffsetDateTime::now_utc().unix_timestamp_nanos(),
crate::storage::STORAGE_KEYS.safe(issuer_key),
crate::storage::STORAGE_KEYS.safe(domain)
);
self.cert_store
.move_private_key_with_lock(issuer_key, domain, &key, guard)
.await?;
Ok(key)
}
async fn renew_cert_locked(
&self,
ct: &CancellationToken,
ctx: RenewalContext<'_>,
) -> Result<()> {
require_live_lock(ctx.guard)?;
emit(
self.options.on_event.as_ref(),
self.options.should_emit.as_ref(),
ct,
EventKind::CertObtaining(CertObtainingData {
identifier: ctx.name.to_owned(),
renewal: true,
forced: ctx.force,
remaining: Some(ctx.remaining),
issuer: Some(ctx.issuer_key.to_owned()),
}),
)
.await?;
require_live_lock(ctx.guard)?;
let issuance = self
.issue_for_name(ct, ctx.name, ctx.attempt, ctx.interactive, ctx.replaces)
.await;
require_live_lock(ctx.guard)?;
let issued = issuance?;
self.save_cert_resource(
&issued.issuer_key,
ctx.name,
&issued.issued,
&issued.private_key_pem,
ctx.guard,
)
.await?;
self.reload_managed_certificate(ct, ctx.name).await?;
emit(
self.options.on_event.as_ref(),
self.options.should_emit.as_ref(),
ct,
EventKind::CertRenewed(CertRenewedData {
identifier: ctx.name.to_owned(),
forced: ctx.force,
issuer: issued.issuer_key.clone(),
}),
)
.await?;
Ok(())
}
}
struct RenewalContext<'a> {
guard: &'a LockGuard,
name: &'a str,
force: bool,
issuer_key: &'a str,
remaining: std::time::Duration,
attempt: u32,
interactive: bool,
replaces: Option<&'a str>,
}
fn require_live_lock(guard: &LockGuard) -> Result<()> {
if guard.is_valid() {
Ok(())
} else {
Err(Error::Storage(StorageError::StaleLock(guard.key().into())).no_retry())
}
}
struct IssuedWithKey {
issuer_key: String,
issued: IssuedCertificate,
private_key_pem: Vec<u8>,
}
impl Config {
async fn save_cert_resource(
&self,
issuer_key: &str,
domain: &str,
issued: &IssuedCertificate,
private_key_pem: &[u8],
guard: &LockGuard,
) -> Result<CertificateResource> {
let names = make_certificate(&issued.certificate, private_key_pem)?.names;
let resource = CertificateResource {
sans: names.clone(),
certificate_pem: issued.certificate.clone(),
private_key_pem: private_key_pem.to_vec(),
issuer_data: issued.metadata.clone(),
};
self.cert_store
.save_with_lock(issuer_key, domain, &resource, guard)
.await?;
Ok(resource)
}
pub async fn load_cert_resource_any_issuer(
&self,
domain: &str,
) -> Result<(String, CertificateResource, Certificate)> {
let name = normalized_name(domain);
let storage = self.storage();
let mut prefixes: Vec<String> = self
.options
.issuers
.iter()
.map(|i| i.issuer_key())
.collect();
if prefixes.is_empty() {
prefixes = storage
.list(crate::storage::CERTS_PREFIX, false)
.await
.unwrap_or_default()
.into_iter()
.filter_map(|k| {
k.strip_prefix(&format!("{}/", crate::storage::CERTS_PREFIX))
.map(str::to_owned)
})
.collect();
}
let mut last: Option<Error> = None;
for issuer_key in prefixes {
match self.cert_store.load(&issuer_key, &name).await? {
Some(resource) => {
let mut cert =
make_certificate(&resource.certificate_pem, &resource.private_key_pem)?;
cert.issuer_key = issuer_key.clone();
return Ok((issuer_key, resource, cert));
}
None => {
last = Some(Error::Storage(StorageError::NotFound(name.clone())));
}
}
}
Err(last.unwrap_or(Error::Storage(StorageError::NotFound(name))))
}
pub async fn storage_has_cert_resources_any_issuer(&self, domain: &str) -> Result<bool> {
match self.load_cert_resource_any_issuer(domain).await {
Ok(_) => Ok(true),
Err(Error::Storage(StorageError::NotFound(_))) => Ok(false),
Err(err) => Err(err),
}
}
async fn load_existing_private_key(&self, domain: &str) -> Option<Vec<u8>> {
for issuer in &self.options.issuers {
if let Ok(Some(resource)) = self.cert_store.load(&issuer.issuer_key(), domain).await {
return Some(resource.private_key_pem);
}
}
None
}
async fn check_storage(&self) -> Result<()> {
let mut probe = vec![0_u8; 10_240];
rand::RngExt::fill(&mut rand::rng(), probe.as_mut_slice());
let key = format!(
"{}-probe-check-{:032x}",
crate::storage::CERTS_PREFIX,
rand::rng().random::<u128>()
);
let storage = self.ground_truth_storage();
storage.store(&key, &probe).await?;
let read_result = storage.load(&key).await;
let delete_result = storage.delete(&key).await;
let read_back = read_result?;
delete_result?;
if read_back != probe {
return Err(Error::Storage(StorageError::Other(
"storage self-check read mismatch".into(),
)));
}
Ok(())
}
async fn transform_subject(&self, ct: &CancellationToken, name: &str) -> String {
let normalized = normalized_name(name);
match &self.options.subject_transformer {
Some(f) => f(ct.clone(), normalized).await,
None => normalized,
}
}
pub async fn cache_managed_certificate(
&self,
_ct: &CancellationToken,
domain: &str,
) -> Result<Certificate> {
let name = normalized_name(domain);
match self.load_cert_resource_any_issuer(&name).await {
Ok((_, _resource, mut cert)) => {
cert.managed = true;
self.cert_cache.cache_certificate(cert.clone());
Ok(cert)
}
Err(Error::Storage(StorageError::NotFound(_))) => {
let mut candidates: Vec<String> = Vec::new();
if let Some((_, rest)) = name.split_once('.') {
candidates.push(format!("*.{rest}"));
}
candidates.push(format!("*.{name}"));
for candidate in candidates {
if let Ok((_, _, mut cert)) =
self.load_cert_resource_any_issuer(&candidate).await
{
cert.managed = true;
self.cert_cache.cache_certificate(cert.clone());
return Ok(cert);
}
}
Err(Error::Storage(StorageError::NotFound(name)))
}
Err(err) => Err(err),
}
}
pub async fn reload_managed_certificate(
&self,
_ct: &CancellationToken,
domain: &str,
) -> Result<Certificate> {
let name = normalized_name(domain);
let (_, _, mut fresh) = self.load_cert_resource_any_issuer(&name).await?;
let old = self
.cert_cache
.get_all_matching_certs(&name)
.into_iter()
.find(|c| c.managed());
fresh.managed = true;
match old {
Some(old) => self.cert_cache.replace_certificate(&old, fresh.clone()),
None => {
self.cert_cache.cache_certificate(fresh.clone());
}
}
Ok(fresh)
}
pub async fn revoke_cert(
&self,
ct: &CancellationToken,
name: &str,
reason: crate::issuer::RevocationReason,
_interactive: bool,
) -> Result<()> {
let name = normalized_name(name);
let (_, resource, _) = self.load_cert_resource_any_issuer(&name).await?;
for issuer in &self.options.issuers {
if issuer.revoke(ct, &resource, reason).await.is_ok() {
let _ = emit(
self.options.on_event.as_ref(),
self.options.should_emit.as_ref(),
ct,
EventKind::CertRevoked(CertRevokedData {
identifier: name.clone(),
issuer: issuer.issuer_key(),
reason: reason as u8,
}),
)
.await;
return Ok(());
}
}
Err(Error::Issuer(IssuerError::Other(
"no issuer could revoke the certificate".into(),
)))
}
pub async fn revoke_cert_non_interactive(
&self,
ct: &CancellationToken,
name: &str,
reason: crate::issuer::RevocationReason,
) -> Result<()> {
self.revoke_cert(ct, name, reason, false).await
}
#[must_use]
pub fn cert_needs_renewal(&self, cert: &Certificate) -> bool {
let Some(info) = &cert.info else {
return false;
};
let interval = self
.cert_cache
.options()
.renew_check_interval
.unwrap_or(crate::cache::DEFAULT_RENEW_CHECK_INTERVAL);
crate::certificate::cert_needs_renewal(
info,
&RenewalDecision {
renewal_window_ratio: self.renewal_window_ratio(),
renew_check_interval: interval,
ari: cert.ari.as_ref(),
},
time::OffsetDateTime::now_utc(),
)
}
}
#[cfg(all(test, feature = "file-storage"))]
mod unmanaged_tests {
use super::*;
use crate::issuer::Issuer;
use rcgen::{CertificateParams, KeyPair};
use std::sync::Arc;
#[derive(Debug, Default)]
struct FailingIssuer;
#[async_trait::async_trait]
impl Issuer for FailingIssuer {
async fn issue(
&self,
_ct: &CancellationToken,
_csr: &crate::issuer::Csr,
_attempt: u32,
) -> Result<IssuedCertificate> {
Err(Error::Issuer(IssuerError::Other("no".into())))
}
fn issuer_key(&self) -> String {
"failing".into()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[tokio::test]
async fn unmanaged_certificate_loads_into_cache() {
let dir = tempfile::tempdir().unwrap();
let storage: Arc<dyn crate::storage::Storage> =
crate::storage::FileStorage::new(dir.path());
let cache = Cache::new(CacheOptions::default()).unwrap();
let config = Config::new(
cache,
ConfigOptions {
issuers: vec![Arc::new(FailingIssuer) as Arc<dyn Issuer>],
storage: Some(storage),
..Default::default()
},
)
.unwrap();
let key = KeyPair::generate().unwrap();
let params = CertificateParams::new(vec!["manual.example.com".into()]).unwrap();
let cert = params.self_signed(&key).unwrap();
let cert_path = dir.path().join("manual.crt");
let key_path = dir.path().join("manual.key");
std::fs::write(&cert_path, cert.pem()).unwrap();
std::fs::write(&key_path, key.serialize_pem()).unwrap();
let hash = config
.cache_unmanaged_certificate_pem_file(&cert_path, &key_path, &["imported".to_string()])
.await
.unwrap();
let cached = config
.cert_cache
.get_all_matching_certs("manual.example.com");
assert_eq!(cached.len(), 1);
assert!(!cached[0].managed(), "unmanaged certs are not managed");
assert!(cached[0].has_tag("imported"));
assert_eq!(cached[0].hash(), hash);
Arc::clone(&config.cert_cache).stop_now();
}
}
#[cfg(all(test, feature = "file-storage"))]
mod orchestration_tests {
use super::*;
use crate::issuer::{IssuedCertificate, Issuer, RevocationReason};
use rcgen::{CertificateParams, KeyPair};
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Default)]
struct MockIssuer {
issued: AtomicUsize,
revoked: AtomicUsize,
}
#[async_trait::async_trait]
impl Issuer for MockIssuer {
async fn issue(
&self,
_ct: &CancellationToken,
csr: &crate::issuer::Csr,
_attempt: u32,
) -> Result<IssuedCertificate> {
self.issued.fetch_add(1, Ordering::SeqCst);
Ok(IssuedCertificate {
certificate: crate::test_csr::issue(&csr.der, &csr.dns_names),
metadata: Some(serde_json::json!({"issuer": "mock"})),
})
}
async fn revoke(
&self,
_ct: &CancellationToken,
_resource: &CertificateResource,
_reason: RevocationReason,
) -> Result<()> {
self.revoked.fetch_add(1, Ordering::SeqCst);
Ok(())
}
fn issuer_key(&self) -> String {
"mock-v1".into()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
async fn test_config() -> (Arc<Config>, tempfile::TempDir, Arc<MockIssuer>) {
let dir = tempfile::tempdir().unwrap();
let storage: Arc<dyn crate::storage::Storage> =
crate::storage::FileStorage::new(dir.path());
let issuer = Arc::new(MockIssuer::default());
let cache = Cache::new(CacheOptions::default()).unwrap();
let config = Config::new(
cache,
ConfigOptions {
issuers: vec![issuer.clone() as Arc<dyn Issuer>],
storage: Some(storage),
disable_storage_check: false,
..Default::default()
},
)
.unwrap();
(config, dir, issuer)
}
#[tokio::test]
async fn review_concurrent_storage_probes_are_independent() {
let (config, _dir, _) = test_config().await;
let probes = (0..16).map(|_| config.check_storage());
for result in futures::future::join_all(probes).await {
result.unwrap();
}
assert!(config.storage().list("", true).await.unwrap().is_empty());
config.cache().stop_and_wait().await;
}
#[tokio::test]
async fn review_loaded_certificates_preserve_issuer_identity() {
let (config, _dir, _) = test_config().await;
let ct = CancellationToken::new();
config
.obtain_cert(&ct, "issuer.example.com", true)
.await
.unwrap();
let cert = config
.cache_managed_certificate(&ct, "issuer.example.com")
.await
.unwrap();
assert_eq!(cert.issuer_key, "mock-v1");
config
.cache()
.remove_managed(&[crate::cache::SubjectIssuer {
subject: "issuer.example.com".into(),
issuer_key: Some("mock-v1".into()),
}]);
assert!(config.cache().get_by_hash(cert.hash()).is_none());
config.cache().stop_and_wait().await;
}
#[tokio::test]
async fn review_expired_managed_certificates_are_renewed() {
let (config, _dir, issuer) = test_config().await;
let key = KeyPair::generate().unwrap();
let mut params = CertificateParams::new(vec!["expired-review.example.com".into()]).unwrap();
params
.distinguished_name
.push(rcgen::DnType::CommonName, "expired-review.example.com");
params.not_before = time::OffsetDateTime::now_utc() - time::Duration::days(90);
params.not_after = time::OffsetDateTime::now_utc() - time::Duration::days(1);
let signed = params.self_signed(&key).unwrap();
let resource = CertificateResource {
sans: vec!["expired-review.example.com".into()],
certificate_pem: signed.pem().into_bytes(),
private_key_pem: key.serialize_pem().into_bytes(),
issuer_data: None,
};
config
.cert_store
.save("mock-v1", "expired-review.example.com", &resource)
.await
.unwrap();
let ct = CancellationToken::new();
let old = config
.cache_managed_certificate(&ct, "expired-review.example.com")
.await
.unwrap();
config.cache().renew_managed_certificates().await;
tokio::time::timeout(std::time::Duration::from_secs(3), async {
while config.cache().get_by_hash(old.hash()).is_some() {
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
})
.await
.expect("expired certificate should be replaced");
assert_eq!(issuer.issued.load(Ordering::SeqCst), 1);
config.cache().stop_and_wait().await;
}
#[tokio::test]
async fn obtain_stores_and_caches() {
let (config, _dir, issuer) = test_config().await;
let ct = CancellationToken::new();
config
.manage_sync(&ct, &["example.com".into()])
.await
.unwrap();
let storage = config.storage();
assert!(
storage
.exists("certificates/mock-v1/example.com/example.com.crt")
.await
.unwrap()
);
assert!(
storage
.exists("certificates/mock-v1/example.com/example.com.key")
.await
.unwrap()
);
assert!(
storage
.exists("certificates/mock-v1/example.com/example.com.json")
.await
.unwrap()
);
let cached = config
.cert_cache
.get_all_matching_certs("example.com")
.into_iter()
.find(|c| c.managed())
.expect("managed cert cached");
assert!(cached.names.contains(&"example.com".to_string()));
config
.manage_sync(&ct, &["example.com".into()])
.await
.unwrap();
assert_eq!(issuer.issued.load(Ordering::SeqCst), 1);
Arc::clone(&config.cert_cache).stop_now();
}
#[tokio::test]
async fn renewal_of_missing_certificate_does_not_reenter_lock() {
let (config, _dir, issuer) = test_config().await;
let result = tokio::time::timeout(
std::time::Duration::from_secs(1),
config.renew_cert_sync(&CancellationToken::new(), "missing.example.com", false),
)
.await
.expect("renewal must not deadlock on a missing resource");
result.unwrap();
assert_eq!(issuer.issued.load(Ordering::SeqCst), 1);
Arc::clone(&config.cert_cache).stop_now();
}
#[tokio::test]
async fn compromised_renewal_moves_old_private_key_before_replacement() {
let (config, _dir, issuer) = test_config().await;
let ct = CancellationToken::new();
config
.manage_sync(&ct, &["example.com".into()])
.await
.unwrap();
let source = crate::storage::STORAGE_KEYS.site_private_key("mock-v1", "example.com");
assert!(config.storage().exists(&source).await.unwrap());
let old_key = config.storage().load(&source).await.unwrap();
config
.renew_cert_compromised(&ct, "example.com", true)
.await
.unwrap();
let new_key = config.storage().load(&source).await.unwrap();
assert_ne!(old_key, new_key, "compromised key must be replaced");
let compromised = config.storage().list("compromised", true).await.unwrap();
assert_eq!(compromised.len(), 1);
assert_eq!(
config.storage().load(&compromised[0]).await.unwrap(),
old_key
);
assert_eq!(issuer.issued.load(Ordering::SeqCst), 2);
Arc::clone(&config.cert_cache).stop_now();
}
#[tokio::test]
async fn forced_renew_replaces_certificate() {
let (config, _dir, issuer) = test_config().await;
let ct = CancellationToken::new();
config
.manage_sync(&ct, &["example.com".into()])
.await
.unwrap();
let before = config
.cert_cache
.get_all_matching_certs("example.com")
.into_iter()
.find(|c| c.managed())
.unwrap();
config
.renew_cert_sync(&ct, "example.com", true)
.await
.unwrap();
let after = config
.cert_cache
.get_all_matching_certs("example.com")
.into_iter()
.find(|c| c.managed())
.expect("renewed cert cached");
assert_ne!(
before.hash(),
after.hash(),
"renewal must produce a new cert"
);
assert_eq!(issuer.issued.load(Ordering::SeqCst), 2);
let (_, _, stored) = config
.load_cert_resource_any_issuer("example.com")
.await
.unwrap();
assert_eq!(stored.hash, after.hash());
Arc::clone(&config.cert_cache).stop_now();
}
#[tokio::test]
async fn revoke_via_issuer() {
let (config, _dir, issuer) = test_config().await;
let ct = CancellationToken::new();
config
.manage_sync(&ct, &["example.com".into()])
.await
.unwrap();
config
.revoke_cert(&ct, "example.com", RevocationReason::Superseded, true)
.await
.unwrap();
assert_eq!(issuer.revoked.load(Ordering::SeqCst), 1);
Arc::clone(&config.cert_cache).stop_now();
}
#[tokio::test]
async fn events_fire_in_order() {
let (dir, _tmp) = {
let d = tempfile::tempdir().unwrap();
(d, ())
};
let _ = dir;
let (_config, _dir, _issuer) = test_config().await;
let got_obtained = Arc::new(AtomicUsize::new(0));
let got_renewed = Arc::new(AtomicUsize::new(0));
let got_revoked = Arc::new(AtomicUsize::new(0));
let g2 = Arc::clone(&got_obtained);
let r2 = Arc::clone(&got_renewed);
let v2 = Arc::clone(&got_revoked);
let on_event: crate::events::OnEventFn = Arc::new(move |_ctx, event| {
let g = Arc::clone(&g2);
let r = Arc::clone(&r2);
let v = Arc::clone(&v2);
Box::pin(async move {
match event {
crate::events::EventKind::CertObtained(_) => {
g.fetch_add(1, Ordering::SeqCst);
}
crate::events::EventKind::CertRenewed(_) => {
r.fetch_add(1, Ordering::SeqCst);
}
crate::events::EventKind::CertRevoked(_) => {
v.fetch_add(1, Ordering::SeqCst);
}
_ => {}
}
Ok(())
})
});
let dir2 = tempfile::tempdir().unwrap();
let storage: Arc<dyn crate::storage::Storage> =
crate::storage::FileStorage::new(dir2.path());
let cache = Cache::new(CacheOptions::default()).unwrap();
let config = Config::new(
cache,
ConfigOptions {
issuers: vec![Arc::new(MockIssuer::default()) as Arc<dyn Issuer>],
storage: Some(storage),
on_event: Some(on_event),
..Default::default()
},
)
.unwrap();
config
.manage_sync(&CancellationToken::new(), &["example.com".into()])
.await
.unwrap();
assert!(
got_obtained.load(Ordering::SeqCst) >= 1,
"cert_obtained must fire"
);
config
.renew_cert_sync(&CancellationToken::new(), "example.com", true)
.await
.unwrap();
config
.revoke_cert(
&CancellationToken::new(),
"example.com",
RevocationReason::Superseded,
true,
)
.await
.unwrap();
assert_eq!(got_renewed.load(Ordering::SeqCst), 1);
assert_eq!(got_revoked.load(Ordering::SeqCst), 1);
Arc::clone(&config.cert_cache).stop_now();
}
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("issuers", &self.options.issuers.len())
.field("on_demand", &self.options.on_demand.is_some())
.field("reuse_private_keys", &self.options.reuse_private_keys)
.field("disable_ari", &self.options.disable_ari)
.finish_non_exhaustive()
}
}
impl Config {
pub async fn client_credentials(
&self,
ct: &CancellationToken,
identifiers: &[String],
) -> Result<Vec<rustls::sign::CertifiedKey>> {
self.manage_all(ct, identifiers, false).await?;
let mut out = Vec::with_capacity(identifiers.len());
for identifier in identifiers {
let name = crate::certificate::normalized_name(identifier);
let cert = self
.cert_cache
.get_all_matching_certs(&name)
.into_iter()
.find(|c| c.managed())
.ok_or(Error::Certificate(crate::error::CertificateError::NoNames))?;
let key = cert.private_key.clone().ok_or(Error::Certificate(
crate::error::CertificateError::NoPrivateKey,
))?;
let signing_key =
crate::tls_integration::signing_key_from_der(&key).ok_or_else(|| {
Error::Certificate(crate::error::CertificateError::Parse(
"unsupported private key for rustls client credentials".into(),
))
})?;
out.push(rustls::sign::CertifiedKey::new(
cert.chain.clone(),
signing_key,
));
}
Ok(out)
}
pub async fn client_config(
&self,
ct: &CancellationToken,
identifier: &str,
) -> Result<rustls::ClientConfig> {
self.manage_all(ct, &[identifier.to_owned()], false).await?;
let name = crate::certificate::normalized_name(identifier);
let cert = self
.cert_cache
.get_all_matching_certs(&name)
.into_iter()
.find(|cert| cert.managed())
.ok_or(Error::Certificate(crate::error::CertificateError::NoNames))?;
let private_key = cert
.private_key
.as_ref()
.map(|key| key.as_ref().clone_key())
.ok_or(Error::Certificate(
crate::error::CertificateError::NoPrivateKey,
))?;
crate::tls_integration::install_default_provider();
rustls::ClientConfig::builder()
.with_root_certificates(rustls::RootCertStore::empty())
.with_client_auth_cert(cert.chain, private_key)
.map_err(|error| Error::Config(ConfigError::Invalid(error.to_string())))
}
}
impl Config {
pub async fn cache_unmanaged_certificate(
&self,
_ct: &CancellationToken,
mut cert: Certificate,
tags: &[String],
) -> Result<String> {
cert.managed = false;
cert.tags = tags.to_vec();
let hash = cert.hash().to_owned();
self.cert_cache.cache_certificate(cert);
Ok(hash)
}
pub async fn cache_unmanaged_certificate_pem_file(
&self,
cert_file: &std::path::Path,
key_file: &std::path::Path,
tags: &[String],
) -> Result<String> {
let cert_pem = tokio::fs::read(cert_file).await?;
let key_pem = tokio::fs::read(key_file).await?;
self.cache_unmanaged_certificate_pem_bytes(&cert_pem, &key_pem, tags)
.await
}
pub async fn cache_unmanaged_certificate_pem_bytes(
&self,
cert_pem: &[u8],
key_pem: &[u8],
tags: &[String],
) -> Result<String> {
let mut cert = make_certificate(cert_pem, key_pem)?;
cert.tags = tags.to_vec();
let hash = self.cert_cache.cache_certificate(cert);
Ok(hash)
}
pub async fn cache_unmanaged_certificate_pem_bytes_as_replacement(
&self,
cert_pem: &[u8],
key_pem: &[u8],
tags: &[String],
) -> Result<String> {
let fresh = make_certificate(cert_pem, key_pem)?;
let fresh_tags = tags.to_vec();
let old = self
.cert_cache
.all_matching_certificates(&fresh.names[0])
.into_iter()
.next();
let mut fresh = fresh;
fresh.tags = fresh_tags;
if let Some(old) = old {
self.cert_cache.replace_certificate(&old, fresh.clone());
} else {
self.cert_cache.cache_certificate(fresh.clone());
}
Ok(fresh.hash)
}
#[cfg(feature = "local-cache")]
pub async fn warm_local_cache(&self, domain: &str) -> Result<()> {
if self.options.local_cache.is_none() {
return Ok(());
}
self.cache_managed_certificate(&CancellationToken::new(), domain)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "file-storage")]
#[tokio::test]
async fn config_binds_to_cache() {
let cache = Cache::new(CacheOptions::default()).unwrap();
let cfg = Config::new(cache, ConfigOptions::default()).unwrap();
assert!(cfg.renewal_window_ratio() > 0.0 && cfg.renewal_window_ratio() <= 1.0);
Arc::clone(&cfg.cert_cache).stop_now();
}
#[test]
fn review_rejects_nonfinite_renewal_ratio() {
for ratio in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let cache = Cache::new_without_maintenance(Default::default()).unwrap();
let result = Config::new(
cache,
ConfigOptions {
renewal_window_ratio: ratio,
..Default::default()
},
);
assert!(matches!(
result,
Err(Error::Config(ConfigError::Invalid(_)))
));
}
}
#[tokio::test]
async fn rejects_bad_ratio() {
let cache = Cache::new(CacheOptions::default()).unwrap();
let opts = ConfigOptions {
renewal_window_ratio: 1.5,
..Default::default()
};
assert!(Config::new(cache, opts).is_err());
}
#[cfg(not(feature = "file-storage"))]
#[tokio::test]
async fn portable_config_without_storage_returns_typed_error() {
if crate::storage::STORAGE_DEFAULT.get().is_some() {
return;
}
let cache = Cache::new_without_maintenance(CacheOptions::default()).unwrap();
let error = Config::new(cache, ConfigOptions::default()).unwrap_err();
assert!(matches!(
error,
Error::Config(ConfigError::Missing(message))
if message.contains("no storage backend configured")
));
}
#[test]
fn defaults_include_a_usable_acme_issuer() {
let options = ConfigOptions::default();
assert_eq!(options.issuers.len(), 1);
assert!(options.issuers[0].issuer_key().starts_with("acme-"));
}
#[test]
fn policy_applies_and_round_trips_builtin_key_generator() {
let policy = Policy::default()
.with_renewal_window_ratio(0.42)
.with_key_type(crate::crypto::KeyType::P384)
.with_must_staple(true)
.with_reuse_private_keys(true)
.with_issuer_policy(IssuerPolicy::UseFirstRandomIssuer)
.with_disable_ari(true)
.with_default_server_name("default.example.com")
.with_fallback_server_name("fallback.example.com")
.with_disable_storage_check(true);
let options = ConfigOptions::default().with_policy(policy.clone());
assert_eq!(options.policy(), policy);
assert_eq!(
options
.key_source
.as_ref()
.and_then(|source| source.key_type()),
Some(crate::crypto::KeyType::P384)
);
assert_eq!(options.default_server_name, "default.example.com");
assert_eq!(options.fallback_server_name, "fallback.example.com");
assert!(options.disable_storage_check);
}
#[test]
fn policy_default_preserves_legacy_defaults() {
let options = ConfigOptions::default().with_policy(Policy::default());
assert_eq!(options.policy(), Policy::default());
assert_eq!(options.renewal_window_ratio, 0.0);
assert!(!options.must_staple);
assert!(!options.reuse_private_keys);
assert!(options.default_server_name.is_empty());
assert!(options.fallback_server_name.is_empty());
assert!(!options.disable_storage_check);
}
#[test]
fn policy_and_direct_options_follow_last_setter() {
let policy = Policy::default()
.with_default_server_name("from-policy.example.com")
.with_fallback_server_name("fallback-policy.example.com")
.with_disable_storage_check(true);
let mut options = ConfigOptions::default();
policy.apply_to(&mut options);
options.default_server_name = "from-options.example.com".into();
options.fallback_server_name = "fallback-options.example.com".into();
options.disable_storage_check = false;
assert_eq!(options.default_server_name, "from-options.example.com");
assert_eq!(options.fallback_server_name, "fallback-options.example.com");
assert!(!options.disable_storage_check);
let options = ConfigOptions::default().with_policy(policy);
assert_eq!(options.default_server_name, "from-policy.example.com");
assert_eq!(options.fallback_server_name, "fallback-policy.example.com");
assert!(options.disable_storage_check);
}
#[cfg(feature = "file-storage")]
#[tokio::test]
async fn builder_policy_and_direct_runtime_setters_follow_order() {
let policy = Policy::default()
.with_default_server_name("policy-default.example.com")
.with_fallback_server_name("policy-fallback.example.com")
.with_disable_storage_check(true);
let direct_wins = ConfigBuilder::new()
.policy(policy.clone())
.default_server_name("direct-default.example.com")
.fallback_server_name("direct-fallback.example.com")
.disable_storage_check(false)
.build()
.unwrap();
assert_eq!(
direct_wins.options.default_server_name,
"direct-default.example.com"
);
assert_eq!(
direct_wins.options.fallback_server_name,
"direct-fallback.example.com"
);
assert!(!direct_wins.options.disable_storage_check);
direct_wins.cache().stop_and_wait().await;
let policy_wins = ConfigBuilder::new()
.default_server_name("direct-default.example.com")
.fallback_server_name("direct-fallback.example.com")
.disable_storage_check(false)
.policy(policy)
.build()
.unwrap();
assert_eq!(
policy_wins.options.default_server_name,
"policy-default.example.com"
);
assert_eq!(
policy_wins.options.fallback_server_name,
"policy-fallback.example.com"
);
assert!(policy_wins.options.disable_storage_check);
policy_wins.cache().stop_and_wait().await;
}
#[cfg(feature = "file-storage")]
#[tokio::test]
async fn builder_binds_default_options_to_a_cache() {
let config = Config::builder().build().unwrap();
assert_eq!(config.cache().size(), 0);
assert_eq!(config.options.issuers.len(), 1);
Arc::clone(config.cache()).stop_now();
}
#[cfg(feature = "file-storage")]
#[tokio::test]
async fn default_acme_issuer_inherits_config_storage() {
let dir = tempfile::tempdir().unwrap();
let storage: Arc<dyn crate::storage::Storage> =
crate::storage::FileStorage::new(dir.path());
let cache = Cache::new(CacheOptions::default()).unwrap();
let config = Config::new(
cache,
ConfigOptions {
storage: Some(storage.clone()),
..Default::default()
},
)
.unwrap();
let issuer = config.options.issuers[0]
.as_any()
.downcast_ref::<crate::acme::AcmeIssuer>()
.unwrap();
assert!(Arc::ptr_eq(issuer.storage.as_ref().unwrap(), &storage));
Arc::clone(&config.cert_cache).stop_now();
}
}