92 lines
3.1 KiB
Rust
92 lines
3.1 KiB
Rust
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() {
|
|
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::<IpAddr>() {
|
|
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())?;
|
|
let mut k = fs::File::create(&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))
|
|
}
|