use std::path::PathBuf; use anyhow::Context; use quinn::ServerConfig; use quinn_proto::crypto::rustls::QuicServerConfig; use rcgen::generate_simple_self_signed; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use rustls::version::TLS13; /// Ensure a self-signed certificate exists on disk and return a QUIC server config. /// When `production` is true, cert and key must already exist (no auto-generation). pub fn build_server_config( cert_path: &PathBuf, key_path: &PathBuf, production: bool, ) -> anyhow::Result { if !cert_path.exists() || !key_path.exists() { if production { anyhow::bail!( "TLS cert or key missing at {:?} / {:?}; production mode forbids auto-generation", cert_path, key_path ); } generate_self_signed_cert(cert_path, key_path)?; } let cert_bytes = std::fs::read(cert_path).context("read cert")?; let key_bytes = std::fs::read(key_path).context("read key")?; // Validate certificate expiry and warn about self-signed certs. validate_certificate(&cert_bytes)?; let cert_chain = vec![CertificateDer::from(cert_bytes)]; let key = PrivateKeyDer::try_from(key_bytes).map_err(|_| anyhow::anyhow!("invalid key"))?; let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&TLS13]) .with_no_client_auth() .with_single_cert(cert_chain, key)?; tls.alpn_protocols = vec![b"capnp".to_vec()]; let crypto = QuicServerConfig::try_from(tls) .map_err(|e| anyhow::anyhow!("invalid server TLS config: {e}"))?; Ok(ServerConfig::with_crypto(std::sync::Arc::new(crypto))) } fn generate_self_signed_cert(cert_path: &PathBuf, key_path: &PathBuf) -> anyhow::Result<()> { if let Some(parent) = cert_path.parent() { std::fs::create_dir_all(parent).context("create cert dir")?; } if let Some(parent) = key_path.parent() { std::fs::create_dir_all(parent).context("create key dir")?; } let subject_alt_names = vec![ "localhost".to_string(), "127.0.0.1".to_string(), "::1".to_string(), ]; let issued = generate_simple_self_signed(subject_alt_names)?; let key_der = issued.key_pair.serialize_der(); std::fs::write(cert_path, issued.cert.der()).context("write cert")?; std::fs::write(key_path, &key_der).context("write key")?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let perms = std::fs::Permissions::from_mode(0o600); std::fs::set_permissions(key_path, perms).context("set key permissions")?; } tracing::info!( cert = %cert_path.display(), key = %key_path.display(), "generated self-signed TLS certificate" ); Ok(()) } /// Validate a DER-encoded X.509 certificate: bail if expired, warn if expiring /// soon or self-signed. fn validate_certificate(der_bytes: &[u8]) -> anyhow::Result<()> { use x509_parser::prelude::*; let (_, cert) = X509Certificate::from_der(der_bytes) .map_err(|e| anyhow::anyhow!("failed to parse X.509 certificate: {e}"))?; let validity = cert.validity(); let now = ASN1Time::now(); if !validity.is_valid_at(now) { anyhow::bail!( "TLS certificate expired (not_after: {})", validity.not_after ); } // Warn if expiring within 30 days. let thirty_days = std::time::Duration::from_secs(30 * 24 * 60 * 60); let cutoff = now.timestamp() + thirty_days.as_secs() as i64; if validity.not_after.timestamp() < cutoff { tracing::warn!( not_after = %validity.not_after, "TLS certificate expires within 30 days" ); } // Warn if self-signed (issuer == subject). if cert.issuer() == cert.subject() { tracing::warn!("TLS certificate is self-signed (issuer == subject)"); } Ok(()) }