1use 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#[derive(Debug, Clone)]
12pub struct Certificate {
13 pub cert_pem: String,
15 pub key_pem: String,
17 pub chain_pem: String,
19 pub domains: Vec<String>,
21 pub expires_at: DateTime<Utc>,
23 pub not_before: DateTime<Utc>,
25}
26
27impl Certificate {
28 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 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 pub fn is_expired(&self) -> bool {
63 Utc::now() >= self.expires_at
64 }
65
66 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 pub fn days_until_expiry(&self) -> i64 {
74 (self.expires_at - Utc::now()).num_days()
75 }
76
77 pub fn hours_until_expiry(&self) -> i64 {
79 (self.expires_at - Utc::now()).num_hours()
80 }
81
82 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 #[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 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 fn parse_certificate_info(
109 cert_pem: &str,
110 ) -> Result<(Vec<String>, DateTime<Utc>, DateTime<Utc>)> {
111 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 let (_, cert) = X509Certificate::from_der(der)
120 .map_err(|e| AcmeError::ValidationFailed(format!("Failed to parse X.509: {}", e)))?;
121
122 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 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 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 pub fn validate_chain(&self) -> Result<()> {
161 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
174pub struct CsrGenerator;
176
177impl CsrGenerator {
178 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 let mut dn = DistinguishedName::new();
189 dn.push(DnType::CommonName, &domains[0]);
190 params.distinguished_name = dn;
191
192 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 let key_pair = match key_type {
208 KeyType::Rsa2048 | KeyType::Rsa4096 => {
209 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 let key_pem = key_pair.serialize_pem();
219
220 let csr = params
222 .serialize_request(&key_pair)
223 .map_err(|e| AcmeError::Other(format!("Failed to serialize CSR: {}", e)))?;
224
225 let csr_der: &[u8] = csr.der();
227
228 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
239pub enum KeyType {
240 #[default]
242 Rsa2048,
243 Rsa4096,
245 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 assert!(cert_path.exists());
370 assert!(key_path.exists());
371
372 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}