Skip to main content

corevpn_config/
generator.rs

1//! Client Configuration Generator
2
3use std::path::Path;
4
5use corevpn_crypto::{CertificateAuthority, Certificate};
6
7use crate::{ClientConfigBuilder, ConfigError, Result, ServerConfig};
8
9/// Client configuration generator
10pub struct ConfigGenerator {
11    /// Server configuration
12    server_config: ServerConfig,
13    /// Certificate Authority
14    ca: CertificateAuthority,
15    /// CA certificate PEM
16    ca_cert_pem: String,
17    /// tls-auth key (if enabled)
18    ta_key: Option<String>,
19}
20
21impl ConfigGenerator {
22    /// Create a new config generator
23    pub fn new(
24        server_config: ServerConfig,
25        ca: CertificateAuthority,
26        ta_key: Option<String>,
27    ) -> Self {
28        let ca_cert_pem = ca.certificate_pem().to_string();
29        Self {
30            server_config,
31            ca,
32            ca_cert_pem,
33            ta_key,
34        }
35    }
36
37    /// Generate client configuration
38    pub fn generate_client_config(
39        &self,
40        username: &str,
41        email: Option<&str>,
42    ) -> Result<GeneratedConfig> {
43        // Issue client certificate
44        let cert = self.ca.issue_client_certificate(
45            username,
46            email,
47            self.server_config.security.client_cert_lifetime_days,
48        ).map_err(|e| ConfigError::ValidationError(e.to_string()))?;
49
50        // Build client config
51        let mut builder = ClientConfigBuilder::new(
52            username,
53            &self.server_config.server.public_host,
54        )
55        .port(self.server_config.server.listen_addr.port())
56        .protocol(&self.server_config.server.protocol)
57        .ca_cert(&self.ca_cert_pem)
58        .client_cert(&cert.cert_pem)
59        .client_key(&cert.key_pem)
60        .cipher(&self.map_cipher(&self.server_config.security.cipher));
61
62        // Add tls-auth if enabled
63        if let Some(ta_key) = &self.ta_key {
64            builder = builder.tls_auth(ta_key, 1);
65        }
66
67        // Add compression stub (disabled for security)
68        builder = builder.extra_option("compress stub-v2");
69
70        let config = builder.build();
71        // Validate configuration before generating .ovpn content
72        config.validate()
73            .map_err(|e| ConfigError::ValidationError(format!("Invalid client config: {}", e)))?;
74        let ovpn_content = config.to_ovpn();
75
76        Ok(GeneratedConfig {
77            username: username.to_string(),
78            ovpn_content,
79            certificate: cert,
80        })
81    }
82
83    /// Generate mobile-optimized client configuration
84    pub fn generate_mobile_config(
85        &self,
86        username: &str,
87        email: Option<&str>,
88    ) -> Result<GeneratedConfig> {
89        let mut generated = self.generate_client_config(username, email)?;
90
91        // Build with mobile optimizations
92        let mut builder = ClientConfigBuilder::new(
93            username,
94            &self.server_config.server.public_host,
95        )
96        .port(self.server_config.server.listen_addr.port())
97        .protocol(&self.server_config.server.protocol)
98        .ca_cert(&self.ca_cert_pem)
99        .client_cert(&generated.certificate.cert_pem)
100        .client_key(&generated.certificate.key_pem)
101        .cipher(&self.map_cipher(&self.server_config.security.cipher))
102        .extra_option("connect-retry 2")
103        .extra_option("connect-retry-max 5")
104        .extra_option("auth-retry interact")
105        .extra_option("compress stub-v2");
106
107        if let Some(ta_key) = &self.ta_key {
108            builder = builder.tls_auth(ta_key, 1);
109        }
110
111        let config = builder.build();
112        // Validate configuration before generating .ovpn content
113        config.validate()
114            .map_err(|e| ConfigError::ValidationError(format!("Invalid mobile config: {}", e)))?;
115        generated.ovpn_content = config.to_ovpn();
116
117        Ok(generated)
118    }
119
120    fn map_cipher(&self, cipher: &str) -> String {
121        match cipher.to_lowercase().as_str() {
122            "chacha20-poly1305" => "CHACHA20-POLY1305".to_string(),
123            "aes-256-gcm" => "AES-256-GCM".to_string(),
124            _ => "AES-256-GCM".to_string(),
125        }
126    }
127}
128
129/// Generated client configuration
130#[derive(Debug, Clone)]
131pub struct GeneratedConfig {
132    /// Username
133    pub username: String,
134    /// .ovpn file contents
135    pub ovpn_content: String,
136    /// Certificate and keys
137    pub certificate: Certificate,
138}
139
140impl GeneratedConfig {
141    /// Get filename for the .ovpn file
142    pub fn filename(&self) -> String {
143        format!("{}.ovpn", self.username.replace(['@', '.', ' '], "_"))
144    }
145
146    /// Save to file
147    pub fn save(&self, dir: &Path) -> Result<std::path::PathBuf> {
148        // Sanitize filename - reject any path separators or parent directory references
149        let filename = self.filename();
150        if filename.contains(std::path::MAIN_SEPARATOR) || filename.contains("..") {
151            return Err(crate::ConfigError::ValidationError(
152                "Invalid filename: contains path separators or parent directory references".into(),
153            ));
154        }
155
156        // Ensure directory exists and is actually a directory
157        if !dir.exists() {
158            std::fs::create_dir_all(dir)?;
159        }
160        if !dir.is_dir() {
161            return Err(crate::ConfigError::ValidationError(
162                "Target path is not a directory".into(),
163            ));
164        }
165
166        // Canonicalize the directory to resolve symlinks
167        let canonical_dir = dir.canonicalize()
168            .map_err(|e| crate::ConfigError::IoError(e))?;
169
170        // Build the final path - using join() ensures we can't escape the directory
171        // as long as filename doesn't contain path separators (which we already checked)
172        let path = canonical_dir.join(&filename);
173
174        // Verify the path is still within the canonical directory
175        // This prevents path traversal attacks even if join() somehow allows it
176        // Since the file doesn't exist yet, we check that the path's parent is within canonical_dir
177        if let Some(parent) = path.parent() {
178            // Canonicalize the parent to resolve any symlinks
179            let canonical_parent = parent.canonicalize()
180                .map_err(|e| crate::ConfigError::IoError(e))?;
181            
182            // Ensure the canonical parent is within the canonical directory
183            if !canonical_parent.starts_with(&canonical_dir) {
184                return Err(crate::ConfigError::ValidationError(
185                    "Path traversal detected: final path outside target directory".into(),
186                ));
187            }
188        } else {
189            // This shouldn't happen, but be defensive
190            return Err(crate::ConfigError::ValidationError(
191                "Invalid path: no parent directory".into(),
192            ));
193        }
194
195        // Additional check: ensure the filename itself doesn't contain any path components
196        // This is redundant but provides defense in depth
197        if path != canonical_dir.join(&filename) {
198            return Err(crate::ConfigError::ValidationError(
199                "Path traversal detected: invalid path construction".into(),
200            ));
201        }
202
203        std::fs::write(&path, &self.ovpn_content)?;
204        Ok(path)
205    }
206}
207
208/// Initialize server PKI (CA, server cert, ta.key)
209pub fn initialize_pki(
210    data_dir: &Path,
211    server_cn: &str,
212    organization: &str,
213) -> Result<(CertificateAuthority, String)> {
214    use std::fs;
215
216    // Create data directory
217    fs::create_dir_all(data_dir)?;
218
219    // Generate CA
220    let ca = CertificateAuthority::new(
221        &format!("{} CA", organization),
222        organization,
223        3650, // 10 years
224    ).map_err(|e| ConfigError::ValidationError(e.to_string()))?;
225
226    // Save CA cert and key
227    fs::write(data_dir.join("ca.crt"), ca.certificate_pem())?;
228    fs::write(data_dir.join("ca.key"), ca.private_key_pem())?;
229
230    // Generate server certificate
231    let server_cert = ca.issue_server_certificate(
232        server_cn,
233        &[server_cn.to_string()],
234        &[],
235        365, // 1 year
236    ).map_err(|e| ConfigError::ValidationError(e.to_string()))?;
237
238    // Save server cert and key
239    fs::write(data_dir.join("server.crt"), &server_cert.cert_pem)?;
240    fs::write(data_dir.join("server.key"), &server_cert.key_pem)?;
241
242    // Generate tls-auth key
243    let ta_key_bytes = corevpn_crypto::cert::generate_static_key();
244    let ta_key = corevpn_crypto::cert::format_static_key(&ta_key_bytes);
245    fs::write(data_dir.join("ta.key"), &ta_key)?;
246
247    Ok((ca, ta_key))
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use tempfile::tempdir;
254
255    #[test]
256    fn test_initialize_pki() {
257        let dir = tempdir().unwrap();
258        let (ca, ta_key) = initialize_pki(
259            dir.path(),
260            "vpn.example.com",
261            "Test Org",
262        ).unwrap();
263
264        assert!(dir.path().join("ca.crt").exists());
265        assert!(dir.path().join("ca.key").exists());
266        assert!(dir.path().join("server.crt").exists());
267        assert!(dir.path().join("server.key").exists());
268        assert!(dir.path().join("ta.key").exists());
269        assert!(!ta_key.is_empty());
270    }
271}