diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b608e2..4566ffa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If ## Fixed - DANE: Verify DNSSEC is supported by the resolver before attempting to validate TLSA records. +- TLS: Update search index when file-backed certificates are refreshed. ## [0.16.10] - 2026-06-21 diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index bce65f06..df5660f1 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -4,11 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::network::acme::ParsedCert; use ahash::{AHashMap, AHashSet}; use rcgen::generate_simple_self_signed; -use registry::schema::{ - prelude::Object, - structs::{Certificate, SystemSettings}, +use registry::{ + schema::{ + prelude::Object, + structs::{Certificate, PublicText, SecretText, SystemSettings}, + }, + types::{datetime::UTCDateTime, map::Map}, }; use rustls::{ SupportedProtocolVersion, @@ -39,20 +43,61 @@ pub(crate) async fn parse_certificates( let mut certs_expired = Vec::new(); let mut certs_expirations = AHashMap::new(); for cert_obj in bp.list_infallible::().await { - let not_valid_after = cert_obj.object.not_valid_after.timestamp(); - let not_valid_before = cert_obj.object.not_valid_before.timestamp(); + let obj_id = cert_obj.id; + let revision = cert_obj.revision; + let mut cert = cert_obj.object; + + let is_file_backed = matches!(cert.certificate, PublicText::File(_)) + || matches!(cert.private_key, SecretText::File(_)); + let mut public = None; + let mut refreshed_meta = None; + if is_file_backed { + let pem = match cert.certificate.value().await { + Ok(value) => value.into_owned().into_bytes(), + Err(err) => { + bp.build_error(obj_id, format!("Failed to obtain certificate value: {err}")); + continue; + } + }; + match ParsedCert::parse(&pem) { + Ok(parsed) => { + let not_valid_after = + UTCDateTime::from_timestamp(parsed.valid_not_after.timestamp()); + let not_valid_before = + UTCDateTime::from_timestamp(parsed.valid_not_before.timestamp()); + let sans = Map::new(parsed.sans); + if cert.not_valid_after != not_valid_after + || cert.not_valid_before != not_valid_before + || cert.issuer != parsed.issuer + || cert.subject_alternative_names != sans + { + refreshed_meta = + Some((not_valid_after, not_valid_before, parsed.issuer, sans)); + } + public = Some(pem); + } + Err(err) => { + bp.build_error(obj_id, format!("Invalid certificate: {err}")); + continue; + } + } + } + + let (not_valid_after, not_valid_before) = match refreshed_meta.as_ref() { + Some((after, before, _, _)) => (after.timestamp(), before.timestamp()), + None => ( + cert.not_valid_after.timestamp(), + cert.not_valid_before.timestamp(), + ), + }; if not_valid_after <= now { certs_expired.push(( - cert_obj.id, - cert_obj - .object - .subject_alternative_names - .clone() - .into_inner(), + obj_id, + cert.subject_alternative_names.clone().into_inner(), Object { - inner: cert_obj.object.into(), - revision: cert_obj.revision, + inner: cert.into(), + revision, }, )); continue; @@ -60,33 +105,57 @@ pub(crate) async fn parse_certificates( continue; // Skip certificates that are not yet valid } - let mut cert = cert_obj.object; let secret = match cert.private_key.secret().await { Ok(secret) => secret.into_owned().into_bytes(), Err(err) => { bp.build_error( - cert_obj.id, + obj_id, format!("Failed to obtain private key secret: {err}"), ); continue; } }; - let public = match cert.certificate.value().await { - Ok(value) => value.into_owned().into_bytes(), - Err(err) => { - bp.build_error( - cert_obj.id, - format!("Failed to obtain certificate value: {err}"), - ); - continue; - } + let public = match public { + Some(public) => public, + None => match cert.certificate.value().await { + Ok(value) => value.into_owned().into_bytes(), + Err(err) => { + bp.build_error(obj_id, format!("Failed to obtain certificate value: {err}")); + continue; + } + }, }; + if let Some((not_valid_after, not_valid_before, issuer, sans)) = refreshed_meta { + let old = Object { + inner: cert.clone().into(), + revision, + }; + cert.not_valid_after = not_valid_after; + cert.not_valid_before = not_valid_before; + cert.issuer = issuer; + cert.subject_alternative_names = sans; + let new = Object { + inner: cert.clone().into(), + revision, + }; + if let Err(err) = bp + .registry + .write(RegistryWrite::update(obj_id.id(), &new, &old)) + .await + { + trc::error!( + err.details("Failed to refresh TLS certificate metadata in registry.") + .caused_by(trc::location!()) + ); + } + } + // Add default certificate if system .default_certificate_id .as_ref() - .is_some_and(|id| *id == cert_obj.id.id()) + .is_some_and(|id| *id == obj_id.id()) { cert.subject_alternative_names .push_unchecked("*".to_string()); @@ -120,7 +189,7 @@ pub(crate) async fn parse_certificates( } } Err(err) => { - bp.build_error(cert_obj.id, format!("Invalid certificate: {err}")); + bp.build_error(obj_id, format!("Invalid certificate: {err}")); } } } diff --git a/crates/common/src/config/smtp/mod.rs b/crates/common/src/config/smtp/mod.rs index 72c94017..64f78989 100644 --- a/crates/common/src/config/smtp/mod.rs +++ b/crates/common/src/config/smtp/mod.rs @@ -61,11 +61,12 @@ impl SmtpConfig { }; if !config.resolvers.dnssec_available - && config - .queue - .tls_strategy - .values() - .any(|t| !matches!(t.dane, RequireOptional::Disable)) + && (config.queue.tls_strategy.is_empty() + || config + .queue + .tls_strategy + .values() + .any(|t| !matches!(t.dane, RequireOptional::Disable))) { bp.build_warning( ObjectType::DnsResolver.singleton(),