use anyhow::{Context, Result};
use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, KeyPair, SanType};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use std::collections::HashMap;
use std::sync::Arc;
use time::{Duration, OffsetDateTime};
use tokio::sync::RwLock;
use tracing::{debug, info, trace};
pub const MAX_CACHED_CERTIFICATES: usize = 1024;
struct CachedCert {
cert_der: Vec<u8>,
key_der: Vec<u8>,
generated_at: crate::utils::clock::Instant,
}
pub struct CertificateCache {
ca_cert: Arc<Certificate>,
ca_key_pair: Arc<KeyPair>,
ca_params: Arc<CertificateParams>,
cache: Arc<RwLock<HashMap<String, CachedCert>>>,
cert_ttl_secs: u64,
}
impl CertificateCache {
pub fn new(ca_cert: Certificate, ca_key_pair: KeyPair, ca_params: CertificateParams) -> Self {
Self {
ca_cert: Arc::new(ca_cert),
ca_key_pair: Arc::new(ca_key_pair),
ca_params: Arc::new(ca_params),
cache: Arc::new(RwLock::new(HashMap::new())),
cert_ttl_secs: 24 * 60 * 60, }
}
pub async fn get_or_generate(
&self,
domain: &str,
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
let domain_normalized = domain.trim().to_lowercase();
{
let cache = self.cache.read().await;
if let Some(cached) = cache.get(&domain_normalized) {
let age = cached.generated_at.elapsed().as_secs();
if age < self.cert_ttl_secs {
trace!(
"Certificate cache HIT for domain '{}' (age: {}s)",
domain_normalized,
age
);
return Self::to_rustls(&cached.cert_der, &cached.key_der);
} else {
debug!(
"Certificate cache EXPIRED for domain '{}' (age: {}s > {}s)",
domain_normalized, age, self.cert_ttl_secs
);
}
} else {
debug!("Certificate cache MISS for domain '{}'", domain_normalized);
}
}
info!(
"Generating new leaf certificate for domain '{}'",
domain_normalized
);
let (cert, key_pair) = self.generate_leaf_cert(&domain_normalized)?;
let cert_der = cert.der().to_vec();
let key_der = key_pair.serialize_der();
{
let mut cache = self.cache.write().await;
if !cache.contains_key(&domain_normalized) && cache.len() >= MAX_CACHED_CERTIFICATES {
let oldest = cache
.iter()
.min_by_key(|(_, entry)| entry.generated_at)
.map(|(domain, _)| domain.clone());
if let Some(oldest) = oldest {
cache.remove(&oldest);
}
}
cache.insert(
domain_normalized.clone(),
CachedCert {
cert_der: cert_der.clone(),
key_der: key_der.clone(),
generated_at: crate::utils::clock::Instant::now(),
},
);
debug!(
"Cached certificate for domain '{}' (cache size: {})",
domain_normalized,
cache.len()
);
}
Self::to_rustls(&cert_der, &key_der)
}
fn generate_leaf_cert(&self, domain: &str) -> Result<(Certificate, KeyPair)> {
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, domain);
dn.push(DnType::OrganizationName, "NetGet MITM Proxy");
params.distinguished_name = dn;
params.subject_alt_names = vec![SanType::DnsName(
domain
.to_string()
.try_into()
.context("Invalid domain name")?,
)];
if !domain.starts_with("*.") && !domain.starts_with("www.") {
let wildcard_domain = format!("*.{}", domain);
if let Ok(wildcard_san) = wildcard_domain.try_into() {
params
.subject_alt_names
.push(SanType::DnsName(wildcard_san));
}
}
if !domain.starts_with("www.") {
let www_domain = format!("www.{}", domain);
if let Ok(www_san) = www_domain.try_into() {
params.subject_alt_names.push(SanType::DnsName(www_san));
}
}
let now = OffsetDateTime::now_utc();
params.not_before = now;
params.not_after = now + Duration::days(1);
params.is_ca = rcgen::IsCa::NoCa;
let key_pair =
KeyPair::generate().context("Failed to generate key pair for leaf certificate")?;
let issuer = rcgen::Issuer::new((*self.ca_params).clone(), self.ca_key_pair.as_ref());
let cert = params
.signed_by(&key_pair, &issuer)
.context("Failed to sign leaf certificate with CA")?;
info!(
"Successfully generated leaf certificate for domain '{}' (valid for 24h, {} SANs)",
domain,
params.subject_alt_names.len()
);
trace!("Leaf certificate SANs: {:?}", params.subject_alt_names);
Ok((cert, key_pair))
}
pub fn get_ca_cert(&self) -> &Certificate {
&self.ca_cert
}
pub fn ca_cert_pem(&self) -> String {
self.ca_cert.pem()
}
pub async fn cleanup_expired(&self) {
let mut cache = self.cache.write().await;
let initial_size = cache.len();
cache.retain(|domain, cached| {
let age = cached.generated_at.elapsed().as_secs();
if age >= self.cert_ttl_secs {
debug!(
"Removing expired certificate for domain '{}' (age: {}s)",
domain, age
);
false
} else {
true
}
});
let removed = initial_size - cache.len();
if removed > 0 {
info!(
"Cleaned up {} expired certificates from cache (remaining: {})",
removed,
cache.len()
);
}
}
pub async fn get_stats(&self) -> CacheStats {
let cache = self.cache.read().await;
let total_certs = cache.len();
let mut expired_count = 0;
for cached in cache.values() {
let age = cached.generated_at.elapsed().as_secs();
if age >= self.cert_ttl_secs {
expired_count += 1;
}
}
CacheStats {
total_certificates: total_certs,
expired_certificates: expired_count,
valid_certificates: total_certs - expired_count,
}
}
fn to_rustls(
cert_der: &[u8],
key_der: &[u8],
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
let cert = CertificateDer::from(cert_der.to_vec());
let key = PrivateKeyDer::try_from(key_der.to_vec())
.map_err(|e| anyhow::anyhow!("Failed to parse private key DER: {}", e))?;
Ok((vec![cert], key))
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub total_certificates: usize,
pub expired_certificates: usize,
pub valid_certificates: usize,
}