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};