1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct CertificateValidation {
14 pub valid: bool,
16
17 pub expired: bool,
19
20 pub not_yet_valid: bool,
22
23 pub trust_path: Vec<String>,
25
26 pub errors: Vec<String>,
28
29 pub warnings: Vec<String>,
31}
32
33impl CertificateValidation {
34 #[must_use]
36 pub fn success(trust_path: Vec<String>) -> Self {
37 Self {
38 valid: true,
39 expired: false,
40 not_yet_valid: false,
41 trust_path,
42 errors: Vec::new(),
43 warnings: Vec::new(),
44 }
45 }
46
47 #[must_use]
49 pub fn failure(error: impl Into<String>) -> Self {
50 Self {
51 valid: false,
52 expired: false,
53 not_yet_valid: false,
54 trust_path: Vec::new(),
55 errors: vec![error.into()],
56 warnings: Vec::new(),
57 }
58 }
59
60 #[must_use]
62 pub fn is_valid(&self) -> bool {
63 self.valid && self.errors.is_empty()
64 }
65
66 #[must_use]
68 pub fn has_warnings(&self) -> bool {
69 !self.warnings.is_empty()
70 }
71
72 pub fn add_error(&mut self, error: impl Into<String>) {
74 self.errors.push(error.into());
75 self.valid = false;
76 }
77
78 pub fn add_warning(&mut self, warning: impl Into<String>) {
80 self.warnings.push(warning.into());
81 }
82}
83
84impl Default for CertificateValidation {
85 fn default() -> Self {
86 Self::failure("Not validated")
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct CertificateInfo {
94 pub subject: String,
96
97 pub issuer: String,
99
100 pub serial_number: String,
102
103 pub not_before: String,
105
106 pub not_after: String,
108
109 pub is_ca: bool,
111
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
114 pub key_usage: Vec<KeyUsage>,
115
116 #[serde(default, skip_serializing_if = "Vec::is_empty")]
118 pub extended_key_usage: Vec<String>,
119
120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
122 pub subject_alt_names: Vec<String>,
123
124 pub fingerprint_sha256: String,
126}
127
128impl CertificateInfo {
129 #[must_use]
131 pub fn new(
132 subject: impl Into<String>,
133 issuer: impl Into<String>,
134 serial_number: impl Into<String>,
135 ) -> Self {
136 Self {
137 subject: subject.into(),
138 issuer: issuer.into(),
139 serial_number: serial_number.into(),
140 not_before: String::new(),
141 not_after: String::new(),
142 is_ca: false,
143 key_usage: Vec::new(),
144 extended_key_usage: Vec::new(),
145 subject_alt_names: Vec::new(),
146 fingerprint_sha256: String::new(),
147 }
148 }
149
150 #[must_use]
152 pub fn is_self_signed(&self) -> bool {
153 self.subject == self.issuer
154 }
155
156 #[must_use]
158 pub fn with_validity(
159 mut self,
160 not_before: impl Into<String>,
161 not_after: impl Into<String>,
162 ) -> Self {
163 self.not_before = not_before.into();
164 self.not_after = not_after.into();
165 self
166 }
167
168 #[must_use]
170 pub fn with_ca(mut self, is_ca: bool) -> Self {
171 self.is_ca = is_ca;
172 self
173 }
174
175 #[must_use]
177 pub fn with_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
178 self.fingerprint_sha256 = fingerprint.into();
179 self
180 }
181
182 #[must_use]
184 pub fn with_key_usage(mut self, usage: KeyUsage) -> Self {
185 self.key_usage.push(usage);
186 self
187 }
188
189 #[must_use]
191 pub fn with_extended_key_usage(mut self, oid: impl Into<String>) -> Self {
192 self.extended_key_usage.push(oid.into());
193 self
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display)]
199#[serde(rename_all = "camelCase")]
200#[strum(serialize_all = "camelCase")]
201pub enum KeyUsage {
202 DigitalSignature,
204 NonRepudiation,
206 KeyEncipherment,
208 DataEncipherment,
210 KeyAgreement,
212 KeyCertSign,
214 #[strum(serialize = "cRLSign")]
216 CrlSign,
217 EncipherOnly,
219 DecipherOnly,
221}
222
223pub mod eku {
225 pub const SERVER_AUTH: &str = "1.3.6.1.5.5.7.3.1";
227 pub const CLIENT_AUTH: &str = "1.3.6.1.5.5.7.3.2";
229 pub const CODE_SIGNING: &str = "1.3.6.1.5.5.7.3.3";
231 pub const EMAIL_PROTECTION: &str = "1.3.6.1.5.5.7.3.4";
233 pub const TIME_STAMPING: &str = "1.3.6.1.5.5.7.3.8";
235 pub const DOCUMENT_SIGNING: &str = "1.3.6.1.5.5.7.3.36";
237}
238
239#[derive(Debug, Clone)]
241pub struct CertificateChain {
242 pub certificates: Vec<CertificateInfo>,
244}
245
246impl CertificateChain {
247 #[must_use]
249 pub fn new(certificates: Vec<CertificateInfo>) -> Self {
250 Self { certificates }
251 }
252
253 #[must_use]
255 pub fn empty() -> Self {
256 Self {
257 certificates: Vec::new(),
258 }
259 }
260
261 #[must_use]
263 pub fn leaf(&self) -> Option<&CertificateInfo> {
264 self.certificates.first()
265 }
266
267 #[must_use]
269 pub fn root(&self) -> Option<&CertificateInfo> {
270 self.certificates.last()
271 }
272
273 #[must_use]
275 pub fn is_empty(&self) -> bool {
276 self.certificates.is_empty()
277 }
278
279 #[must_use]
281 pub fn len(&self) -> usize {
282 self.certificates.len()
283 }
284
285 pub fn push(&mut self, cert: CertificateInfo) {
287 self.certificates.push(cert);
288 }
289
290 #[must_use]
303 pub fn validate_structure(&self) -> CertificateValidation {
304 if self.certificates.is_empty() {
305 return CertificateValidation::failure("Certificate chain is empty");
306 }
307
308 let mut result = CertificateValidation::success(Vec::new());
309
310 for cert in &self.certificates {
312 result.trust_path.push(cert.subject.clone());
313 }
314
315 for i in 0..self.certificates.len() - 1 {
317 let cert = &self.certificates[i];
318 let issuer = &self.certificates[i + 1];
319
320 if cert.issuer != issuer.subject {
322 result.add_error(format!(
323 "Chain broken: '{}' issuer '{}' does not match next certificate subject '{}'",
324 cert.subject, cert.issuer, issuer.subject
325 ));
326 }
327
328 if !issuer.is_ca {
330 result.add_warning(format!("Issuer '{}' is not marked as a CA", issuer.subject));
331 }
332 }
333
334 if let Some(root) = self.root() {
336 if !root.is_self_signed() {
337 result.add_warning(format!(
338 "Root certificate '{}' is not self-signed (issuer: '{}')",
339 root.subject, root.issuer
340 ));
341 }
342 }
343
344 result
345 }
346
347 #[must_use]
351 pub fn validate_trust(&self, trusted_roots: &[CertificateInfo]) -> CertificateValidation {
352 let mut result = self.validate_structure();
354 if !result.valid {
355 return result;
356 }
357
358 if let Some(root) = self.root() {
360 let is_trusted = trusted_roots.iter().any(|trusted| {
361 trusted.fingerprint_sha256 == root.fingerprint_sha256
362 && !trusted.fingerprint_sha256.is_empty()
363 });
364
365 if !is_trusted {
366 result.add_error(format!(
367 "Root certificate '{}' is not in the trusted roots",
368 root.subject
369 ));
370 }
371 }
372
373 result
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 fn create_test_chain() -> CertificateChain {
382 let leaf = CertificateInfo::new("CN=leaf.example.com", "CN=Intermediate CA", "1234")
383 .with_fingerprint("aabbccdd")
384 .with_key_usage(KeyUsage::DigitalSignature);
385
386 let intermediate = CertificateInfo::new("CN=Intermediate CA", "CN=Root CA", "5678")
387 .with_ca(true)
388 .with_fingerprint("eeff0011")
389 .with_key_usage(KeyUsage::KeyCertSign);
390
391 let root = CertificateInfo::new("CN=Root CA", "CN=Root CA", "9999")
392 .with_ca(true)
393 .with_fingerprint("11223344")
394 .with_key_usage(KeyUsage::KeyCertSign);
395
396 CertificateChain::new(vec![leaf, intermediate, root])
397 }
398
399 #[test]
400 fn test_certificate_validation_success() {
401 let result = CertificateValidation::success(vec!["leaf".to_string(), "root".to_string()]);
402 assert!(result.is_valid());
403 assert!(!result.has_warnings());
404 assert_eq!(result.trust_path.len(), 2);
405 }
406
407 #[test]
408 fn test_certificate_validation_failure() {
409 let result = CertificateValidation::failure("Invalid certificate");
410 assert!(!result.is_valid());
411 assert_eq!(result.errors.len(), 1);
412 }
413
414 #[test]
415 fn test_certificate_info_self_signed() {
416 let self_signed = CertificateInfo::new("CN=Root CA", "CN=Root CA", "1234");
417 assert!(self_signed.is_self_signed());
418
419 let not_self_signed = CertificateInfo::new("CN=Leaf", "CN=Root CA", "5678");
420 assert!(!not_self_signed.is_self_signed());
421 }
422
423 #[test]
424 fn test_certificate_chain_structure() {
425 let chain = create_test_chain();
426
427 assert_eq!(chain.len(), 3);
428 assert!(!chain.is_empty());
429 assert_eq!(chain.leaf().unwrap().subject, "CN=leaf.example.com");
430 assert_eq!(chain.root().unwrap().subject, "CN=Root CA");
431 }
432
433 #[test]
434 fn test_validate_structure_valid() {
435 let chain = create_test_chain();
436 let result = chain.validate_structure();
437
438 assert!(result.is_valid());
439 assert_eq!(result.trust_path.len(), 3);
440 }
441
442 #[test]
443 fn test_validate_structure_empty_chain() {
444 let chain = CertificateChain::empty();
445 let result = chain.validate_structure();
446
447 assert!(!result.is_valid());
448 assert!(result.errors[0].contains("empty"));
449 }
450
451 #[test]
452 fn test_validate_structure_broken_chain() {
453 let leaf = CertificateInfo::new("CN=leaf.example.com", "CN=Wrong Issuer", "1234");
454 let root = CertificateInfo::new("CN=Root CA", "CN=Root CA", "9999").with_ca(true);
455
456 let chain = CertificateChain::new(vec![leaf, root]);
457 let result = chain.validate_structure();
458
459 assert!(!result.is_valid());
460 assert!(result.errors[0].contains("Chain broken"));
461 }
462
463 #[test]
464 fn test_validate_trust_trusted_root() {
465 let chain = create_test_chain();
466 let trusted_root =
467 CertificateInfo::new("CN=Root CA", "CN=Root CA", "9999").with_fingerprint("11223344");
468
469 let result = chain.validate_trust(&[trusted_root]);
470 assert!(result.is_valid());
471 }
472
473 #[test]
474 fn test_validate_trust_untrusted_root() {
475 let chain = create_test_chain();
476 let other_root = CertificateInfo::new("CN=Other Root", "CN=Other Root", "0000")
477 .with_fingerprint("99887766");
478
479 let result = chain.validate_trust(&[other_root]);
480 assert!(!result.is_valid());
481 assert!(result.errors[0].contains("not in the trusted roots"));
482 }
483
484 #[test]
485 fn test_key_usage_display() {
486 assert_eq!(KeyUsage::DigitalSignature.to_string(), "digitalSignature");
487 assert_eq!(KeyUsage::KeyCertSign.to_string(), "keyCertSign");
488 }
489
490 #[test]
491 fn test_certificate_info_serialization() {
492 let cert = CertificateInfo::new("CN=Test", "CN=Issuer", "1234")
493 .with_validity("2024-01-01T00:00:00Z", "2025-01-01T00:00:00Z")
494 .with_ca(true)
495 .with_fingerprint("abcd1234")
496 .with_key_usage(KeyUsage::DigitalSignature)
497 .with_extended_key_usage(eku::DOCUMENT_SIGNING);
498
499 let json = serde_json::to_string_pretty(&cert).unwrap();
500 assert!(json.contains("\"subject\": \"CN=Test\""));
501 assert!(json.contains("\"isCa\": true"));
502
503 let deserialized: CertificateInfo = serde_json::from_str(&json).unwrap();
504 assert_eq!(deserialized.subject, "CN=Test");
505 assert!(deserialized.is_ca);
506 }
507
508 #[test]
509 fn test_eku_constants() {
510 assert_eq!(eku::SERVER_AUTH, "1.3.6.1.5.5.7.3.1");
511 assert_eq!(eku::DOCUMENT_SIGNING, "1.3.6.1.5.5.7.3.36");
512 }
513}