1use anyhow::{Context, Result};
2use rcgen::{CertificateParams, DnType, KeyPair, SanType};
3use sha2::{Sha256, Digest};
4use std::fs;
5use std::net::IpAddr;
6use std::path::PathBuf;
7use std::sync::Arc;
8use tokio_rustls::rustls;
9use tracing::{info, warn};
10
11const CERT_FILENAME: &str = "cert.pem";
12const KEY_FILENAME: &str = "key.pem";
13const EXTRA_SANS_FILENAME: &str = "cert-extra-sans.json";
14
15pub struct TlsConfig {
17 #[allow(dead_code)]
19 pub cert_path: PathBuf,
20 #[allow(dead_code)]
22 pub key_path: PathBuf,
23 pub fingerprint: String,
25 pub acceptor: tokio_rustls::TlsAcceptor,
27}
28
29impl TlsConfig {
30 pub fn load_or_generate(config_dir: &PathBuf, extra_sans: &[String]) -> Result<Self> {
33 let cert_path = config_dir.join(CERT_FILENAME);
34 let key_path = config_dir.join(KEY_FILENAME);
35 let extra_sans_path = config_dir.join(EXTRA_SANS_FILENAME);
36
37 if cert_path.exists() && key_path.exists() {
39 if !extra_sans.is_empty() {
40 let mut sorted = extra_sans.to_vec();
41 sorted.sort();
42 let current_json = serde_json::to_string(&sorted).unwrap_or_default();
43
44 let stored_json = fs::read_to_string(&extra_sans_path).unwrap_or_default();
45 if stored_json.trim() != current_json.trim() {
46 warn!("⚠️ Tailscale address changed since last certificate generation. Regenerating TLS certificate (mobile app will need to re-pair).");
47 let _ = fs::remove_file(&cert_path);
48 let _ = fs::remove_file(&key_path);
49 }
50 }
51 }
52
53 if cert_path.exists() && key_path.exists() {
54 info!("🔐 Loading existing TLS certificate");
55 Self::load_existing(&cert_path, &key_path)
56 } else {
57 info!("🔐 Generating new self-signed TLS certificate");
58 let result = Self::generate_new(&cert_path, &key_path, extra_sans)?;
59 if !extra_sans.is_empty() {
61 let mut sorted = extra_sans.to_vec();
62 sorted.sort();
63 let json = serde_json::to_string(&sorted).unwrap_or_default();
64 let _ = fs::write(&extra_sans_path, json);
65 }
66 Ok(result)
67 }
68 }
69
70 fn load_existing(cert_path: &PathBuf, key_path: &PathBuf) -> Result<Self> {
72 let cert_pem = fs::read_to_string(cert_path)
73 .context("Failed to read certificate file")?;
74 let key_pem = fs::read_to_string(key_path)
75 .context("Failed to read private key file")?;
76
77 let fingerprint = Self::calculate_fingerprint(&cert_pem)?;
78 let acceptor = Self::create_acceptor(&cert_pem, &key_pem)?;
79
80 Ok(Self {
81 cert_path: cert_path.clone(),
82 key_path: key_path.clone(),
83 fingerprint,
84 acceptor,
85 })
86 }
87
88 fn generate_new(cert_path: &PathBuf, key_path: &PathBuf, extra_sans: &[String]) -> Result<Self> {
90 let mut params = CertificateParams::default();
92 params.distinguished_name.push(DnType::CommonName, "ACP Bridge");
93 params.distinguished_name.push(DnType::OrganizationName, "Local Development");
94
95 params.subject_alt_names = vec![
97 SanType::DnsName("localhost".try_into().unwrap()),
98 SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))),
99 ];
100
101 if let Ok(local_ip) = local_ip_address::local_ip() {
103 params.subject_alt_names.push(SanType::IpAddress(local_ip));
104 }
105
106 for san in extra_sans {
108 if let Ok(ip) = san.parse::<IpAddr>() {
109 params.subject_alt_names.push(SanType::IpAddress(ip));
110 } else {
111 let dns = san.clone().try_into();
112 match dns {
113 Ok(d) => params.subject_alt_names.push(SanType::DnsName(d)),
114 Err(_) => warn!("Skipping invalid SAN '{}': not a valid IP or DNS name", san),
115 }
116 }
117 }
118
119 params.not_before = time::OffsetDateTime::now_utc();
121 params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365);
122
123 let key_pair = KeyPair::generate()
125 .context("Failed to generate key pair")?;
126 let cert = params.self_signed(&key_pair)
127 .context("Failed to generate self-signed certificate")?;
128
129 let cert_pem = cert.pem();
130 let key_pem = key_pair.serialize_pem();
131
132 if let Some(parent) = cert_path.parent() {
134 fs::create_dir_all(parent)
135 .context("Failed to create certificate directory")?;
136 }
137
138 fs::write(cert_path, &cert_pem)
140 .context("Failed to write certificate file")?;
141 fs::write(key_path, &key_pem)
142 .context("Failed to write private key file")?;
143
144 #[cfg(unix)]
146 {
147 use std::os::unix::fs::PermissionsExt;
148 let perms = fs::Permissions::from_mode(0o600);
149 fs::set_permissions(cert_path, perms.clone())?;
150 fs::set_permissions(key_path, perms)?;
151 }
152
153 info!("✅ TLS certificate generated and saved");
154
155 let fingerprint = Self::calculate_fingerprint(&cert_pem)?;
156 let acceptor = Self::create_acceptor(&cert_pem, &key_pem)?;
157
158 Ok(Self {
159 cert_path: cert_path.clone(),
160 key_path: key_path.clone(),
161 fingerprint,
162 acceptor,
163 })
164 }
165
166 fn calculate_fingerprint(cert_pem: &str) -> Result<String> {
168 let mut reader = std::io::BufReader::new(cert_pem.as_bytes());
170 let certs = rustls_pemfile::certs(&mut reader)
171 .collect::<Result<Vec<_>, _>>()
172 .context("Failed to parse certificate PEM")?;
173
174 let cert_der = certs.first()
175 .context("No certificate found in PEM")?;
176
177 let mut hasher = Sha256::new();
179 hasher.update(cert_der.as_ref());
180 let hash = hasher.finalize();
181
182 let fingerprint = hash.iter()
184 .map(|b| format!("{:02X}", b))
185 .collect::<Vec<_>>()
186 .join(":");
187
188 Ok(fingerprint)
189 }
190
191 fn create_acceptor(cert_pem: &str, key_pem: &str) -> Result<tokio_rustls::TlsAcceptor> {
193 let mut cert_reader = std::io::BufReader::new(cert_pem.as_bytes());
195 let certs = rustls_pemfile::certs(&mut cert_reader)
196 .collect::<Result<Vec<_>, _>>()
197 .context("Failed to parse certificate")?;
198
199 let mut key_reader = std::io::BufReader::new(key_pem.as_bytes());
201 let key = rustls_pemfile::private_key(&mut key_reader)
202 .context("Failed to read private key")?
203 .context("No private key found")?;
204
205 let config = rustls::ServerConfig::builder()
207 .with_no_client_auth()
208 .with_single_cert(certs, key)
209 .context("Failed to build TLS config")?;
210
211 Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
212 }
213
214 pub fn fingerprint_short(&self) -> String {
216 self.fingerprint.chars().take(23).collect()
218 }
219}