Skip to main content

cdx_core/security/
certificate.rs

1//! Certificate chain validation (offline).
2//!
3//! This module provides certificate parsing and chain validation functionality
4//! for verifying X.509 certificate chains used in document signatures.
5//!
6//! Note: Online revocation checks (OCSP, CRL) are deferred to a separate
7//! feature-gated module and require network access.
8
9use serde::{Deserialize, Serialize};
10
11/// Result of certificate chain validation.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct CertificateValidation {
14    /// Whether the certificate chain is valid (structurally correct and trusted).
15    pub valid: bool,
16
17    /// Whether any certificate in the chain has expired.
18    pub expired: bool,
19
20    /// Whether any certificate is not yet valid (notBefore is in the future).
21    pub not_yet_valid: bool,
22
23    /// The trust path from leaf to root (subject names).
24    pub trust_path: Vec<String>,
25
26    /// Validation errors encountered.
27    pub errors: Vec<String>,
28
29    /// Warnings (non-fatal issues).
30    pub warnings: Vec<String>,
31}
32
33impl CertificateValidation {
34    /// Create a successful validation result.
35    #[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    /// Create a failed validation result.
48    #[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    /// Check if validation passed without errors.
61    #[must_use]
62    pub fn is_valid(&self) -> bool {
63        self.valid && self.errors.is_empty()
64    }
65
66    /// Check if validation passed but with warnings.
67    #[must_use]
68    pub fn has_warnings(&self) -> bool {
69        !self.warnings.is_empty()
70    }
71
72    /// Add an error to the validation result.
73    pub fn add_error(&mut self, error: impl Into<String>) {
74        self.errors.push(error.into());
75        self.valid = false;
76    }
77
78    /// Add a warning to the validation result.
79    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/// Information extracted from a certificate.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct CertificateInfo {
94    /// Subject distinguished name.
95    pub subject: String,
96
97    /// Issuer distinguished name.
98    pub issuer: String,
99
100    /// Serial number (hex encoded).
101    pub serial_number: String,
102
103    /// Not valid before (ISO 8601).
104    pub not_before: String,
105
106    /// Not valid after (ISO 8601).
107    pub not_after: String,
108
109    /// Whether this is a CA certificate.
110    pub is_ca: bool,
111
112    /// Key usage flags.
113    #[serde(default, skip_serializing_if = "Vec::is_empty")]
114    pub key_usage: Vec<KeyUsage>,
115
116    /// Extended key usage OIDs.
117    #[serde(default, skip_serializing_if = "Vec::is_empty")]
118    pub extended_key_usage: Vec<String>,
119
120    /// Subject alternative names.
121    #[serde(default, skip_serializing_if = "Vec::is_empty")]
122    pub subject_alt_names: Vec<String>,
123
124    /// SHA-256 fingerprint of the certificate (hex encoded).
125    pub fingerprint_sha256: String,
126}
127
128impl CertificateInfo {
129    /// Create certificate info with minimal required fields.
130    #[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    /// Check if the certificate is self-signed.
151    #[must_use]
152    pub fn is_self_signed(&self) -> bool {
153        self.subject == self.issuer
154    }
155
156    /// Set the validity period.
157    #[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    /// Set the CA flag.
169    #[must_use]
170    pub fn with_ca(mut self, is_ca: bool) -> Self {
171        self.is_ca = is_ca;
172        self
173    }
174
175    /// Set the fingerprint.
176    #[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    /// Add a key usage.
183    #[must_use]
184    pub fn with_key_usage(mut self, usage: KeyUsage) -> Self {
185        self.key_usage.push(usage);
186        self
187    }
188
189    /// Add an extended key usage OID.
190    #[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/// Key usage flags for X.509 certificates.
198#[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    /// Digital signature.
203    DigitalSignature,
204    /// Non-repudiation (content commitment).
205    NonRepudiation,
206    /// Key encipherment.
207    KeyEncipherment,
208    /// Data encipherment.
209    DataEncipherment,
210    /// Key agreement.
211    KeyAgreement,
212    /// Key certificate signing.
213    KeyCertSign,
214    /// CRL signing.
215    #[strum(serialize = "cRLSign")]
216    CrlSign,
217    /// Encipher only (with key agreement).
218    EncipherOnly,
219    /// Decipher only (with key agreement).
220    DecipherOnly,
221}
222
223/// Common extended key usage OIDs.
224pub mod eku {
225    /// Server authentication (1.3.6.1.5.5.7.3.1)
226    pub const SERVER_AUTH: &str = "1.3.6.1.5.5.7.3.1";
227    /// Client authentication (1.3.6.1.5.5.7.3.2)
228    pub const CLIENT_AUTH: &str = "1.3.6.1.5.5.7.3.2";
229    /// Code signing (1.3.6.1.5.5.7.3.3)
230    pub const CODE_SIGNING: &str = "1.3.6.1.5.5.7.3.3";
231    /// Email protection (1.3.6.1.5.5.7.3.4)
232    pub const EMAIL_PROTECTION: &str = "1.3.6.1.5.5.7.3.4";
233    /// Time stamping (1.3.6.1.5.5.7.3.8)
234    pub const TIME_STAMPING: &str = "1.3.6.1.5.5.7.3.8";
235    /// Document signing (1.3.6.1.5.5.7.3.36)
236    pub const DOCUMENT_SIGNING: &str = "1.3.6.1.5.5.7.3.36";
237}
238
239/// A certificate chain for validation.
240#[derive(Debug, Clone)]
241pub struct CertificateChain {
242    /// Certificates in the chain, from leaf to root.
243    pub certificates: Vec<CertificateInfo>,
244}
245
246impl CertificateChain {
247    /// Create a new certificate chain.
248    #[must_use]
249    pub fn new(certificates: Vec<CertificateInfo>) -> Self {
250        Self { certificates }
251    }
252
253    /// Create an empty certificate chain.
254    #[must_use]
255    pub fn empty() -> Self {
256        Self {
257            certificates: Vec::new(),
258        }
259    }
260
261    /// Get the leaf (end-entity) certificate.
262    #[must_use]
263    pub fn leaf(&self) -> Option<&CertificateInfo> {
264        self.certificates.first()
265    }
266
267    /// Get the root certificate.
268    #[must_use]
269    pub fn root(&self) -> Option<&CertificateInfo> {
270        self.certificates.last()
271    }
272
273    /// Check if the chain is empty.
274    #[must_use]
275    pub fn is_empty(&self) -> bool {
276        self.certificates.is_empty()
277    }
278
279    /// Get the number of certificates in the chain.
280    #[must_use]
281    pub fn len(&self) -> usize {
282        self.certificates.len()
283    }
284
285    /// Add a certificate to the chain.
286    pub fn push(&mut self, cert: CertificateInfo) {
287        self.certificates.push(cert);
288    }
289
290    /// Validate the certificate chain structure (offline).
291    ///
292    /// This performs basic structural validation:
293    /// - Chain is not empty
294    /// - Each certificate is issued by the next one in the chain
295    /// - Root certificate is self-signed
296    /// - CA certificates have the CA flag set
297    ///
298    /// Note: This does NOT validate:
299    /// - Cryptographic signatures (requires parsing actual X.509)
300    /// - Expiration dates (requires current time)
301    /// - Revocation status (requires network)
302    #[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        // Build trust path
311        for cert in &self.certificates {
312            result.trust_path.push(cert.subject.clone());
313        }
314
315        // Validate chain linkage
316        for i in 0..self.certificates.len() - 1 {
317            let cert = &self.certificates[i];
318            let issuer = &self.certificates[i + 1];
319
320            // Check that cert's issuer matches issuer's subject
321            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            // Check that intermediate/root certs have CA flag
329            if !issuer.is_ca {
330                result.add_warning(format!("Issuer '{}' is not marked as a CA", issuer.subject));
331            }
332        }
333
334        // Check that root is self-signed
335        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    /// Validate that the chain is trusted by the given trust anchors.
348    ///
349    /// The chain's root must match one of the trusted roots by fingerprint.
350    #[must_use]
351    pub fn validate_trust(&self, trusted_roots: &[CertificateInfo]) -> CertificateValidation {
352        // First do structural validation
353        let mut result = self.validate_structure();
354        if !result.valid {
355            return result;
356        }
357
358        // Check if root is trusted
359        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}