Skip to main content

structured_pkix/crl/
entry.rs

1//! Deferred entry extension decoding in the current execution account.
2
3use super::{Authenticated, IssuingDistributionPoint, RevocationTarget, RevokedCertificate};
4use crate::error::{Error, ErrorCode, Located, Result};
5use crate::extension::{self, Extension};
6use crate::general_name::{self, GeneralName};
7use crate::{Time, oid};
8use structured_asn1::memory::{OwnedVec, Work};
9use structured_asn1::{Oid, Span, Tag, universal};
10
11/// Entry search over an authenticated source. This is not a freshness or
12/// distribution-point applicability decision.
13#[derive(Debug, Clone, Copy)]
14pub enum EntryLookup<'r, 'a> {
15    /// No entry with the target's issuer and serial was listed.
16    Absent,
17    /// Exactly one matching entry was listed.
18    Found(&'r RevokedCertificate<'a>),
19    /// Multiple matching entries leave their intended status ambiguous.
20    Conflict {
21        /// Earlier original entry.
22        first: &'r RevokedCertificate<'a>,
23        /// Later original entry.
24        second: &'r RevokedCertificate<'a>,
25    },
26    /// A critical demand prevents using this CRL for any certificate status.
27    Unsupported {
28        /// Unknown critical extension identifier.
29        kind: Oid<'a>,
30        /// Original location, including an unmatched entry's demand.
31        span: Span,
32    },
33}
34
35impl<'r, 'a> Authenticated<'r, 'a> {
36    /// Search by issuer plus serial, retaining indirect issuer inheritance.
37    /// Unknown critical extensions anywhere in the list invalidate use for all
38    /// statuses, including demands on entries unrelated to this target.
39    pub fn entry<'t, T: RevocationTarget<'t>, W: Work>(
40        &self,
41        target: &T,
42        execution: &W,
43    ) -> Result<EntryLookup<'r, 'a>> {
44        const FIELD: &str = "certificateList.entryLookup";
45        let crl = self.crl();
46        let execution = &crl.tbs().signature().query_in(execution);
47        let target_execution = target.execution_in(execution);
48        target_execution
49            .charge_work(1)
50            .map_err(|e| Error::resource(e, FIELD, target.span().start))?;
51        execution
52            .charge_work(1)
53            .map_err(|reason| Error::resource(reason, FIELD, crl.span().start))?;
54        let is_delta = delta_source(crl, execution)?;
55        let indirect = indirect_source(crl, execution)?;
56        if let Some(extension) = check_source_metadata(crl, is_delta, execution)? {
57            return Ok(EntryLookup::Unsupported {
58                kind: extension.kind(),
59                span: extension.span(),
60            });
61        }
62        let target_serial = target.serial().as_bytes();
63        let target_issuer = target.issuer().as_bytes();
64        let mut inherited = None;
65        let mut default_matches = None;
66        let mut found = None;
67        let mut conflict = None;
68        for entry in crl.tbs().revoked_certificates() {
69            execution
70                .charge_work(1)
71                .map_err(|reason| Error::resource(reason, FIELD, entry.span.start))?;
72            // RFC 5280 §5.3: an unknown critical entry extension prevents
73            // using this CRL for the status of ANY certificate.
74            // https://www.rfc-editor.org/rfc/rfc5280#section-5.3
75            if let Some(extension) = unsupported_entry(entry, execution)? {
76                return Ok(EntryLookup::Unsupported {
77                    kind: extension.kind(),
78                    span: extension.span(),
79                });
80            }
81            // Known extension syntax is checked even on unmatched entries.
82            validate_status_extensions(entry, is_delta, execution)?;
83            if let Some(names) = validated_issuer(entry, indirect, execution)? {
84                inherited = Some(names);
85            }
86            // Explicit certificateIssuer inherits §5.3.3's original DN
87            // encoding identity rule. The default CRL issuer instead uses
88            // the ordinary DN equality rules of §7.1.
89            // https://www.rfc-editor.org/rfc/rfc5280#section-5.3.3
90            let mut issuer_matches = false;
91            if let Some(names) = &inherited {
92                for name in names {
93                    execution
94                        .charge_work(1)
95                        .map_err(|reason| Error::resource(reason, FIELD, entry.span.start))?;
96                    if let GeneralName::Directory(name) = name {
97                        target_execution
98                            .charge_work(target_issuer.len() as u64)
99                            .map_err(|e| Error::resource(e, FIELD, target.span().start))?;
100                        if equal_bytes(name.as_bytes(), target_issuer, execution, entry.span.start)?
101                        {
102                            issuer_matches = true;
103                            break;
104                        }
105                    }
106                }
107            } else {
108                issuer_matches = if let Some(matched) = default_matches {
109                    matched
110                } else {
111                    let matched = target
112                        .issuer()
113                        .matches(crl.tbs().issuer(), &target_execution)?;
114                    default_matches = Some(matched);
115                    matched
116                };
117            }
118            if issuer_matches {
119                target_execution
120                    .charge_work(target_serial.len() as u64)
121                    .map_err(|e| Error::resource(e, FIELD, target.span().start))?;
122            }
123            if issuer_matches
124                && equal_bytes(
125                    entry.serial.as_bytes(),
126                    target_serial,
127                    execution,
128                    entry.span.start,
129                )?
130            {
131                if let Some(previous) = found {
132                    if conflict.is_none() {
133                        conflict = Some((previous, entry));
134                    }
135                } else {
136                    found = Some(entry);
137                }
138            }
139        }
140        if let Some((first, second)) = conflict {
141            return Ok(EntryLookup::Conflict { first, second });
142        }
143        Ok(match found {
144            Some(entry) => EntryLookup::Found(entry),
145            None => EntryLookup::Absent,
146        })
147    }
148}
149
150fn indirect_source(crl: &super::CertificateList<'_>, execution: &impl Work) -> Result<bool> {
151    let idp = crl
152        .tbs()
153        .extensions()
154        .get(oid::ISSUING_DISTRIBUTION_POINT, execution)?
155        .map(|extension| IssuingDistributionPoint::read(extension, execution))
156        .transpose()?;
157    Ok(idp
158        .as_ref()
159        .is_some_and(IssuingDistributionPoint::is_indirect))
160}
161
162fn unsupported_entry<'r, 'a>(
163    entry: &'r RevokedCertificate<'a>,
164    execution: &impl Work,
165) -> Result<Option<&'r Extension<'a>>> {
166    for extension in entry.extensions.criticals(execution) {
167        let extension = extension?;
168        execution.charge_work(19).map_err(|reason| {
169            Error::resource(
170                reason,
171                "certificateList.entryLookup",
172                extension.span().start,
173            )
174        })?;
175        let kind = extension.kind();
176        if kind != oid::CRL_REASON
177            && kind != oid::INVALIDITY_DATE
178            && kind != oid::CERTIFICATE_ISSUER
179        {
180            return Ok(Some(extension));
181        }
182    }
183    Ok(None)
184}
185
186fn validate_status_extensions<W: Work>(
187    entry: &RevokedCertificate<'_>,
188    delta: bool,
189    execution: &W,
190) -> Result<()> {
191    // RFC5280 §5.3.1: removeFromCRL may only appear in deltas, including
192    // when the malformed entry is unrelated to the requested certificate.
193    // https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1
194    if entry.reason(execution)? == Some(Reason::RemoveFromCrl) && !delta {
195        return Err(Error::new(
196            ErrorCode::MalformedValue,
197            "extensions.reasonCode",
198            entry.span.start,
199        ));
200    }
201    entry.invalidity_date(execution)?;
202    Ok(())
203}
204
205fn delta_source<W: Work>(crl: &super::CertificateList<'_>, execution: &W) -> Result<bool> {
206    let Some(indicator) = crl
207        .tbs()
208        .extensions()
209        .get(oid::DELTA_CRL_INDICATOR, execution)?
210    else {
211        return Ok(false);
212    };
213    if !indicator.is_critical() {
214        return Err(Error::new(
215            ErrorCode::MalformedValue,
216            "extensions.deltaCRLIndicator",
217            indicator.span().start,
218        ));
219    }
220    super::delta::number(indicator, execution)?;
221    Ok(true)
222}
223
224fn check_source_metadata<'r, 'a, W: Work>(
225    crl: &'r super::CertificateList<'a>,
226    is_delta: bool,
227    execution: &W,
228) -> Result<Option<&'r Extension<'a>>> {
229    const FIELD: &str = "certificateList.extensions";
230    for extension in crl.tbs().extensions().iter(execution) {
231        let extension = extension?;
232        execution
233            // Metadata admission belongs to the common iterator; this
234            // previously combined cost retains only the OID/profile work.
235            .charge_work(52)
236            .map_err(|reason| Error::resource(reason, FIELD, extension.span().start))?;
237        let kind = extension.kind();
238        let malformed = || Error::new(ErrorCode::MalformedValue, FIELD, extension.span().start);
239        if kind == oid::ISSUING_DISTRIBUTION_POINT {
240            // RFC5280 §5.2.5: this extension MUST be critical. The caller
241            // already decoded its scope once before this metadata pass.
242            if !extension.is_critical() {
243                return Err(malformed());
244            }
245        } else if kind == oid::CRL_NUMBER {
246            // RFC5280 §5.2.3: noncritical INTEGER (0..MAX), including on
247            // ordinary complete sources, not only delta binding.
248            // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.3
249            if extension.is_critical() {
250                return Err(malformed());
251            }
252            super::delta::number(extension, execution)?;
253        } else if kind == oid::DELTA_CRL_INDICATOR {
254            // delta_source validated the critical indicator's number once.
255        } else if kind == oid::AUTHORITY_KEY_IDENTIFIER {
256            // RFC5280 §§5.2.1/4.2.1.1: present CRL AKI uses keyIdentifier
257            // and is noncritical. Absence is not a blanket v1 receiver refusal.
258            // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.1
259            if extension.is_critical() {
260                return Err(malformed());
261            }
262            let identifier = crate::identifier::AuthorityKeyIdentifier::read(extension, execution)?;
263            if identifier.key_id().is_none() {
264                return Err(malformed());
265            }
266        } else if kind == oid::ISSUER_ALT_NAME {
267            general_name::read_alternative(extension, execution)?;
268        } else if kind == oid::AUTHORITY_INFO_ACCESS {
269            // RFC5280 §5.2.7 forbids critical AIA. Noncritical fetch hints are
270            // preserved, not interpreted or used to establish status here.
271            // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.7
272            if extension.is_critical() {
273                return Err(malformed());
274            }
275        } else if kind == oid::FRESHEST_CRL {
276            // RFC5280 §5.2.6: noncritical, complete sources only, with no
277            // reasons or cRLIssuer in its distribution points. No fetching.
278            // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.6
279            if extension.is_critical() || is_delta {
280                return Err(malformed());
281            }
282            let points = crate::access::DistributionPoints::read(extension, execution)?;
283            for point in points.points() {
284                execution
285                    .charge_work(1)
286                    .map_err(|reason| Error::resource(reason, FIELD, extension.span().start))?;
287                if point.reasons().is_some() || !point.issuer().is_empty() {
288                    return Err(malformed());
289                }
290            }
291        } else if kind == oid::EXPIRED_CERTS_ON_CRL {
292            read_expired_certs(extension, execution)?;
293        } else if extension.is_critical() {
294            return Ok(Some(extension));
295        }
296    }
297    Ok(None)
298}
299
300fn validated_issuer<'a, W: Work>(
301    entry: &RevokedCertificate<'a>,
302    indirect: bool,
303    execution: &W,
304) -> Result<Option<OwnedVec<GeneralName<'a>>>> {
305    const FIELD: &str = "extensions.certificateIssuer";
306    let Some(extension) = entry.extensions.get(oid::CERTIFICATE_ISSUER, execution)? else {
307        return Ok(None);
308    };
309    if !indirect || !extension.is_critical() {
310        return Err(Error::new(
311            ErrorCode::MalformedValue,
312            FIELD,
313            extension.span().start,
314        ));
315    }
316    let mut reader = extension::sequence(extension, FIELD, execution)?;
317    let names = general_name::read_list(&mut reader, FIELD, extension.span().start)?;
318    let mut directory = false;
319    for name in &names {
320        execution
321            .charge_work(1)
322            .map_err(|reason| Error::resource(reason, FIELD, extension.span().start))?;
323        if matches!(name, GeneralName::Directory(name) if !name.is_empty()) {
324            directory = true;
325        }
326    }
327    if !directory {
328        return Err(Error::new(
329            ErrorCode::MalformedValue,
330            FIELD,
331            extension.span().start,
332        ));
333    }
334    Ok(Some(names))
335}
336
337fn equal_bytes(left: &[u8], right: &[u8], execution: &impl Work, at: u64) -> Result<bool> {
338    let work = (left.len().max(right.len()) as u64)
339        .checked_mul(2)
340        .and_then(|n| n.checked_add(1))
341        .ok_or_else(|| {
342            Error::resource(
343                structured_asn1::memory::MemoryError::WorkExceeded,
344                "certificateList.entryLookup",
345                at,
346            )
347        })?;
348    execution
349        .charge_work(work)
350        .map_err(|reason| Error::resource(reason, "certificateList.entryLookup", at))?;
351    Ok(left == right)
352}
353
354/// RFC 5280 §5.3.1 reasonCode values. Enumeration numbers differ from
355/// `ReasonFlags` bit positions for privilegeWithdrawn and aACompromise.
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum Reason {
358    /// No more specific reason was stated.
359    Unspecified,
360    /// Subject private key compromise.
361    KeyCompromise,
362    /// Certification authority private key compromise.
363    AuthorityCompromise,
364    /// Subject affiliation changed.
365    AffiliationChanged,
366    /// Certificate replaced.
367    Superseded,
368    /// The certified operation ceased.
369    CessationOfOperation,
370    /// Temporary suspension.
371    CertificateHold,
372    /// Delta CRL removes a prior hold or expired entry; not a new revocation.
373    RemoveFromCrl,
374    /// Privilege withdrawn (enumeration 9, reason flag 7).
375    PrivilegeWithdrawn,
376    /// Attribute authority compromise (enumeration 10, reason flag 8).
377    AttributeAuthorityCompromise,
378}
379
380impl<'a> RevokedCertificate<'a> {
381    /// Stated reason, independently of whether this CRL is a valid complete or
382    /// delta list. An omitted extension stays absent rather than rewritten.
383    pub fn reason<W: Work>(&self, execution: &W) -> Result<Option<Reason>> {
384        self.extensions
385            .get(oid::CRL_REASON, execution)?
386            .map(|extension| read_reason(extension, execution))
387            .transpose()
388    }
389
390    /// When the issuer believes invalidity began, distinct from revocationDate.
391    pub fn invalidity_date<W: Work>(&self, execution: &W) -> Result<Option<Time>> {
392        const FIELD: &str = "extensions.invalidityDate";
393        let Some(extension) = self.extensions.get(oid::INVALIDITY_DATE, execution)? else {
394            return Ok(None);
395        };
396        let mut reader = extension.reader_scoped(execution).field(FIELD)?;
397        let element = reader
398            .expect(Tag::primitive(universal::GENERALIZED_TIME))
399            .field(FIELD)?;
400        super::end(&reader, FIELD, extension.span().start)?;
401        crate::time::read_in(&reader, &element, FIELD).map(Some)
402    }
403
404    /// Explicit issuer names on this entry. None means inheritance from the
405    /// previous entry (or CRL issuer on the first), never an absent identity.
406    pub fn certificate_issuer<W: Work>(
407        &self,
408        execution: &W,
409    ) -> Result<Option<OwnedVec<GeneralName<'a>>>> {
410        const FIELD: &str = "extensions.certificateIssuer";
411        let Some(extension) = self.extensions.get(oid::CERTIFICATE_ISSUER, execution)? else {
412            return Ok(None);
413        };
414        let mut reader = extension::sequence(extension, FIELD, execution)?;
415        general_name::read_list(&mut reader, FIELD, extension.span().start).map(Some)
416    }
417}
418
419impl super::TbsCertList<'_> {
420    /// The time from which this list keeps entries of expired certificates:
421    /// its silence still covers a certificate whose `notAfter` is at or after
422    /// it, where RFC 5280 §3.3 would otherwise let such an entry be dropped.
423    /// Decimal fractions retain their subnanosecond remainder; comparing an
424    /// expiry to `cutoff.start()` alone must not discard that remainder.
425    pub fn expired_certs_on_crl<W: Work>(
426        &self,
427        execution: &W,
428    ) -> Result<Option<crate::temporal::Reading>> {
429        self.extensions
430            .get(oid::EXPIRED_CERTS_ON_CRL, execution)?
431            .map(|extension| read_expired_certs(extension, execution))
432            .transpose()
433    }
434}
435
436// X.509 §8.5.2.12: `ExpiredCertsOnCRL ::= GeneralizedTime`, and "this
437// extension is always non-critical", so a critical one is a malformed list.
438fn read_expired_certs<W: Work>(
439    extension: &Extension<'_>,
440    execution: &W,
441) -> Result<crate::temporal::Reading> {
442    const FIELD: &str = "extensions.expiredCertsOnCRL";
443    if extension.is_critical() {
444        return Err(Error::new(
445            ErrorCode::MalformedValue,
446            FIELD,
447            extension.span().start,
448        ));
449    }
450    let mut reader = extension.reader_scoped(execution).field(FIELD)?;
451    let element = reader
452        .expect(Tag::primitive(universal::GENERALIZED_TIME))
453        .field(FIELD)?;
454    super::end(&reader, FIELD, extension.span().start)?;
455    crate::time::read_generalized_in(&reader, &element, FIELD)
456}
457
458fn read_reason<W: Work>(extension: &Extension<'_>, execution: &W) -> Result<Reason> {
459    const FIELD: &str = "extensions.reasonCode";
460    let mut reader = extension.reader_scoped(execution).field(FIELD)?;
461    let element = reader.expect(Tag::primitive(10)).field(FIELD)?;
462    super::end(&reader, FIELD, extension.span().start)?;
463    reason_element(&reader, &element, FIELD)
464}
465
466/// Shared reasonCode schema for CRL entries and OCSP `RevokedInfo`.
467pub(crate) fn reason_element(
468    parent: &structured_asn1::ScopedReader<'_, '_>,
469    element: &structured_asn1::Element<'_>,
470    field: &'static str,
471) -> Result<Reason> {
472    parent.charge(9).field(field)?;
473    let value = parent.integer(element).field(field)?;
474    // ENUMERATED content uses the same two's-complement/minimal encoding as
475    // INTEGER (X.690 §8.4); the reason schema selects the permitted values.
476    // https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1
477    match value.as_u64() {
478        Some(0) => Ok(Reason::Unspecified),
479        Some(1) => Ok(Reason::KeyCompromise),
480        Some(2) => Ok(Reason::AuthorityCompromise),
481        Some(3) => Ok(Reason::AffiliationChanged),
482        Some(4) => Ok(Reason::Superseded),
483        Some(5) => Ok(Reason::CessationOfOperation),
484        Some(6) => Ok(Reason::CertificateHold),
485        Some(8) => Ok(Reason::RemoveFromCrl),
486        Some(9) => Ok(Reason::PrivilegeWithdrawn),
487        Some(10) => Ok(Reason::AttributeAuthorityCompromise),
488        _ => Err(Error::new(
489            ErrorCode::MalformedValue,
490            field,
491            element.span().start,
492        )),
493    }
494}