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 extension.is_critical() {
260            return Ok(Some(extension));
261        }
262    }
263    Ok(None)
264}
265
266fn validated_issuer<'a, W: Work>(
267    entry: &RevokedCertificate<'a>,
268    indirect: bool,
269    execution: &W,
270) -> Result<Option<OwnedVec<GeneralName<'a>>>> {
271    const FIELD: &str = "extensions.certificateIssuer";
272    let Some(extension) = entry.extensions.get(oid::CERTIFICATE_ISSUER, execution)? else {
273        return Ok(None);
274    };
275    if !indirect || !extension.is_critical() {
276        return Err(Error::new(
277            ErrorCode::MalformedValue,
278            FIELD,
279            extension.span().start,
280        ));
281    }
282    let mut reader = extension::sequence(extension, FIELD, execution)?;
283    let names = general_name::read_list(&mut reader, FIELD, extension.span().start)?;
284    let mut directory = false;
285    for name in &names {
286        execution
287            .charge_work(1)
288            .map_err(|reason| Error::resource(reason, FIELD, extension.span().start))?;
289        if matches!(name, GeneralName::Directory(name) if !name.is_empty()) {
290            directory = true;
291        }
292    }
293    if !directory {
294        return Err(Error::new(
295            ErrorCode::MalformedValue,
296            FIELD,
297            extension.span().start,
298        ));
299    }
300    Ok(Some(names))
301}
302
303fn equal_bytes(left: &[u8], right: &[u8], execution: &impl Work, at: u64) -> Result<bool> {
304    let work = (left.len().max(right.len()) as u64)
305        .checked_mul(2)
306        .and_then(|n| n.checked_add(1))
307        .ok_or_else(|| {
308            Error::resource(
309                structured_asn1::memory::MemoryError::WorkExceeded,
310                "certificateList.entryLookup",
311                at,
312            )
313        })?;
314    execution
315        .charge_work(work)
316        .map_err(|reason| Error::resource(reason, "certificateList.entryLookup", at))?;
317    Ok(left == right)
318}
319
320/// RFC 5280 §5.3.1 reasonCode values. Enumeration numbers differ from
321/// `ReasonFlags` bit positions for privilegeWithdrawn and aACompromise.
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum Reason {
324    /// No more specific reason was stated.
325    Unspecified,
326    /// Subject private key compromise.
327    KeyCompromise,
328    /// Certification authority private key compromise.
329    AuthorityCompromise,
330    /// Subject affiliation changed.
331    AffiliationChanged,
332    /// Certificate replaced.
333    Superseded,
334    /// The certified operation ceased.
335    CessationOfOperation,
336    /// Temporary suspension.
337    CertificateHold,
338    /// Delta CRL removes a prior hold or expired entry; not a new revocation.
339    RemoveFromCrl,
340    /// Privilege withdrawn (enumeration 9, reason flag 7).
341    PrivilegeWithdrawn,
342    /// Attribute authority compromise (enumeration 10, reason flag 8).
343    AttributeAuthorityCompromise,
344}
345
346impl<'a> RevokedCertificate<'a> {
347    /// Stated reason, independently of whether this CRL is a valid complete or
348    /// delta list. An omitted extension stays absent rather than rewritten.
349    pub fn reason<W: Work>(&self, execution: &W) -> Result<Option<Reason>> {
350        self.extensions
351            .get(oid::CRL_REASON, execution)?
352            .map(|extension| read_reason(extension, execution))
353            .transpose()
354    }
355
356    /// When the issuer believes invalidity began, distinct from revocationDate.
357    pub fn invalidity_date<W: Work>(&self, execution: &W) -> Result<Option<Time>> {
358        const FIELD: &str = "extensions.invalidityDate";
359        let Some(extension) = self.extensions.get(oid::INVALIDITY_DATE, execution)? else {
360            return Ok(None);
361        };
362        let mut reader = extension.reader_scoped(execution).field(FIELD)?;
363        let element = reader
364            .expect(Tag::primitive(universal::GENERALIZED_TIME))
365            .field(FIELD)?;
366        super::end(&reader, FIELD, extension.span().start)?;
367        crate::time::read_in(&reader, &element, FIELD).map(Some)
368    }
369
370    /// Explicit issuer names on this entry. None means inheritance from the
371    /// previous entry (or CRL issuer on the first), never an absent identity.
372    pub fn certificate_issuer<W: Work>(
373        &self,
374        execution: &W,
375    ) -> Result<Option<OwnedVec<GeneralName<'a>>>> {
376        const FIELD: &str = "extensions.certificateIssuer";
377        let Some(extension) = self.extensions.get(oid::CERTIFICATE_ISSUER, execution)? else {
378            return Ok(None);
379        };
380        let mut reader = extension::sequence(extension, FIELD, execution)?;
381        general_name::read_list(&mut reader, FIELD, extension.span().start).map(Some)
382    }
383}
384
385fn read_reason<W: Work>(extension: &Extension<'_>, execution: &W) -> Result<Reason> {
386    const FIELD: &str = "extensions.reasonCode";
387    let mut reader = extension.reader_scoped(execution).field(FIELD)?;
388    let element = reader.expect(Tag::primitive(10)).field(FIELD)?;
389    super::end(&reader, FIELD, extension.span().start)?;
390    reason_element(&reader, &element, FIELD)
391}
392
393/// Shared reasonCode schema for CRL entries and OCSP `RevokedInfo`.
394pub(crate) fn reason_element(
395    parent: &structured_asn1::ScopedReader<'_, '_>,
396    element: &structured_asn1::Element<'_>,
397    field: &'static str,
398) -> Result<Reason> {
399    parent.charge(9).field(field)?;
400    let value = parent.integer(element).field(field)?;
401    // ENUMERATED content uses the same two's-complement/minimal encoding as
402    // INTEGER (X.690 §8.4); the reason schema selects the permitted values.
403    // https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1
404    match value.as_u64() {
405        Some(0) => Ok(Reason::Unspecified),
406        Some(1) => Ok(Reason::KeyCompromise),
407        Some(2) => Ok(Reason::AuthorityCompromise),
408        Some(3) => Ok(Reason::AffiliationChanged),
409        Some(4) => Ok(Reason::Superseded),
410        Some(5) => Ok(Reason::CessationOfOperation),
411        Some(6) => Ok(Reason::CertificateHold),
412        Some(8) => Ok(Reason::RemoveFromCrl),
413        Some(9) => Ok(Reason::PrivilegeWithdrawn),
414        Some(10) => Ok(Reason::AttributeAuthorityCompromise),
415        _ => Err(Error::new(
416            ErrorCode::MalformedValue,
417            field,
418            element.span().start,
419        )),
420    }
421}