Skip to main content

netget/server/
tls_cert_manager.rs

1//! TLS certificate management for DoT and DoH servers
2//!
3//! Provides LLM-controlled certificate generation with self-signed fallback.
4
5use anyhow::{Context, Result};
6use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, KeyPair, SanType};
7use rustls::pki_types::{CertificateDer, PrivateKeyDer};
8use rustls::ServerConfig;
9use std::sync::Arc;
10use time::{Duration, OffsetDateTime};
11use tracing::{debug, info};
12
13/// Certificate specification from LLM or defaults
14#[derive(Debug, Clone)]
15pub struct CertificateSpec {
16    /// Common name (CN) for the certificate
17    pub common_name: String,
18    /// Subject Alternative Names (DNS names)
19    pub san_dns_names: Vec<String>,
20    /// Certificate validity period in days
21    pub validity_days: i64,
22    /// Organization name
23    pub organization: Option<String>,
24    /// Organizational unit
25    pub organizational_unit: Option<String>,
26}
27
28impl Default for CertificateSpec {
29    fn default() -> Self {
30        Self {
31            common_name: "netget-dns-server".to_string(),
32            san_dns_names: vec!["localhost".to_string(), "*.local".to_string()],
33            validity_days: 365,
34            organization: Some("NetGet".to_string()),
35            organizational_unit: Some("DNS Server".to_string()),
36        }
37    }
38}
39
40impl CertificateSpec {
41    /// Create a new certificate spec from LLM parameters
42    pub fn from_llm_params(
43        common_name: Option<String>,
44        san_dns_names: Option<Vec<String>>,
45        validity_days: Option<i64>,
46        organization: Option<String>,
47        organizational_unit: Option<String>,
48    ) -> Self {
49        let defaults = Self::default();
50        Self {
51            common_name: common_name.unwrap_or(defaults.common_name),
52            san_dns_names: san_dns_names.unwrap_or(defaults.san_dns_names),
53            validity_days: validity_days.unwrap_or(defaults.validity_days),
54            organization: organization.or(defaults.organization),
55            organizational_unit: organizational_unit.or(defaults.organizational_unit),
56        }
57    }
58}
59
60/// Generate a self-signed TLS certificate based on spec
61/// Returns both the Certificate and its KeyPair
62pub fn generate_self_signed_cert(spec: &CertificateSpec) -> Result<(Certificate, KeyPair)> {
63    info!("Generating self-signed TLS certificate");
64    debug!("Certificate spec: {:?}", spec);
65
66    let mut params = CertificateParams::default();
67
68    // Set distinguished name
69    let mut dn = DistinguishedName::new();
70    dn.push(DnType::CommonName, &spec.common_name);
71
72    if let Some(ref org) = spec.organization {
73        dn.push(DnType::OrganizationName, org);
74    }
75
76    if let Some(ref ou) = spec.organizational_unit {
77        dn.push(DnType::OrganizationalUnitName, ou);
78    }
79
80    params.distinguished_name = dn;
81
82    // Add Subject Alternative Names
83    params.subject_alt_names = spec
84        .san_dns_names
85        .iter()
86        .map(|name| {
87            name.clone()
88                .try_into()
89                .map(SanType::DnsName)
90                .with_context(|| format!("Invalid certificate DNS name: {name:?}"))
91        })
92        .collect::<Result<Vec<_>>>()?;
93
94    // Startup parameters can contain the full i64 range. Both constructing a duration
95    // and adding it to a date can overflow, so reject invalid lifetimes before signing.
96    anyhow::ensure!(
97        spec.validity_days > 0,
98        "Certificate validity_days must be positive"
99    );
100    let validity_seconds = spec
101        .validity_days
102        .checked_mul(86_400)
103        .context("Certificate validity_days is too large")?;
104    let now = OffsetDateTime::now_utc();
105    params.not_before = now;
106    params.not_after = now
107        .checked_add(Duration::seconds(validity_seconds))
108        .context("Certificate validity_days exceeds the supported date range")?;
109
110    // Generate key pair and self-sign
111    let key_pair = KeyPair::generate().context("Failed to generate key pair")?;
112
113    let cert = params
114        .self_signed(&key_pair)
115        .context("Failed to create self-signed certificate")?;
116
117    info!(
118        "Successfully generated self-signed certificate for CN={}",
119        spec.common_name
120    );
121
122    Ok((cert, key_pair))
123}
124
125/// Create a rustls ServerConfig from an rcgen Certificate and KeyPair
126pub fn create_rustls_server_config(
127    cert: &Certificate,
128    key_pair: &KeyPair,
129) -> Result<Arc<ServerConfig>> {
130    // Get the certificate DER
131    let cert_der = cert.der();
132    let cert_der_owned = CertificateDer::from(cert_der.to_vec());
133
134    // Get the private key DER from the KeyPair
135    let key_der_vec = key_pair.serialize_der();
136    let key_der_owned = PrivateKeyDer::try_from(key_der_vec)
137        .map_err(|e| anyhow::anyhow!("Failed to parse private key DER: {}", e))?;
138
139    // Install the default crypto provider (ring) for rustls if not already installed
140    // This is required for ServerConfig::builder() to work
141    let _ = rustls::crypto::ring::default_provider().install_default();
142
143    // Build rustls ServerConfig
144    let config = ServerConfig::builder()
145        .with_no_client_auth()
146        .with_single_cert(vec![cert_der_owned], key_der_owned)
147        .context("Failed to create rustls ServerConfig")?;
148
149    Ok(Arc::new(config))
150}
151
152/// Generate a default self-signed certificate and create rustls ServerConfig
153pub fn generate_default_tls_config() -> Result<Arc<ServerConfig>> {
154    let spec = CertificateSpec::default();
155    let (cert, key_pair) = generate_self_signed_cert(&spec)?;
156    create_rustls_server_config(&cert, &key_pair)
157}
158
159/// Like [`generate_default_tls_config`], but advertising `alpn` in the TLS handshake.
160///
161/// Deliberately a separate function rather than a change to the default: `dot`, `tls` and
162/// `quic` share that config and document themselves as ALPN-less, and advertising a protocol
163/// they do not speak would break clients that select on it.
164///
165/// A server that negotiates nothing forces every client to assume the protocol out of band.
166/// That is why `doh` needs this: RFC 8484 runs over HTTP/2, and an HTTP/2 client that is not
167/// told `h2` during the handshake will either fall back to HTTP/1.1 — which this server does
168/// not speak — or refuse the connection outright.
169pub fn generate_default_tls_config_with_alpn(alpn: &[&str]) -> Result<Arc<ServerConfig>> {
170    let config = generate_default_tls_config()?;
171    // Freshly built above, so this is the only reference and the unwrap cannot fire.
172    let mut config = Arc::try_unwrap(config)
173        .map_err(|_| anyhow::anyhow!("TLS config was unexpectedly shared before ALPN was set"))?;
174    config.alpn_protocols = alpn.iter().map(|p| p.as_bytes().to_vec()).collect();
175    Ok(Arc::new(config))
176}
177
178/// Generate a custom TLS configuration from LLM-specified parameters
179pub fn generate_custom_tls_config(
180    common_name: Option<String>,
181    san_dns_names: Option<Vec<String>>,
182    validity_days: Option<i64>,
183    organization: Option<String>,
184    organizational_unit: Option<String>,
185) -> Result<Arc<ServerConfig>> {
186    let spec = CertificateSpec::from_llm_params(
187        common_name,
188        san_dns_names,
189        validity_days,
190        organization,
191        organizational_unit,
192    );
193
194    let (cert, key_pair) = generate_self_signed_cert(&spec)?;
195    create_rustls_server_config(&cert, &key_pair)
196}
197
198/// Load TLS configuration from certificate and key files
199pub fn load_tls_config_from_files(cert_path: &str, key_path: &str) -> Result<Arc<ServerConfig>> {
200    info!("Loading TLS certificate from files");
201    debug!("Certificate path: {}", cert_path);
202    debug!("Key path: {}", key_path);
203
204    // Read certificate file
205    let cert_pem = std::fs::read_to_string(cert_path)
206        .with_context(|| format!("Failed to read certificate file: {}", cert_path))?;
207
208    // Read key file
209    let key_pem = std::fs::read_to_string(key_path)
210        .with_context(|| format!("Failed to read private key file: {}", key_path))?;
211
212    // Parse certificate
213    let cert_der = rustls_pemfile::certs(&mut cert_pem.as_bytes())
214        .collect::<Result<Vec<_>, _>>()
215        .context("Failed to parse certificate PEM")?;
216
217    if cert_der.is_empty() {
218        return Err(anyhow::anyhow!("No certificates found in certificate file"));
219    }
220
221    // Parse private key
222    let mut key_reader = key_pem.as_bytes();
223    let key_der = rustls_pemfile::private_key(&mut key_reader)
224        .context("Failed to parse private key PEM")?
225        .ok_or_else(|| anyhow::anyhow!("No private key found in key file"))?;
226
227    // As in create_rustls_server_config: ServerConfig::builder() panics unless a process-level
228    // provider is installed, which the binary does at startup and an in-process caller may not.
229    let _ = rustls::crypto::ring::default_provider().install_default();
230
231    // Build rustls ServerConfig
232    let config = ServerConfig::builder()
233        .with_no_client_auth()
234        .with_single_cert(cert_der, key_der)
235        .context("Failed to create rustls ServerConfig from loaded certificate")?;
236
237    info!("Successfully loaded TLS certificate from files");
238
239    Ok(Arc::new(config))
240}
241
242/// Create TLS configuration from startup parameters
243///
244/// If cert_path and key_path are provided, loads certificate from files.
245/// Otherwise, generates a self-signed certificate with optional customization.
246pub fn create_tls_config(
247    cert_path: Option<&str>,
248    key_path: Option<&str>,
249    common_name: Option<String>,
250    san_dns_names: Option<Vec<String>>,
251    validity_days: Option<i64>,
252    organization: Option<String>,
253    organizational_unit: Option<String>,
254) -> Result<Arc<ServerConfig>> {
255    match (cert_path, key_path) {
256        (Some(cert), Some(key)) => {
257            // Load from files
258            load_tls_config_from_files(cert, key)
259        }
260        (Some(_), None) | (None, Some(_)) => Err(anyhow::anyhow!(
261            "Both cert_path and key_path must be provided together"
262        )),
263        (None, None) => {
264            // Generate self-signed certificate
265            if common_name.is_some()
266                || san_dns_names.is_some()
267                || validity_days.is_some()
268                || organization.is_some()
269                || organizational_unit.is_some()
270            {
271                // Use custom parameters
272                generate_custom_tls_config(
273                    common_name,
274                    san_dns_names,
275                    validity_days,
276                    organization,
277                    organizational_unit,
278                )
279            } else {
280                // Use defaults
281                generate_default_tls_config()
282            }
283        }
284    }
285}
286
287/// Get TLS startup parameters for protocols
288pub fn get_tls_startup_parameters() -> Vec<crate::llm::actions::ParameterDefinition> {
289    use crate::llm::actions::ParameterDefinition;
290
291    vec![
292        ParameterDefinition {
293            name: "tls_enabled".to_string(),
294            type_hint: "boolean".to_string(),
295            description: "Enable TLS/SSL encryption (default: false for HTTP/HTTP2, always true for QUIC)".to_string(),
296            required: false,
297            example: serde_json::json!(true),
298            default: None,
299        },
300        ParameterDefinition {
301            name: "cert_path".to_string(),
302            type_hint: "string".to_string(),
303            description: "Path to TLS certificate file (PEM format). If not provided, a self-signed certificate will be generated.".to_string(),
304            required: false,
305            example: serde_json::json!("/path/to/cert.pem"),
306            default: None,
307        },
308        ParameterDefinition {
309            name: "key_path".to_string(),
310            type_hint: "string".to_string(),
311            description: "Path to TLS private key file (PEM format). Required if cert_path is provided.".to_string(),
312            required: false,
313            example: serde_json::json!("/path/to/key.pem"),
314            default: None,
315        },
316        ParameterDefinition {
317            name: "common_name".to_string(),
318            type_hint: "string".to_string(),
319            description: "Common Name (CN) for self-signed certificate (default: netget-dns-server)".to_string(),
320            required: false,
321            example: serde_json::json!("example.com"),
322            default: None,
323        },
324        ParameterDefinition {
325            name: "san_dns_names".to_string(),
326            type_hint: "array".to_string(),
327            description: "Subject Alternative Names (DNS names) for self-signed certificate (default: [\"localhost\", \"*.local\"])".to_string(),
328            required: false,
329            example: serde_json::json!(["example.com", "*.example.com"]),
330            default: None,
331        },
332        ParameterDefinition {
333            name: "validity_days".to_string(),
334            type_hint: "number".to_string(),
335            description: "Positive certificate validity period in days for self-signed certificate, within the supported date range (default: 365)".to_string(),
336            required: false,
337            example: serde_json::json!(365),
338            default: None,
339        },
340        ParameterDefinition {
341            name: "organization".to_string(),
342            type_hint: "string".to_string(),
343            description: "Organization name for self-signed certificate (default: NetGet)".to_string(),
344            required: false,
345            example: serde_json::json!("My Organization"),
346            default: None,
347        },
348        ParameterDefinition {
349            name: "organizational_unit".to_string(),
350            type_hint: "string".to_string(),
351            description: "Organizational unit for self-signed certificate".to_string(),
352            required: false,
353            example: serde_json::json!("IT Department"),
354            default: None,
355        },
356    ]
357}
358
359/// Extract TLS configuration from startup parameters
360pub fn extract_tls_config_from_params(
361    params: &crate::protocol::StartupParams,
362) -> Result<Option<Arc<ServerConfig>>> {
363    // Check if TLS is enabled
364    let tls_enabled = params.get_optional_bool("tls_enabled")?.unwrap_or(false);
365
366    if !tls_enabled {
367        return Ok(None);
368    }
369
370    extract_required_tls_config_from_params(params).map(Some)
371}
372
373/// Build mandatory TLS (for QUIC) with the same validation as optional TLS.
374pub fn extract_required_tls_config_from_params(
375    params: &crate::protocol::StartupParams,
376) -> Result<Arc<ServerConfig>> {
377    // Extract TLS parameters
378    let cert_path = params.get_optional_string("cert_path")?;
379    let key_path = params.get_optional_string("key_path")?;
380    let common_name = params.get_optional_string("common_name")?;
381    let san_dns_names = params
382        .get_optional_array("san_dns_names")?
383        .map(|arr| {
384            arr.iter()
385                .enumerate()
386                .map(|(index, value)| {
387                    value
388                        .as_str()
389                        .map(str::to_owned)
390                        .with_context(|| format!("san_dns_names[{index}] must be a string"))
391                })
392                .collect::<Result<Vec<_>>>()
393        })
394        .transpose()?;
395    let validity_days = params.get_optional_i64("validity_days")?;
396    let organization = params.get_optional_string("organization")?;
397    let organizational_unit = params.get_optional_string("organizational_unit")?;
398
399    // Create TLS config
400    create_tls_config(
401        cert_path.as_deref(),
402        key_path.as_deref(),
403        common_name,
404        san_dns_names,
405        validity_days,
406        organization,
407        organizational_unit,
408    )
409}