Fix ACME: Add freshness check when renewing certificates

This commit is contained in:
Maurus Decimus
2026-06-15 15:19:14 +02:00
parent 2cb765ed37
commit 91f9e77934
4 changed files with 144 additions and 92 deletions

View File

@@ -27,14 +27,13 @@ use x509_parser::prelude::{GeneralName, ParsedExtension};
const HOSTNAMES: &[&str] = &["mta-sts", "ua-auto-config", "autoconfig", "autodiscover"];
impl AcmeRequestBuilder {
pub async fn renew(
pub fn build_domains(
&self,
server: &Server,
domain: &str,
hostnames: &[String],
dns_parameters: Option<AcmeDnsParameters>,
) -> AcmeResult<PemCert> {
let domains = if hostnames.is_empty() {
) -> Vec<String> {
if hostnames.is_empty() {
if matches!(
self.challenge,
ChallengeType::Dns01 | ChallengeType::DnsPersist01
@@ -87,8 +86,15 @@ impl AcmeRequestBuilder {
}
})
.collect()
};
}
}
pub async fn renew(
&self,
server: &Server,
domains: Vec<String>,
dns_parameters: Option<AcmeDnsParameters>,
) -> AcmeResult<PemCert> {
let mut params = CertificateParams::new(domains.clone()).map_err(|err| {
AcmeError::Crypto(format!("Failed to create certificate params: {}", err))
})?;
@@ -339,78 +345,66 @@ impl AcmeRequestBuilder {
impl ParsedCert {
pub fn parse(certificate: impl AsRef<[u8]>) -> AcmeResult<ParsedCert> {
pem::parse_many(certificate)
.map_err(|err| AcmeError::Crypto(format!("Failed to parse PEM: {}", err)))
.and_then(|pems| {
pems.into_iter()
.next()
.ok_or_else(|| AcmeError::Crypto("No certificates found in PEM".to_string()))
})
.and_then(|der| {
parse_x509_certificate(der.contents())
.map_err(|err| {
AcmeError::Crypto(format!("Failed to parse X.509 certificate: {}", err))
})
.and_then(|(_, cert)| {
// Add CNs and SANs to the list of names
let mut names: BTreeSet<String> = BTreeSet::new();
for name in cert.subject().iter_common_name() {
if let Ok(name) = name.as_str() {
names.insert(name.into());
}
}
for ext in cert.extensions() {
if let ParsedExtension::SubjectAlternativeName(san) =
ext.parsed_extension()
{
for name in &san.general_names {
let name = match name {
GeneralName::DNSName(name) => (*name).into(),
GeneralName::IPAddress(ip) => match ip.len() {
4 => Ipv4Addr::from(<[u8; 4]>::try_from(*ip).unwrap())
.to_string(),
16 => {
Ipv6Addr::from(<[u8; 16]>::try_from(*ip).unwrap())
.to_string()
}
_ => continue,
},
_ => {
continue;
}
};
names.insert(name);
}
}
}
let der = pem::parse_many(certificate)
.map_err(|err| AcmeError::Crypto(format!("Failed to parse PEM: {}", err)))?
.into_iter()
.next()
.ok_or_else(|| AcmeError::Crypto("No certificates found in PEM".to_string()))?;
Self::parse_der(der.contents())
}
Ok(ParsedCert {
sans: names.into_iter().collect(),
issuer: cert.tbs_certificate.issuer().to_string(),
valid_not_before: Utc
.timestamp_opt(
cert.tbs_certificate.validity().not_before.timestamp(),
0,
)
.single()
.ok_or_else(|| {
AcmeError::Crypto(
"Certificate not_before time is out of range".to_string(),
)
})?,
valid_not_after: Utc
.timestamp_opt(
cert.tbs_certificate.validity().not_after.timestamp(),
0,
)
.single()
.ok_or_else(|| {
AcmeError::Crypto(
"Certificate not_after time is out of range".to_string(),
)
})?,
})
})
pub fn parse_der(der: &[u8]) -> AcmeResult<ParsedCert> {
parse_x509_certificate(der)
.map_err(|err| AcmeError::Crypto(format!("Failed to parse X.509 certificate: {}", err)))
.and_then(|(_, cert)| {
// Add CNs and SANs to the list of names
let mut names: BTreeSet<String> = BTreeSet::new();
for name in cert.subject().iter_common_name() {
if let Ok(name) = name.as_str() {
names.insert(name.into());
}
}
for ext in cert.extensions() {
if let ParsedExtension::SubjectAlternativeName(san) = ext.parsed_extension() {
for name in &san.general_names {
let name = match name {
GeneralName::DNSName(name) => (*name).into(),
GeneralName::IPAddress(ip) => match ip.len() {
4 => Ipv4Addr::from(<[u8; 4]>::try_from(*ip).unwrap())
.to_string(),
16 => Ipv6Addr::from(<[u8; 16]>::try_from(*ip).unwrap())
.to_string(),
_ => continue,
},
_ => {
continue;
}
};
names.insert(name);
}
}
}
Ok(ParsedCert {
sans: names.into_iter().collect(),
issuer: cert.tbs_certificate.issuer().to_string(),
valid_not_before: Utc
.timestamp_opt(cert.tbs_certificate.validity().not_before.timestamp(), 0)
.single()
.ok_or_else(|| {
AcmeError::Crypto(
"Certificate not_before time is out of range".to_string(),
)
})?,
valid_not_after: Utc
.timestamp_opt(cert.tbs_certificate.validity().not_after.timestamp(), 0)
.single()
.ok_or_else(|| {
AcmeError::Crypto(
"Certificate not_after time is out of range".to_string(),
)
})?,
})
})
}
}

View File

@@ -55,10 +55,24 @@ impl Server {
cert.acme_provider_id
)));
};
let challenge_type = acme_provider.challenge_type;
let renew_before = acme_provider.renew_before;
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()) {
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 acme_provider.challenge_type == AcmeChallengeType::Dns01 =>
{
DnsManagement::Automatic(props) if challenge_type == AcmeChallengeType::Dns01 => {
match self.build_dns_updater(props.dns_server_id).await? {
Ok(updater) => Some(AcmeDnsParameters {
updater,
@@ -74,22 +88,13 @@ impl Server {
}
_ => None,
};
if acme_provider.challenge_type == AcmeChallengeType::Dns01 && dns_parameters.is_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 renew_before = acme_provider.renew_before;
let pem_cert = AcmeRequestBuilder::new(acme_provider)
.await?
.renew(
self,
&domain.name,
&cert.subject_alternative_names.into_inner(),
dns_parameters,
)
.await?;
let pem_cert = request.renew(self, domains, dns_parameters).await?;
let parsed_cert = ParsedCert::parse(&pem_cert.certificate)?;
let mut new_sans = parsed_cert.sans.clone();
new_sans.sort();
@@ -205,4 +210,56 @@ impl Server {
err => Err(AcmeError::Registry(err)),
}
}
fn acme_certificate_renewal_due(
&self,
domains: &[String],
renew_before: AcmeRenewBefore,
now: u64,
) -> Option<u64> {
let mut target = domains.iter().map(|d| d.as_str()).collect::<Vec<_>>();
target.sort();
let now = now as i64;
let certificates = self.inner.data.tls_certificates.load();
for name in &target {
let Some(certified_key) = certificates.get(name.strip_prefix("*.").unwrap_or(name))
else {
continue;
};
let Ok(leaf) = certified_key.end_entity_cert() else {
continue;
};
let Ok(parsed) = ParsedCert::parse_der(leaf.as_ref()) else {
continue;
};
let mut sans = parsed.sans;
sans.sort();
if sans != target {
continue;
}
let not_valid_after = parsed.valid_not_after.timestamp();
if not_valid_after <= now {
return None;
}
let not_valid_before = parsed.valid_not_before.timestamp();
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),
};
let renew_at = not_valid_before + total * numerator / denominator;
return if now < renew_at {
Some(renew_at as u64)
} else {
None
};
}
None
}
}