Skip to main content

pkix_revocation/
crl.rs

1//! Offline CRL-based revocation checker.
2//!
3//! Enabled by the `crl` feature.
4
5use crate::{Error, RevocationChecker};
6use der::{Decode as _, Encode as _};
7use pkix_path::{names_match, SignatureVerifier, TrustAnchor};
8use spki::der::referenced::OwnedToRef as _;
9use x509_cert::{
10    crl::{CertificateList, RevokedCert},
11    ext::pkix::crl::CrlReason,
12    Certificate,
13};
14
15// OID 2.5.29.21 — id-ce-CRLReasons (RFC 5280 §5.3.1)
16const OID_CRL_REASONS: der::asn1::ObjectIdentifier =
17    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.21");
18
19/// OID for `CRLNumber` extension (RFC 5280 §5.2.3) — id-ce-cRLNumber: 2.5.29.20
20const OID_CRL_NUMBER: der::asn1::ObjectIdentifier =
21    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.20");
22
23/// OID for deltaCRLIndicator extension (RFC 5280 §5.2.4) — id-ce-deltaCRLIndicator: 2.5.29.27
24/// This extension is CRITICAL; its presence marks a delta CRL.
25const OID_DELTA_CRL_INDICATOR: der::asn1::ObjectIdentifier =
26    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.27");
27
28/// OID for issuingDistributionPoint extension (RFC 5280 §5.2.5) — 2.5.29.28
29/// Note: x509-cert 0.2.5 has a wrong `AssociatedOid` for `IssuingDistributionPoint`
30/// (it uses `SubjectInfoAccess` OID instead). Always look up this extension by
31/// raw OID rather than using AssociatedOid-based helpers.
32const OID_ISSUING_DISTRIBUTION_POINT: der::asn1::ObjectIdentifier =
33    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.28");
34
35/// OID for cRLDistributionPoints extension (RFC 5280 §4.2.1.13) — 2.5.29.31.
36/// Used to extract the certificate's distribution-point claim for matching
37/// against a CRL's `IssuingDistributionPoint`.
38const OID_CRL_DISTRIBUTION_POINTS: der::asn1::ObjectIdentifier =
39    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.31");
40
41/// OID for `KeyUsage` extension (RFC 5280 §4.2.1.3) — id-ce-keyUsage: 2.5.29.15
42/// Used to check the `cRLSign` bit on the CRL issuer.
43const OID_KEY_USAGE_CRL: der::asn1::ObjectIdentifier =
44    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.15");
45
46/// OID for the `certificateIssuer` CRL entry extension (RFC 5280 §5.3.3).
47///
48/// Critical extension that, in indirect CRLs, identifies the actual issuer
49/// of the cert that an entry refers to (which may differ from the CRL's own
50/// issuer). Per §5.3.3 this extension MUST be marked critical.
51const OID_CERTIFICATE_ISSUER: der::asn1::ObjectIdentifier =
52    der::asn1::ObjectIdentifier::new_unwrap("2.5.29.29");
53
54/// Offline CRL-based revocation checker.
55///
56/// Parses a DER-encoded [`CertificateList`][x509_cert::crl::CertificateList],
57/// verifies its signature against the issuer's SPKI, checks the
58/// `thisUpdate`/`nextUpdate` validity window, and reports whether the
59/// certificate's serial number appears in the revoked list.
60///
61/// To also apply a delta CRL (RFC 5280 §5.2.4), use [`CrlChecker::with_delta`].
62///
63/// # Feature
64///
65/// Only available when the `crl` feature is enabled.
66///
67/// # Return value semantics
68///
69/// [`RevocationChecker::check_revocation`] returns `Ok(())` when the CRL covers
70/// the certificate type and the serial number was not found in the revoked list
71/// (**not revoked**).
72///
73/// When the CRL's `IssuingDistributionPoint` scope flags
74/// (`onlyContainsUserCerts`, `onlyContainsCACerts`, `onlyContainsAttributeCerts`)
75/// indicate the CRL does not apply to this certificate type, the checker returns
76/// `Err(`[`Error::OutOfScope`]`)` with a variant describing the mismatch.
77/// Callers enforcing a **hard-fail** revocation policy should treat `OutOfScope`
78/// as a non-determination and require that at least one CRL or OCSP response
79/// actually covers the certificate in question.
80///
81/// # Indirect CRLs (RFC 5280 §5.2.6)
82///
83/// When the CRL is signed by a separate `cRLIssuer` certificate (rather
84/// than by the cert's own issuer), construct the checker with
85/// [`CrlChecker::new_with_crl_issuer`] / [`CrlChecker::with_delta_and_crl_issuer`].
86/// The CRL's `IssuingDistributionPoint.indirectCRL` flag must be `TRUE`,
87/// and per-entry `certificateIssuer` extensions (RFC 5280 §5.3.3) are
88/// honored to identify the actual issuer of each revoked entry. The
89/// caller is responsible for having pre-validated the cRLIssuer's chain
90/// back to a trusted anchor.
91///
92/// # Limitations
93///
94/// - **CRL distribution-point name matching** (CDP vs IDP `distributionPoint`)
95///   is implemented for both forms (`fullName` and `nameRelativeToCRLIssuer`),
96///   with `nameRelativeToCRLIssuer` resolved by appending the relative RDN to
97///   the appropriate base DN (the certificate's issuer for the cert's CDP, the
98///   CRL's issuer for the CRL's IDP). Same-form direct comparison and
99///   cross-form resolved comparison both work, so a cert whose CDP uses
100///   `nameRelativeToCRLIssuer` matches a CRL whose IDP uses `fullName` (and
101///   vice versa) when both resolve to the same DN.
102///   `GeneralName::DirectoryName` entries compare via
103///   [`pkix_path::names_match`] (proper DN equivalence); other variants
104///   (URI, dNSName, rfc822Name, IP address, etc.) compare via byte-exact DER
105///   encoding equality.
106///   The per-DP `cRLIssuer` field on a `DistributionPoint` entry is **not**
107///   currently honored when resolving the cert's CDP base DN; the
108///   certificate's issuer is always used. This is correct for the common
109///   case (RFC 5280 §4.2.1.13: "If the certificate issuer is also the CRL
110///   issuer, then conforming CAs MUST omit the cRLIssuer field"). Indirect
111///   CRLs with a non-issuer cRLIssuer that also use `nameRelativeToCRLIssuer`
112///   on the cert's CDP would resolve against the wrong base; that scenario
113///   has no PKITS coverage and is deferred.
114/// - **Reasons-subset check** (`onlySomeReasons` on the IDP must cover the
115///   reasons the cert's CDP asks to be checked, RFC 5280 §6.3.3(b)(1)) is
116///   not implemented. PKITS §4.14 fixtures do not exercise this. Tracked
117///   as future work; a separate `OutOfScopeReason` variant will be added at
118///   that time.
119/// - The checker enforces `onlyContainsUserCerts`, `onlyContainsCACerts`,
120///   and `onlyContainsAttributeCerts` scope flags directly; see
121///   `OutOfScopeReason` for the surfaced variants.
122/// - The cRLIssuer's chain is NOT validated by this crate — callers must
123///   present an already-validated cRLIssuer cert. Composing this with
124///   `pkix-path` to validate the cRLIssuer chain in-process is the umbrella
125///   crate's responsibility (`pkix-chain`).
126/// - Path-level CRL signer discovery (RFC 5280 §6.3.3(f)) IS supported via
127///   [`CrlChecker::new_with_signer_discovery`] and the free
128///   [`discover_crl_signer`][crate::discover_crl_signer] helper, both gated
129///   by the `crl` feature. Discovery uses `AuthorityKeyIdentifier` →
130///   `SubjectKeyIdentifier` matching with issuer-DN fallback, and verifies
131///   the discovered signer has `cRLSign` in `KeyUsage` and reaches a
132///   self-signed cert in the supplied bundle. Full RFC 5280 §6.1 validation
133///   of the signer's chain remains the responsibility of higher-layer
134///   composers; see that constructor's "Limitations" section for the
135///   project's documented stance on the lenient-vs-strict tradeoff.
136/// - The `certificateIssuer` extension's `issuerAltName` form (a non-DN
137///   GeneralName) is not currently used for entry-issuer lookup; only the
138///   `directoryName` form is. Real-world indirect CRLs use directoryName.
139/// - [`RevocationChecker::check_revocation_against_anchor`] is overridden.
140///   For the certificate issued directly by a trust anchor, the CRL is verified
141///   using the anchor's subject DN and SPKI in place of the missing issuer
142///   `Certificate`.  The `cRLSign` `KeyUsage` check is omitted for trust anchors
143///   (anchors are trusted by construction; they carry no `KeyUsage` to inspect).
144///   If the CRL's issuer name does not match the anchor, the method returns
145///   [`Error::CrlIssuerMismatch`] rather than `Ok(())`.
146///
147/// [`check_revocation`]: crate::RevocationChecker::check_revocation
148/// [`RevocationChecker::check_revocation_against_anchor`]: crate::RevocationChecker::check_revocation_against_anchor
149#[derive(Clone, Debug)]
150pub struct CrlChecker<V> {
151    /// Pre-parsed base CRL. Decoded once at construction; reused on every
152    /// [`RevocationChecker::check_revocation`] call.
153    crl: CertificateList,
154    /// Optional pre-parsed delta CRL. When present, its entries are merged
155    /// with the base CRL in `check_revocation` (RFC 5280 §5.2.4).
156    delta_crl: Option<CertificateList>,
157    /// Optional cRLIssuer cert when the CRL is indirect (RFC 5280 §5.2.6)
158    /// or the signer is discovered via [`CrlChecker::new_with_signer_discovery`].
159    /// `Some` ⇔ the stored cert's SPKI is used for the CRL signature check,
160    /// its KeyUsage for the cRLSign bit check, and its subject DN for the
161    /// CRL-issuer-identity match. The caller (or the discovery routine) is
162    /// responsible for any chain validation of the cert.
163    /// `None` ⇔ direct CRL: the `issuer` argument's identity is used.
164    crl_issuer_cert: Option<Certificate>,
165    /// Whether the stored `crl_issuer_cert` was located by path-level
166    /// signer discovery rather than supplied explicitly via an indirect-CRL
167    /// constructor.
168    ///
169    /// The two cases:
170    ///
171    /// - `true` — **signer from bundle**: the cert was discovered by
172    ///   [`CrlChecker::new_with_signer_discovery`] via AKI/SKI or
173    ///   issuer-DN matching against the caller-supplied certificate
174    ///   bundle. `check_revocation` accepts the stored cert as the
175    ///   effective signer regardless of whether the CRL declares itself
176    ///   indirect via `IDP.indirectCRL`. This bypasses the indirect-CRL
177    ///   cross-check, which is required to support PKITS §4.5 chains
178    ///   where the CRL is a direct (non-indirect) CRL whose signer
179    ///   happens to differ from the EE's stated issuer cert because of a
180    ///   self-issued key-rollover bridge — the §4.5 CRLs do **not** set
181    ///   `IDP.indirectCRL`, so the legacy `_with_crl_issuer` path would
182    ///   (correctly) reject them with `IndirectCrlIssuerUnexpected`.
183    ///
184    /// - `false` — **normal signer selection**: either no `crl_issuer_cert`
185    ///   is stored (direct CRL, signer is the `issuer` argument) or one
186    ///   was supplied explicitly via `new_with_crl_issuer` /
187    ///   `with_delta_and_crl_issuer` (indirect CRL). The legacy
188    ///   `IndirectCrlIssuerMissing` / `IndirectCrlIssuerUnexpected`
189    ///   cross-check between the constructor choice and the CRL's
190    ///   `IDP.indirectCRL` flag applies.
191    signer_discovered: bool,
192    now_unix: u64,
193    verifier: V,
194}
195
196impl<V: SignatureVerifier> CrlChecker<V> {
197    /// Create a new `CrlChecker`.
198    ///
199    /// - `crl_der`  — DER-encoded `CertificateList` (any `AsRef<[u8]>`, e.g. `Vec<u8>` or `&[u8]`)
200    /// - `now_unix` — current time as seconds since the Unix epoch
201    /// - `verifier` — signature verifier used to authenticate the CRL
202    ///
203    /// The DER is parsed once at construction time and the parsed
204    /// [`CertificateList`] is reused on every check, eliminating per-check
205    /// re-parse work.
206    ///
207    /// # Errors
208    ///
209    /// Returns [`Error::CrlParseError`] if `crl_der` cannot be DER-decoded.
210    ///
211    /// # Security
212    ///
213    /// **Do not pass a delta CRL** (a `CertificateList` containing a
214    /// `deltaCRLIndicator` extension) as the sole `crl_der` argument. Delta CRLs
215    /// contain only the changes since the last base CRL; using one alone silently
216    /// under-covers revocations. Pass it via [`CrlChecker::with_delta`] together
217    /// with the matching base CRL to get correct coverage.
218    pub fn new(crl_der: impl AsRef<[u8]>, now_unix: u64, verifier: V) -> crate::Result<Self> {
219        let crl = CertificateList::from_der(crl_der.as_ref())
220            .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
221        Ok(Self {
222            crl,
223            delta_crl: None,
224            crl_issuer_cert: None,
225            signer_discovered: false,
226            now_unix,
227            verifier,
228        })
229    }
230
231    /// Create a `CrlChecker` for an indirect CRL (RFC 5280 §5.2.6).
232    ///
233    /// `crl_issuer_cert` is the certificate that signed the CRL. It MUST
234    /// have its chain pre-validated by the caller back to a trusted
235    /// anchor — this crate verifies only the cRLIssuer-cert-to-CRL
236    /// relationship, not the cRLIssuer-cert-to-anchor chain.
237    ///
238    /// The supplied CRL must declare itself indirect via its
239    /// `IssuingDistributionPoint.indirectCRL` flag (RFC 5280 §5.2.5);
240    /// otherwise [`Error::IndirectCrlIssuerUnexpected`] is returned at
241    /// `check_revocation` time.
242    ///
243    /// # Errors
244    ///
245    /// Returns [`Error::CrlParseError`] if `crl_der` cannot be DER-decoded.
246    pub fn new_with_crl_issuer(
247        crl_der: impl AsRef<[u8]>,
248        crl_issuer_cert: Certificate,
249        now_unix: u64,
250        verifier: V,
251    ) -> crate::Result<Self> {
252        let crl = CertificateList::from_der(crl_der.as_ref())
253            .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
254        Ok(Self {
255            crl,
256            delta_crl: None,
257            crl_issuer_cert: Some(crl_issuer_cert),
258            signer_discovered: false,
259            now_unix,
260            verifier,
261        })
262    }
263
264    /// Create a `CrlChecker` that performs path-level CRL signer discovery
265    /// (RFC 5280 §6.3.3(f)).
266    ///
267    /// The caller supplies an unordered `bundle` of candidate certificates
268    /// (typically the certs already collected for the chain plus any local
269    /// CRL-signer candidates) and the certificate that will subsequently be
270    /// revocation-checked. The function:
271    ///
272    /// 1. Parses the CRL DER.
273    /// 2. Calls [`discover_crl_signer`][crate::discover_crl_signer] to locate the cert in `bundle` that
274    ///    signed the CRL (AKI/SKI walk with issuer-DN fallback).
275    /// 3. Verifies the discovered signer has `cRLSign` in its `KeyUsage`
276    ///    extension (RFC 5280 §6.3.3(f)). A signer with no `KeyUsage`
277    ///    extension passes this check (RFC 5280 leaves the extension
278    ///    optional and `pkix-revocation` follows the same fail-open
279    ///    interpretation as [`CrlChecker::new`] / `new_with_crl_issuer`).
280    /// 4. Verifies the discovered signer chains structurally back to a
281    ///    self-signed (anchor-like) certificate present in the same
282    ///    bundle (see "Limitations" below for what "chains structurally"
283    ///    means and why it differs from full RFC 5280 §6.1 validation).
284    /// 5. Stores the discovered signer for use as the effective CRL
285    ///    signer when [`RevocationChecker::check_revocation`] is later
286    ///    called on `cert_to_check`. The signer is used regardless of
287    ///    whether the CRL's `IssuingDistributionPoint.indirectCRL` flag
288    ///    is set, which is required to support PKITS §4.5 (direct CRLs
289    ///    signed by self-issued key-rollover bridge certs).
290    ///
291    /// `cert_to_check` is the certificate whose revocation status will be
292    /// queried via `check_revocation`. It is taken at construction time
293    /// only for ergonomic symmetry with the discovery flow; the actual
294    /// revocation lookup happens at `check_revocation` call time. Passing
295    /// a different `cert` argument to `check_revocation` later is well-
296    /// defined: the stored signer is the one whose SPKI is used to
297    /// authenticate the CRL, but the `(issuer, serial)` lookup is driven
298    /// by the `cert` argument at call time.
299    ///
300    /// # Errors
301    ///
302    /// - [`Error::CrlParseError`] if `crl_der` cannot be DER-decoded.
303    /// - [`Error::CrlSignerNotFound`] if no cert in `bundle` could be
304    ///   identified as the CRL's signer via AKI/SKI or issuer-DN match.
305    /// - [`Error::CrlSignMissing`] if the discovered signer has a
306    ///   `KeyUsage` extension that does not include `cRLSign`.
307    /// - [`Error::CrlSignerNotTrusted`] if the discovered signer cannot
308    ///   reach a self-signed cert in `bundle` by repeated AKI/SKI or
309    ///   issuer-DN walks.
310    ///
311    /// # Limitations
312    ///
313    /// **No signature verification is performed on the signer's chain.**
314    /// Step 4 above is a structural reachability check — it verifies the
315    /// bundle *contains* an anchor for the signer, not that any of the
316    /// signatures along the way actually verify. This intentional
317    /// limitation preserves the project's one-way dependency direction
318    /// (`pkix-chain` → `pkix-revocation` → `pkix-path`): pulling
319    /// `pkix-path::validate_path` into this constructor would invert that
320    /// direction.
321    ///
322    /// Higher-layer composers that need full RFC 5280 §6.1 validation of
323    /// the signer's path (signature, validity, name constraints, …) MUST
324    /// do that separately and then pass the validated cert via
325    /// [`CrlChecker::new_with_crl_issuer`]. This constructor is the
326    /// right choice when:
327    ///
328    /// - The bundle is already known to be path-valid (e.g.,
329    ///   `pkix-chain` already validated it), OR
330    /// - The caller accepts the project's documented stance that bundle
331    ///   pre-validation is the caller's responsibility.
332    ///
333    /// **The bundle slice is not retained.** Only the discovered signer
334    /// `Certificate` is cloned and stored.
335    pub fn new_with_signer_discovery(
336        crl_der: impl AsRef<[u8]>,
337        bundle: &[Certificate],
338        _cert_to_check: &Certificate,
339        now_unix: u64,
340        verifier: V,
341    ) -> crate::Result<Self> {
342        let crl = CertificateList::from_der(crl_der.as_ref())
343            .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
344
345        let signer = crate::signer_discovery::discover_crl_signer(bundle, &crl)
346            .ok_or(Error::CrlSignerNotFound)?;
347
348        // RFC 5280 §6.3.3(f): the signer cert MUST assert cRLSign in
349        // KeyUsage when the extension is present. Reuse the same fail-open
350        // semantics as check_crl_sign (absent extension = no constraint).
351        check_crl_sign(signer)?;
352
353        // Structural anchor-reachability check. See the "Limitations"
354        // section of this method's rustdoc for what this does and does
355        // not guarantee.
356        if !crate::signer_discovery::reaches_self_signed(bundle, signer) {
357            return Err(Error::CrlSignerNotTrusted);
358        }
359
360        Ok(Self {
361            crl,
362            delta_crl: None,
363            crl_issuer_cert: Some(signer.clone()),
364            signer_discovered: true,
365            now_unix,
366            verifier,
367        })
368    }
369
370    /// Create a `CrlChecker` with a base CRL and a delta CRL.
371    ///
372    /// The delta CRL is merged into the base CRL per RFC 5280 §5.2.4:
373    /// - Entries in the delta that are not in the base are added.
374    /// - Entries in the delta with reason `removeFromCRL` are removed from the
375    ///   base.
376    /// - The merged result is used for all subsequent `check_revocation` calls.
377    ///
378    /// # Errors
379    ///
380    /// Returns `Err(Error::CrlParseError)` if either the base or delta CRL DER
381    /// cannot be decoded.
382    ///
383    /// Returns `Err(Error::DeltaCrlBaseMismatch)` if:
384    /// - The delta CRL's `BaseCRLNumber` is absent (not a delta CRL), or
385    /// - The delta's `BaseCRLNumber` is greater than the base CRL's `CRLNumber`
386    ///   (the delta was produced against a newer base than the one supplied).
387    pub fn with_delta(
388        base_der: impl AsRef<[u8]>,
389        delta_der: impl AsRef<[u8]>,
390        now_unix: u64,
391        verifier: V,
392    ) -> crate::Result<Self> {
393        // Parse both to validate structure and extract CRL numbers.
394        let base_crl = CertificateList::from_der(base_der.as_ref())
395            .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
396        let delta_crl = CertificateList::from_der(delta_der.as_ref())
397            .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
398
399        // The base CRL MUST NOT itself be a delta CRL (RFC 5280 §5.2.4: only a
400        // full CRL may serve as the base).  Detect by OID presence alone — do not
401        // rely on successful decode, since a malformed deltaCRLIndicator value
402        // would cause base_crl_number() to return None and silently pass as a base.
403        if has_delta_crl_indicator(&base_crl) {
404            return Err(Error::DeltaCrlBaseMismatch);
405        }
406
407        // The delta MUST have a deltaCRLIndicator extension (marks it as a delta CRL).
408        // Check presence by OID first to distinguish "absent" from "present but malformed":
409        //   - Extension absent           → not a delta CRL → DeltaCrlBaseMismatch
410        //   - Extension present, value malformed → CrlParseError (structural error)
411        if !has_delta_crl_indicator(&delta_crl) {
412            // No deltaCRLIndicator OID → this is not a delta CRL.
413            return Err(Error::DeltaCrlBaseMismatch);
414        }
415        // has_delta_crl_indicator confirmed OID presence above; None is unreachable.
416        let delta_base_num = base_crl_number(&delta_crl)
417            .ok_or(Error::DeltaCrlBaseMismatch)? // can only happen if code invariant broken
418            .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
419
420        // The base CRL and delta CRL MUST have the same issuer.
421        if !names_match(
422            &base_crl.tbs_cert_list.issuer,
423            &delta_crl.tbs_cert_list.issuer,
424        ) {
425            return Err(Error::DeltaCrlBaseMismatch);
426        }
427
428        // If the base CRL has a CRL number, the delta's BaseCRLNumber must be
429        // ≤ it (we have a base that is at least as current as what the delta expects).
430        // A malformed or overflowing base CRLNumber is treated as CrlParseError
431        // rather than silently skipping the freshness check.
432        if let Some(base_num_result) = crl_number(&base_crl) {
433            let base_num = base_num_result.map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
434            if delta_base_num > base_num {
435                return Err(Error::CrlNumberMismatch);
436            }
437        }
438
439        Ok(Self {
440            crl: base_crl,
441            delta_crl: Some(delta_crl),
442            crl_issuer_cert: None,
443            signer_discovered: false,
444            now_unix,
445            verifier,
446        })
447    }
448
449    /// Same as [`CrlChecker::new_with_crl_issuer`] plus a delta CRL.
450    ///
451    /// The delta CRL must be signed by the same cRLIssuer cert and be
452    /// declared indirect via its own `IssuingDistributionPoint.indirectCRL`
453    /// flag (verified at `check_revocation` time).
454    ///
455    /// # Errors
456    ///
457    /// Same as [`CrlChecker::with_delta`], plus the indirect-CRL gates
458    /// described in [`CrlChecker::new_with_crl_issuer`].
459    pub fn with_delta_and_crl_issuer(
460        base_der: impl AsRef<[u8]>,
461        delta_der: impl AsRef<[u8]>,
462        crl_issuer_cert: Certificate,
463        now_unix: u64,
464        verifier: V,
465    ) -> crate::Result<Self> {
466        // Reuse the with_delta path for the structural cross-checks
467        // (issuer match, CRL number ordering, delta-base relationship),
468        // then attach the cRLIssuer cert.
469        let mut checker = Self::with_delta(base_der, delta_der, now_unix, verifier)?;
470        checker.crl_issuer_cert = Some(crl_issuer_cert);
471        Ok(checker)
472    }
473}
474
475impl<V: SignatureVerifier> RevocationChecker for CrlChecker<V> {
476    fn check_revocation(&self, cert: &Certificate, issuer: &Certificate) -> crate::Result<()> {
477        // (1) Reuse the pre-parsed base CRL (parsed once at construction).
478        let crl = &self.crl;
479
480        // (2) Determine the effective CRL signer: either the cert's
481        //     issuer (direct CRL) or a separate cRLIssuer cert (indirect
482        //     CRL, RFC 5280 §5.2.6). The selection is decided at
483        //     construction time:
484        //     - new() / with_delta()                        → direct
485        //     - new_with_crl_issuer() / _with_delta_…()    → indirect
486        //
487        //     Cross-check the construction choice against the CRL's own
488        //     IDP.indirectCRL flag: mismatches are rejected with a
489        //     specific error so the caller learns they used the wrong
490        //     constructor (rather than being told "signature invalid"
491        //     after the wrong key is used to verify).
492        let parsed_idp = parse_issuing_dp(crl)?;
493        let crl_declares_indirect = parsed_idp
494            .as_ref()
495            .map(|idp| idp.indirect_crl)
496            .unwrap_or(false);
497        let signer_subject: &x509_cert::name::Name;
498        let signer_spki: spki::SubjectPublicKeyInfoRef<'_>;
499        let signer_for_crlsign_check: Option<&Certificate>;
500        match (&self.crl_issuer_cert, crl_declares_indirect) {
501            (Some(crl_issuer), true) => {
502                signer_subject = &crl_issuer.tbs_certificate.subject;
503                signer_spki = crl_issuer
504                    .tbs_certificate
505                    .subject_public_key_info
506                    .owned_to_ref();
507                signer_for_crlsign_check = Some(crl_issuer);
508            }
509            (Some(discovered), false) if self.signer_discovered => {
510                // Path-level signer discovery (new_with_signer_discovery).
511                // The CRL may be direct (§4.5 self-issued key-rollover bridge)
512                // or indirect (§4.14.22-26 indirect CRLs); the cert was located
513                // structurally and the cross-check between "is the CRL declared
514                // indirect" and "did the caller supply a signer" is intentionally
515                // bypassed here. The discovered signer's SPKI is authoritative
516                // for the CRL signature verification regardless of the IDP
517                // indirectCRL flag.
518                signer_subject = &discovered.tbs_certificate.subject;
519                signer_spki = discovered
520                    .tbs_certificate
521                    .subject_public_key_info
522                    .owned_to_ref();
523                signer_for_crlsign_check = Some(discovered);
524            }
525            (Some(_), false) => {
526                // Caller asserted indirect via new_with_crl_issuer; CRL says
527                // direct. Reject — this rejects the wrong-constructor case.
528                return Err(Error::IndirectCrlIssuerUnexpected);
529            }
530            (None, true) => {
531                // CRL says indirect; caller did not supply cRLIssuer cert.
532                return Err(Error::IndirectCrlIssuerMissing);
533            }
534            (None, false) => {
535                signer_subject = &issuer.tbs_certificate.subject;
536                signer_spki = issuer
537                    .tbs_certificate
538                    .subject_public_key_info
539                    .owned_to_ref();
540                signer_for_crlsign_check = Some(issuer);
541            }
542        }
543
544        // (2a) The CRL issuer name must match the effective signer's
545        //      subject DN. This guards against a caller passing a
546        //      cRLIssuer cert with a different DN than the CRL claims to
547        //      have come from.
548        if !names_match(&crl.tbs_cert_list.issuer, signer_subject) {
549            return Err(Error::CrlIssuerMismatch);
550        }
551        // (2b) The cert under check must be issued by a CA in the
552        //      domain this CRL covers. For direct CRLs, that is exactly
553        //      the CRL issuer (cert.issuer == CRL.issuer). For indirect
554        //      CRLs, cert.issuer may differ from CRL.issuer (the
555        //      cRLIssuer's subject); the per-entry effective-issuer
556        //      check below handles the actual matching. We still
557        //      require that the supplied `issuer` cert match cert.issuer
558        //      as a defense-in-depth check on the caller-supplied
559        //      issuer-of-cert identity.
560        if !names_match(
561            &issuer.tbs_certificate.subject,
562            &cert.tbs_certificate.issuer,
563        ) {
564            return Err(Error::CrlIssuerMismatch);
565        }
566        // For direct CRLs only: the cert's issuer must also match the
567        // CRL issuer (this is the legacy invariant). For indirect CRLs
568        // this check is intentionally skipped — the whole point of
569        // §5.2.6 is that they may differ. Discovery mode also skips this
570        // check: the §4.5 case has cert.issuer == CRL.issuer by DN but
571        // a different signing key, so the DN-equality test still holds;
572        // for the indirect §4.14.22-26 cases the DN differs by design,
573        // and the per-entry certificateIssuer extension (in indirect
574        // CRLs) carries the actual issuer of each entry.
575        if self.crl_issuer_cert.is_none()
576            && !names_match(&cert.tbs_certificate.issuer, &crl.tbs_cert_list.issuer)
577        {
578            return Err(Error::CrlIssuerMismatch);
579        }
580
581        // (3) RFC 5280 §6.3.3(f): the CRL signer must have cRLSign in KeyUsage when present.
582        //     For indirect CRLs this check runs on the cRLIssuer cert, NOT the
583        //     cert's own issuer (that's the whole point of separation of duty).
584        if let Some(signer_cert) = signer_for_crlsign_check {
585            check_crl_sign(signer_cert)?;
586        }
587
588        // (3b) Check CRL validity window before verifying the signature.
589        //     Rejecting stale CRLs early avoids a potentially expensive signature
590        //     verification on a CRL we would discard anyway.
591        //     Absent nextUpdate is treated as expired: an indefinitely valid CRL would
592        //     allow a stale revocation list to suppress detection of revoked certificates.
593        let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs();
594        if self.now_unix < this_update {
595            return Err(Error::CrlExpired);
596        }
597        let next_update = crl
598            .tbs_cert_list
599            .next_update
600            .as_ref()
601            .ok_or(Error::CrlExpired)?;
602        if self.now_unix > next_update.to_unix_duration().as_secs() {
603            return Err(Error::CrlExpired);
604        }
605
606        // (4) Verify the CRL signature against the effective signer's SPKI
607        //     (issuer for direct CRLs, cRLIssuer cert for indirect CRLs).
608        //     Clone the SPKI ref so the delta path below (if it runs) can
609        //     consume the original — the spki ref-struct is small.
610        let tbs_bytes = crl
611            .tbs_cert_list
612            .to_der()
613            .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
614        self.verifier
615            .verify_signature(
616                crl.signature_algorithm.owned_to_ref(),
617                signer_spki.clone(),
618                &tbs_bytes,
619                crl.signature.raw_bytes(),
620            )
621            // Verifier returns an opaque error; no additional context available.
622            .map_err(|_| Error::CrlSignatureInvalid)?;
623
624        // (5) RFC 5280 §5.2.5: if the CRL has an IssuingDistributionPoint extension
625        //     (critical), check scope constraints against the certificate.
626        // Scope mismatches surface as Error::OutOfScope so callers can distinguish
627        // "verified not-revoked" (Ok(())) from "no determination made"
628        // (Err(OutOfScope(...))). Hard-fail revocation policies should treat
629        // OutOfScope as a failure.
630        if let Some(idp) = &parsed_idp {
631            // onlyContainsAttributeCerts: attribute cert validation is out of scope
632            // for pkix-revocation (RFC 5755 is handled by pkix-ac).
633            if idp.only_contains_attribute_certs {
634                return Err(Error::OutOfScope(
635                    crate::OutOfScopeReason::CrlOnlyAttributeCerts,
636                ));
637            }
638            let cert_is_ca = cert_is_ca_cert(cert)?;
639            // onlyContainsUserCerts: CRL only covers end-entity (non-CA) certs.
640            if idp.only_contains_user_certs && cert_is_ca {
641                return Err(Error::OutOfScope(crate::OutOfScopeReason::CrlOnlyUserCerts));
642            }
643            // onlyContainsCACerts: CRL only covers CA certs.
644            if idp.only_contains_ca_certs && !cert_is_ca {
645                return Err(Error::OutOfScope(crate::OutOfScopeReason::CrlOnlyCaCerts));
646            }
647            // RFC 5280 §6.3.3(b)(1): IDP distributionPoint must match the
648            // cert's CDP (cross-form `nameRelativeToCRLIssuer` ↔ `fullName`
649            // resolution is performed by `dpn_to_general_names`).
650            check_idp_dp_scope(cert, idp, &cert.tbs_certificate.issuer, signer_subject)?;
651        }
652
653        // (6) §5.2.4 delta CRL merge: if a delta CRL is present, verify and collect
654        //     its revoked entries.  verify_delta_crl_and_collect handles sig, expiry,
655        //     and the primary issuer-name check.  The extra checks below are
656        //     defense-in-depth: they guard against any future code path that bypasses
657        //     the with_delta() constructor and against subtle cross-name mismatches.
658        let delta_entries: &[RevokedCert] = if let Some(delta_crl) = &self.delta_crl {
659            // Reuse the pre-parsed delta CRL (parsed once at construction).
660
661            // Extra check: delta CRL issuer must also match the base CRL issuer
662            // (construction-time invariant, re-checked here for defense-in-depth).
663            if !names_match(&delta_crl.tbs_cert_list.issuer, &crl.tbs_cert_list.issuer) {
664                return Err(Error::CrlIssuerMismatch);
665            }
666            // The delta CRL must be signed by the same effective signer as the base.
667            // This is guaranteed by issuer DN match plus the cRLIssuer-vs-issuer
668            // selection above, but verify_delta_crl_and_collect will recheck.
669            verify_delta_crl_and_collect(
670                delta_crl,
671                &self.verifier,
672                signer_spki,
673                signer_subject,
674                self.now_unix,
675            )?
676        } else {
677            &[]
678        };
679
680        // (7) Search for the certificate's (issuer, serial) in the delta and base
681        //     CRL entries. Delta CRL entries take precedence (RFC 5280 §5.2.4);
682        //     a removeFromCRL reason in the delta un-revokes a cert.
683        //
684        //     For indirect CRLs the per-entry `certificateIssuer` extension may
685        //     change the effective issuer of subsequent entries (RFC 5280 §5.3.3);
686        //     entries inherit the previous extension's value, defaulting to the
687        //     CRL's own issuer for the first entry.
688        let cert_issuer = &cert.tbs_certificate.issuer;
689        let cert_serial = &cert.tbs_certificate.serial_number;
690        let crl_default_issuer = &crl.tbs_cert_list.issuer;
691        let is_indirect = self.crl_issuer_cert.is_some();
692        check_revocation_status_indirect(
693            cert_issuer,
694            cert_serial,
695            delta_entries,
696            crl,
697            crl_default_issuer,
698            is_indirect,
699        )
700    }
701
702    /// Check revocation for `cert` issued directly by a trust anchor.
703    ///
704    /// Uses the anchor's `subject` and `subject_public_key_info` in place of
705    /// an issuer `Certificate` to verify the CRL.  The `cRLSign` `KeyUsage` bit
706    /// check is omitted because trust anchors do not carry a `Certificate` with
707    /// extensions to inspect.
708    ///
709    /// # Limitations
710    ///
711    /// CRL discovery via the `cRLDistributionPoints` extension is by
712    /// design not implemented in `pkix-revocation` itself — this crate
713    /// stays `no_std` for its core types and does not perform network
714    /// I/O. The CRL DER must be supplied at construction time; for
715    /// online fetching from CRL DPs, use `pkix-revocation-http`.
716    ///
717    /// Path-level CRL signer discovery (RFC 5280 §6.3.3(f)) IS supported
718    /// via [`CrlChecker::new_with_signer_discovery`] on the
719    /// `check_revocation` flow, but is **not** wired into the
720    /// `check_revocation_against_anchor` path: trust anchors do not carry
721    /// a `Certificate` for the bundle walk to terminate at, so the
722    /// anchor flow continues to require an explicit `cRLIssuer` cert for
723    /// indirect CRLs. Direct CRLs at the anchor level are unaffected.
724    ///
725    /// If the CRL's issuer name does not match the anchor's subject, this
726    /// method returns [`Error::CrlIssuerMismatch`] rather than `Ok(())`,
727    /// ensuring a mismatched CRL is surfaced rather than silently skipped.
728    fn check_revocation_against_anchor(
729        &self,
730        cert: &Certificate,
731        anchor: &TrustAnchor,
732    ) -> crate::Result<()> {
733        // (1) Reuse the pre-parsed base CRL (parsed once at construction).
734        let crl = &self.crl;
735
736        // (2) Determine the effective CRL signer as in check_revocation.
737        //     For the anchor flow, the "issuer" identity is the anchor itself.
738        let parsed_idp = parse_issuing_dp(crl)?;
739        let crl_declares_indirect = parsed_idp
740            .as_ref()
741            .map(|idp| idp.indirect_crl)
742            .unwrap_or(false);
743        let signer_subject: &x509_cert::name::Name;
744        let signer_spki: spki::SubjectPublicKeyInfoRef<'_>;
745        let signer_for_crlsign_check: Option<&Certificate>;
746        match (&self.crl_issuer_cert, crl_declares_indirect) {
747            (Some(crl_issuer), true) => {
748                signer_subject = &crl_issuer.tbs_certificate.subject;
749                signer_spki = crl_issuer
750                    .tbs_certificate
751                    .subject_public_key_info
752                    .owned_to_ref();
753                signer_for_crlsign_check = Some(crl_issuer);
754            }
755            (Some(discovered), false) if self.signer_discovered => {
756                // Same discovery-mode semantics as check_revocation: the
757                // discovered signer is authoritative regardless of the
758                // CRL's IDP.indirectCRL flag. See the matching arm in
759                // check_revocation for the full rationale.
760                signer_subject = &discovered.tbs_certificate.subject;
761                signer_spki = discovered
762                    .tbs_certificate
763                    .subject_public_key_info
764                    .owned_to_ref();
765                signer_for_crlsign_check = Some(discovered);
766            }
767            (Some(_), false) => return Err(Error::IndirectCrlIssuerUnexpected),
768            (None, true) => return Err(Error::IndirectCrlIssuerMissing),
769            (None, false) => {
770                signer_subject = &anchor.subject;
771                signer_spki = anchor.subject_public_key_info.owned_to_ref();
772                // Trust anchors have no KeyUsage extension accessible; the
773                // cRLSign check is skipped — anchors are trusted by construction.
774                signer_for_crlsign_check = None;
775            }
776        }
777
778        // (2a) The CRL issuer must match the effective signer's subject DN.
779        if !names_match(&crl.tbs_cert_list.issuer, signer_subject) {
780            return Err(Error::CrlIssuerMismatch);
781        }
782        // (2b) The cert under check must be issued by the anchor for the
783        //      anchor flow regardless of indirect-vs-direct: the anchor
784        //      is the cert's *issuer*, even when the CRL is signed by a
785        //      separate cRLIssuer.
786        if !names_match(&cert.tbs_certificate.issuer, &anchor.subject) {
787            return Err(Error::CrlIssuerMismatch);
788        }
789
790        // (3) cRLSign check on the cRLIssuer cert in the indirect case;
791        //     skipped for the anchor itself in the direct case (anchors
792        //     are trusted by construction; see signer_for_crlsign_check
793        //     above).
794        if let Some(signer_cert) = signer_for_crlsign_check {
795            check_crl_sign(signer_cert)?;
796        }
797
798        // (4) Check CRL validity window before verifying the signature.
799        //     Same rationale as check_revocation: reject stale CRLs early.
800        let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs();
801        if self.now_unix < this_update {
802            return Err(Error::CrlExpired);
803        }
804        let next_update = crl
805            .tbs_cert_list
806            .next_update
807            .as_ref()
808            .ok_or(Error::CrlExpired)?;
809        if self.now_unix > next_update.to_unix_duration().as_secs() {
810            return Err(Error::CrlExpired);
811        }
812
813        // (5) Verify the CRL signature against the effective signer's SPKI.
814        //     Clone for the same reason as in check_revocation: the delta
815        //     path below may consume the original.
816        let tbs_bytes = crl
817            .tbs_cert_list
818            .to_der()
819            .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
820        self.verifier
821            .verify_signature(
822                crl.signature_algorithm.owned_to_ref(),
823                signer_spki.clone(),
824                &tbs_bytes,
825                crl.signature.raw_bytes(),
826            )
827            .map_err(|_| Error::CrlSignatureInvalid)?;
828
829        // (6) IssuingDistributionPoint scope check.
830        if let Some(idp) = &parsed_idp {
831            if idp.only_contains_attribute_certs {
832                return Err(Error::OutOfScope(
833                    crate::OutOfScopeReason::CrlOnlyAttributeCerts,
834                ));
835            }
836            let cert_is_ca = cert_is_ca_cert(cert)?;
837            if idp.only_contains_user_certs && cert_is_ca {
838                return Err(Error::OutOfScope(crate::OutOfScopeReason::CrlOnlyUserCerts));
839            }
840            if idp.only_contains_ca_certs && !cert_is_ca {
841                return Err(Error::OutOfScope(crate::OutOfScopeReason::CrlOnlyCaCerts));
842            }
843            // RFC 5280 §6.3.3(b)(1): IDP distributionPoint must match the
844            // cert's CDP. The cert's CDP base DN for resolving
845            // `nameRelativeToCRLIssuer` is the cert's issuer; the CRL's IDP
846            // base DN is the CRL signer's subject (anchor's subject in the
847            // direct anchor flow, cRLIssuer's subject in the indirect flow).
848            check_idp_dp_scope(cert, idp, &cert.tbs_certificate.issuer, signer_subject)?;
849        }
850
851        // (7) Delta CRL merge.
852        let delta_entries: &[RevokedCert] = if let Some(delta_crl) = &self.delta_crl {
853            if !names_match(&delta_crl.tbs_cert_list.issuer, &crl.tbs_cert_list.issuer) {
854                return Err(Error::CrlIssuerMismatch);
855            }
856            verify_delta_crl_and_collect(
857                delta_crl,
858                &self.verifier,
859                signer_spki,
860                signer_subject,
861                self.now_unix,
862            )?
863        } else {
864            &[]
865        };
866
867        // (8) Look up the cert's (issuer, serial) in delta then base entries,
868        //     honoring per-entry certificateIssuer for indirect CRLs.
869        let cert_issuer = &cert.tbs_certificate.issuer;
870        let cert_serial = &cert.tbs_certificate.serial_number;
871        let crl_default_issuer = &crl.tbs_cert_list.issuer;
872        let is_indirect = self.crl_issuer_cert.is_some();
873        check_revocation_status_indirect(
874            cert_issuer,
875            cert_serial,
876            delta_entries,
877            crl,
878            crl_default_issuer,
879            is_indirect,
880        )
881    }
882}
883
884// ---------------------------------------------------------------------------
885// Revocation status helper (with indirect-CRL per-entry issuer tracking)
886// ---------------------------------------------------------------------------
887
888/// Search for the cert's `(issuer, serial)` in the delta entries (higher
889/// priority) and then in the base `crl`, returning the appropriate
890/// revocation result.
891///
892/// RFC 5280 §5.2.4: delta CRL entries take precedence over base entries.
893/// RFC 5280 §5.3.3: in indirect CRLs, the per-entry `certificateIssuer`
894/// extension identifies the actual issuer of each entry. Entries inherit
895/// the previous entry's effective issuer; the first entry defaults to the
896/// CRL's own issuer.
897///
898/// - If found in `delta_entries` with reason `RemoveFromCRL` → `Ok(())`.
899/// - If found in `delta_entries` for any other reason → `Err(Revoked)`.
900/// - If found in the base CRL → `Err(Revoked)`.
901/// - If not found in either → `Ok(())` (not revoked according to this CRL).
902///
903/// When `is_indirect = false` the per-entry `certificateIssuer` extension
904/// is ignored and lookup degenerates to serial-only matching against
905/// `crl_default_issuer` (the CRL's own issuer). This preserves the legacy
906/// direct-CRL behaviour bit-for-bit.
907fn check_revocation_status_indirect(
908    cert_issuer: &x509_cert::name::Name,
909    cert_serial: &x509_cert::serial_number::SerialNumber,
910    delta_entries: &[RevokedCert],
911    crl: &CertificateList,
912    crl_default_issuer: &x509_cert::name::Name,
913    is_indirect: bool,
914) -> crate::Result<()> {
915    // Delta entries take precedence (RFC 5280 §5.2.4).
916    if let Some(entry) = find_matching_entry(
917        cert_issuer,
918        cert_serial,
919        delta_entries,
920        crl_default_issuer,
921        is_indirect,
922    )? {
923        let reason = extract_reason_code(entry);
924        if reason == Some(CrlReason::RemoveFromCRL) {
925            // certificateHold was lifted; cert is not revoked.
926            return Ok(());
927        }
928        return Err(Error::Revoked {
929            serial: cert_serial.clone(),
930            reason_code: reason,
931        });
932    }
933
934    // Fall through to base entries.
935    let base_entries: &[RevokedCert] = crl
936        .tbs_cert_list
937        .revoked_certificates
938        .as_deref()
939        .unwrap_or(&[]);
940    if let Some(entry) = find_matching_entry(
941        cert_issuer,
942        cert_serial,
943        base_entries,
944        crl_default_issuer,
945        is_indirect,
946    )? {
947        return Err(Error::Revoked {
948            serial: cert_serial.clone(),
949            reason_code: extract_reason_code(entry),
950        });
951    }
952
953    Ok(())
954}
955
956/// Walk `entries` in DER order, tracking the effective issuer per RFC 5280
957/// §5.3.3, and return the first entry whose `(effective_issuer, serial)`
958/// matches `(cert_issuer, cert_serial)`. The `is_indirect` flag gates
959/// whether `certificateIssuer` extensions are honored — for direct CRLs
960/// the effective issuer is always `crl_default_issuer`.
961///
962/// Errors out (`CrlParseError`) if a `certificateIssuer` extension is
963/// present but cannot be parsed; this is the fail-closed treatment for a
964/// critical entry extension we recognize.
965fn find_matching_entry<'a>(
966    cert_issuer: &x509_cert::name::Name,
967    cert_serial: &x509_cert::serial_number::SerialNumber,
968    entries: &'a [RevokedCert],
969    crl_default_issuer: &x509_cert::name::Name,
970    is_indirect: bool,
971) -> crate::Result<Option<&'a RevokedCert>> {
972    use std::borrow::Cow;
973
974    let mut effective: Cow<'_, x509_cert::name::Name> = Cow::Borrowed(crl_default_issuer);
975    for entry in entries {
976        // Only honor certificateIssuer in indirect CRLs (where it has
977        // defined semantics per RFC 5280 §5.2.5/§5.3.3). Direct CRLs
978        // should not contain this extension; if one does, ignoring it
979        // here matches the conservative interpretation used by major
980        // verifiers.
981        if is_indirect {
982            if let Some(exts) = entry.crl_entry_extensions.as_deref() {
983                if let Some(ce_ext) = exts.iter().find(|e| e.extn_id == OID_CERTIFICATE_ISSUER) {
984                    effective = Cow::Owned(parse_certificate_issuer_dn(ce_ext.extn_value.as_bytes())?);
985                }
986            }
987        }
988        if &entry.serial_number == cert_serial && names_match(&effective, cert_issuer) {
989            return Ok(Some(entry));
990        }
991    }
992    Ok(None)
993}
994
995/// Parse the `certificateIssuer` CRL entry extension (RFC 5280 §5.3.3),
996/// extracting the first `directoryName` GeneralName. Returns
997/// `Err(CrlParseError)` if the extension cannot be DER-decoded or if no
998/// `directoryName` is present (the only GeneralName form supported here).
999fn parse_certificate_issuer_dn(ext_value_der: &[u8]) -> crate::Result<x509_cert::name::Name> {
1000    use x509_cert::ext::pkix::name::{GeneralName, GeneralNames};
1001
1002    let general_names = GeneralNames::from_der(ext_value_der)
1003        .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
1004    for gn in general_names {
1005        if let GeneralName::DirectoryName(name) = gn {
1006            return Ok(name);
1007        }
1008    }
1009    // RFC 5280 §5.3.3 allows multiple GeneralName forms (including
1010    // issuerAltName entries); for chain-validation-style lookup we only
1011    // use the directoryName because that is what the cert's `issuer`
1012    // field carries. A cert-issuer-extension carrying only non-DN
1013    // GeneralNames is unusable for our (issuer, serial) match.
1014    //
1015    // The error uses `ErrorKind::Value` + `Tag::Sequence` to signal
1016    // "structurally valid extension but no directoryName GeneralName
1017    // found", distinguishing it from a DER parse failure.
1018    Err(Error::CrlParseError(crate::DerError::new(der::Error::new(
1019        der::ErrorKind::Value { tag: der::Tag::Sequence },
1020        der::Length::ZERO,
1021    ))))
1022}
1023
1024// ---------------------------------------------------------------------------
1025// Delta-CRL helper
1026// ---------------------------------------------------------------------------
1027
1028/// Verify a delta CRL and return its revoked-certificate entries.
1029///
1030/// Performs (in order):
1031/// 1. Check that the delta CRL issuer matches `expected_issuer_name`.
1032/// 2. Check the delta validity window against `now_unix`.
1033/// 3. Verify the delta signature using `issuer_spki`.
1034/// 4. Return the delta's revoked-certificates list (empty if absent).
1035///
1036/// The caller is responsible for parsing the `CertificateList` and for any
1037/// additional issuer-name cross-checks needed by the calling context (e.g.,
1038/// checking the delta issuer against the base CRL issuer or the subject
1039/// certificate's issuer).
1040fn verify_delta_crl_and_collect<'a, V: SignatureVerifier>(
1041    delta_crl: &'a CertificateList,
1042    verifier: &V,
1043    issuer_spki: spki::SubjectPublicKeyInfoRef<'_>,
1044    expected_issuer_name: &x509_cert::name::Name,
1045    now_unix: u64,
1046) -> crate::Result<&'a [RevokedCert]> {
1047    if !names_match(&delta_crl.tbs_cert_list.issuer, expected_issuer_name) {
1048        return Err(Error::CrlIssuerMismatch);
1049    }
1050
1051    // Check validity window before signature verification (same rationale as
1052    // the base CRL paths: reject stale deltas early without paying sig-verify cost).
1053    let delta_this_update = delta_crl
1054        .tbs_cert_list
1055        .this_update
1056        .to_unix_duration()
1057        .as_secs();
1058    if now_unix < delta_this_update {
1059        return Err(Error::CrlExpired);
1060    }
1061    let delta_next_update = delta_crl
1062        .tbs_cert_list
1063        .next_update
1064        .as_ref()
1065        .ok_or(Error::CrlExpired)?;
1066    if now_unix > delta_next_update.to_unix_duration().as_secs() {
1067        return Err(Error::CrlExpired);
1068    }
1069
1070    let delta_tbs_bytes = delta_crl
1071        .tbs_cert_list
1072        .to_der()
1073        .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
1074    verifier
1075        .verify_signature(
1076            delta_crl.signature_algorithm.owned_to_ref(),
1077            issuer_spki,
1078            &delta_tbs_bytes,
1079            delta_crl.signature.raw_bytes(),
1080        )
1081        // Verifier returns an opaque error; no additional context available.
1082        .map_err(|_| Error::CrlSignatureInvalid)?;
1083
1084    Ok(delta_crl
1085        .tbs_cert_list
1086        .revoked_certificates
1087        .as_deref()
1088        .unwrap_or(&[]))
1089}
1090
1091// ---------------------------------------------------------------------------
1092// Extension helpers
1093// ---------------------------------------------------------------------------
1094
1095/// Convert a DER [`Uint`][der::asn1::Uint] to a `u64`, padding from the left.
1096///
1097/// Returns `None` if the integer is larger than 8 bytes (would overflow `u64`).
1098/// CRL numbers in PKITS are small (1–5), so this is not a practical limit.
1099fn uint_to_u64(n: &der::asn1::Uint) -> Option<u64> {
1100    let b = n.as_bytes();
1101    if b.len() > 8 {
1102        return None; // too large for u64
1103    }
1104    let mut arr = [0u8; 8];
1105    arr[8 - b.len()..].copy_from_slice(b);
1106    Some(u64::from_be_bytes(arr))
1107}
1108
1109/// Extract the CRL number from a `CertificateList`'s extensions.
1110///
1111/// Returns:
1112/// - `None` — `CRLNumber` extension absent (field is optional per RFC 5280 §5.2.3)
1113/// - `Some(Ok(n))` — extension present and successfully decoded
1114/// - `Some(Err(e))` — extension present but the INTEGER value is malformed or
1115///   too large to fit in a `u64` (a `CRLNumber` > 2^64 is implausible in any
1116///   deployed PKI but must not silently disable the staleness check)
1117fn crl_number(crl: &CertificateList) -> Option<Result<u64, der::Error>> {
1118    let ext = crl
1119        .tbs_cert_list
1120        .crl_extensions
1121        .as_deref()
1122        .unwrap_or(&[])
1123        .iter()
1124        .find(|e| e.extn_id == OID_CRL_NUMBER)?;
1125    let result = der::asn1::Uint::from_der(ext.extn_value.as_bytes())
1126        .and_then(|n| uint_to_u64(&n).ok_or_else(|| der::Error::from(der::ErrorKind::Overflow)));
1127    Some(result)
1128}
1129
1130/// Returns `true` if `crl` contains a `deltaCRLIndicator` extension (OID 2.5.29.27),
1131/// regardless of whether the extension value can be decoded.
1132///
1133/// Presence of this OID (which MUST be critical) is the canonical marker that a
1134/// CRL is a delta CRL per RFC 5280 §5.2.4.  Checking presence — not decode success —
1135/// is important: a malformed value still makes the CRL a delta CRL and must prevent
1136/// it from being used as a base.
1137fn has_delta_crl_indicator(crl: &CertificateList) -> bool {
1138    crl.tbs_cert_list
1139        .crl_extensions
1140        .as_deref()
1141        .unwrap_or(&[])
1142        .iter()
1143        .any(|e| e.extn_id == OID_DELTA_CRL_INDICATOR)
1144}
1145
1146/// Extract the `BaseCRLNumber` from a delta CRL's extensions.
1147///
1148/// The `deltaCRLIndicator` extension value IS the `BaseCRLNumber` — it is an
1149/// INTEGER encoding the CRL number of the base CRL this delta updates.
1150/// This extension MUST be critical (RFC 5280 §5.2.4).
1151///
1152/// Returns:
1153/// - `None` — extension absent (CRL is not a delta CRL)
1154/// - `Some(Ok(n))` — extension present and successfully decoded
1155/// - `Some(Err(e))` — extension present but the INTEGER value is malformed
1156fn base_crl_number(crl: &CertificateList) -> Option<Result<u64, der::Error>> {
1157    let ext = crl
1158        .tbs_cert_list
1159        .crl_extensions
1160        .as_deref()
1161        .unwrap_or(&[])
1162        .iter()
1163        .find(|e| e.extn_id == OID_DELTA_CRL_INDICATOR)?;
1164    let result = der::asn1::Uint::from_der(ext.extn_value.as_bytes())
1165        .and_then(|n| uint_to_u64(&n).ok_or_else(|| der::Error::from(der::ErrorKind::Overflow)));
1166    Some(result)
1167}
1168
1169/// Checks that the CRL issuer certificate has `cRLSign` set in its `KeyUsage`
1170/// extension, or that `KeyUsage` is absent entirely (no constraint).
1171///
1172/// RFC 5280 §6.3.3(f): a CRL issuer that has a `KeyUsage` extension MUST assert
1173/// the `cRLSign` bit. If `KeyUsage` is absent, there is no constraint.
1174///
1175/// # Errors
1176///
1177/// - `Err(CrlParseError)` — the `KeyUsage` extension value is structurally malformed.
1178/// - `Err(CrlSignMissing)` — the extension is present but `cRLSign` bit is not set.
1179fn check_crl_sign(cert: &Certificate) -> crate::Result<()> {
1180    use x509_cert::ext::pkix::KeyUsage;
1181
1182    let Some(ku_ext) = cert
1183        .tbs_certificate
1184        .extensions
1185        .as_deref()
1186        .unwrap_or(&[])
1187        .iter()
1188        .find(|e| e.extn_id == OID_KEY_USAGE_CRL)
1189    else {
1190        return Ok(()); // KeyUsage absent (or no extensions) → no constraint
1191    };
1192    let ku = KeyUsage::from_der(ku_ext.extn_value.as_bytes())
1193        .map_err(|e| Error::CrlParseError(crate::DerError::new(e)))?;
1194    if ku.crl_sign() {
1195        Ok(())
1196    } else {
1197        Err(Error::CrlSignMissing)
1198    }
1199}
1200
1201/// Extract the `CRLReason` code from a revoked cert entry's extensions, if present.
1202///
1203/// Returns the `CrlReason` (RFC 5280 §5.3.1), or `None` if the extension is absent.
1204fn extract_reason_code(entry: &RevokedCert) -> Option<CrlReason> {
1205    let exts = entry.crl_entry_extensions.as_ref()?;
1206    exts.iter()
1207        .find(|ext| ext.extn_id == OID_CRL_REASONS)
1208        .and_then(|ext| CrlReason::from_der(ext.extn_value.as_bytes()).ok())
1209}
1210
1211/// Extract the `IssuingDistributionPoint` from a CRL, if present.
1212///
1213/// Uses raw OID lookup because x509-cert 0.2.5 has a wrong `AssociatedOid` for
1214/// this type (it maps to `SubjectInfoAccess` instead of 2.5.29.28).
1215fn parse_issuing_dp(
1216    crl: &CertificateList,
1217) -> crate::Result<Option<x509_cert::ext::pkix::crl::IssuingDistributionPoint>> {
1218    use x509_cert::ext::pkix::crl::IssuingDistributionPoint;
1219
1220    crl.tbs_cert_list
1221        .crl_extensions
1222        .as_deref()
1223        .unwrap_or(&[])
1224        .iter()
1225        .find(|e| e.extn_id == OID_ISSUING_DISTRIBUTION_POINT)
1226        .map(|e| {
1227            IssuingDistributionPoint::from_der(e.extn_value.as_bytes())
1228                .map_err(|err| Error::CrlParseError(crate::DerError::new(err)))
1229        })
1230        .transpose()
1231}
1232
1233/// Extract the `cRLDistributionPoints` extension from `cert`, if present.
1234///
1235/// Returns `Ok(None)` if the extension is absent — that means the cert
1236/// does not constrain which CRL(s) determine its revocation status.
1237/// Returns `Err(CrlParseError)` if the extension is present but cannot
1238/// be DER-decoded (fail-closed: a malformed CDP is a structural defect
1239/// in the cert, not a "no constraint" condition).
1240fn cert_crl_distribution_points(
1241    cert: &Certificate,
1242) -> crate::Result<Option<x509_cert::ext::pkix::CrlDistributionPoints>> {
1243    use x509_cert::ext::pkix::CrlDistributionPoints;
1244
1245    cert.tbs_certificate
1246        .extensions
1247        .as_deref()
1248        .unwrap_or(&[])
1249        .iter()
1250        .find(|e| e.extn_id == OID_CRL_DISTRIBUTION_POINTS)
1251        .map(|e| {
1252            CrlDistributionPoints::from_der(e.extn_value.as_bytes())
1253                .map_err(|err| Error::CrlParseError(crate::DerError::new(err)))
1254        })
1255        .transpose()
1256}
1257
1258/// Resolve a `DistributionPointName` to its canonical list of
1259/// `GeneralName`s.
1260///
1261/// `FullName(names)` is returned as-is (borrowed). `NameRelativeToCRLIssuer(rdn)`
1262/// is materialized: the relative RDN is appended to `base_dn` to produce a
1263/// full DN, and the result is wrapped in a one-element `GeneralNames` list
1264/// containing a single `DirectoryName`. RFC 5280 §4.2.1.13: "the relative
1265/// distinguished name … is the relative distinguished name of the CRL
1266/// distribution point with respect to the issuer of the CRL".
1267///
1268/// `base_dn` is the certificate issuer's DN when resolving a
1269/// `cRLDistributionPoints` entry, and the CRL signer's subject DN when
1270/// resolving an `IssuingDistributionPoint`. Per the RFC, in the common case
1271/// where the certificate issuer also signs the CRL these two base DNs are
1272/// equal, and a cert that uses `NameRelativeToCRLIssuer` for its CDP can
1273/// match a CRL that uses `FullName` for its IDP (or vice versa).
1274///
1275/// This function lives in the `crl` feature path which requires `std`; it
1276/// therefore uses `std`-prelude `Vec` and `vec!` macros directly rather
1277/// than going through `alloc::*` (the latter would require an
1278/// `extern crate alloc;` declaration that the crate currently does not
1279/// have, since all heap-using paths are gated behind `std`).
1280fn dpn_to_general_names<'a>(
1281    dpn: &'a x509_cert::ext::pkix::name::DistributionPointName,
1282    base_dn: &x509_cert::name::Name,
1283) -> crate::Result<std::borrow::Cow<'a, [x509_cert::ext::pkix::name::GeneralName]>> {
1284    use std::borrow::Cow;
1285    use x509_cert::ext::pkix::name::{DistributionPointName, GeneralName};
1286
1287    match dpn {
1288        DistributionPointName::FullName(names) => Ok(Cow::Borrowed(names.as_slice())),
1289        DistributionPointName::NameRelativeToCRLIssuer(rdn) => {
1290            // Append the RDN onto the base DN. `Name = RdnSequence(Vec<RDN>)`.
1291            // Cloning here is unavoidable because the resolved DN is a new
1292            // value; cert/CRL DNs are typically <10 RDNs each so this is cheap.
1293            let mut full_name = base_dn.clone();
1294            full_name.0.push(rdn.clone());
1295            Ok(Cow::Owned(vec![GeneralName::DirectoryName(full_name)]))
1296        }
1297    }
1298}
1299
1300/// Compare two `GeneralName`s for equivalence.
1301///
1302/// `DirectoryName` uses [`pkix_path::names_match`] (RFC 4518-style DN
1303/// equivalence). Other variants use the upstream-derived `PartialEq`
1304/// implementation on `GeneralName`, which compares the inner values
1305/// directly (e.g., `Ia5String` for URI/dNSName/rfc822Name, `OctetString`
1306/// for iPAddress, `ObjectIdentifier` for registeredID). This avoids
1307/// heap-allocating DER bytes for a comparison that the type system
1308/// already supports natively.
1309///
1310/// `EdiPartyName` and `OtherName` also use the derived `PartialEq`;
1311/// they have no canonicalization specified for IDP/CDP matching purposes
1312/// and are extremely rare in modern PKI.
1313///
1314/// Mismatched variants always return `false` (no cross-variant matching;
1315/// the derived `PartialEq` handles this correctly).
1316fn general_name_matches(
1317    a: &x509_cert::ext::pkix::name::GeneralName,
1318    b: &x509_cert::ext::pkix::name::GeneralName,
1319) -> bool {
1320    use x509_cert::ext::pkix::name::GeneralName as GN;
1321
1322    match (a, b) {
1323        (GN::DirectoryName(a_dn), GN::DirectoryName(b_dn)) => names_match(a_dn, b_dn),
1324        // Non-DN variants: use the upstream-derived PartialEq, which
1325        // compares inner fields directly without DER re-encoding.
1326        // Mismatched variants return false via the derived impl.
1327        _ => a == b,
1328    }
1329}
1330
1331/// Returns `true` if the two `GeneralNames` lists share at least one
1332/// equivalent name (RFC 5280 §6.3.3(b)(1) "matches").
1333fn general_names_intersect(
1334    a: &[x509_cert::ext::pkix::name::GeneralName],
1335    b: &[x509_cert::ext::pkix::name::GeneralName],
1336) -> bool {
1337    a.iter()
1338        .any(|ga| b.iter().any(|gb| general_name_matches(ga, gb)))
1339}
1340
1341/// Check that the CRL's `IssuingDistributionPoint.distributionPoint`
1342/// matches the certificate's `cRLDistributionPoints` per RFC 5280 §6.3.3(b)(1).
1343///
1344/// Returns `Err(Error::OutOfScope(CrlIdpDistributionPointMismatch))` if
1345/// the CRL is structurally well-formed but does not cover the certificate's
1346/// distribution point claim.
1347///
1348/// Algorithm:
1349///
1350/// | cert CDP | CRL IDP DP | Outcome |
1351/// |---|---|---|
1352/// | absent   | absent     | match (no DP scoping either side) |
1353/// | absent   | present    | mismatch (CRL is scoped, cert isn't) |
1354/// | present  | absent     | match (IDP doesn't constrain DP) |
1355/// | present  | present    | at least one CDP entry's resolved name must intersect IDP's resolved name |
1356///
1357/// `cert_issuer` is the certificate's issuer DN (used as the base when the
1358/// cert's CDP uses `NameRelativeToCRLIssuer`). `crl_signer_subject` is the
1359/// CRL signer's subject DN (used as the base when the CRL's IDP uses
1360/// `NameRelativeToCRLIssuer`).
1361fn check_idp_dp_scope(
1362    cert: &Certificate,
1363    idp: &x509_cert::ext::pkix::crl::IssuingDistributionPoint,
1364    cert_issuer: &x509_cert::name::Name,
1365    crl_signer_subject: &x509_cert::name::Name,
1366) -> crate::Result<()> {
1367    let cert_cdps = cert_crl_distribution_points(cert)?;
1368
1369    match (cert_cdps.as_ref(), idp.distribution_point.as_ref()) {
1370        (None, None) | (Some(_), None) => Ok(()),
1371        (None, Some(_)) => Err(Error::OutOfScope(
1372            crate::OutOfScopeReason::CrlIdpDistributionPointMismatch,
1373        )),
1374        (Some(cert_cdps), Some(idp_dpn)) => {
1375            let idp_names = dpn_to_general_names(idp_dpn, crl_signer_subject)?;
1376            // At least one cert CDP entry must have a distributionPoint that
1377            // intersects the IDP's. CDP entries with no distributionPoint
1378            // (RFC 5280 §4.2.1.13: "If the distributionPoint field is omitted,
1379            // the cRL distribution point must not include cRLIssuer that
1380            // …") cannot constrain to a specific name, so they cannot match
1381            // a non-empty IDP DPName here.
1382            let matched = cert_cdps.0.iter().any(|dp| {
1383                let Some(cdp_dpn) = &dp.distribution_point else {
1384                    return false;
1385                };
1386                let cdp_names = match dpn_to_general_names(cdp_dpn, cert_issuer) {
1387                    Ok(n) => n,
1388                    Err(_) => return false,
1389                };
1390                general_names_intersect(&cdp_names, &idp_names)
1391            });
1392            if matched {
1393                Ok(())
1394            } else {
1395                Err(Error::OutOfScope(
1396                    crate::OutOfScopeReason::CrlIdpDistributionPointMismatch,
1397                ))
1398            }
1399        }
1400    }
1401}
1402
1403/// Returns `Ok(true)` if `cert` is a CA certificate (`BasicConstraints`
1404/// `cA = TRUE`), `Ok(false)` if the extension is absent or `cA = FALSE`,
1405/// and [`Error::MalformedCertificate`] if the extension is present but
1406/// undecodable.
1407///
1408/// Fail-closed: a malformed `BasicConstraints` is propagated so the IDP
1409/// scope check cannot silently skip a CRL that should cover the certificate.
1410///
1411/// Thin wrapper over [`pkix_path::cert_is_ca`] that maps the opaque
1412/// [`pkix_path::DerError`] to this crate's [`Error::MalformedCertificate`].
1413fn cert_is_ca_cert(cert: &Certificate) -> crate::Result<bool> {
1414    pkix_path::cert_is_ca(cert).map_err(|_| Error::MalformedCertificate)
1415}