Skip to main content

cdx_core/security/
revocation.rs

1//! Certificate revocation checking via OCSP and CRL.
2//!
3//! This module provides online revocation checking for X.509 certificates
4//! using OCSP (Online Certificate Status Protocol) and CRL (Certificate
5//! Revocation Lists).
6//!
7//! # Feature Flag
8//!
9//! This module requires the `ocsp` feature:
10//!
11//! ```toml
12//! [dependencies]
13//! cdx-core = { version = "0.1", features = ["ocsp"] }
14//! ```
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use cdx_core::security::revocation::{RevocationChecker, RevocationStatus};
20//!
21//! let checker = RevocationChecker::new();
22//! let status = checker.check_ocsp(certificate_der, issuer_der).await?;
23//!
24//! match status {
25//!     RevocationStatus::Good => println!("Certificate is valid"),
26//!     RevocationStatus::Revoked { reason, time } => {
27//!         println!("Certificate revoked: {:?}", reason);
28//!     }
29//!     RevocationStatus::Unknown => println!("Status unknown"),
30//! }
31//! ```
32
33use std::fmt;
34use std::time::Duration;
35
36use serde::{Deserialize, Serialize};
37
38#[cfg(feature = "ocsp")]
39use crate::error::{invalid_certificate, network_error};
40
41/// Revocation status of a certificate.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(tag = "status", rename_all = "camelCase")]
44pub enum RevocationStatus {
45    /// Certificate is not revoked.
46    Good,
47
48    /// Certificate has been revoked.
49    Revoked {
50        /// Reason for revocation.
51        #[serde(skip_serializing_if = "Option::is_none")]
52        reason: Option<RevocationReason>,
53        /// Time of revocation (ISO 8601).
54        #[serde(skip_serializing_if = "Option::is_none")]
55        revocation_time: Option<String>,
56    },
57
58    /// Revocation status is unknown.
59    Unknown,
60
61    /// Revocation check failed.
62    Error {
63        /// Error message.
64        message: String,
65    },
66}
67
68impl RevocationStatus {
69    /// Check if the certificate is known to be good.
70    #[must_use]
71    pub fn is_good(&self) -> bool {
72        matches!(self, Self::Good)
73    }
74
75    /// Check if the certificate is revoked.
76    #[must_use]
77    pub fn is_revoked(&self) -> bool {
78        matches!(self, Self::Revoked { .. })
79    }
80
81    /// Check if there was an error checking revocation.
82    #[must_use]
83    pub fn is_error(&self) -> bool {
84        matches!(self, Self::Error { .. })
85    }
86}
87
88impl fmt::Display for RevocationStatus {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::Good => write!(f, "good"),
92            Self::Revoked { reason, .. } => {
93                if let Some(r) = reason {
94                    write!(f, "revoked ({r})")
95                } else {
96                    write!(f, "revoked")
97                }
98            }
99            Self::Unknown => write!(f, "unknown"),
100            Self::Error { message } => write!(f, "error: {message}"),
101        }
102    }
103}
104
105/// Reason for certificate revocation (RFC 5280).
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display)]
107#[serde(rename_all = "camelCase")]
108#[repr(u8)]
109pub enum RevocationReason {
110    /// Unspecified reason.
111    #[strum(serialize = "unspecified")]
112    Unspecified = 0,
113    /// Key has been compromised.
114    #[strum(serialize = "key compromise")]
115    KeyCompromise = 1,
116    /// CA key has been compromised.
117    #[strum(serialize = "CA compromise")]
118    CaCompromise = 2,
119    /// Affiliation has changed.
120    #[strum(serialize = "affiliation changed")]
121    AffiliationChanged = 3,
122    /// Certificate has been superseded.
123    #[strum(serialize = "superseded")]
124    Superseded = 4,
125    /// Certificate is no longer needed.
126    #[strum(serialize = "cessation of operation")]
127    CessationOfOperation = 5,
128    /// Certificate is on hold.
129    #[strum(serialize = "certificate hold")]
130    CertificateHold = 6,
131    /// Removed from CRL (not revoked).
132    #[strum(serialize = "remove from CRL")]
133    RemoveFromCrl = 8,
134    /// Privilege has been withdrawn.
135    #[strum(serialize = "privilege withdrawn")]
136    PrivilegeWithdrawn = 9,
137    /// AA has been compromised.
138    #[strum(serialize = "AA compromise")]
139    AaCompromise = 10,
140}
141
142impl RevocationReason {
143    /// Create from RFC 5280 reason code.
144    #[must_use]
145    pub fn from_code(code: u8) -> Option<Self> {
146        match code {
147            0 => Some(Self::Unspecified),
148            1 => Some(Self::KeyCompromise),
149            2 => Some(Self::CaCompromise),
150            3 => Some(Self::AffiliationChanged),
151            4 => Some(Self::Superseded),
152            5 => Some(Self::CessationOfOperation),
153            6 => Some(Self::CertificateHold),
154            8 => Some(Self::RemoveFromCrl),
155            9 => Some(Self::PrivilegeWithdrawn),
156            10 => Some(Self::AaCompromise),
157            _ => None,
158        }
159    }
160
161    /// Get the RFC 5280 reason code.
162    #[must_use]
163    pub const fn code(&self) -> u8 {
164        *self as u8
165    }
166}
167
168/// Result of a revocation check.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct RevocationResult {
172    /// The revocation status.
173    pub status: RevocationStatus,
174
175    /// Method used for the check.
176    pub method: RevocationMethod,
177
178    /// URL of the responder or CRL.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub responder_url: Option<String>,
181
182    /// When this check was performed (ISO 8601).
183    pub checked_at: String,
184
185    /// When the response was produced (ISO 8601).
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub produced_at: Option<String>,
188
189    /// When this response should be considered stale (ISO 8601).
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub next_update: Option<String>,
192
193    /// Certificate serial number that was checked.
194    pub serial_number: String,
195}
196
197impl RevocationResult {
198    /// Create a new revocation result.
199    #[must_use]
200    pub fn new(status: RevocationStatus, method: RevocationMethod, serial_number: String) -> Self {
201        Self {
202            status,
203            method,
204            responder_url: None,
205            checked_at: chrono::Utc::now().to_rfc3339(),
206            produced_at: None,
207            next_update: None,
208            serial_number,
209        }
210    }
211
212    /// Set the responder URL.
213    #[must_use]
214    pub fn with_responder(mut self, url: impl Into<String>) -> Self {
215        self.responder_url = Some(url.into());
216        self
217    }
218
219    /// Set the produced_at time.
220    #[must_use]
221    pub fn with_produced_at(mut self, time: impl Into<String>) -> Self {
222        self.produced_at = Some(time.into());
223        self
224    }
225
226    /// Set the next_update time.
227    #[must_use]
228    pub fn with_next_update(mut self, time: impl Into<String>) -> Self {
229        self.next_update = Some(time.into());
230        self
231    }
232
233    /// Check if the result indicates the certificate is valid.
234    #[must_use]
235    pub fn is_valid(&self) -> bool {
236        self.status.is_good()
237    }
238}
239
240/// Method used for revocation checking.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display)]
242#[serde(rename_all = "camelCase")]
243pub enum RevocationMethod {
244    /// OCSP (Online Certificate Status Protocol).
245    #[strum(serialize = "OCSP")]
246    Ocsp,
247    /// CRL (Certificate Revocation List).
248    #[strum(serialize = "CRL")]
249    Crl,
250    /// OCSP stapled in TLS handshake.
251    #[strum(serialize = "OCSP Stapling")]
252    OcspStapling,
253}
254
255/// Configuration for revocation checking.
256#[derive(Debug, Clone)]
257pub struct RevocationConfig {
258    /// Timeout for network requests.
259    pub timeout: Duration,
260
261    /// Whether to prefer OCSP over CRL.
262    pub prefer_ocsp: bool,
263
264    /// Whether to use OCSP stapling when available.
265    pub use_stapling: bool,
266
267    /// Whether to require a valid revocation response.
268    /// If false, unknown status is treated as valid.
269    pub strict_mode: bool,
270
271    /// Maximum age of cached CRL responses in seconds.
272    pub max_crl_age: u64,
273
274    /// Custom OCSP responder URL (overrides AIA).
275    pub ocsp_responder: Option<String>,
276}
277
278impl Default for RevocationConfig {
279    fn default() -> Self {
280        Self {
281            timeout: Duration::from_secs(10),
282            prefer_ocsp: true,
283            use_stapling: true,
284            strict_mode: false,
285            max_crl_age: 86400, // 24 hours
286            ocsp_responder: None,
287        }
288    }
289}
290
291impl RevocationConfig {
292    /// Create a new configuration with default settings.
293    #[must_use]
294    pub fn new() -> Self {
295        Self::default()
296    }
297
298    /// Set the network timeout.
299    #[must_use]
300    pub fn with_timeout(mut self, timeout: Duration) -> Self {
301        self.timeout = timeout;
302        self
303    }
304
305    /// Set whether to prefer OCSP over CRL.
306    #[must_use]
307    pub fn with_prefer_ocsp(mut self, prefer: bool) -> Self {
308        self.prefer_ocsp = prefer;
309        self
310    }
311
312    /// Set strict mode (require valid revocation response).
313    #[must_use]
314    pub fn with_strict_mode(mut self, strict: bool) -> Self {
315        self.strict_mode = strict;
316        self
317    }
318
319    /// Set a custom OCSP responder URL.
320    #[must_use]
321    pub fn with_ocsp_responder(mut self, url: impl Into<String>) -> Self {
322        self.ocsp_responder = Some(url.into());
323        self
324    }
325}
326
327/// Certificate revocation checker.
328///
329/// This provides methods for checking certificate revocation status
330/// using OCSP and CRL protocols.
331pub struct RevocationChecker {
332    config: RevocationConfig,
333    #[cfg(feature = "ocsp")]
334    client: reqwest::Client,
335}
336
337impl RevocationChecker {
338    /// Create a new revocation checker with default configuration.
339    ///
340    /// # Errors
341    ///
342    /// Returns an error if the HTTP client cannot be initialized.
343    #[cfg(feature = "ocsp")]
344    pub fn new() -> Result<Self, crate::Error> {
345        Self::with_config(RevocationConfig::default())
346    }
347
348    /// Create a new revocation checker with custom configuration.
349    ///
350    /// # Errors
351    ///
352    /// Returns an error if the HTTP client cannot be initialized.
353    #[cfg(feature = "ocsp")]
354    pub fn with_config(config: RevocationConfig) -> Result<Self, crate::Error> {
355        let client = reqwest::Client::builder()
356            .timeout(config.timeout)
357            .build()
358            .map_err(|e| network_error(format!("Failed to create HTTP client: {e}")))?;
359
360        Ok(Self { config, client })
361    }
362
363    /// Check certificate revocation status via OCSP.
364    ///
365    /// # Arguments
366    ///
367    /// * `cert_der` - DER-encoded certificate to check
368    /// * `issuer_der` - DER-encoded issuer certificate
369    ///
370    /// # Returns
371    ///
372    /// The revocation status of the certificate.
373    ///
374    /// # Errors
375    ///
376    /// Returns an error if the OCSP check fails.
377    #[cfg(feature = "ocsp")]
378    pub async fn check_ocsp(
379        &self,
380        cert_der: &[u8],
381        issuer_der: &[u8],
382    ) -> Result<RevocationResult, crate::Error> {
383        use der::Decode;
384        use x509_cert::Certificate;
385
386        // Parse the certificates
387        let cert = Certificate::from_der(cert_der)
388            .map_err(|e| invalid_certificate(format!("Failed to parse certificate: {e}")))?;
389
390        let issuer = Certificate::from_der(issuer_der)
391            .map_err(|e| invalid_certificate(format!("Failed to parse issuer certificate: {e}")))?;
392
393        // Get serial number as hex string
394        let serial_bytes = cert.tbs_certificate().serial_number().as_bytes();
395        let serial = bytes_to_hex(serial_bytes);
396
397        // Get OCSP responder URL
398        let responder_url = self
399            .config
400            .ocsp_responder
401            .clone()
402            .or_else(|| extract_ocsp_url(&cert))
403            .ok_or_else(|| crate::Error::InvalidCertificate {
404                reason: "No OCSP responder URL found in certificate".to_string(),
405            })?;
406
407        // Build OCSP request
408        let request_body = build_ocsp_request(&cert, &issuer)?;
409
410        // Send OCSP request
411        let response = self
412            .client
413            .post(&responder_url)
414            .header("Content-Type", "application/ocsp-request")
415            .body(request_body)
416            .send()
417            .await
418            .map_err(|e| network_error(format!("OCSP request failed: {e}")))?;
419
420        if !response.status().is_success() {
421            return Ok(RevocationResult::new(
422                RevocationStatus::Error {
423                    message: format!("OCSP responder returned status {}", response.status()),
424                },
425                RevocationMethod::Ocsp,
426                serial,
427            )
428            .with_responder(&responder_url));
429        }
430
431        let response_body = response
432            .bytes()
433            .await
434            .map_err(|e| network_error(format!("Failed to read OCSP response: {e}")))?;
435
436        // Parse OCSP response
437        let status = parse_ocsp_response(&response_body);
438
439        Ok(
440            RevocationResult::new(status, RevocationMethod::Ocsp, serial)
441                .with_responder(&responder_url),
442        )
443    }
444
445    /// Check certificate revocation status via CRL.
446    ///
447    /// # Arguments
448    ///
449    /// * `cert_der` - DER-encoded certificate to check
450    ///
451    /// # Returns
452    ///
453    /// The revocation status of the certificate.
454    ///
455    /// # Errors
456    ///
457    /// Returns an error if the CRL check fails.
458    #[cfg(feature = "ocsp")]
459    pub async fn check_crl(&self, cert_der: &[u8]) -> Result<RevocationResult, crate::Error> {
460        use der::Decode;
461        use x509_cert::Certificate;
462
463        // Parse the certificate
464        let cert = Certificate::from_der(cert_der)
465            .map_err(|e| invalid_certificate(format!("Failed to parse certificate: {e}")))?;
466
467        // Get serial number as hex string
468        let serial_bytes = cert.tbs_certificate().serial_number().as_bytes();
469        let serial = bytes_to_hex(serial_bytes);
470
471        // Get CRL distribution point
472        let crl_url = extract_crl_url(&cert)
473            .ok_or_else(|| invalid_certificate("No CRL distribution point found in certificate"))?;
474
475        // Fetch CRL
476        let response = self
477            .client
478            .get(&crl_url)
479            .send()
480            .await
481            .map_err(|e| network_error(format!("CRL fetch failed: {e}")))?;
482
483        if !response.status().is_success() {
484            return Ok(RevocationResult::new(
485                RevocationStatus::Error {
486                    message: format!("CRL server returned status {}", response.status()),
487                },
488                RevocationMethod::Crl,
489                serial,
490            )
491            .with_responder(&crl_url));
492        }
493
494        let crl_data = response
495            .bytes()
496            .await
497            .map_err(|e| network_error(format!("Failed to read CRL: {e}")))?;
498
499        // Parse and check CRL
500        let status = check_crl_for_serial(&crl_data, cert.tbs_certificate().serial_number())?;
501
502        Ok(RevocationResult::new(status, RevocationMethod::Crl, serial).with_responder(&crl_url))
503    }
504
505    /// Check certificate revocation status using the preferred method.
506    ///
507    /// Tries OCSP first if configured, falls back to CRL.
508    ///
509    /// # Arguments
510    ///
511    /// * `cert_der` - DER-encoded certificate to check
512    /// * `issuer_der` - DER-encoded issuer certificate (for OCSP)
513    ///
514    /// # Returns
515    ///
516    /// The revocation status of the certificate.
517    ///
518    /// # Errors
519    ///
520    /// Returns an error if all revocation checks fail.
521    #[cfg(feature = "ocsp")]
522    pub async fn check(
523        &self,
524        cert_der: &[u8],
525        issuer_der: Option<&[u8]>,
526    ) -> Result<RevocationResult, crate::Error> {
527        if self.config.prefer_ocsp {
528            // Try OCSP first
529            let ocsp_err = if let Some(issuer) = issuer_der {
530                match self.check_ocsp(cert_der, issuer).await {
531                    Ok(result) if !result.status.is_error() => return Ok(result),
532                    Ok(result) => Some(format!("OCSP returned error: {}", result.status)),
533                    Err(e) => Some(format!("OCSP check failed: {e}")),
534                }
535            } else {
536                None
537            };
538
539            // Fall back to CRL
540            match self.check_crl(cert_der).await {
541                Ok(result) if !result.status.is_error() => Ok(result),
542                Ok(result) => Err(crate::Error::InvalidCertificate {
543                    reason: format!(
544                        "CRL returned error: {}{}",
545                        result.status,
546                        ocsp_err
547                            .as_ref()
548                            .map_or(String::new(), |e| format!("; prior {e}"))
549                    ),
550                }),
551                Err(crl_err) => Err(crate::Error::InvalidCertificate {
552                    reason: format!(
553                        "CRL check failed: {crl_err}{}",
554                        ocsp_err
555                            .as_ref()
556                            .map_or(String::new(), |e| format!("; prior {e}"))
557                    ),
558                }),
559            }
560        } else {
561            // Try CRL first
562            let crl_err = match self.check_crl(cert_der).await {
563                Ok(result) if !result.status.is_error() => return Ok(result),
564                Ok(result) => Some(format!("CRL returned error: {}", result.status)),
565                Err(e) => Some(format!("CRL check failed: {e}")),
566            };
567
568            // Fall back to OCSP
569            if let Some(issuer) = issuer_der {
570                match self.check_ocsp(cert_der, issuer).await {
571                    Ok(result) if !result.status.is_error() => Ok(result),
572                    Ok(result) => Err(crate::Error::InvalidCertificate {
573                        reason: format!(
574                            "OCSP returned error: {}{}",
575                            result.status,
576                            crl_err
577                                .as_ref()
578                                .map_or(String::new(), |e| format!("; prior {e}"))
579                        ),
580                    }),
581                    Err(ocsp_err) => Err(crate::Error::InvalidCertificate {
582                        reason: format!(
583                            "OCSP check failed: {ocsp_err}{}",
584                            crl_err
585                                .as_ref()
586                                .map_or(String::new(), |e| format!("; prior {e}"))
587                        ),
588                    }),
589                }
590            } else {
591                Err(crate::Error::InvalidCertificate {
592                    reason: format!(
593                        "No issuer provided for OCSP fallback{}",
594                        crl_err
595                            .as_ref()
596                            .map_or(String::new(), |e| format!("; prior {e}"))
597                    ),
598                })
599            }
600        }
601    }
602
603    /// Get the current configuration.
604    #[must_use]
605    pub fn config(&self) -> &RevocationConfig {
606        &self.config
607    }
608}
609
610impl std::fmt::Debug for RevocationChecker {
611    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
612        f.debug_struct("RevocationChecker")
613            .field("config", &self.config)
614            .finish_non_exhaustive()
615    }
616}
617
618// Helper functions for OCSP
619
620/// Convert bytes to uppercase hex string.
621#[cfg(feature = "ocsp")]
622fn bytes_to_hex(bytes: &[u8]) -> String {
623    use std::fmt::Write;
624    bytes
625        .iter()
626        .fold(String::with_capacity(bytes.len() * 2), |mut acc, b| {
627            let _ = write!(acc, "{b:02X}");
628            acc
629        })
630}
631
632#[cfg(feature = "ocsp")]
633fn extract_ocsp_url(cert: &x509_cert::Certificate) -> Option<String> {
634    use x509_cert::ext::pkix::AuthorityInfoAccessSyntax;
635
636    let extensions = cert.tbs_certificate().extensions()?;
637
638    for ext in extensions {
639        // OID for Authority Information Access: 1.3.6.1.5.5.7.1.1
640        if ext.extn_id.to_string() == "1.3.6.1.5.5.7.1.1" {
641            if let Ok(aia) =
642                <AuthorityInfoAccessSyntax as der::Decode>::from_der(ext.extn_value.as_bytes())
643            {
644                for access_desc in &aia.0 {
645                    // OID for OCSP: 1.3.6.1.5.5.7.48.1
646                    if access_desc.access_method.to_string() == "1.3.6.1.5.5.7.48.1" {
647                        if let x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(
648                            uri,
649                        ) = &access_desc.access_location
650                        {
651                            return Some(uri.to_string());
652                        }
653                    }
654                }
655            }
656        }
657    }
658
659    None
660}
661
662#[cfg(feature = "ocsp")]
663fn extract_crl_url(cert: &x509_cert::Certificate) -> Option<String> {
664    let extensions = cert.tbs_certificate().extensions()?;
665
666    for ext in extensions {
667        // OID for CRL Distribution Points: 2.5.29.31
668        if ext.extn_id.to_string() == "2.5.29.31" {
669            // Parse the CRL distribution points
670            // This is a simplified extraction - in practice you'd fully parse the ASN.1
671            let bytes = ext.extn_value.as_bytes();
672            // Look for http:// or https:// in the extension value
673            if let Ok(s) = std::str::from_utf8(bytes) {
674                if let Some(start) = s.find("http://").or_else(|| s.find("https://")) {
675                    let end = s[start..]
676                        .find(|c: char| c.is_control() || c == '\0')
677                        .map_or(s.len(), |e| start + e);
678                    return Some(s[start..end].to_string());
679                }
680            }
681        }
682    }
683
684    None
685}
686
687#[cfg(feature = "ocsp")]
688fn build_ocsp_request(
689    _cert: &x509_cert::Certificate,
690    _issuer: &x509_cert::Certificate,
691) -> Result<Vec<u8>, crate::Error> {
692    // Build a minimal OCSP request
693    // In a full implementation, this would:
694    // 1. Hash the issuer's name and key
695    // 2. Include the certificate serial number
696    // 3. Optionally add a nonce for replay protection
697
698    // For now, return a placeholder that indicates this needs full implementation
699    // with a proper ASN.1 OCSP request builder
700    Err(crate::Error::NotImplemented {
701        feature: "Full OCSP request building requires ocsp-rs or similar crate".to_string(),
702    })
703}
704
705#[cfg(feature = "ocsp")]
706fn parse_ocsp_response(_response: &[u8]) -> RevocationStatus {
707    // OCSP response parsing is not yet implemented.
708    // A full implementation would:
709    // 1. Check the response status (successful, malformed, etc.)
710    // 2. Verify the response signature
711    // 3. Extract the certificate status
712    //
713    // Return Error rather than Unknown so callers know the check was not performed,
714    // rather than incorrectly treating an unparsed response as indeterminate.
715    RevocationStatus::Error {
716        message: "OCSP response parsing not yet implemented".to_string(),
717    }
718}
719
720#[cfg(feature = "ocsp")]
721fn check_crl_for_serial(
722    crl_data: &[u8],
723    serial: &x509_cert::serial_number::SerialNumber,
724) -> Result<RevocationStatus, crate::Error> {
725    use der::Decode;
726    use x509_cert::crl::CertificateList;
727
728    // Parse the CRL
729    let crl = CertificateList::from_der(crl_data)
730        .map_err(|e| invalid_certificate(format!("Failed to parse CRL: {e}")))?;
731
732    // Check if the serial number is in the revoked list
733    if let Some(revoked_certs) = &crl.tbs_cert_list.revoked_certificates {
734        for revoked in revoked_certs {
735            if &revoked.serial_number == serial {
736                // Found in CRL - certificate is revoked
737                let reason = revoked.crl_entry_extensions.as_ref().and_then(|exts| {
738                    exts.iter().find_map(|ext| {
739                        // OID for CRL Reason: 2.5.29.21
740                        if ext.extn_id.to_string() == "2.5.29.21" {
741                            let bytes = ext.extn_value.as_bytes();
742                            if bytes.len() >= 3 {
743                                RevocationReason::from_code(bytes[2])
744                            } else {
745                                None
746                            }
747                        } else {
748                            None
749                        }
750                    })
751                });
752
753                return Ok(RevocationStatus::Revoked {
754                    reason,
755                    revocation_time: Some(revoked.revocation_date.to_string()),
756                });
757            }
758        }
759    }
760
761    // Not found in CRL - certificate is good
762    Ok(RevocationStatus::Good)
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768
769    #[test]
770    fn test_revocation_status_display() {
771        assert_eq!(RevocationStatus::Good.to_string(), "good");
772        assert_eq!(RevocationStatus::Unknown.to_string(), "unknown");
773        assert_eq!(
774            RevocationStatus::Revoked {
775                reason: Some(RevocationReason::KeyCompromise),
776                revocation_time: None,
777            }
778            .to_string(),
779            "revoked (key compromise)"
780        );
781    }
782
783    #[test]
784    fn test_revocation_status_checks() {
785        assert!(RevocationStatus::Good.is_good());
786        assert!(!RevocationStatus::Good.is_revoked());
787
788        let revoked = RevocationStatus::Revoked {
789            reason: None,
790            revocation_time: None,
791        };
792        assert!(!revoked.is_good());
793        assert!(revoked.is_revoked());
794
795        let error = RevocationStatus::Error {
796            message: "test".to_string(),
797        };
798        assert!(error.is_error());
799    }
800
801    #[test]
802    fn test_revocation_reason_from_code() {
803        assert_eq!(
804            RevocationReason::from_code(0),
805            Some(RevocationReason::Unspecified)
806        );
807        assert_eq!(
808            RevocationReason::from_code(1),
809            Some(RevocationReason::KeyCompromise)
810        );
811        assert_eq!(
812            RevocationReason::from_code(5),
813            Some(RevocationReason::CessationOfOperation)
814        );
815        assert_eq!(RevocationReason::from_code(7), None); // No reason code 7
816        assert_eq!(RevocationReason::from_code(255), None);
817    }
818
819    #[test]
820    fn test_revocation_reason_code() {
821        assert_eq!(RevocationReason::Unspecified.code(), 0);
822        assert_eq!(RevocationReason::KeyCompromise.code(), 1);
823        assert_eq!(RevocationReason::AaCompromise.code(), 10);
824    }
825
826    #[test]
827    fn test_revocation_reason_display() {
828        assert_eq!(
829            RevocationReason::KeyCompromise.to_string(),
830            "key compromise"
831        );
832        assert_eq!(
833            RevocationReason::CessationOfOperation.to_string(),
834            "cessation of operation"
835        );
836    }
837
838    #[test]
839    fn test_revocation_config_default() {
840        let config = RevocationConfig::default();
841        assert_eq!(config.timeout, Duration::from_secs(10));
842        assert!(config.prefer_ocsp);
843        assert!(config.use_stapling);
844        assert!(!config.strict_mode);
845        assert!(config.ocsp_responder.is_none());
846    }
847
848    #[test]
849    fn test_revocation_config_builder() {
850        let config = RevocationConfig::new()
851            .with_timeout(Duration::from_secs(30))
852            .with_prefer_ocsp(false)
853            .with_strict_mode(true)
854            .with_ocsp_responder("http://ocsp.example.com");
855
856        assert_eq!(config.timeout, Duration::from_secs(30));
857        assert!(!config.prefer_ocsp);
858        assert!(config.strict_mode);
859        assert_eq!(
860            config.ocsp_responder,
861            Some("http://ocsp.example.com".to_string())
862        );
863    }
864
865    #[test]
866    fn test_revocation_result_new() {
867        let result = RevocationResult::new(
868            RevocationStatus::Good,
869            RevocationMethod::Ocsp,
870            "1234ABCD".to_string(),
871        );
872
873        assert!(result.is_valid());
874        assert_eq!(result.method, RevocationMethod::Ocsp);
875        assert_eq!(result.serial_number, "1234ABCD");
876        assert!(result.responder_url.is_none());
877    }
878
879    #[test]
880    fn test_revocation_result_builder() {
881        let result = RevocationResult::new(
882            RevocationStatus::Good,
883            RevocationMethod::Ocsp,
884            "1234".to_string(),
885        )
886        .with_responder("http://ocsp.example.com")
887        .with_produced_at("2024-01-01T00:00:00Z")
888        .with_next_update("2024-01-02T00:00:00Z");
889
890        assert_eq!(
891            result.responder_url,
892            Some("http://ocsp.example.com".to_string())
893        );
894        assert_eq!(result.produced_at, Some("2024-01-01T00:00:00Z".to_string()));
895        assert_eq!(result.next_update, Some("2024-01-02T00:00:00Z".to_string()));
896    }
897
898    #[test]
899    fn test_revocation_method_display() {
900        assert_eq!(RevocationMethod::Ocsp.to_string(), "OCSP");
901        assert_eq!(RevocationMethod::Crl.to_string(), "CRL");
902        assert_eq!(RevocationMethod::OcspStapling.to_string(), "OCSP Stapling");
903    }
904
905    #[test]
906    fn test_revocation_status_serialization() {
907        let good = RevocationStatus::Good;
908        let json = serde_json::to_string(&good).unwrap();
909        assert!(json.contains("\"status\":\"good\""));
910
911        let revoked = RevocationStatus::Revoked {
912            reason: Some(RevocationReason::KeyCompromise),
913            revocation_time: Some("2024-01-01T00:00:00Z".to_string()),
914        };
915        let json = serde_json::to_string(&revoked).unwrap();
916        assert!(json.contains("\"status\":\"revoked\""));
917        assert!(json.contains("\"reason\":\"keyCompromise\""));
918    }
919}