Skip to main content

rusmes_acme/
cert.rs

1//! Certificate management
2
3use crate::{AcmeError, Result};
4use chrono::{DateTime, Utc};
5use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, SanType};
6use std::convert::TryInto;
7use tracing::{debug, info};
8use x509_parser::prelude::*;
9
10/// Certificate with metadata
11#[derive(Debug, Clone)]
12pub struct Certificate {
13    /// PEM-encoded certificate
14    pub cert_pem: String,
15    /// PEM-encoded private key
16    pub key_pem: String,
17    /// PEM-encoded certificate chain
18    pub chain_pem: String,
19    /// Domain names in certificate
20    pub domains: Vec<String>,
21    /// Certificate expiration time
22    pub expires_at: DateTime<Utc>,
23    /// Certificate not-before time
24    pub not_before: DateTime<Utc>,
25}
26
27impl Certificate {
28    /// Create a new certificate
29    pub fn new(
30        cert_pem: String,
31        key_pem: String,
32        chain_pem: String,
33        domains: Vec<String>,
34        expires_at: DateTime<Utc>,
35        not_before: DateTime<Utc>,
36    ) -> Self {
37        Self {
38            cert_pem,
39            key_pem,
40            chain_pem,
41            domains,
42            expires_at,
43            not_before,
44        }
45    }
46
47    /// Parse certificate from PEM
48    pub fn from_pem(cert_pem: String, key_pem: String) -> Result<Self> {
49        let (domains, expires_at, not_before) = Self::parse_certificate_info(&cert_pem)?;
50
51        Ok(Self {
52            cert_pem,
53            key_pem,
54            chain_pem: String::new(),
55            domains,
56            expires_at,
57            not_before,
58        })
59    }
60
61    /// Check if certificate is expired
62    pub fn is_expired(&self) -> bool {
63        Utc::now() >= self.expires_at
64    }
65
66    /// Check if certificate should be renewed
67    pub fn should_renew(&self, days_before_expiry: u32) -> bool {
68        let renewal_time = self.expires_at - chrono::Duration::days(days_before_expiry as i64);
69        Utc::now() >= renewal_time
70    }
71
72    /// Get days until expiry
73    pub fn days_until_expiry(&self) -> i64 {
74        (self.expires_at - Utc::now()).num_days()
75    }
76
77    /// Get hours until expiry
78    pub fn hours_until_expiry(&self) -> i64 {
79        (self.expires_at - Utc::now()).num_hours()
80    }
81
82    /// Save certificate to files
83    pub async fn save(&self, cert_path: &str, key_path: &str) -> Result<()> {
84        tokio::fs::write(cert_path, &self.cert_pem).await?;
85        tokio::fs::write(key_path, &self.key_pem).await?;
86
87        // Set appropriate permissions on Unix
88        #[cfg(unix)]
89        {
90            use std::os::unix::fs::PermissionsExt;
91            let key_perms = std::fs::Permissions::from_mode(0o600);
92            std::fs::set_permissions(key_path, key_perms)?;
93        }
94
95        info!("Certificate saved to {} and {}", cert_path, key_path);
96        Ok(())
97    }
98
99    /// Load certificate from files
100    pub async fn load(cert_path: &str, key_path: &str) -> Result<Self> {
101        let cert_pem = tokio::fs::read_to_string(cert_path).await?;
102        let key_pem = tokio::fs::read_to_string(key_path).await?;
103
104        Self::from_pem(cert_pem, key_pem)
105    }
106
107    /// Parse certificate information from PEM
108    fn parse_certificate_info(
109        cert_pem: &str,
110    ) -> Result<(Vec<String>, DateTime<Utc>, DateTime<Utc>)> {
111        // Extract DER from PEM using x509-parser's pem module
112        let pem = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
113            .map_err(|e| AcmeError::ValidationFailed(format!("Failed to parse PEM: {}", e)))?
114            .1;
115
116        let der = &pem.contents;
117
118        // Parse X.509 certificate
119        let (_, cert) = X509Certificate::from_der(der)
120            .map_err(|e| AcmeError::ValidationFailed(format!("Failed to parse X.509: {}", e)))?;
121
122        // Extract domains from subject alternative names
123        let mut domains = Vec::new();
124        if let Ok(Some(san_ext)) = cert.subject_alternative_name() {
125            for name in &san_ext.value.general_names {
126                if let GeneralName::DNSName(dns) = name {
127                    domains.push(dns.to_string());
128                }
129            }
130        }
131
132        // If no SAN, try to get from common name
133        if domains.is_empty() {
134            if let Some(cn) = cert.subject().iter_common_name().next() {
135                if let Ok(cn_str) = cn.as_str() {
136                    domains.push(cn_str.to_string());
137                }
138            }
139        }
140
141        // Extract validity dates
142        let not_after = cert.validity().not_after.timestamp();
143        let not_before = cert.validity().not_before.timestamp();
144
145        let expires_at = DateTime::from_timestamp(not_after, 0)
146            .ok_or_else(|| AcmeError::ValidationFailed("Invalid expiration time".to_string()))?;
147
148        let not_before_dt = DateTime::from_timestamp(not_before, 0)
149            .ok_or_else(|| AcmeError::ValidationFailed("Invalid not-before time".to_string()))?;
150
151        debug!(
152            "Certificate expires at: {}, domains: {:?}",
153            expires_at, domains
154        );
155
156        Ok((domains, expires_at, not_before_dt))
157    }
158
159    /// Validate certificate chain
160    pub fn validate_chain(&self) -> Result<()> {
161        // Basic validation - in production, would do full chain validation
162        if self.cert_pem.is_empty() {
163            return Err(AcmeError::ValidationFailed("Empty certificate".to_string()));
164        }
165
166        if self.key_pem.is_empty() {
167            return Err(AcmeError::ValidationFailed("Empty private key".to_string()));
168        }
169
170        Ok(())
171    }
172}
173
174/// Certificate Signing Request (CSR) generator
175pub struct CsrGenerator;
176
177impl CsrGenerator {
178    /// Generate a CSR for the given domains
179    pub fn generate(domains: Vec<String>, key_type: KeyType) -> Result<(String, String)> {
180        if domains.is_empty() {
181            return Err(AcmeError::Other("No domains specified for CSR".to_string()));
182        }
183
184        let mut params = CertificateParams::new(domains.clone())
185            .map_err(|e| AcmeError::Other(format!("Failed to create certificate params: {}", e)))?;
186
187        // Set subject
188        let mut dn = DistinguishedName::new();
189        dn.push(DnType::CommonName, &domains[0]);
190        params.distinguished_name = dn;
191
192        // Add SANs — rcgen 0.14 uses Ia5String (inferred via TryInto) for DnsName
193        params.subject_alt_names = domains
194            .iter()
195            .map(|d| {
196                let ia5: std::result::Result<_, _> = d.as_str().try_into();
197                ia5.map(SanType::DnsName)
198                    .map_err(|e| AcmeError::Other(format!("Invalid domain name '{}': {}", d, e)))
199            })
200            .collect::<Result<Vec<_>>>()?;
201
202        // Generate key pair based on type.
203        // rcgen 0.14 uses KeyPair::generate() (P-256 by default) or
204        // KeyPair::generate_for(alg) for explicit algorithm selection.
205        // For RSA keys we fall back to ECDSA P-256 since rcgen does not
206        // expose RSA key generation without openssl feature.
207        let key_pair = match key_type {
208            KeyType::Rsa2048 | KeyType::Rsa4096 => {
209                // Use ECDSA P-256 as rcgen doesn't support RSA key generation out of the box
210                KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
211                    .map_err(|e| AcmeError::Other(format!("Failed to generate key: {}", e)))?
212            }
213            KeyType::EcdsaP256 => KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
214                .map_err(|e| AcmeError::Other(format!("Failed to generate ECDSA key: {}", e)))?,
215        };
216
217        // Capture the serialized private key PEM before moving key_pair into CSR generation
218        let key_pem = key_pair.serialize_pem();
219
220        // Generate CSR — rcgen 0.14 API: CertificateParams::serialize_request(&key_pair)
221        let csr = params
222            .serialize_request(&key_pair)
223            .map_err(|e| AcmeError::Other(format!("Failed to serialize CSR: {}", e)))?;
224
225        // Get the DER-encoded CSR bytes
226        let csr_der: &[u8] = csr.der();
227
228        // Encode as PEM
229        let csr_pem =
230            pem_rfc7468::encode_string("CERTIFICATE REQUEST", pem_rfc7468::LineEnding::LF, csr_der)
231                .map_err(|e| AcmeError::Other(format!("Failed to encode CSR PEM: {}", e)))?;
232
233        Ok((csr_pem, key_pem))
234    }
235}
236
237/// Private key type
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
239pub enum KeyType {
240    /// RSA 2048-bit
241    #[default]
242    Rsa2048,
243    /// RSA 4096-bit
244    Rsa4096,
245    /// ECDSA P-256
246    EcdsaP256,
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_certificate_expiry() {
255        let cert = Certificate::new(
256            "cert".to_string(),
257            "key".to_string(),
258            "chain".to_string(),
259            vec!["example.com".to_string()],
260            Utc::now() - chrono::Duration::days(1),
261            Utc::now() - chrono::Duration::days(90),
262        );
263
264        assert!(cert.is_expired());
265        assert!(cert.should_renew(30));
266    }
267
268    #[test]
269    fn test_certificate_not_expired() {
270        let cert = Certificate::new(
271            "cert".to_string(),
272            "key".to_string(),
273            "chain".to_string(),
274            vec!["example.com".to_string()],
275            Utc::now() + chrono::Duration::days(60),
276            Utc::now() - chrono::Duration::days(30),
277        );
278
279        assert!(!cert.is_expired());
280        assert!(!cert.should_renew(30));
281    }
282
283    #[test]
284    fn test_should_renew() {
285        let cert = Certificate::new(
286            "cert".to_string(),
287            "key".to_string(),
288            "chain".to_string(),
289            vec!["example.com".to_string()],
290            Utc::now() + chrono::Duration::days(20),
291            Utc::now() - chrono::Duration::days(70),
292        );
293
294        assert!(!cert.is_expired());
295        assert!(cert.should_renew(30));
296        assert!(!cert.should_renew(10));
297    }
298
299    #[test]
300    fn test_days_until_expiry() {
301        let cert = Certificate::new(
302            "cert".to_string(),
303            "key".to_string(),
304            "chain".to_string(),
305            vec!["example.com".to_string()],
306            Utc::now() + chrono::Duration::days(45),
307            Utc::now() - chrono::Duration::days(45),
308        );
309
310        let days = cert.days_until_expiry();
311        assert!((44..=45).contains(&days));
312    }
313
314    #[test]
315    fn test_csr_generation_rsa2048() {
316        let domains = vec!["example.com".to_string(), "www.example.com".to_string()];
317        let result = CsrGenerator::generate(domains, KeyType::Rsa2048);
318
319        if let Err(ref e) = result {
320            eprintln!("CSR generation failed: {}", e);
321        }
322        assert!(result.is_ok());
323
324        let (csr_pem, key_pem) = result.unwrap();
325        assert!(csr_pem.contains("BEGIN CERTIFICATE REQUEST"));
326        assert!(key_pem.contains("PRIVATE KEY"));
327    }
328
329    #[test]
330    fn test_csr_generation_ecdsa() {
331        let domains = vec!["example.com".to_string()];
332        let result = CsrGenerator::generate(domains, KeyType::EcdsaP256);
333        assert!(result.is_ok());
334
335        let (csr_pem, key_pem) = result.unwrap();
336        assert!(csr_pem.contains("BEGIN CERTIFICATE REQUEST"));
337        assert!(key_pem.contains("BEGIN PRIVATE KEY") || key_pem.contains("BEGIN EC PRIVATE KEY"));
338    }
339
340    #[test]
341    fn test_csr_generation_no_domains() {
342        let domains = vec![];
343        let result = CsrGenerator::generate(domains, KeyType::Rsa2048);
344        assert!(result.is_err());
345    }
346
347    #[tokio::test]
348    async fn test_certificate_save_load() {
349        use tempfile::tempdir;
350
351        let dir = tempdir().unwrap();
352        let cert_path = dir.path().join("cert.pem");
353        let key_path = dir.path().join("key.pem");
354
355        let cert = Certificate::new(
356            "TEST_CERT_PEM".to_string(),
357            "TEST_KEY_PEM".to_string(),
358            "TEST_CHAIN_PEM".to_string(),
359            vec!["example.com".to_string()],
360            Utc::now() + chrono::Duration::days(90),
361            Utc::now(),
362        );
363
364        cert.save(cert_path.to_str().unwrap(), key_path.to_str().unwrap())
365            .await
366            .unwrap();
367
368        // Verify files exist
369        assert!(cert_path.exists());
370        assert!(key_path.exists());
371
372        // Verify content
373        let saved_cert = tokio::fs::read_to_string(&cert_path).await.unwrap();
374        let saved_key = tokio::fs::read_to_string(&key_path).await.unwrap();
375
376        assert_eq!(saved_cert, "TEST_CERT_PEM");
377        assert_eq!(saved_key, "TEST_KEY_PEM");
378    }
379
380    #[test]
381    fn test_certificate_validation() {
382        let cert = Certificate::new(
383            "cert".to_string(),
384            "key".to_string(),
385            "chain".to_string(),
386            vec!["example.com".to_string()],
387            Utc::now() + chrono::Duration::days(90),
388            Utc::now(),
389        );
390
391        assert!(cert.validate_chain().is_ok());
392    }
393
394    #[test]
395    fn test_certificate_validation_empty_cert() {
396        let cert = Certificate::new(
397            String::new(),
398            "key".to_string(),
399            "chain".to_string(),
400            vec!["example.com".to_string()],
401            Utc::now() + chrono::Duration::days(90),
402            Utc::now(),
403        );
404
405        assert!(cert.validate_chain().is_err());
406    }
407
408    #[test]
409    fn test_certificate_validation_empty_key() {
410        let cert = Certificate::new(
411            "cert".to_string(),
412            String::new(),
413            "chain".to_string(),
414            vec!["example.com".to_string()],
415            Utc::now() + chrono::Duration::days(90),
416            Utc::now(),
417        );
418
419        assert!(cert.validate_chain().is_err());
420    }
421
422    #[test]
423    fn test_key_type_default() {
424        assert_eq!(KeyType::default(), KeyType::Rsa2048);
425    }
426
427    #[test]
428    fn test_hours_until_expiry() {
429        let cert = Certificate::new(
430            "cert".to_string(),
431            "key".to_string(),
432            "chain".to_string(),
433            vec!["example.com".to_string()],
434            Utc::now() + chrono::Duration::hours(48),
435            Utc::now(),
436        );
437
438        let hours = cert.hours_until_expiry();
439        assert!((47..=48).contains(&hours));
440    }
441}