Skip to main content

Mountain/Binary/Build/
TlsCommands.rs

1//! # TLS Certificate Management Commands
2//!
3//! This module provides Tauri commands for managing TLS certificates from the
4//! webview.
5//!
6//! ## Available Commands
7//!
8//! - `tls_get_ca_cert` - Returns the CA certificate PEM for trust store
9//!   installation
10//! - `tls_get_server_cert_info` - Returns information about a server
11//!   certificate
12//! - `tls_renew_certificate` - Forces renewal of a server certificate
13//! - `tls_get_all_certs` - Lists all cached server certificates
14//! - `tls_initialize` - Initializes the certificate manager and loads/generates
15//!   CA
16//! - `tls_check_cert_status` - Checks if a certificate needs renewal
17//!
18//! ## Usage Example
19//!
20//! ```typescript
21//! // Get CA certificate for installation
22//! const caCert = await invoke('tls_get_ca_cert');
23//! console.log('CA Certificate:', caCert);
24//!
25//! // Get server certificate info
26//! const certInfo = await invoke('tls_get_server_cert_info', {
27//!   hostname: 'code.editor.land'
28//! });
29//! console.log('Valid until:', certInfo.valid_until);
30//!
31//! // Renew a certificate
32//! await invoke('tls_renew_certificate', {
33//!   hostname: 'code.editor.land'
34//! });
35//! ```
36
37use std::sync::{Arc, Mutex};
38
39use serde::{Deserialize, Serialize};
40use tauri::{AppHandle, Manager}; // Manager trait provides try_state() method
41
42use crate::dev_log;
43use super::CertificateManager::{CertificateInfo, CertificateManager};
44
45/// Initialize TLS certificate manager
46///
47/// This must be called before any other TLS operations.
48/// It will load an existing CA from the keyring or generate a new one.
49#[tauri::command]
50pub async fn tls_initialize(app_handle:AppHandle) -> Result<String, String> {
51	dev_log!("security", "TLS certificate manager initializing");
52
53	// Try to get existing certificate manager from state
54	let state = app_handle
55		.try_state::<Arc<Mutex<CertificateManager>>>()
56		.ok_or("Certificate manager not initialized in app state")?;
57	let cert_manager = state.clone(); // Clone the Arc from State
58
59	let mut manager = cert_manager.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
60
61	manager
62		.initialize_ca()
63		.await
64		.map_err(|e| format!("Failed to initialize CA: {}", e))?;
65
66	dev_log!("security", "TLS certificate manager initialized");
67	Ok("TLS certificate manager initialized".to_string())
68}
69
70/// Get the CA certificate in PEM format
71///
72/// This can be used to install the CA in the system trust store
73/// or configure the webview to trust it.
74///
75/// Returns the CA certificate PEM string, or an error if not initialized.
76#[tauri::command]
77pub async fn tls_get_ca_cert(app_handle:AppHandle) -> Result<String, String> {
78	dev_log!("security", "getting CA certificate");
79
80	let state = app_handle
81		.try_state::<Arc<Mutex<CertificateManager>>>()
82		.ok_or("Certificate manager not found")?;
83	let cert_manager = state.clone();
84
85	let manager = cert_manager.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
86	let cert_pem = manager.get_ca_cert_pem().ok_or("CA certificate not initialized")?;
87
88	Ok(String::from_utf8(cert_pem).map_err(|e| format!("Invalid certificate UTF-8: {}", e))?)
89}
90
91/// Get information about a server certificate
92///
93/// # Arguments
94///
95/// * `hostname` - The hostname (e.g., "code.editor.land")
96///
97/// Returns certificate information including validity period and subject.
98#[tauri::command]
99pub async fn tls_get_server_cert_info(
100	app_handle:AppHandle,
101	hostname:String,
102) -> Result<Option<CertificateInfo>, String> {
103	dev_log!("security", "getting server cert info for {}", hostname);
104
105	let state = app_handle
106		.try_state::<Arc<Mutex<CertificateManager>>>()
107		.ok_or("Certificate manager not found")?;
108	let cert_manager = state.clone();
109
110	let manager = cert_manager.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
111	Ok(manager.get_server_cert_info(&hostname))
112}
113
114/// Force renewal of a server certificate
115///
116/// This will generate a new certificate signed by the CA and cache it.
117///
118/// # Arguments
119///
120/// * `hostname` - The hostname whose certificate should be renewed
121#[tauri::command]
122pub async fn tls_renew_certificate(app_handle:AppHandle, hostname:String) -> Result<String, String> {
123	dev_log!("security", "renewing certificate for {}", hostname);
124
125	let state = app_handle
126		.try_state::<Arc<Mutex<CertificateManager>>>()
127		.ok_or("Certificate manager not found")?;
128	let cert_manager = state.clone();
129
130	// TODO: The Mutex needs to be held across await points - consider using
131	// tokio::sync::Mutex For now, clone the manager or refactor to avoid this
132	// issue
133	{
134		let mut manager = cert_manager.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
135
136		let _result = manager.renew_certificate(&hostname);
137		// TODO: Handle result properly - this needs refactoring
138	}
139
140	Ok(format!("Certificate renewed for {}", hostname))
141}
142
143/// Get all cached server certificates
144///
145/// Returns a mapping of hostnames to certificate information.
146#[tauri::command]
147pub async fn tls_get_all_certs(
148	app_handle:AppHandle,
149) -> Result<std::collections::HashMap<String, CertificateInfo>, String> {
150	dev_log!("security", "getting all server certificates");
151
152	let state = app_handle
153		.try_state::<Arc<Mutex<CertificateManager>>>()
154		.ok_or("Certificate manager not found")?;
155	let cert_manager = state.clone();
156
157	let manager = cert_manager.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
158	Ok(manager.get_all_certs())
159}
160
161/// Check if a certificate needs renewal
162///
163/// # Arguments
164///
165/// * `hostname` - The hostname to check
166///
167/// Returns true if the certificate is expiring within 30 days.
168#[tauri::command]
169pub async fn tls_check_cert_status(app_handle:AppHandle, hostname:String) -> Result<CertificateStatus, String> {
170	dev_log!("security", "checking certificate status for {}", hostname);
171
172	let state = app_handle
173		.try_state::<Arc<Mutex<CertificateManager>>>()
174		.ok_or("Certificate manager not found")?;
175	let cert_manager = state.clone();
176
177	let manager = cert_manager.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
178
179	// Try to get certificate info
180	if let Some(cert_info) = manager.get_server_cert_info(&hostname) {
181		// Parse the expiry time
182		let valid_until = chrono::DateTime::parse_from_rfc3339(&cert_info.valid_until)
183			.map_err(|e| format!("Invalid certificate expiry time: {}", e))?
184			.with_timezone(&chrono::Utc);
185
186		let now = chrono::Utc::now();
187		let days_until_expiry = (valid_until - now).num_days();
188		let needs_renewal = days_until_expiry <= CertificateManager::RENEWAL_THRESHOLD_DAYS;
189
190		Ok(CertificateStatus {
191			exists:true,
192			is_valid:now <= valid_until,
193			days_until_expiry,
194			needs_renewal,
195			valid_until:cert_info.valid_until,
196		})
197	} else {
198		Ok(CertificateStatus {
199			exists:false,
200			is_valid:false,
201			days_until_expiry:0,
202			needs_renewal:true,
203			valid_until:String::new(),
204		})
205	}
206}
207
208/// Generate a server certificate for a hostname
209///
210/// This will generate a new certificate if one doesn't exist,
211/// or return an existing valid certificate.
212///
213/// # Arguments
214///
215/// * `hostname` - The hostname (e.g., "code.editor.land")
216///
217/// Returns success message with certificate details.
218#[tauri::command]
219pub async fn tls_generate_cert(app_handle:AppHandle, hostname:String) -> Result<CertificateGenerationResult, String> {
220	dev_log!("security", "generating certificate for {}", hostname);
221
222	let state = app_handle
223		.try_state::<Arc<Mutex<CertificateManager>>>()
224		.ok_or("Certificate manager not found")?;
225
226	let cert_manager = state.clone();
227	let manager = cert_manager.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
228	let hostname_clone = hostname.clone();
229
230	let _server_config = manager
231		.get_server_cert(&hostname)
232		.await
233		.map_err(|e| format!("Failed to generate certificate: {}", e))?;
234
235	// Get certificate info directly from previous call
236	let cert_info:CertificateInfo = manager
237		.get_server_cert_info(&hostname)
238		.ok_or_else(|| "Certificate not found after generation".to_string())?;
239
240	Ok(CertificateGenerationResult {
241		hostname:hostname_clone,
242		success:true,
243		valid_until:cert_info.valid_until,
244		message:format!("Certificate generated successfully for {}", hostname),
245	})
246}
247
248/// Delete a certificate from the cache
249///
250/// This forces the certificate to be regenerated on next use.
251///
252/// # Arguments
253///
254/// * `hostname` - The hostname whose certificate should be deleted
255#[tauri::command]
256pub async fn tls_delete_cert(app_handle:AppHandle, hostname:String) -> Result<String, String> {
257	dev_log!("security", "deleting certificate for {}", hostname);
258
259	let state = app_handle
260		.try_state::<Arc<Mutex<CertificateManager>>>()
261		.ok_or("Certificate manager not found")?;
262	let cert_manager = state.clone();
263
264	// We need to implement this in CertificateManager
265	// For now, just call renew which effectively regenerates
266	{
267		let mut manager = cert_manager.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
268
269		let _result = manager.renew_certificate(&hostname);
270		// TODO: Handle result properly - this needs refactoring
271	}
272
273	Ok(format!("Certificate deleted for {}", hostname))
274}
275
276/// Certificate status information
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct CertificateStatus {
279	/// Whether the certificate exists
280	pub exists:bool,
281	/// Whether the certificate is currently valid
282	pub is_valid:bool,
283	/// Days until expiry (negative if expired)
284	pub days_until_expiry:i64,
285	/// Whether the certificate needs renewal
286	pub needs_renewal:bool,
287	/// Certificate expiry time (ISO 8601)
288	pub valid_until:String,
289}
290
291/// Certificate generation result
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct CertificateGenerationResult {
294	/// The hostname
295	pub hostname:String,
296	/// Whether generation was successful
297	pub success:bool,
298	/// Certificate expiry time (ISO 8601)
299	pub valid_until:String,
300	/// Status message
301	pub message:String,
302}
303
304#[cfg(test)]
305mod tests {
306	use super::*;
307
308	#[test]
309	fn test_certificate_status_serialization() {
310		let status = CertificateStatus {
311			exists:true,
312			is_valid:true,
313			days_until_expiry:30,
314			needs_renewal:true,
315			valid_until:"2025-01-01T00:00:00Z".to_string(),
316		};
317
318		let json = serde_json::to_string(&status).unwrap();
319		assert_eq!(status.exists, true);
320
321		let deserialized:CertificateStatus = serde_json::from_str(&json).unwrap();
322		assert_eq!(deserialized.exists, status.exists);
323	}
324}