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