use anyhow::{Context, Result};
use rcgen::{Certificate, CertificateParams, DnType, SanType};
use sha2::{Sha256, Digest};
use std::fs;
use std::net::IpAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tokio_rustls::rustls;
use tracing::{info, warn};
const CERT_FILENAME: &str = "cert.pem";
const KEY_FILENAME: &str = "key.pem";
const EXTRA_SANS_FILENAME: &str = "cert-extra-sans.json";
pub struct TlsConfig {
#[allow(dead_code)]
pub cert_path: PathBuf,
#[allow(dead_code)]
pub key_path: PathBuf,
pub fingerprint: String,
pub acceptor: tokio_rustls::TlsAcceptor,
}
impl TlsConfig {
pub fn load_or_generate(config_dir: &PathBuf, extra_sans: &[String]) -> Result<Self> {
let cert_path = config_dir.join(CERT_FILENAME);
let key_path = config_dir.join(KEY_FILENAME);
let extra_sans_path = config_dir.join(EXTRA_SANS_FILENAME);
if cert_path.exists() && key_path.exists() {
if !extra_sans.is_empty() {
let mut sorted = extra_sans.to_vec();
sorted.sort();
let current_json = serde_json::to_string(&sorted).unwrap_or_default();
let stored_json = fs::read_to_string(&extra_sans_path).unwrap_or_default();
if stored_json.trim() != current_json.trim() {
warn!("⚠️ Tailscale address changed since last certificate generation. Regenerating TLS certificate (mobile app will need to re-pair).");
let _ = fs::remove_file(&cert_path);
let _ = fs::remove_file(&key_path);
}
}
}
if cert_path.exists() && key_path.exists() {
info!("🔐 Loading existing TLS certificate");
Self::load_existing(&cert_path, &key_path)
} else {
info!("🔐 Generating new self-signed TLS certificate");
let result = Self::generate_new(&cert_path, &key_path, extra_sans)?;
if !extra_sans.is_empty() {
let mut sorted = extra_sans.to_vec();
sorted.sort();
let json = serde_json::to_string(&sorted).unwrap_or_default();
let _ = fs::write(&extra_sans_path, json);
}
Ok(result)
}
}
fn load_existing(cert_path: &PathBuf, key_path: &PathBuf) -> Result<Self> {
let cert_pem = fs::read_to_string(cert_path)
.context("Failed to read certificate file")?;
let key_pem = fs::read_to_string(key_path)
.context("Failed to read private key file")?;
let fingerprint = Self::calculate_fingerprint(&cert_pem)?;
let acceptor = Self::create_acceptor(&cert_pem, &key_pem)?;
Ok(Self {
cert_path: cert_path.clone(),
key_path: key_path.clone(),
fingerprint,
acceptor,
})
}
fn generate_new(cert_path: &PathBuf, key_path: &PathBuf, extra_sans: &[String]) -> Result<Self> {
let mut params = CertificateParams::default();
params.distinguished_name.push(DnType::CommonName, "ACP Bridge");
params.distinguished_name.push(DnType::OrganizationName, "Local Development");
params.subject_alt_names = vec![
SanType::DnsName("localhost".try_into().unwrap()),
SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))),
];
if let Ok(local_ip) = local_ip_address::local_ip() {
params.subject_alt_names.push(SanType::IpAddress(local_ip));
}
for san in extra_sans {
if let Ok(ip) = san.parse::<IpAddr>() {
params.subject_alt_names.push(SanType::IpAddress(ip));
} else {
let dns = san.clone().try_into();
match dns {
Ok(d) => params.subject_alt_names.push(SanType::DnsName(d)),
Err(_) => warn!("Skipping invalid SAN '{}': not a valid IP or DNS name", san),
}
}
}
params.not_before = time::OffsetDateTime::now_utc();
params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365);
let cert = Certificate::from_params(params)
.context("Failed to generate self-signed certificate")?;
let cert_pem = cert.serialize_pem()
.context("Failed to serialize certificate to PEM")?;
let key_pem = cert.serialize_private_key_pem();
if let Some(parent) = cert_path.parent() {
fs::create_dir_all(parent)
.context("Failed to create certificate directory")?;
}
fs::write(cert_path, &cert_pem)
.context("Failed to write certificate file")?;
fs::write(key_path, &key_pem)
.context("Failed to write private key file")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o600);
fs::set_permissions(cert_path, perms.clone())?;
fs::set_permissions(key_path, perms)?;
}
info!("✅ TLS certificate generated and saved");
let fingerprint = Self::calculate_fingerprint(&cert_pem)?;
let acceptor = Self::create_acceptor(&cert_pem, &key_pem)?;
Ok(Self {
cert_path: cert_path.clone(),
key_path: key_path.clone(),
fingerprint,
acceptor,
})
}
fn calculate_fingerprint(cert_pem: &str) -> Result<String> {
let mut reader = std::io::BufReader::new(cert_pem.as_bytes());
let certs = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.context("Failed to parse certificate PEM")?;
let cert_der = certs.first()
.context("No certificate found in PEM")?;
let mut hasher = Sha256::new();
hasher.update(cert_der.as_ref());
let hash = hasher.finalize();
let fingerprint = hash.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<_>>()
.join(":");
Ok(fingerprint)
}
fn create_acceptor(cert_pem: &str, key_pem: &str) -> Result<tokio_rustls::TlsAcceptor> {
let mut cert_reader = std::io::BufReader::new(cert_pem.as_bytes());
let certs = rustls_pemfile::certs(&mut cert_reader)
.collect::<Result<Vec<_>, _>>()
.context("Failed to parse certificate")?;
let mut key_reader = std::io::BufReader::new(key_pem.as_bytes());
let key = rustls_pemfile::private_key(&mut key_reader)
.context("Failed to read private key")?
.context("No private key found")?;
let config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.context("Failed to build TLS config")?;
Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
}
pub fn fingerprint_short(&self) -> String {
self.fingerprint.chars().take(23).collect()
}
}