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;