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}