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::{error, 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 cert_bytes: Vec<Vec<u8>> = certs(&mut cert_reader).map_err(|e| {
36        mockforge_core::Error::generic(format!(
37            "Failed to parse certificate file {}: {}",
38            config.cert_file, e
39        ))
40    })?;
41    let server_certs = cert_bytes.into_iter().map(rustls::Certificate).collect::<Vec<_>>();
42
43    if server_certs.is_empty() {
44        return Err(mockforge_core::Error::generic(format!(
45            "No certificates found in {}",
46            config.cert_file
47        )));
48    }
49
50    // Load private key
51    let key_file = File::open(&config.key_file).map_err(|e| {
52        mockforge_core::Error::generic(format!(
53            "Failed to open private key file {}: {}",
54            config.key_file, e
55        ))
56    })?;
57    let mut key_reader = BufReader::new(key_file);
58    let mut keys: Vec<Vec<u8>> = pkcs8_private_keys(&mut key_reader).map_err(|e| {
59        mockforge_core::Error::generic(format!(
60            "Failed to parse private key file {}: {}",
61            config.key_file, e
62        ))
63    })?;
64
65    if keys.is_empty() {
66        return Err(mockforge_core::Error::generic(format!(
67            "No private keys found in {}",
68            config.key_file
69        )));
70    }
71
72    // Build TLS server configuration with version support
73    // Note: rustls uses safe defaults, so we configure during builder creation
74    let server_config = if config.require_client_cert {
75        // Mutual TLS: require client certificates
76        if let Some(ref ca_file_path) = config.ca_file {
77            // Load CA certificate for client verification
78            let ca_file = File::open(ca_file_path).map_err(|e| {
79                mockforge_core::Error::generic(format!(
80                    "Failed to open CA certificate file {}: {}",
81                    ca_file_path, e
82                ))
83            })?;
84            let mut ca_reader = BufReader::new(ca_file);
85            let ca_certs: Vec<Vec<u8>> = certs(&mut ca_reader).map_err(|e| {
86                mockforge_core::Error::generic(format!(
87                    "Failed to parse CA certificate file {}: {}",
88                    ca_file_path, e
89                ))
90            })?;
91
92            let ca_certs = ca_certs.into_iter().map(rustls::Certificate).collect::<Vec<_>>();
93
94            let mut root_store = rustls::RootCertStore::empty();
95            for cert in ca_certs {
96                root_store.add(&cert).map_err(|e| {
97                    mockforge_core::Error::generic(format!(
98                        "Failed to add CA certificate to root store: {}",
99                        e
100                    ))
101                })?;
102            }
103
104            // Build with mTLS support
105            rustls::server::ServerConfig::builder()
106                .with_safe_defaults()
107                .with_client_cert_verifier(Arc::new(
108                    rustls::server::AllowAnyAuthenticatedClient::new(root_store),
109                ))
110                .with_single_cert(server_certs, rustls::PrivateKey(keys.remove(0)))
111                .map_err(|e| {
112                    mockforge_core::Error::generic(format!("TLS config error (mTLS): {}", e))
113                })?
114        } else {
115            return Err(mockforge_core::Error::generic(
116                "Client certificate required (require_client_cert=true) but no CA file provided",
117            ));
118        }
119    } else {
120        // Standard TLS: no client certificate required
121        rustls::server::ServerConfig::builder()
122            .with_safe_defaults()
123            .with_no_client_auth()
124            .with_single_cert(server_certs, rustls::PrivateKey(keys.remove(0)))
125            .map_err(|e| mockforge_core::Error::generic(format!("TLS config error: {}", e)))?
126    };
127
128    // Note: TLS version configuration is handled by with_safe_defaults()
129    // which supports TLS 1.2 and 1.3. The min_version config option is
130    // documented but rustls uses safe defaults that include both versions.
131    if config.min_version == "1.3" {
132        info!("TLS 1.3 requested (rustls safe defaults support both 1.2 and 1.3)");
133    } else if config.min_version != "1.2" && !config.min_version.is_empty() {
134        tracing::warn!(
135            "Unsupported TLS version: {}, using rustls safe defaults (1.2+)",
136            config.min_version
137        );
138    }
139
140    // Configure cipher suites if specified
141    if !config.cipher_suites.is_empty() {
142        // Note: rustls uses safe defaults, so we don't override cipher suites
143        // unless there's a specific need. The config is accepted but may not be used.
144        info!("Custom cipher suites specified but rustls uses safe defaults");
145    }
146
147    info!("TLS acceptor configured successfully");
148    Ok(TlsAcceptor::from(Arc::new(server_config)))
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::io::Write;
155    use tempfile::NamedTempFile;
156
157    fn create_test_cert() -> (NamedTempFile, NamedTempFile) {
158        // Create minimal test certificates (these won't actually work for real TLS,
159        // but allow us to test the parsing logic)
160        let cert = NamedTempFile::new().unwrap();
161        let key = NamedTempFile::new().unwrap();
162
163        // Write minimal PEM structure (not valid, but tests file reading)
164        writeln!(cert.as_file(), "-----BEGIN CERTIFICATE-----").unwrap();
165        writeln!(cert.as_file(), "TEST").unwrap();
166        writeln!(cert.as_file(), "-----END CERTIFICATE-----").unwrap();
167
168        writeln!(key.as_file(), "-----BEGIN PRIVATE KEY-----").unwrap();
169        writeln!(key.as_file(), "TEST").unwrap();
170        writeln!(key.as_file(), "-----END PRIVATE KEY-----").unwrap();
171
172        (cert, key)
173    }
174
175    #[test]
176    fn test_tls_config_validation() {
177        let (cert, key) = create_test_cert();
178
179        let config = HttpTlsConfig {
180            enabled: true,
181            cert_file: cert.path().to_string_lossy().to_string(),
182            key_file: key.path().to_string_lossy().to_string(),
183            ca_file: None,
184            min_version: "1.2".to_string(),
185            cipher_suites: Vec::new(),
186            require_client_cert: false,
187        };
188
189        // This will fail because the certificates are not valid,
190        // but it tests that the function attempts to load them
191        let result = load_tls_acceptor(&config);
192        assert!(result.is_err()); // Should fail on invalid cert
193    }
194
195    #[test]
196    fn test_mtls_requires_ca() {
197        let (cert, key) = create_test_cert();
198
199        let config = HttpTlsConfig {
200            enabled: true,
201            cert_file: cert.path().to_string_lossy().to_string(),
202            key_file: key.path().to_string_lossy().to_string(),
203            ca_file: None,
204            min_version: "1.2".to_string(),
205            cipher_suites: Vec::new(),
206            require_client_cert: true, // Requires client cert but no CA file
207        };
208
209        let result = load_tls_acceptor(&config);
210        assert!(result.is_err());
211        let err_msg = format!("{}", result.err().unwrap());
212        assert!(err_msg.contains("no CA file provided") || err_msg.contains("CA file"));
213    }
214}