use rcgen::{CertificateParams, DistinguishedName, DnType, IsCa, SanType}; use std::{ fs, io::Write, net::{IpAddr, Ipv4Addr}, path::{Path, PathBuf}, }; use time::{Duration, OffsetDateTime}; fn config_dir() -> PathBuf { std::env::var_os("XDG_CONFIG_HOME") .map(PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".config"))) .unwrap_or_else(|| PathBuf::from(".")) .join("socktop_agent") .join("tls") } pub fn cert_paths() -> (PathBuf, PathBuf) { let dir = config_dir(); (dir.join("cert.pem"), dir.join("key.pem")) } pub fn ensure_self_signed_cert() -> anyhow::Result<(PathBuf, PathBuf)> { let (cert_path, key_path) = cert_paths(); if cert_path.exists() && key_path.exists() { // Keys generated by agents older than 1.60 were written with the // default umask (typically 0644): tighten them on startup. #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Ok(meta) = fs::metadata(&key_path) && meta.permissions().mode() & 0o077 != 0 { let _ = fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600)); } } return Ok((cert_path, key_path)); } fs::create_dir_all(cert_path.parent().unwrap())?; let hostname = hostname::get() .ok() .and_then(|s| s.into_string().ok()) .unwrap_or_else(|| "localhost".to_string()); let mut params = CertificateParams::new(vec![hostname.clone(), "localhost".into()])?; params .subject_alt_names .push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))); params.subject_alt_names.push(SanType::IpAddress(IpAddr::V6( ::std::net::Ipv6Addr::LOCALHOST, ))); params .subject_alt_names .push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::UNSPECIFIED))); // Allow operator to provide extra SANs (comma-separated), e.g. IPs or DNS names if let Ok(extra) = std::env::var("SOCKTOP_AGENT_EXTRA_SANS") { for raw in extra.split(',') { let s = raw.trim(); if s.is_empty() { continue; } if let Ok(ip) = s.parse::() { params.subject_alt_names.push(SanType::IpAddress(ip)); } else { match s.to_string().try_into() { Ok(dns) => params.subject_alt_names.push(SanType::DnsName(dns)), Err(_) => eprintln!("socktop_agent: ignoring invalid SAN entry: {s}"), } } } } let mut dn = DistinguishedName::new(); dn.push(DnType::CommonName, hostname.clone()); params.distinguished_name = dn; params.is_ca = IsCa::NoCa; // Dynamic validity: start slightly in the past to avoid clock skew issues, end ~397 days later let now = OffsetDateTime::now_utc(); params.not_before = now - Duration::minutes(5); params.not_after = now + Duration::days(397); // Generate key pair (default is ECDSA P256 SHA256) let key_pair = rcgen::KeyPair::generate()?; // defaults to ECDSA P256 SHA256 let cert = params.self_signed(&key_pair)?; let cert_pem = cert.pem(); let key_pem = key_pair.serialize_pem(); let mut f = fs::File::create(&cert_path)?; f.write_all(cert_pem.as_bytes())?; // The private key must not be world-readable (File::create honors the // umask, which typically yields 0644). let mut key_opts = fs::OpenOptions::new(); key_opts.write(true).create(true).truncate(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; key_opts.mode(0o600); } let mut k = key_opts.open(&key_path)?; k.write_all(key_pem.as_bytes())?; println!( "socktop_agent: generated self-signed TLS certificate at {}", cert_path.display() ); println!("socktop_agent: private key at {}", key_path.display()); Ok((cert_path, key_path)) }