Skip to main content

structured_pkix/crl/
entry.rs

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