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//!
21//! # Security: anchor-issued certificate revocation
22//!
23//! [`RevocationChecker::check_revocation_against_anchor`] has a default
24//! implementation that returns `Ok(())` (i.e., skips the check). Implementors
25//! that require **full-chain** revocation coverage — including the certificate
26//! issued directly by a trust anchor — **MUST** override this method. Failing
27//! to override it will silently leave the anchor-issued certificate unchecked
28//! with no compile error or runtime warning. See that method's documentation
29//! for details.
30//!
31//! # Limitations
32//!
33//! - **No network I/O.** `CrlChecker` and `OcspChecker` operate on
34//!   caller-supplied DER bytes; this crate never opens a socket. Online
35//!   fetching from `CRLDistributionPoints` / `AuthorityInfoAccess` URIs
36//!   lives in the optional `pkix-revocation-http` adapter crate.
37//! - **OCSP response only.** OCSP request construction (the DER bytes a
38//!   client POSTs to a responder) lives in `pkix-revocation-http` so it can
39//!   stay paired with the HTTP transport. The `OcspChecker` in this crate
40//!   validates already-fetched responses.
41//! - **No OCSP stapling helpers.** TLS-layer parsing of stapled responses
42//!   (RFC 6066 §8, multi-stapling RFC 6961) is a transport-protocol
43//!   concern handled by the TLS stack; once extracted, the response bytes
44//!   feed `OcspChecker` like any other.
45//! - **Algorithm coverage tracks `pkix-path`.** CRL and OCSP-response
46//!   signature verification is delegated to a `SignatureVerifier`; the
47//!   same algorithm gaps documented in `pkix-path` (Ed25519, P-521,
48//!   RSA-PSS — tracked under `PKIX-gphz`) apply here.
49
50use pkix_path::TrustAnchor;
51use x509_cert::{ext::pkix::crl::CrlReason, serial_number::SerialNumber, Certificate};
52
53/// Opaque wrapper around an underlying ASN.1 / DER error.
54///
55/// Re-exported from [`pkix_path::DerError`] so callers can match
56/// [`Error::CrlParseError`] / [`Error::OcspParseError`] against the
57/// same diagnostic type used by `pkix-path::Error::Der`. The wrapped
58/// `der::Error` is internal; only the [`Display`] message is in the
59/// public API. This insulates callers from semver-breaking changes
60/// in the `der` crate's error variants and makes the type
61/// cache-friendly (Clone + PartialEq + Eq + serde-friendly).
62///
63/// [`Display`]: core::fmt::Display
64pub use pkix_path::DerError;
65
66#[cfg(feature = "serde")]
67mod crl_reason_serde {
68    //! Serde shims for `Option<CrlReason>`. `CrlReason` is `repr(u32)`
69    //! upstream with stable RFC 5280 §5.3.1 numeric codes; we serialize
70    //! via the discriminant value. Unknown codes round-trip as `None`
71    //! so older consumers stay forward-compatible with new revocation
72    //! reasons that upstream adds.
73    use serde::{Deserialize as _, Deserializer, Serializer};
74    use x509_cert::ext::pkix::crl::CrlReason;
75
76    /// Convert a `CrlReason` to its RFC 5280 §5.3.1 numeric code.
77    const fn to_code(r: CrlReason) -> u32 {
78        match r {
79            CrlReason::Unspecified => 0,
80            CrlReason::KeyCompromise => 1,
81            CrlReason::CaCompromise => 2,
82            CrlReason::AffiliationChanged => 3,
83            CrlReason::Superseded => 4,
84            CrlReason::CessationOfOperation => 5,
85            CrlReason::CertificateHold => 6,
86            CrlReason::RemoveFromCRL => 8,
87            CrlReason::PrivilegeWithdrawn => 9,
88            CrlReason::AaCompromise => 10,
89        }
90    }
91
92    /// Inverse of [`to_code`]; returns `None` for unrecognised values
93    /// so future reason codes round-trip non-destructively.
94    const fn from_code(c: u32) -> Option<CrlReason> {
95        match c {
96            0 => Some(CrlReason::Unspecified),
97            1 => Some(CrlReason::KeyCompromise),
98            2 => Some(CrlReason::CaCompromise),
99            3 => Some(CrlReason::AffiliationChanged),
100            4 => Some(CrlReason::Superseded),
101            5 => Some(CrlReason::CessationOfOperation),
102            6 => Some(CrlReason::CertificateHold),
103            8 => Some(CrlReason::RemoveFromCRL),
104            9 => Some(CrlReason::PrivilegeWithdrawn),
105            10 => Some(CrlReason::AaCompromise),
106            _ => None,
107        }
108    }
109
110    pub fn serialize_opt<S: Serializer>(
111        v: &Option<CrlReason>,
112        s: S,
113    ) -> Result<S::Ok, S::Error> {
114        use serde::Serialize as _;
115        v.map(to_code).serialize(s)
116    }
117
118    pub fn deserialize_opt<'de, D: Deserializer<'de>>(
119        d: D,
120    ) -> Result<Option<CrlReason>, D::Error> {
121        let opt = Option::<u32>::deserialize(d)?;
122        Ok(opt.and_then(from_code))
123    }
124}
125
126/// Reason a revocation check produced no determination.
127///
128/// Carried by [`Error::OutOfScope`] to identify which scope-mismatch case the
129/// checker hit. Distinct from `Crl*Error` (parse / signature / validity
130/// failures): an `OutOfScope` outcome is structurally well-formed but the
131/// revocation source's stated scope excludes the certificate being checked.
132///
133/// Hard-fail callers should treat any `OutOfScope` as a failure (no
134/// revocation determination was made). Soft-fail callers can match on the
135/// reason and decide which scopes to tolerate (for example, treating
136/// `CrlOnlyAttributeCerts` as "expected and tolerable" while still hard-failing
137/// on `CrlOnlyCaCerts` when checking a CA certificate).
138#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
139#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
140#[non_exhaustive]
141pub enum OutOfScopeReason {
142    /// The CRL's `IssuingDistributionPoint` extension has
143    /// `onlyContainsAttributeCerts = true`. Attribute-certificate revocation
144    /// is out of scope for `pkix-revocation` (RFC 5755 attribute certificates
145    /// are handled by `pkix-ac`); the certificate being checked is a public-key
146    /// certificate, so the CRL cannot apply.
147    CrlOnlyAttributeCerts,
148    /// The CRL's `IssuingDistributionPoint` extension has
149    /// `onlyContainsUserCerts = true` but the certificate being checked is a
150    /// CA certificate (`BasicConstraints` `cA = TRUE`).
151    CrlOnlyUserCerts,
152    /// The CRL's `IssuingDistributionPoint` extension has
153    /// `onlyContainsCACerts = true` but the certificate being checked is not a
154    /// CA certificate.
155    CrlOnlyCaCerts,
156    /// The CRL's `IssuingDistributionPoint` `distributionPoint` field does
157    /// not match (or is incompatible with) any of the certificate's
158    /// `cRLDistributionPoints` extension entries (RFC 5280 §6.3.3(b)(1)).
159    ///
160    /// This case covers two sub-conditions, which are not distinguished in
161    /// the public API to avoid leaking implementation detail:
162    ///
163    /// 1. The CRL's IDP names a specific distribution point but the
164    ///    certificate carries no `cRLDistributionPoints` extension at all.
165    /// 2. Both sides name distribution points but no entry in the
166    ///    certificate's CDP resolves to a name that intersects the IDP's
167    ///    distributionPoint name.
168    ///
169    /// Hard-fail callers should treat this exactly like the other
170    /// `OutOfScope` reasons: the CRL is structurally well-formed but does
171    /// not cover the certificate, and a separate CRL/OCSP source must be
172    /// consulted.
173    CrlIdpDistributionPointMismatch,
174}
175
176impl core::fmt::Display for OutOfScopeReason {
177    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
178        match self {
179            Self::CrlOnlyAttributeCerts => f.write_str(
180                "CRL onlyContainsAttributeCerts=TRUE; subject is a public-key certificate",
181            ),
182            Self::CrlOnlyUserCerts => {
183                f.write_str("CRL onlyContainsUserCerts=TRUE; subject is a CA certificate")
184            }
185            Self::CrlOnlyCaCerts => {
186                f.write_str("CRL onlyContainsCACerts=TRUE; subject is an end-entity certificate")
187            }
188            Self::CrlIdpDistributionPointMismatch => f.write_str(
189                "CRL IssuingDistributionPoint distributionPoint does not match the certificate's CRLDistributionPoints",
190            ),
191        }
192    }
193}
194
195/// Errors returned by revocation checking.
196///
197/// # Variant naming convention
198///
199/// Most variants carry a `Crl*` or `Ocsp*` prefix indicating which revocation
200/// source produced the failure. Four variants intentionally do not:
201///
202/// - [`Error::Revoked`] applies to both CRL and OCSP outcomes; no prefix is
203///   correct. This is what [`RevocationChecker::check_revocation`] returns
204///   generically when a serial is found in either kind of response.
205/// - [`Error::MalformedCertificate`] fires on the *subject* certificate being
206///   checked (e.g., a missing serial number), not on the CRL or OCSP response.
207/// - [`Error::DeltaCrlBaseMismatch`] uses `DeltaCrl*` rather than `CrlDelta*`
208///   because the failure is scoped to the delta-CRL workflow — the prefix
209///   reads as the noun phrase "delta CRL" rather than as a sub-namespace of
210///   `Crl*`.
211/// - [`Error::OutOfScope`] applies whenever a revocation source's stated
212///   scope excludes the certificate being checked. Today only CRL `IDP`
213///   scope mismatches produce this; the variant is named generically so that
214///   future OCSP / SCT / OCSP-stapling scope-mismatch cases can reuse it
215///   without an additional rename.
216///
217/// Renames are a semver break; do not "normalize" these without coordinating
218/// a major version.
219#[derive(Clone, Debug, PartialEq, Eq)]
220#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
221#[non_exhaustive]
222pub enum Error {
223    /// The certificate has been revoked.
224    Revoked {
225        /// Serial number of the revoked certificate (for logging/diagnostics).
226        #[cfg_attr(feature = "serde", serde(with = "pkix_path::serde_der"))]
227        serial: SerialNumber,
228        /// RFC 5280 §5.3.1 reason code from the CRL/OCSP entry, if present.
229        /// `None` means no reason code was provided.
230        #[cfg_attr(
231            feature = "serde",
232            serde(
233                serialize_with = "crl_reason_serde::serialize_opt",
234                deserialize_with = "crl_reason_serde::deserialize_opt"
235            )
236        )]
237        reason_code: Option<CrlReason>,
238    },
239
240    /// The CRL validity window check failed.
241    ///
242    /// This covers two cases:
243    /// - `now < thisUpdate`: the CRL is not yet valid (clock skew or future-dated CRL)
244    /// - `now > nextUpdate`: the CRL has expired
245    /// - `nextUpdate` absent: treated as expired (no expiry information means stale)
246    CrlExpired,
247
248    /// The CRL issuer name does not match the certificate's issuer.
249    ///
250    /// The CRL's `issuer` field must match the certificate's `issuer` field for the
251    /// CRL to apply to that certificate. A mismatch indicates the wrong CRL was provided.
252    CrlIssuerMismatch,
253
254    /// The CRL signature did not verify against the issuer's SPKI.
255    CrlSignatureInvalid,
256
257    /// DER decoding of a CRL failed.
258    CrlParseError(DerError),
259
260    /// An OCSP response signature did not verify against the responder's key.
261    OcspSignatureInvalid,
262
263    /// The OCSP `ResponderId` does not match the expected issuer identity.
264    ///
265    /// Returned when the `byName` DN or `byKey` SHA-1 hash in the OCSP response
266    /// does not match the issuer (or trust anchor) used for this check.
267    ///
268    /// - `byName`: the name in the `ResponderId` does not match the issuer's
269    ///   subject DN (RFC 4518 comparison).
270    /// - `byKey`: the hash in the `ResponderId` does not match SHA-1 of the
271    ///   issuer's `subjectPublicKey` bit string (raw bytes, with tag, length,
272    ///   and unused-bits prefix stripped — not SHA-1 of the full SPKI DER).
273    ///
274    /// This is a distinct failure from [`Error::OcspSignatureInvalid`]: the
275    /// response may be cryptographically valid, but it was produced by a
276    /// different responder than expected.
277    OcspResponderIdMismatch,
278
279    /// The OCSP response's `CertID` issuer hashes do not match the expected issuer.
280    ///
281    /// The `issuerNameHash` or `issuerKeyHash` field in a `SingleResponse`
282    /// identifies which issuer the status assertion covers. A mismatch means
283    /// the response was produced for a certificate from a *different* CA
284    /// (or was tampered with) — it is not a responder-reported "unknown"
285    /// status. Callers MUST NOT treat this error as "try another responder".
286    OcspCertIdMismatch,
287
288    /// The `issuer` argument passed to [`RevocationChecker::check_revocation`] is
289    /// not the issuer of `cert`.
290    ///
291    /// This is a caller-contract violation: the subject DN of `issuer` does not
292    /// match the issuer DN of `cert`. The OCSP response was not consulted.
293    OcspIssuerCertMismatch,
294
295    /// The OCSP responder returned an `unknown` status (hard-fail mode).
296    OcspStatusUnknown,
297
298    /// The OCSP response's validity window is in the past (stale) or absent.
299    ///
300    /// Returned in two cases:
301    /// - `now > nextUpdate`: the `SingleResponse` has expired
302    /// - `nextUpdate` absent: no freshness guarantee is available; treated as stale
303    OcspExpired,
304
305    /// DER decoding of an OCSP response failed.
306    OcspParseError(DerError),
307
308    /// The OCSP response is structurally invalid per RFC 6960 but DER-decodable.
309    ///
310    /// Currently returned in two cases:
311    /// - `responseBytes` is absent in a `Successful` response (RFC 6960 §4.2.1)
312    /// - `responseType` is not `id-pkix-ocsp-basic` (unrecognized response format)
313    OcspMalformed,
314
315    /// A delegated OCSP responder cert in the response's `certs` field
316    /// lacks the `id-kp-OCSPSigning` Extended Key Usage (RFC 6960
317    /// §4.2.2.2). Without this EKU the cert cannot legitimately sign OCSP
318    /// responses, so the response is rejected.
319    OcspResponderEkuMissing,
320
321    /// A delegated OCSP responder cert's `ExtendedKeyUsage` extension is
322    /// present but cannot be DER-decoded.
323    ///
324    /// Fail-closed: a malformed EKU on a candidate responder cert rejects
325    /// the response rather than silently treating the cert as if it lacked
326    /// the OCSPSigning purpose.
327    OcspResponderEkuMalformed,
328
329    /// A delegated OCSP responder cert was found whose ResponderId
330    /// matches, but it was issued by a different CA than the certificate
331    /// being checked.
332    ///
333    /// RFC 6960 §4.2.2.2 requires a "CA Designated Responder" cert to be
334    /// issued directly by the CA whose certificates the responder asserts
335    /// status for. A responder cert with the OCSPSigning EKU obtained
336    /// from another CA could otherwise be used to forge revocation
337    /// status claims on certs from a different CA.
338    OcspResponderCertNotIssuedByCa,
339
340    /// A delegated OCSP responder cert's validity period does not include
341    /// the response's `producedAt` timestamp. The signing key was not
342    /// authoritative when the response was generated.
343    OcspResponderCertExpired,
344
345    /// The CA-supplied signature on a delegated OCSP responder cert
346    /// failed to verify against the issuer's SPKI.
347    ///
348    /// Distinct from [`Error::OcspSignatureInvalid`] (which is the
349    /// response's own signature failing): this is the issuer-of-cert's
350    /// signature on the responder cert's TBS, validated to confirm the
351    /// responder cert was actually issued by the expected CA.
352    OcspResponderCertSigInvalid,
353
354    /// The CRL declares itself an indirect CRL (RFC 5280 §5.2.6:
355    /// `IssuingDistributionPoint.indirectCRL = TRUE`) but the checker
356    /// was constructed without a `cRLIssuer` certificate.
357    ///
358    /// Use [`crate::CrlChecker::new_with_crl_issuer`] (or its delta
359    /// sibling) and supply the cert that actually signed the CRL.
360    IndirectCrlIssuerMissing,
361
362    /// The CRL does NOT declare itself an indirect CRL but the checker
363    /// was constructed with a `cRLIssuer` certificate.
364    ///
365    /// This rejects the inverse of [`Error::IndirectCrlIssuerMissing`]:
366    /// a caller asserting a separate CRL signer for what is actually a
367    /// direct CRL signed by the cert's own issuer. Direct CRLs should
368    /// be loaded via [`crate::CrlChecker::new`] / `with_delta`.
369    IndirectCrlIssuerUnexpected,
370
371    /// The CRL issuer certificate does not have the `cRLSign` bit set in
372    /// its `KeyUsage` extension (RFC 5280 §6.3.3(f)).
373    ///
374    /// Returned when the certificate used to verify a CRL's signature has
375    /// a `KeyUsage` extension present but the `cRLSign` bit (bit 6) is not
376    /// asserted. If the `KeyUsage` extension is absent entirely, this
377    /// error is **not** raised (no extension = no constraint).
378    ///
379    /// **Disambiguation:** [`pkix_path::Error::CrlSignMissing`] (same
380    /// variant name, different crate) fires during *path validation* when
381    /// an intermediate CA cert in the chain lacks `cRLSign` and the caller
382    /// opted into [`pkix_path::ValidationPolicy::require_crl_sign_on_cas`].
383    /// This variant fires during *CRL verification* when the CRL signer
384    /// cert itself lacks `cRLSign`.
385    ///
386    /// [`pkix_path::Error::CrlSignMissing`]: https://docs.rs/pkix-path/latest/pkix_path/enum.Error.html#variant.CrlSignMissing
387    CrlSignMissing,
388
389    /// Path-level CRL signer discovery (RFC 5280 §6.3.3(f)) could not
390    /// locate a certificate in the caller-supplied bundle that signed
391    /// the CRL.
392    ///
393    /// Returned by [`CrlChecker::new_with_signer_discovery`] when neither
394    /// the CRL's `AuthorityKeyIdentifier` matches any bundle cert's
395    /// `SubjectKeyIdentifier`, nor any bundle cert's subject DN matches
396    /// the CRL's issuer DN. The caller must either supply a more
397    /// complete bundle or use a different constructor.
398    ///
399    /// [`CrlChecker::new_with_signer_discovery`]: crate::CrlChecker::new_with_signer_discovery
400    CrlSignerNotFound,
401
402    /// Path-level CRL signer discovery found a candidate cert in the
403    /// bundle, but the candidate does not chain back to a self-signed
404    /// (anchor-like) cert in the same bundle.
405    ///
406    /// Returned by [`CrlChecker::new_with_signer_discovery`]. This is
407    /// the structural half of RFC 5280 §6.3.3(f)'s "chain back to a
408    /// trust anchor" gate; it ensures the bundle is not missing the
409    /// signer's CA path. Full RFC 5280 §6.1 signature/policy validation
410    /// of the signer's chain is the responsibility of higher-layer
411    /// composers such as `pkix-chain` and is intentionally not
412    /// performed here.
413    ///
414    /// [`CrlChecker::new_with_signer_discovery`]: crate::CrlChecker::new_with_signer_discovery
415    CrlSignerNotTrusted,
416
417    /// The base/delta CRL pair cannot be used together.
418    ///
419    /// Returned in any of these cases:
420    /// - The supplied "base" CRL is itself a delta CRL (has a `deltaCRLIndicator`
421    ///   extension) — RFC 5280 §5.2.4 requires a full CRL as the base.
422    /// - The supplied "delta" CRL has no `deltaCRLIndicator` extension and is
423    ///   therefore not a delta CRL at all.
424    /// - The base and delta CRL have different issuers.
425    ///
426    /// Note: when the delta's `BaseCRLNumber` exceeds the base CRL's `CRLNumber`
427    /// (a staleness mismatch), [`Error::CrlNumberMismatch`] is returned instead.
428    DeltaCrlBaseMismatch,
429
430    /// The CRL's CRL number is lower than expected (base CRL must have a number
431    /// ≥ the delta's `BaseCRLNumber`).
432    CrlNumberMismatch,
433
434    /// A subject certificate's `BasicConstraints` extension is present but
435    /// could not be DER-decoded.
436    ///
437    /// Returned when the IDP scope check (`onlyContainsCACerts` /
438    /// `onlyContainsUserCerts`) cannot determine whether a CRL applies to
439    /// `cert` because `cert`'s own `BasicConstraints` is malformed.
440    /// This is a fail-closed alternative to silently treating the cert as
441    /// not-a-CA (which would let CA-scoped CRLs be skipped for an actual CA).
442    MalformedCertificate,
443
444    /// The revocation source's stated scope excludes the certificate being
445    /// checked, so the checker made **no determination** about its revocation
446    /// status.
447    ///
448    /// This is distinct from "verified not-revoked" (the historic ambiguous
449    /// `Ok(())` return that this variant replaces). Hard-fail callers should
450    /// treat any `OutOfScope` as a failure; soft-fail callers can match on
451    /// the [`OutOfScopeReason`] and decide which scopes to tolerate.
452    ///
453    /// Currently produced by [`CrlChecker`] for the three
454    /// `IssuingDistributionPoint` scope-flag mismatches in RFC 5280 §5.2.5
455    /// (`onlyContainsAttributeCerts`, `onlyContainsUserCerts`, and
456    /// `onlyContainsCACerts`). [`OcspChecker`] does **not** produce this
457    /// variant: it returns [`Error::OcspStatusUnknown`] when no matching
458    /// `SingleResponse` is found, which is its analogue of "not covered" and
459    /// already fail-closed.
460    ///
461    /// [`CrlChecker`]: crate::CrlChecker
462    /// [`OcspChecker`]: crate::OcspChecker
463    OutOfScope(OutOfScopeReason),
464
465    /// All known sources for revocation data failed to produce a usable
466    /// response.
467    ///
468    /// Returned by network-fetching adapters (`pkix-revocation-http`'s
469    /// `HttpCrlFetcher` / `HttpOcspFetcher`, future LDAP / out-of-band
470    /// adapters) when every URL extracted from the certificate failed
471    /// either at the transport layer (network, TLS, HTTP error) or at
472    /// the response layer (DER parse, signature, validity). The variant
473    /// is intentionally generic so that revocation sources beyond HTTP
474    /// can reuse it.
475    ///
476    /// Distinct from:
477    /// - [`Error::Revoked`] — source reached and reports revoked
478    /// - [`Error::OcspStatusUnknown`] — responder reached, reports unknown
479    /// - [`Error::OutOfScope`] — structurally-valid response that does
480    ///   not cover the certificate
481    ///
482    /// Hard-fail callers MUST reject the chain on this variant.
483    /// Soft-fail callers MAY treat it permissively.
484    ///
485    /// `description` is a human-readable summary suitable for logs; it
486    /// includes per-URL transport / status hints from the adapter. The
487    /// shape is deliberately a `String` rather than structured data so
488    /// the variant remains `Clone + PartialEq + Eq` (matching the rest
489    /// of `Error`) without leaking adapter-specific types into the
490    /// trait surface. Adapters surface structured failure information
491    /// through their own APIs.
492    ///
493    /// The variant is feature-gated behind `std` because `String` is
494    /// not available in the bare `no_std` build path. Network-fetching
495    /// adapters all require `std` anyway, so no-std consumers never
496    /// need to construct or match this variant.
497    #[cfg(feature = "std")]
498    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
499    RevocationFetchFailed {
500        /// Human-readable summary of the failures, one URL per line.
501        description: String,
502    },
503}
504
505impl core::fmt::Display for Error {
506    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
507        match self {
508            Self::Revoked {
509                serial,
510                reason_code,
511            } => match reason_code {
512                Some(code) => write!(
513                    f,
514                    "certificate {serial} is revoked (reason {})",
515                    crl_reason_name(*code)
516                ),
517                None => write!(f, "certificate {serial} is revoked"),
518            },
519            Self::CrlExpired => f.write_str("CRL validity window check failed"),
520            Self::CrlIssuerMismatch => f.write_str("CRL issuer does not match certificate issuer"),
521            Self::CrlSignatureInvalid => f.write_str("CRL signature is invalid"),
522            Self::CrlParseError(e) => write!(f, "CRL parse error: {e}"),
523            Self::OcspSignatureInvalid => f.write_str("OCSP response signature is invalid"),
524            Self::OcspResponderIdMismatch => {
525                f.write_str("OCSP ResponderId does not match the expected issuer identity")
526            }
527            Self::OcspCertIdMismatch => {
528                f.write_str("OCSP CertID issuer hashes do not match the expected issuer")
529            }
530            Self::OcspIssuerCertMismatch => f.write_str(
531                "issuer certificate subject DN does not match the certificate's issuer DN",
532            ),
533            Self::OcspStatusUnknown => f.write_str("OCSP responder returned unknown status"),
534            Self::OcspExpired => f.write_str("OCSP response is stale or has no nextUpdate"),
535            Self::OcspParseError(e) => write!(f, "OCSP response parse error: {e}"),
536            Self::OcspMalformed => {
537                f.write_str("OCSP response is structurally invalid (malformed per RFC 6960)")
538            }
539            Self::OcspResponderEkuMissing => f.write_str(
540                "delegated OCSP responder cert lacks id-kp-OCSPSigning Extended Key Usage",
541            ),
542            Self::OcspResponderEkuMalformed => {
543                f.write_str("delegated OCSP responder cert ExtendedKeyUsage extension is malformed")
544            }
545            Self::OcspResponderCertNotIssuedByCa => {
546                f.write_str("delegated OCSP responder cert was not issued by the certificate's CA")
547            }
548            Self::OcspResponderCertExpired => f.write_str(
549                "delegated OCSP responder cert validity does not include the response's producedAt",
550            ),
551            Self::OcspResponderCertSigInvalid => {
552                f.write_str("CA signature on delegated OCSP responder cert is invalid")
553            }
554            Self::IndirectCrlIssuerMissing => f.write_str(
555                "CRL declares indirectCRL=TRUE but no cRLIssuer certificate was provided",
556            ),
557            Self::IndirectCrlIssuerUnexpected => {
558                f.write_str("cRLIssuer certificate was provided but the CRL is not indirect")
559            }
560            Self::CrlSignMissing => {
561                f.write_str("CRL issuer KeyUsage does not include cRLSign (RFC 5280 §6.3.3(f))")
562            }
563            Self::CrlSignerNotFound => f.write_str(
564                "no certificate in the supplied bundle signed the CRL (path-level discovery)",
565            ),
566            Self::CrlSignerNotTrusted => f.write_str(
567                "discovered CRL signer does not chain back to a self-signed anchor in the supplied bundle",
568            ),
569            Self::DeltaCrlBaseMismatch => {
570                f.write_str("delta CRL BaseCRLNumber does not match the base CRL's CRLNumber")
571            }
572            Self::CrlNumberMismatch => f.write_str("CRL number is lower than expected"),
573            Self::MalformedCertificate => f.write_str(
574                "certificate BasicConstraints extension is present but cannot be decoded",
575            ),
576            Self::OutOfScope(reason) => {
577                write!(f, "revocation source out of scope: {reason}")
578            }
579            #[cfg(feature = "std")]
580            Self::RevocationFetchFailed { description } => {
581                write!(f, "revocation data fetch failed: {description}")
582            }
583        }
584    }
585}
586
587/// Map a `CrlReason` variant to its RFC 5280 §5.3.1 camelCase name.
588const fn crl_reason_name(r: CrlReason) -> &'static str {
589    match r {
590        CrlReason::Unspecified => "unspecified",
591        CrlReason::KeyCompromise => "keyCompromise",
592        CrlReason::CaCompromise => "cACompromise",
593        CrlReason::AffiliationChanged => "affiliationChanged",
594        CrlReason::Superseded => "superseded",
595        CrlReason::CessationOfOperation => "cessationOfOperation",
596        CrlReason::CertificateHold => "certificateHold",
597        CrlReason::RemoveFromCRL => "removeFromCRL",
598        CrlReason::PrivilegeWithdrawn => "privilegeWithdrawn",
599        CrlReason::AaCompromise => "aACompromise",
600    }
601}
602
603#[cfg(feature = "std")]
604impl std::error::Error for Error {
605    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
606        match self {
607            Self::CrlParseError(e) | Self::OcspParseError(e) => Some(e),
608            _ => None,
609        }
610    }
611}
612
613/// Result alias for this crate.
614pub type Result<T> = core::result::Result<T, Error>;
615
616/// Pluggable revocation checking.
617///
618/// Called once per certificate in the chain, in leaf-to-issuer order,
619/// after path signature validation has succeeded.
620///
621/// Implement this trait to plug CRL, OCSP, or a custom revocation mechanism
622/// into `pkix_chain::verify_chain`. Use [`NoRevocation`] for offline or
623/// embedded environments.
624/// # Implementing this trait
625///
626/// Implementors MUST provide [`RevocationChecker::check_revocation`].
627///
628/// Implementors that want **full-chain** revocation coverage — i.e., revocation
629/// checking for every certificate including the one issued directly by a trust
630/// anchor — MUST also override
631/// [`RevocationChecker::check_revocation_against_anchor`]. The default
632/// implementation skips the check silently; forgetting to override it will
633/// leave the anchor-issued certificate unchecked with no compile error or
634/// runtime warning.
635pub trait RevocationChecker {
636    /// Check whether `cert` has been revoked.
637    ///
638    /// - `cert`   — the certificate being checked
639    /// - `issuer` — the certificate that issued `cert` (signature-validated)
640    ///
641    /// # Return value
642    ///
643    /// `Ok(())` means **verified not-revoked**: the revocation source covers
644    /// this certificate and the serial number was not found in the revoked
645    /// list. This is an unambiguous "not revoked" determination.
646    ///
647    /// "Not covered" — i.e., the revocation source's scope excludes the
648    /// certificate so no determination was made — surfaces as
649    /// <code>Err([Error::OutOfScope]([OutOfScopeReason]))</code> for CRL
650    /// scope-flag mismatches and as
651    /// <code>Err([Error::OcspStatusUnknown])</code> for OCSP responses with no
652    /// matching `SingleResponse`. Hard-fail callers should treat both as
653    /// failures; soft-fail callers can match on the specific variant /
654    /// reason and decide which non-determinations to tolerate.
655    ///
656    /// # Errors
657    ///
658    /// - [`Error::Revoked`] — the certificate's serial number appears in the
659    ///   CRL's or OCSP response's revoked list.
660    /// - [`Error::CrlExpired`] — the CRL has passed its `nextUpdate` timestamp.
661    /// - [`Error::OcspMalformed`] — the OCSP response is structurally invalid or
662    ///   its validity window check failed.
663    /// - [`Error::OcspStatusUnknown`] — no matching `SingleResponse` covered
664    ///   the certificate (OCSP-side "not covered").
665    /// - [`Error::OutOfScope`] — a CRL `IssuingDistributionPoint` scope flag
666    ///   excludes the certificate being checked (CRL-side "not covered").
667    /// - Other [`Error`] variants for parse failures, signature verification
668    ///   failures, or structural constraint violations.
669    fn check_revocation(&self, cert: &Certificate, issuer: &Certificate) -> crate::Result<()>;
670
671    /// Check whether `cert` (issued directly by a trust anchor) has been revoked.
672    ///
673    /// Called by `verify_chain` for the last certificate in the chain — the one
674    /// whose issuer is a [`TrustAnchor`] rather than another certificate in the
675    /// chain. For example, in the chain `[leaf, intermediate_CA]` this method is
676    /// called with `cert = intermediate_CA` and `anchor` set to the matched anchor.
677    ///
678    /// **Default implementation returns `Ok(())` (skip).** Override this method
679    /// to enforce revocation checking for certificates issued directly by a trust
680    /// anchor (e.g., fetch and verify the CA's CRL using the anchor's public key).
681    ///
682    /// `NoRevocation` inherits this default and skips the check, matching its
683    /// overall no-op behaviour. `CrlChecker` and `OcspChecker` both override
684    /// this method: they verify the pre-loaded CRL or OCSP response against the
685    /// anchor's subject DN and SPKI.
686    ///
687    /// # Security
688    ///
689    /// **The default implementation silently skips revocation checking for the
690    /// anchor-issued certificate.** If your threat model requires revocation
691    /// checking for every certificate in the chain — including the one issued
692    /// directly by the trust anchor — you MUST override this method. There is
693    /// no compile-time or runtime warning when the default is used; the skip
694    /// is intentional for environments (e.g., embedded, offline, short-lived
695    /// certificates) where anchor-level revocation data is unavailable.
696    ///
697    /// Failing to override this method in a context that requires full-chain
698    /// revocation coverage is a silent security gap.
699    ///
700    /// # Note: default is a no-op
701    ///
702    /// The default implementation performs **no revocation check** and always
703    /// returns `Ok(())`. Any implementor that does not override this method
704    /// silently skips revocation for the certificate directly issued by the
705    /// trust anchor. Override this method to enable anchor-level revocation.
706    ///
707    /// # Errors
708    ///
709    /// The default implementation always returns `Ok(())`; override this method
710    /// to enable error-returning revocation checks.
711    fn check_revocation_against_anchor(
712        &self,
713        _cert: &Certificate,
714        _anchor: &TrustAnchor,
715    ) -> crate::Result<()> {
716        Ok(())
717    }
718}
719
720/// A no-op revocation checker that always reports certificates as not revoked.
721///
722/// Use this when:
723/// - Running in embedded / offline environments with no revocation infrastructure
724/// - Revocation is enforced at a higher layer
725/// - In tests and development environments
726///
727/// # Security note
728///
729/// `NoRevocation` does **not** consult CRLs or OCSP. A revoked certificate
730/// will pass validation. Only use this when your threat model permits
731/// unenforced revocation (e.g., closed networks, short-lived certificates,
732/// hardware attestation where issuance itself is the control).
733#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
734pub struct NoRevocation;
735
736impl RevocationChecker for NoRevocation {
737    #[inline]
738    fn check_revocation(&self, _cert: &Certificate, _issuer: &Certificate) -> crate::Result<()> {
739        Ok(())
740    }
741}
742
743#[cfg(feature = "crl")]
744mod crl;
745#[cfg(feature = "crl")]
746mod signer_discovery;
747#[cfg(feature = "crl")]
748#[cfg_attr(docsrs, doc(cfg(feature = "crl")))]
749pub use crl::CrlChecker;
750#[cfg(feature = "crl")]
751#[cfg_attr(docsrs, doc(cfg(feature = "crl")))]
752pub use signer_discovery::discover_crl_signer;
753
754#[cfg(feature = "ocsp")]
755mod ocsp;
756#[cfg(feature = "ocsp")]
757#[cfg_attr(docsrs, doc(cfg(feature = "ocsp")))]
758pub use ocsp::OcspChecker;
759
760// ---------------------------------------------------------------------------
761// Send + Sync compile-time assertions (AGENTS.md non-negotiable #6, PKIX-2l0v.2)
762// ---------------------------------------------------------------------------
763
764const _: fn() = || {
765    fn _assert_send_sync<T: Send + Sync>() {}
766    _assert_send_sync::<Error>();
767};