mockforge_http/
tls.rs

1//! TLS/HTTPS support for HTTP server
2//!
3//! This module provides TLS configuration and certificate loading for secure HTTP connections.
4
5use mockforge_core::config::HttpTlsConfig;
6use mockforge_core::Result;
7use std::sync::Arc;
8use std::sync::Once;
9use tokio_rustls::TlsAcceptor;
10use tracing::info;
11
12static CRYPTO_INIT: Once = Once::new();
13
14/// Initialize the rustls crypto provider.
15///
16/// This must be called before any TLS operations. It is safe to call multiple times
17/// as it uses `Once` to ensure initialization happens exactly once.
18///
19/// Uses the `ring` crypto provider for rustls.
20pub fn init_crypto_provider() {
21    CRYPTO_INIT.call_once(|| {
22        // Install the ring crypto provider as the default for rustls
23        let _ = rustls::crypto::ring::default_provider().install_default();
24    });
25}
26
27/// Load TLS acceptor from certificate and key files
28///
29/// This function loads server certificates and private keys from PEM files
30/// and creates a TLS acceptor for use with the HTTP server.
31///
32/// For mutual TLS (mTLS), provide a CA certificate file via `ca_file`.
33pub fn load_tls_acceptor(config: &HttpTlsConfig) -> Result<TlsAcceptor> {
34    use rustls_pemfile::{certs, pkcs8_private_keys};
35    use std::fs::File;
36    use std::io::BufReader;
37
38    // Ensure crypto provider is initialized
39    init_crypto_provider();
40
41    info!(
42        "Loading TLS certificate from {} and key from {}",
43        config.cert_file, config.key_file
44    );
45
46    // Load certificate chain
47    let cert_file = File::open(&config.cert_file).map_err(|e| {
48        mockforge_core::Error::generic(format!(
49            "Failed to open certificate file {}: {}",
50            config.cert_file, e
51        ))
52    })?;
53    let mut cert_reader = BufReader::new(cert_file);
54    let server_certs: Vec<rustls::pki_types::CertificateDer<'static>> = certs(&mut cert_reader)
55        .collect::<std::result::Result<Vec<_>, _>>()
56        .map_err(|e| {
57            mockforge_core::Error::generic(format!(
58                "Failed to parse certificate file {}: {}",
59                config.cert_file, e
60            ))
61        })?;
62
63    if server_certs.is_empty() {
64        return Err(mockforge_core::Error::generic(format!(
65            "No certificates found in {}",
66            config.cert_file
67        )));
68    }
69
70    // Load private key
71    let key_file = File::open(&config.key_file).map_err(|e| {
72        mockforge_core::Error::generic(format!(
73            "Failed to open private key file {}: {}",
74            config.key_file, e
75        ))
76    })?;
77    let mut key_reader = BufReader::new(key_file);
78    let pkcs8_keys: Vec<rustls::pki_types::PrivatePkcs8KeyDer<'static>> =
79        pkcs8_private_keys(&mut key_reader)
80            .collect::<std::result::Result<Vec<_>, _>>()
81            .map_err(|e| {
82                mockforge_core::Error::generic(format!(
83                    "Failed to parse private key file {}: {}",
84                    config.key_file, e
85                ))
86            })?;
87    let mut keys: Vec<rustls::pki_types::PrivateKeyDer<'static>> = pkcs8_keys
88        .into_iter()
89        .map(|k| rustls::pki_types::PrivateKeyDer::Pkcs8(k))
90        .collect();
91
92    if keys.is_empty() {
93        return Err(mockforge_core::Error::generic(format!(
94            "No private keys found in {}",
95            config.key_file
96        )));
97    }
98
99    // Build TLS server configuration with version support
100    // Note: rustls uses safe defaults, so we configure during builder creation
101    // Determine mTLS mode: use mtls_mode if set, otherwise fall back to require_client_cert for backward compatibility
102    let mtls_mode = if !config.mtls_mode.is_empty() && config.mtls_mode != "off" {
103        config.mtls_mode.as_str()
104    } else if config.require_client_cert {
105        "required"
106    } else {
107        "off"
108    };
109
110    let server_config = match mtls_mode {
111        "required" => {
112            // Mutual TLS: require client certificates
113            if let Some(ref ca_file_path) = config.ca_file {
114                // Load CA certificate for client verification
115                let ca_file = File::open(ca_file_path).map_err(|e| {
116                    mockforge_core::Error::generic(format!(
117                        "Failed to open CA certificate file {}: {}",
118                        ca_file_path, e
119                    ))
120                })?;
121                let mut ca_reader = BufReader::new(ca_file);
122                let ca_certs: Vec<rustls::pki_types::CertificateDer<'static>> =
123                    certs(&mut ca_reader).collect::<std::result::Result<Vec<_>, _>>().map_err(
124                        |e| {
125                            mockforge_core::Error::generic(format!(
126                                "Failed to parse CA certificate file {}: {}",
127                                ca_file_path, e
128                            ))
129                        },
130                    )?;
131
132                let mut root_store = rustls::RootCertStore::empty();
133                for cert in &ca_certs {
134                    root_store.add(cert.clone()).map_err(|e| {
135                        mockforge_core::Error::generic(format!(
136                            "Failed to add CA certificate to root store: {}",
137                            e
138                        ))
139                    })?;
140                }
141
142                let client_verifier =
143                    rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
144                        .build()
145                        .map_err(|e| {
146                            mockforge_core::Error::generic(format!(
147                                "Failed to build client verifier: {}",
148                                e
149                            ))
150                        })?;
151
152                let key = keys.remove(0);
153
154                // Build with mTLS support (required)
155                rustls::server::ServerConfig::builder()
156                    .with_client_cert_verifier(client_verifier.into())
157                    .with_single_cert(server_certs, key)
158                    .map_err(|e| {
159                        mockforge_core::Error::generic(format!(
160                            "TLS config error (mTLS required): {}",
161                            e
162                        ))
163                    })?
164            } else {
165                return Err(mockforge_core::Error::generic(
166                    "mTLS mode 'required' requires --tls-ca (CA certificate file)",
167                ));
168            }
169        }
170        "optional" => {
171            // Mutual TLS: accept client certificates if provided, but don't require
172            if let Some(ref ca_file_path) = config.ca_file {
173                // Load CA certificate for client verification
174                let ca_file = File::open(ca_file_path).map_err(|e| {
175                    mockforge_core::Error::generic(format!(
176                        "Failed to open CA certificate file {}: {}",
177                        ca_file_path, e
178                    ))
179                })?;
180                let mut ca_reader = BufReader::new(ca_file);
181                let ca_certs: Vec<rustls::pki_types::CertificateDer<'static>> =
182                    certs(&mut ca_reader).collect::<std::result::Result<Vec<_>, _>>().map_err(
183                        |e| {
184                            mockforge_core::Error::generic(format!(
185                                "Failed to parse CA certificate file {}: {}",
186                                ca_file_path, e
187                            ))
188                        },
189                    )?;
190
191                let mut root_store = rustls::RootCertStore::empty();
192                for cert in &ca_certs {
193                    root_store.add(cert.clone()).map_err(|e| {
194                        mockforge_core::Error::generic(format!(
195                            "Failed to add CA certificate to root store: {}",
196                            e
197                        ))
198                    })?;
199                }
200
201                let client_verifier =
202                    rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
203                        .build()
204                        .map_err(|e| {
205                            mockforge_core::Error::generic(format!(
206                                "Failed to build client verifier: {}",
207                                e
208                            ))
209                        })?;
210
211                let key = keys.remove(0);
212
213                // Build with optional mTLS support
214                // Note: rustls doesn't have a built-in "optional" mode, so we use
215                // WebPkiClientVerifier which accepts any client cert that validates,
216                // but connections without certs will also work (we can't enforce optional-only)
217                // For true optional mTLS, we'd need custom verifier logic
218                rustls::server::ServerConfig::builder()
219                    .with_client_cert_verifier(client_verifier.into())
220                    .with_single_cert(server_certs, key)
221                    .map_err(|e| {
222                        mockforge_core::Error::generic(format!(
223                            "TLS config error (mTLS optional): {}",
224                            e
225                        ))
226                    })?
227            } else {
228                // Optional mTLS without CA: just standard TLS
229                info!("mTLS optional mode specified but no CA file provided, using standard TLS");
230                let key = keys.remove(0);
231                rustls::server::ServerConfig::builder()
232                    .with_no_client_auth()
233                    .with_single_cert(server_certs, key)
234                    .map_err(|e| {
235                        mockforge_core::Error::generic(format!("TLS config error: {}", e))
236                    })?
237            }
238        }
239        _ => {
240            // Standard TLS: no client certificate required
241            let key = keys.remove(0);
242            rustls::server::ServerConfig::builder()
243                .with_no_client_auth()
244                .with_single_cert(server_certs, key)
245                .map_err(|e| mockforge_core::Error::generic(format!("TLS config error: {}", e)))?
246        }
247    };
248
249    // Note: TLS version configuration is handled by with_safe_defaults()
250    // which supports TLS 1.2 and 1.3. The min_version config option is
251    // documented but rustls uses safe defaults that include both versions.
252    if config.min_version == "1.3" {
253        info!("TLS 1.3 requested (rustls safe defaults support both 1.2 and 1.3)");
254    } else if config.min_version != "1.2" && !config.min_version.is_empty() {
255        tracing::warn!(
256            "Unsupported TLS version: {}, using rustls safe defaults (1.2+)",
257            config.min_version
258        );
259    }
260
261    // Configure cipher suites if specified
262    if !config.cipher_suites.is_empty() {
263        // Note: rustls uses safe defaults, so we don't override cipher suites
264        // unless there's a specific need. The config is accepted but may not be used.
265        info!("Custom cipher suites specified but rustls uses safe defaults");
266    }
267
268    info!("TLS acceptor configured successfully");
269    Ok(TlsAcceptor::from(Arc::new(server_config)))
270}
271
272/// Load TLS server configuration for use with axum-server
273///
274/// This function is similar to load_tls_acceptor but returns the ServerConfig
275/// directly for use with axum-server's RustlsConfig.
276pub fn load_tls_server_config(
277    config: &HttpTlsConfig,
278) -> std::result::Result<Arc<rustls::server::ServerConfig>, Box<dyn std::error::Error + Send + Sync>>
279{
280    use rustls_pemfile::{certs, pkcs8_private_keys};
281    use std::fs::File;
282    use std::io::BufReader;
283    use std::sync::Arc;
284
285    // Ensure crypto provider is initialized
286    init_crypto_provider();
287
288    info!(
289        "Loading TLS certificate from {} and key from {}",
290        config.cert_file, config.key_file
291    );
292
293    // Load certificate chain
294    let cert_file = File::open(&config.cert_file)
295        .map_err(|e| format!("Failed to open certificate file {}: {}", config.cert_file, e))?;
296    let mut cert_reader = BufReader::new(cert_file);
297    let server_certs: Vec<rustls::pki_types::CertificateDer<'static>> = certs(&mut cert_reader)
298        .collect::<std::result::Result<Vec<_>, _>>()
299        .map_err(|e| format!("Failed to parse certificate file {}: {}", config.cert_file, e))?;
300
301    if server_certs.is_empty() {
302        return Err(format!("No certificates found in {}", config.cert_file).into());
303    }
304
305    // Load private key
306    let key_file = File::open(&config.key_file)
307        .map_err(|e| format!("Failed to open private key file {}: {}", config.key_file, e))?;
308    let mut key_reader = BufReader::new(key_file);
309    let pkcs8_keys: Vec<rustls::pki_types::PrivatePkcs8KeyDer<'static>> =
310        pkcs8_private_keys(&mut key_reader)
311            .collect::<std::result::Result<Vec<_>, _>>()
312            .map_err(|e| format!("Failed to parse private key file {}: {}", config.key_file, e))?;
313    let mut keys: Vec<rustls::pki_types::PrivateKeyDer<'static>> = pkcs8_keys
314        .into_iter()
315        .map(|k| rustls::pki_types::PrivateKeyDer::Pkcs8(k))
316        .collect();
317
318    if keys.is_empty() {
319        return Err(format!("No private keys found in {}", config.key_file).into());
320    }
321
322    // Determine mTLS mode
323    let mtls_mode = if !config.mtls_mode.is_empty() && config.mtls_mode != "off" {
324        config.mtls_mode.as_str()
325    } else if config.require_client_cert {
326        "required"
327    } else {
328        "off"
329    };
330
331    let server_config = match mtls_mode {
332        "required" => {
333            if let Some(ref ca_file_path) = config.ca_file {
334                let ca_file = File::open(ca_file_path).map_err(|e| {
335                    format!("Failed to open CA certificate file {}: {}", ca_file_path, e)
336                })?;
337                let mut ca_reader = BufReader::new(ca_file);
338                let ca_certs: Vec<rustls::pki_types::CertificateDer<'static>> =
339                    certs(&mut ca_reader).collect::<std::result::Result<Vec<_>, _>>().map_err(
340                        |e| format!("Failed to parse CA certificate file {}: {}", ca_file_path, e),
341                    )?;
342
343                let mut root_store = rustls::RootCertStore::empty();
344                for cert in &ca_certs {
345                    root_store.add(cert.clone()).map_err(|e| {
346                        format!("Failed to add CA certificate to root store: {}", e)
347                    })?;
348                }
349
350                let client_verifier =
351                    rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
352                        .build()
353                        .map_err(|e| format!("Failed to build client verifier: {}", e))?;
354
355                let key = keys.remove(0);
356
357                rustls::server::ServerConfig::builder()
358                    .with_client_cert_verifier(client_verifier.into())
359                    .with_single_cert(server_certs, key)
360                    .map_err(|e| format!("TLS config error (mTLS required): {}", e))?
361            } else {
362                return Err("mTLS mode 'required' requires CA certificate file".to_string().into());
363            }
364        }
365        "optional" => {
366            if let Some(ref ca_file_path) = config.ca_file {
367                let ca_file = File::open(ca_file_path).map_err(|e| {
368                    format!("Failed to open CA certificate file {}: {}", ca_file_path, e)
369                })?;
370                let mut ca_reader = BufReader::new(ca_file);
371                let ca_certs: Vec<rustls::pki_types::CertificateDer<'static>> =
372                    certs(&mut ca_reader).collect::<std::result::Result<Vec<_>, _>>().map_err(
373                        |e| format!("Failed to parse CA certificate file {}: {}", ca_file_path, e),
374                    )?;
375
376                let mut root_store = rustls::RootCertStore::empty();
377                for cert in &ca_certs {
378                    root_store.add(cert.clone()).map_err(|e| {
379                        format!("Failed to add CA certificate to root store: {}", e)
380                    })?;
381                }
382
383                let client_verifier =
384                    rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
385                        .build()
386                        .map_err(|e| format!("Failed to build client verifier: {}", e))?;
387
388                let key = keys.remove(0);
389
390                rustls::server::ServerConfig::builder()
391                    .with_client_cert_verifier(client_verifier.into())
392                    .with_single_cert(server_certs, key)
393                    .map_err(|e| format!("TLS config error (mTLS optional): {}", e))?
394            } else {
395                let key = keys.remove(0);
396                rustls::server::ServerConfig::builder()
397                    .with_no_client_auth()
398                    .with_single_cert(server_certs, key)
399                    .map_err(|e| format!("TLS config error: {}", e))?
400            }
401        }
402        _ => {
403            let key = keys.remove(0);
404            rustls::server::ServerConfig::builder()
405                .with_no_client_auth()
406                .with_single_cert(server_certs, key)
407                .map_err(|e| format!("TLS config error: {}", e))?
408        }
409    };
410
411    Ok(Arc::new(server_config))
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use std::io::Write;
418    use tempfile::NamedTempFile;
419
420    // Tests use the module-level init_crypto_provider() from super::*
421
422    fn create_test_cert() -> (NamedTempFile, NamedTempFile) {
423        // Create minimal test certificates (these won't actually work for real TLS,
424        // but allow us to test the parsing logic)
425        let cert = NamedTempFile::new().unwrap();
426        let key = NamedTempFile::new().unwrap();
427
428        // Write minimal PEM structure (not valid, but tests file reading)
429        writeln!(cert.as_file(), "-----BEGIN CERTIFICATE-----").unwrap();
430        writeln!(cert.as_file(), "TEST").unwrap();
431        writeln!(cert.as_file(), "-----END CERTIFICATE-----").unwrap();
432
433        writeln!(key.as_file(), "-----BEGIN PRIVATE KEY-----").unwrap();
434        writeln!(key.as_file(), "TEST").unwrap();
435        writeln!(key.as_file(), "-----END PRIVATE KEY-----").unwrap();
436
437        (cert, key)
438    }
439
440    #[test]
441    fn test_tls_config_validation() {
442        init_crypto_provider();
443        let (cert, key) = create_test_cert();
444
445        let config = HttpTlsConfig {
446            enabled: true,
447            cert_file: cert.path().to_string_lossy().to_string(),
448            key_file: key.path().to_string_lossy().to_string(),
449            ca_file: None,
450            min_version: "1.2".to_string(),
451            cipher_suites: Vec::new(),
452            require_client_cert: false,
453            mtls_mode: "off".to_string(),
454        };
455
456        // This will fail because the certificates are not valid,
457        // but it tests that the function attempts to load them
458        let result = load_tls_acceptor(&config);
459        assert!(result.is_err()); // Should fail on invalid cert
460    }
461
462    #[test]
463    fn test_mtls_requires_ca() {
464        init_crypto_provider();
465        let (cert, key) = create_test_cert();
466
467        let config = HttpTlsConfig {
468            enabled: true,
469            cert_file: cert.path().to_string_lossy().to_string(),
470            key_file: key.path().to_string_lossy().to_string(),
471            ca_file: None,
472            min_version: "1.2".to_string(),
473            cipher_suites: Vec::new(),
474            require_client_cert: true, // Requires client cert but no CA file
475            mtls_mode: "required".to_string(),
476        };
477
478        let result = load_tls_acceptor(&config);
479        assert!(result.is_err());
480        let err_msg = format!("{}", result.err().unwrap());
481        assert!(
482            err_msg.contains("CA") || err_msg.contains("--tls-ca"),
483            "Expected error message about CA certificate, got: {}",
484            err_msg
485        );
486    }
487}