Files
inbuxa-server/crates/common/src/network/acme/renew.rs
T
jcoffey-dev beb6c33e63
ci / fork-checks (pull_request) Successful in 14s
ci / build (pull_request) Successful in 16m46s
ACME: a renewal that isn't due yet is rescheduled, not failed for good
When a valid certificate already covered a domain's names (one stored
by hand before the domain was switched to automatic, for instance), the
renewal task ended with NotDue, which the task manager treats as a
permanent failure. Nothing rescheduled it, so the certificate expired
unrenewed. The renewal now returns a new AcmeRenewal task due when the
certificate falls due, the same way a successful renewal does, and logs
it as a backoff.

The ACME integration suite checks that renewing again right after
issuance hands back one AcmeRenewal for that domain, due at the
certificate's renewal point.
2026-09-28 12:15:05 -07:00

326 lines
12 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use crate::{
Server,
ipc::{BroadcastEvent, RegistryChange},
network::acme::{
AcmeDnsParameters, AcmeError, AcmeResult, ParsedCert, directory::AcmeRequestBuilder,
},
};
use registry::{
schema::{
enums::{AcmeChallengeType, AcmeRenewBefore, DnsRecordType},
prelude::{ObjectType, Property},
structs::{
AcmeProvider, Certificate, CertificateManagement, DnsManagement, Domain, PublicText,
PublicTextValue, SecretText, SecretTextValue, SystemSettings, Task, TaskDnsManagement,
TaskDomainManagement, TaskStatus,
},
},
types::{datetime::UTCDateTime, id::ObjectId, map::Map},
};
use store::{
registry::{
RegistryQuery,
write::{RegistryWrite, RegistryWriteResult},
},
write::now,
};
use types::id::Id;
impl Server {
pub async fn acme_renew(&self, domain_id: Id) -> AcmeResult<Vec<Task>> {
let Some(domain) = self.registry().object::<Domain>(domain_id).await? else {
return Err(AcmeError::Invalid(format!(
"Domain with ID {} not found",
domain_id
)));
};
let cert = match domain.certificate_management {
CertificateManagement::Manual => {
return Err(AcmeError::Invalid(
"ACME not configured for domain".to_string(),
));
}
CertificateManagement::Automatic(props) => props,
};
let Some(acme_provider) = self
.registry()
.object::<AcmeProvider>(cert.acme_provider_id)
.await?
else {
return Err(AcmeError::Invalid(format!(
"ACME provider with ID {} not found",
cert.acme_provider_id
)));
};
let challenge_type = acme_provider.challenge_type;
let renew_before = acme_provider.renew_before;
let reuse_key = acme_provider.reuse_key;
let request = AcmeRequestBuilder::new(acme_provider).await?;
let domains = request.build_domains(
self,
&domain.name,
&cert.subject_alternative_names.into_inner(),
);
if let Some(renew_at) = self
.acme_certificate_renewal_due(&domains, renew_before, now())
.await?
{
// INBUXA: a certificate already covering these names (one stored by
// hand before the domain was switched to automatic, say) isn't a
// failure: schedule the renewal for when it falls due. Returning
// NotDue here ended the task for good, and nothing renewed the
// certificate before it expired.
trc::event!(
Acme(trc::AcmeEvent::RenewBackoff),
Domain = domain.name.clone(),
Hostname = domains.as_slice(),
Details = "A valid certificate already covers these names",
NextRetry = trc::Value::Timestamp(renew_at),
);
return Ok(vec![Task::AcmeRenewal(TaskDomainManagement {
domain_id,
status: TaskStatus::at(renew_at as i64),
})]);
}
let dns_parameters = match &domain.dns_management {
DnsManagement::Automatic(props) if challenge_type == AcmeChallengeType::Dns01 => {
match self.build_dns_updater(props.dns_server_id).await? {
Ok(updater) => Some(AcmeDnsParameters {
updater,
origin: props.origin.clone(),
}),
Err(err) => {
return Err(AcmeError::Invalid(format!(
"Failed to build DNS updater: {}",
err
)));
}
}
}
_ => None,
};
if challenge_type == AcmeChallengeType::Dns01 && dns_parameters.is_none() {
return Err(AcmeError::Invalid(
"ACME provider requires DNS challenge but a DNS provider was not configured"
.to_string(),
));
}
let reuse_key_pem = if reuse_key {
match self.acme_certificate_by_domains(&domains).await? {
Some(certificate) => certificate
.private_key
.secret()
.await
.map(std::borrow::Cow::into_owned)
.map_err(|err| {
AcmeError::Crypto(format!("Failed to load certificate private key: {err}"))
})?
.into(),
None => None,
}
} else {
None
};
let pem_cert = request
.renew(self, domains, reuse_key_pem, dns_parameters)
.await?;
let parsed_cert = ParsedCert::parse(&pem_cert.certificate)?;
let mut new_sans = parsed_cert.sans.clone();
new_sans.sort();
let certificate = Certificate {
private_key: SecretText::Text(SecretTextValue {
secret: pem_cert.private_key,
}),
certificate: PublicText::Text(PublicTextValue {
value: pem_cert.certificate,
}),
issuer: parsed_cert.issuer,
not_valid_after: UTCDateTime::from_timestamp(parsed_cert.valid_not_after.timestamp()),
not_valid_before: UTCDateTime::from_timestamp(parsed_cert.valid_not_before.timestamp()),
subject_alternative_names: Map::new(parsed_cert.sans),
};
let now = now();
let expires_in = (parsed_cert.valid_not_after.timestamp() as u64).saturating_sub(now);
if expires_in < 3600 {
return Err(AcmeError::Invalid(format!(
"Certificate expires in {} seconds, expected at least 3600 seconds",
expires_in
)));
}
match self
.registry()
.write(RegistryWrite::insert(&certificate.into()))
.await?
{
RegistryWriteResult::Success(id) => {
// Repoint the default certificate to the renewed object when it
// tracks the same SAN set, so its id does not go stale
if let Some(old) = self
.registry()
.get(ObjectType::SystemSettings.singleton())
.await?
{
let mut settings = SystemSettings::from(old.clone());
if let Some(default_id) = settings.default_certificate_id
&& let Some(default_cert) =
self.registry().object::<Certificate>(default_id).await?
{
let mut default_sans =
default_cert.subject_alternative_names.clone().into_inner();
default_sans.sort();
if default_sans == new_sans {
settings.default_certificate_id = Some(id);
if let Err(err) = self
.registry()
.write(RegistryWrite::update(
Id::singleton(),
&settings.into(),
&old,
))
.await
{
trc::error!(
err.details(
"Failed to update default certificate after ACME renewal."
)
.caused_by(trc::location!())
);
}
}
}
}
// Reload registry
let change = RegistryChange::Insert(ObjectId::new(ObjectType::Certificate, id));
Box::pin(self.reload_registry(change)).await?;
self.cluster_broadcast(BroadcastEvent::RegistryChange(change))
.await;
let mut tasks = Vec::new();
let renew_at = Self::acme_renewal_due_at(
parsed_cert.valid_not_before.timestamp(),
parsed_cert.valid_not_after.timestamp(),
renew_before,
);
trc::event!(
Acme(trc::AcmeEvent::OrderCompleted),
Domain = domain.name.clone(),
Hostname = new_sans.as_slice(),
Id = id.to_string(),
ValidFrom =
trc::Value::Timestamp(parsed_cert.valid_not_before.timestamp() as u64),
ValidTo = trc::Value::Timestamp(parsed_cert.valid_not_after.timestamp() as u64),
NextRetry = trc::Value::Timestamp(renew_at as u64),
);
tasks.push(Task::AcmeRenewal(TaskDomainManagement {
domain_id,
status: TaskStatus::at(renew_at),
}));
// Update TLSA records
if let DnsManagement::Automatic(props) = &domain.dns_management
&& props.publish_records.contains(&DnsRecordType::Tlsa)
{
tasks.push(Task::DnsManagement(TaskDnsManagement {
domain_id,
on_success_renew_certificate: false,
status: TaskStatus::now(),
update_records: Map::new(vec![DnsRecordType::Tlsa]),
}));
}
Ok(tasks)
}
err => Err(AcmeError::Registry(err)),
}
}
async fn acme_certificate_by_domains(
&self,
domains: &[String],
) -> AcmeResult<Option<Certificate>> {
let mut wanted = domains.iter().collect::<Vec<_>>();
wanted.sort();
let Some(reference) = wanted.first() else {
return Ok(None);
};
let candidate_ids = self
.registry()
.query::<Vec<Id>>(
RegistryQuery::new(ObjectType::Certificate)
.text(Property::SubjectAlternativeNames, reference.as_str()),
)
.await?;
for id in candidate_ids {
let Some(certificate) = self.registry().object::<Certificate>(id).await? else {
continue;
};
let mut sans = certificate
.subject_alternative_names
.iter()
.collect::<Vec<_>>();
sans.sort();
if sans == wanted {
return Ok(Some(certificate));
}
}
Ok(None)
}
async fn acme_certificate_renewal_due(
&self,
domains: &[String],
renew_before: AcmeRenewBefore,
now: u64,
) -> AcmeResult<Option<u64>> {
let now = now as i64;
let Some(certificate) = self.acme_certificate_by_domains(domains).await? else {
return Ok(None);
};
let not_valid_after = certificate.not_valid_after.timestamp();
if not_valid_after <= now {
return Ok(None);
}
let not_valid_before = certificate.not_valid_before.timestamp();
let renew_at = Self::acme_renewal_due_at(not_valid_before, not_valid_after, renew_before);
Ok(if now < renew_at {
Some(renew_at as u64)
} else {
None
})
}
fn acme_renewal_due_at(
not_valid_before: i64,
not_valid_after: i64,
renew_before: AcmeRenewBefore,
) -> i64 {
let total = not_valid_after.saturating_sub(not_valid_before);
let (numerator, denominator) = match renew_before {
AcmeRenewBefore::R12 => (1, 2),
AcmeRenewBefore::R23 => (2, 3),
AcmeRenewBefore::R34 => (3, 4),
AcmeRenewBefore::R45 => (4, 5),
};
not_valid_before + total * numerator / denominator
}
}