Skip to main content

bridge/
tls.rs

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
15/// TLS configuration for the bridge
16pub struct TlsConfig {
17    /// Path to the certificate file
18    #[allow(dead_code)]
19    pub cert_path: PathBuf,
20    /// Path to the private key file
21    #[allow(dead_code)]
22    pub key_path: PathBuf,
23    /// SHA256 fingerprint of the certificate (hex encoded)
24    pub fingerprint: String,
25    /// TLS acceptor for incoming connections
26    pub acceptor: tokio_rustls::TlsAcceptor,
27}
28
29impl TlsConfig {
30    /// Load or generate TLS configuration.
31    /// `extra_sans` is a list of additional IP addresses or DNS names to include in the certificate SANs.
32    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 exists, check whether extra_sans have changed
38        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            // Persist extra_sans for future change detection
60            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    /// Load existing certificate and key
71    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    /// Generate new self-signed certificate
89    fn generate_new(cert_path: &PathBuf, key_path: &PathBuf, extra_sans: &[String]) -> Result<Self> {
90        // Set up certificate parameters
91        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        // Add base SANs for local connections
96        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        // Add local network IP
102        if let Ok(local_ip) = local_ip_address::local_ip() {
103            params.subject_alt_names.push(SanType::IpAddress(local_ip));
104        }
105
106        // Add extra SANs (Tailscale IP/hostname, etc.)
107        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        // Valid for 1 year
120        params.not_before = time::OffsetDateTime::now_utc();
121        params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365);
122
123        // Generate self-signed certificate
124        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        // Ensure the directory exists before writing
133        if let Some(parent) = cert_path.parent() {
134            fs::create_dir_all(parent)
135                .context("Failed to create certificate directory")?;
136        }
137
138        // Save to files
139        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        // Set restrictive permissions on Unix
145        #[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    /// Calculate SHA256 fingerprint of certificate
167    fn calculate_fingerprint(cert_pem: &str) -> Result<String> {
168        // Parse PEM to get DER bytes
169        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        // Calculate SHA256 hash
178        let mut hasher = Sha256::new();
179        hasher.update(cert_der.as_ref());
180        let hash = hasher.finalize();
181
182        // Format as hex with colons (e.g., "AB:CD:EF:...")
183        let fingerprint = hash.iter()
184            .map(|b| format!("{:02X}", b))
185            .collect::<Vec<_>>()
186            .join(":");
187
188        Ok(fingerprint)
189    }
190
191    /// Create TLS acceptor from PEM strings
192    fn create_acceptor(cert_pem: &str, key_pem: &str) -> Result<tokio_rustls::TlsAcceptor> {
193        // Parse certificate
194        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        // Parse private key
200        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        // Build TLS config
206        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    /// Get the fingerprint in a format suitable for display
215    pub fn fingerprint_short(&self) -> String {
216        // Return first 16 chars (8 bytes) for brevity
217        self.fingerprint.chars().take(23).collect()
218    }
219}