Fix ACME: Add freshness check when renewing certificates
This commit is contained in:
@@ -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(),
|
||||
)
|
||||
})?,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user