corevpn_config/
generator.rs1use std::path::Path;
4
5use corevpn_crypto::{CertificateAuthority, Certificate};
6
7use crate::{ClientConfigBuilder, ConfigError, Result, ServerConfig};
8
9pub struct ConfigGenerator {
11 server_config: ServerConfig,
13 ca: CertificateAuthority,
15 ca_cert_pem: String,
17 ta_key: Option<String>,
19}
20
21impl ConfigGenerator {
22 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 pub fn generate_client_config(
39 &self,
40 username: &str,
41 email: Option<&str>,
42 ) -> Result<GeneratedConfig> {
43 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 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 if let Some(ta_key) = &self.ta_key {
64 builder = builder.tls_auth(ta_key, 1);
65 }
66
67 builder = builder.extra_option("compress stub-v2");
69
70 let config = builder.build();
71 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 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 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 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#[derive(Debug, Clone)]
131pub struct GeneratedConfig {
132 pub username: String,
134 pub ovpn_content: String,
136 pub certificate: Certificate,
138}
139
140impl GeneratedConfig {
141 pub fn filename(&self) -> String {
143 format!("{}.ovpn", self.username.replace(['@', '.', ' '], "_"))
144 }
145
146 pub fn save(&self, dir: &Path) -> Result<std::path::PathBuf> {
148 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 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 let canonical_dir = dir.canonicalize()
168 .map_err(|e| crate::ConfigError::IoError(e))?;
169
170 let path = canonical_dir.join(&filename);
173
174 if let Some(parent) = path.parent() {
178 let canonical_parent = parent.canonicalize()
180 .map_err(|e| crate::ConfigError::IoError(e))?;
181
182 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 return Err(crate::ConfigError::ValidationError(
191 "Invalid path: no parent directory".into(),
192 ));
193 }
194
195 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
208pub fn initialize_pki(
210 data_dir: &Path,
211 server_cn: &str,
212 organization: &str,
213) -> Result<(CertificateAuthority, String)> {
214 use std::fs;
215
216 fs::create_dir_all(data_dir)?;
218
219 let ca = CertificateAuthority::new(
221 &format!("{} CA", organization),
222 organization,
223 3650, ).map_err(|e| ConfigError::ValidationError(e.to_string()))?;
225
226 fs::write(data_dir.join("ca.crt"), ca.certificate_pem())?;
228 fs::write(data_dir.join("ca.key"), ca.private_key_pem())?;
229
230 let server_cert = ca.issue_server_certificate(
232 server_cn,
233 &[server_cn.to_string()],
234 &[],
235 365, ).map_err(|e| ConfigError::ValidationError(e.to_string()))?;
237
238 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 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}