Skip to main content

pkix_revocation/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![forbid(unsafe_code)]
4#![warn(missing_docs, rust_2018_idioms)]
5
6//! Certificate revocation checking for `pkix-path` and `pkix-chain`.
7//!
8//! Provides the [`RevocationChecker`] trait and implementations:
9//!
10//! | Type | Feature | Description |
11//! |---|---|---|
12//! | [`NoRevocation`] | (always) | Zero-cost; always reports not-revoked |
13//! | `CrlChecker` | `crl` | Offline CRL validation (you supply DER bytes) |
14//! | `OcspChecker` | `ocsp` | Offline OCSP response validation |
15//!
16//! # `no_std` note
17//!
18//! The core trait and `NoRevocation` are `no_std`. Feature-gated checkers
19//! that perform network I/O are `std`-only and gated behind separate features.
20
21use pkix_path::TrustAnchor;
22use x509_cert::{ext::pkix::crl::CrlReason, serial_number::SerialNumber, Certificate};
23
24/// Errors returned by revocation checking.
25#[derive(Clone, Debug, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum Error {
28    /// The certificate has been revoked.
29    Revoked {
30        /// Serial number of the revoked certificate (for logging/diagnostics).
31        serial: SerialNumber,
32        /// RFC 5280 §5.3.1 reason code from the CRL/OCSP entry, if present.
33        /// `None` means no reason code was provided.
34        reason_code: Option<CrlReason>,
35    },
36
37    /// The CRL validity window check failed.
38    ///
39    /// This covers two cases:
40    /// - `now < thisUpdate`: the CRL is not yet valid (clock skew or future-dated CRL)
41    /// - `now > nextUpdate`: the CRL has expired
42    /// - `nextUpdate` absent: treated as expired (no expiry information means stale)
43    CrlExpired,
44
45    /// The CRL issuer name does not match the certificate's issuer.
46    ///
47    /// The CRL's `issuer` field must match the certificate's `issuer` field for the
48    /// CRL to apply to that certificate. A mismatch indicates the wrong CRL was provided.
49    CrlIssuerMismatch,
50
51    /// The CRL signature did not verify against the issuer's SPKI.
52    CrlSignatureInvalid,
53
54    /// DER decoding of a CRL failed.
55    CrlParseError(der::Error),
56
57    /// An OCSP response signature did not verify against the responder's key.
58    OcspSignatureInvalid,
59
60    /// The OCSP responder returned an `unknown` status (hard-fail mode).
61    OcspStatusUnknown,
62
63    /// DER decoding of an OCSP response failed.
64    OcspParseError(der::Error),
65
66    /// The OCSP response is structurally invalid per RFC 6960 but DER-decodable.
67    ///
68    /// Currently returned in two cases:
69    /// - `responseBytes` is absent in a `Successful` response (RFC 6960 §4.2.1)
70    /// - `responseType` is not `id-pkix-ocsp-basic` (unrecognized response format)
71    OcspMalformed,
72
73    /// The CRL issuer certificate does not have the `cRLSign` bit set in KeyUsage
74    /// (RFC 5280 §6.3.3(f)).
75    CrlSignMissing,
76
77    /// A delta CRL was supplied but no base CRL is available, or the delta's
78    /// `BaseCRLNumber` does not match the base CRL's `CRLNumber`.
79    DeltaCrlBaseMismatch,
80
81    /// The CRL's CRL number is lower than expected (base CRL must have a number
82    /// ≥ the delta's `BaseCRLNumber`).
83    CrlNumberMismatch,
84}
85
86impl core::fmt::Display for Error {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        match self {
89            Error::Revoked {
90                serial,
91                reason_code,
92            } => match reason_code {
93                Some(code) => write!(
94                    f,
95                    "certificate {serial} is revoked (reason {})",
96                    crl_reason_name(*code)
97                ),
98                None => write!(f, "certificate {serial} is revoked"),
99            },
100            Error::CrlExpired => f.write_str("CRL validity window check failed"),
101            Error::CrlIssuerMismatch => f.write_str("CRL issuer does not match certificate issuer"),
102            Error::CrlSignatureInvalid => f.write_str("CRL signature is invalid"),
103            Error::CrlParseError(e) => write!(f, "CRL parse error: {e}"),
104            Error::OcspSignatureInvalid => f.write_str("OCSP response signature is invalid"),
105            Error::OcspStatusUnknown => f.write_str("OCSP responder returned unknown status"),
106            Error::OcspParseError(e) => write!(f, "OCSP response parse error: {e}"),
107            Error::OcspMalformed => {
108                f.write_str("OCSP response is structurally invalid (malformed per RFC 6960)")
109            }
110            Error::CrlSignMissing => {
111                f.write_str("CRL issuer KeyUsage does not include cRLSign (RFC 5280 §6.3.3(f))")
112            }
113            Error::DeltaCrlBaseMismatch => {
114                f.write_str("delta CRL BaseCRLNumber does not match the base CRL's CRLNumber")
115            }
116            Error::CrlNumberMismatch => f.write_str("CRL number is lower than expected"),
117        }
118    }
119}
120
121/// Map a `CrlReason` variant to its RFC 5280 §5.3.1 camelCase name.
122fn crl_reason_name(r: CrlReason) -> &'static str {
123    match r {
124        CrlReason::Unspecified => "unspecified",
125        CrlReason::KeyCompromise => "keyCompromise",
126        CrlReason::CaCompromise => "cACompromise",
127        CrlReason::AffiliationChanged => "affiliationChanged",
128        CrlReason::Superseded => "superseded",
129        CrlReason::CessationOfOperation => "cessationOfOperation",
130        CrlReason::CertificateHold => "certificateHold",
131        CrlReason::RemoveFromCRL => "removeFromCRL",
132        CrlReason::PrivilegeWithdrawn => "privilegeWithdrawn",
133        CrlReason::AaCompromise => "aACompromise",
134    }
135}
136
137#[cfg(feature = "std")]
138impl std::error::Error for Error {
139    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
140        match self {
141            Error::CrlParseError(e) => Some(e),
142            Error::OcspParseError(e) => Some(e),
143            _ => None,
144        }
145    }
146}
147
148/// Result alias for this crate.
149pub type Result<T> = core::result::Result<T, Error>;
150
151/// Pluggable revocation checking.
152///
153/// Called once per certificate in the chain, in leaf-to-issuer order,
154/// after path signature validation has succeeded.
155///
156/// Implement this trait to plug CRL, OCSP, or a custom revocation mechanism
157/// into `pkix_chain::verify_chain`. Use [`NoRevocation`] for offline or
158/// embedded environments.
159/// # Implementing this trait
160///
161/// Implementors MUST provide [`RevocationChecker::check_revocation`].
162///
163/// Implementors that want **full-chain** revocation coverage — i.e., revocation
164/// checking for every certificate including the one issued directly by a trust
165/// anchor — MUST also override
166/// [`RevocationChecker::check_revocation_against_anchor`]. The default
167/// implementation skips the check silently; forgetting to override it will
168/// leave the anchor-issued certificate unchecked with no compile error or
169/// runtime warning.
170pub trait RevocationChecker {
171    /// Check whether `cert` has been revoked.
172    ///
173    /// - `cert`   — the certificate being checked
174    /// - `issuer` — the certificate that issued `cert` (signature-validated)
175    ///
176    /// Returns `Ok(())` if the certificate is not revoked, or an `Err` if it
177    /// is revoked or if revocation status cannot be determined and the policy
178    /// requires a definitive answer (hard-fail mode).
179    ///
180    /// # Errors
181    ///
182    /// - [`Error::Revoked`] — the certificate's serial number appears in the
183    ///   CRL's or OCSP response's revoked list.
184    /// - [`Error::CrlExpired`] — the CRL has passed its `nextUpdate` timestamp.
185    /// - [`Error::OcspMalformed`] — the OCSP response is structurally invalid or
186    ///   its validity window check failed.
187    /// - Other [`Error`] variants for parse failures, signature verification
188    ///   failures, or structural constraint violations.
189    ///
190    /// **`Ok(())` dual semantics**: implementations may return `Ok(())` both when
191    /// a certificate is confirmed not-revoked *and* when the revocation source does
192    /// not cover this certificate type (see [`CrlChecker`] for details). Hard-fail
193    /// callers must ensure at least one revocation source covers the certificate.
194    #[must_use = "revocation check result must not be silently discarded"]
195    fn check_revocation(&self, cert: &Certificate, issuer: &Certificate) -> crate::Result<()>;
196
197    /// Check whether `cert` (issued directly by a trust anchor) has been revoked.
198    ///
199    /// Called by `verify_chain` for the last certificate in the chain — the one
200    /// whose issuer is a [`TrustAnchor`] rather than another certificate in the
201    /// chain. For example, in the chain `[leaf, intermediate_CA]` this method is
202    /// called with `cert = intermediate_CA` and `anchor` set to the matched anchor.
203    ///
204    /// **Default implementation returns `Ok(())` (skip).** Override this method
205    /// to enforce revocation checking for certificates issued directly by a trust
206    /// anchor (e.g., fetch and verify the CA's CRL using the anchor's public key).
207    ///
208    /// `NoRevocation` inherits this default and skips the check, matching its
209    /// overall no-op behaviour. `CrlChecker` and `OcspChecker` also inherit the
210    /// default for v0.1; a future version will override when an issuer cert is
211    /// available.
212    #[must_use = "revocation check result must not be silently discarded"]
213    fn check_revocation_against_anchor(
214        &self,
215        _cert: &Certificate,
216        _anchor: &TrustAnchor,
217    ) -> crate::Result<()> {
218        Ok(())
219    }
220}
221
222/// A no-op revocation checker that always reports certificates as not revoked.
223///
224/// Use this when:
225/// - Running in embedded / offline environments with no revocation infrastructure
226/// - Revocation is enforced at a higher layer
227/// - In tests and development environments
228///
229/// # Security note
230///
231/// `NoRevocation` does **not** consult CRLs or OCSP. A revoked certificate
232/// will pass validation. Only use this when your threat model permits
233/// unenforced revocation (e.g., closed networks, short-lived certificates,
234/// hardware attestation where issuance itself is the control).
235#[derive(Clone, Copy, Debug, Default)]
236pub struct NoRevocation;
237
238impl RevocationChecker for NoRevocation {
239    #[inline]
240    fn check_revocation(&self, _cert: &Certificate, _issuer: &Certificate) -> crate::Result<()> {
241        Ok(())
242    }
243}
244
245#[cfg(feature = "crl")]
246mod crl;
247#[cfg(feature = "crl")]
248pub use crl::CrlChecker;
249
250#[cfg(feature = "ocsp")]
251mod ocsp;
252#[cfg(feature = "ocsp")]
253pub use ocsp::OcspChecker;