diff --git a/crates/network/lib/tls/certgen.rs b/crates/network/lib/tls/certgen.rs index 93f11fdf..405f45dd 100644 --- a/crates/network/lib/tls/certgen.rs +++ b/crates/network/lib/tls/certgen.rs @@ -17,6 +17,8 @@ pub struct DomainCert { pub chain: Vec>, /// Leaf certificate private key. pub key: PrivateKeyDer<'static>, + /// Expiry time for the generated leaf certificate. + pub expires_at: OffsetDateTime, /// Pre-built `ServerConfig` for this domain (avoids per-connection rebuild). pub server_config: std::sync::Arc, } @@ -27,10 +29,12 @@ pub struct DomainCert { /// Generate a certificate for `domain` signed by the given CA. pub fn generate_domain_cert(domain: &str, ca: &CertAuthority, validity_hours: u64) -> DomainCert { - let now = OffsetDateTime::now_utc(); - let params = build_domain_cert_params(domain, validity_hours, now); + let now: OffsetDateTime = OffsetDateTime::now_utc(); + let params: CertificateParams = build_domain_cert_params(domain, validity_hours, now); + let expires_at: OffsetDateTime = params.not_after; - let key_pair = rcgen::KeyPair::generate().expect("failed to generate domain key pair"); + let key_pair: rcgen::KeyPair = + rcgen::KeyPair::generate().expect("failed to generate domain key pair"); let cert_der = params .signed_by(&key_pair, &ca.cert, &ca.key_pair) @@ -51,6 +55,7 @@ pub fn generate_domain_cert(domain: &str, ca: &CertAuthority, validity_hours: u6 DomainCert { chain, key, + expires_at, server_config: std::sync::Arc::new(server_config), } } diff --git a/crates/network/lib/tls/state.rs b/crates/network/lib/tls/state.rs index 752ee3be..75df4bd5 100644 --- a/crates/network/lib/tls/state.rs +++ b/crates/network/lib/tls/state.rs @@ -7,6 +7,7 @@ use lru::LruCache; use rustls::DigitallySignedStruct; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; +use time::{Duration, OffsetDateTime}; use tokio_rustls::TlsConnector; use super::ca::CertAuthority; @@ -51,6 +52,10 @@ enum BypassPattern { #[derive(Debug)] struct NoVerify; +/// Refresh cached leaf certs shortly before expiry so long-lived sandboxes +/// do not start serving an already-expired intercept certificate. +const CERT_REFRESH_WINDOW: Duration = Duration::minutes(5); + //-------------------------------------------------------------------------------------------------- // Methods //-------------------------------------------------------------------------------------------------- @@ -102,7 +107,9 @@ impl TlsState { /// Get or generate a certificate for the given domain. pub fn get_or_generate_cert(&self, domain: &str) -> Arc { let mut cache = self.cert_cache.lock().unwrap(); - if let Some(cert) = cache.get(domain) { + if let Some(cert) = cache.get(domain) + && cert.expires_at > OffsetDateTime::now_utc() + CERT_REFRESH_WINDOW + { return cert.clone(); } @@ -132,6 +139,35 @@ impl TlsState { } } +#[cfg(test)] +mod tests { + use super::*; + use crate::secrets::config::SecretsConfig; + + #[test] + fn regenerates_cached_domain_cert_when_near_expiry() { + let _ = rustls::crypto::ring::default_provider().install_default(); + let state = TlsState::new(TlsConfig::default(), SecretsConfig::default()); + let first = state.get_or_generate_cert("openrouter.ai"); + let original_expires_at = first.expires_at; + + { + let mut cache = state.cert_cache.lock().unwrap(); + let stale = Arc::new(DomainCert { + chain: first.chain.clone(), + key: first.key.clone_key(), + expires_at: OffsetDateTime::now_utc() + Duration::seconds(30), + server_config: first.server_config.clone(), + }); + cache.put("openrouter.ai".to_string(), stale); + } + + let refreshed = state.get_or_generate_cert("openrouter.ai"); + assert!(refreshed.expires_at > OffsetDateTime::now_utc() + Duration::hours(23)); + assert!(refreshed.expires_at > original_expires_at - Duration::minutes(10)); + } +} + //-------------------------------------------------------------------------------------------------- // Trait Implementations //--------------------------------------------------------------------------------------------------