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::{Memory, OwnedVec};
9use structured_asn1::{Oid, Reader, 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(
40        &self,
41        target: &Certificate<'_>,
42        execution: &Memory,
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(
155    entry: &RevokedCertificate<'_>,
156    delta: bool,
157    execution: &Memory,
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(crl: &super::CertificateList<'_>, execution: &Memory) -> 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>(
193    crl: &'r super::CertificateList<'a>,
194    is_delta: bool,
195    execution: &Memory,
196) -> Result<Option<&'r Extension<'a>>> {
197    const FIELD: &str = "certificateList.extensions";
198    for extension in crl.tbs().extensions().iter() {
199        execution
200            .charge_work(53)
201            .map_err(|reason| Error::resource(reason, FIELD, extension.span().start))?;
202        let kind = extension.kind();
203        let malformed = || Error::new(ErrorCode::MalformedValue, FIELD, extension.span().start);
204        if kind == oid::ISSUING_DISTRIBUTION_POINT {
205            // RFC5280 §5.2.5: this extension MUST be critical. The caller
206            // already decoded its scope once before this metadata pass.
207            if !extension.is_critical() {
208                return Err(malformed());
209            }
210        } else if kind == oid::CRL_NUMBER {
211            // RFC5280 §5.2.3: noncritical INTEGER (0..MAX), including on
212            // ordinary complete sources, not only delta binding.
213            // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.3
214            if extension.is_critical() {
215                return Err(malformed());
216            }
217            super::delta::number(extension, execution)?;
218        } else if kind == oid::DELTA_CRL_INDICATOR {
219            // delta_source validated the critical indicator's number once.
220        } else if kind == oid::AUTHORITY_KEY_IDENTIFIER {
221            // RFC5280 §§5.2.1/4.2.1.1: present CRL AKI uses keyIdentifier
222            // and is noncritical. Absence is not a blanket v1 receiver refusal.
223            // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.1
224            if extension.is_critical() {
225                return Err(malformed());
226            }
227            let identifier = crate::identifier::AuthorityKeyIdentifier::read(extension, execution)?;
228            if identifier.key_id().is_none() {
229                return Err(malformed());
230            }
231        } else if kind == oid::ISSUER_ALT_NAME {
232            general_name::read_alternative(extension, execution)?;
233        } else if kind == oid::AUTHORITY_INFO_ACCESS {
234            // RFC5280 §5.2.7 forbids critical AIA. Noncritical fetch hints are
235            // preserved, not interpreted or used to establish status here.
236            // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.7
237            if extension.is_critical() {
238                return Err(malformed());
239            }
240        } else if kind == oid::FRESHEST_CRL {
241            // RFC5280 §5.2.6: noncritical, complete sources only, with no
242            // reasons or cRLIssuer in its distribution points. No fetching.
243            // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.6
244            if extension.is_critical() || is_delta {
245                return Err(malformed());
246            }
247            let points = crate::access::DistributionPoints::read(extension, execution)?;
248            for point in points.points() {
249                execution
250                    .charge_work(1)
251                    .map_err(|reason| Error::resource(reason, FIELD, extension.span().start))?;
252                if point.reasons().is_some() || !point.issuer().is_empty() {
253                    return Err(malformed());
254                }
255            }
256        } else if extension.is_critical() {
257            return Ok(Some(extension));
258        }
259    }
260    Ok(None)
261}
262
263fn validated_issuer<'a>(
264    entry: &RevokedCertificate<'a>,
265    indirect: bool,
266    execution: &Memory,
267) -> Result<Option<OwnedVec<GeneralName<'a>>>> {
268    const FIELD: &str = "extensions.certificateIssuer";
269    let Some(extension) = entry.extensions.get(oid::CERTIFICATE_ISSUER, execution)? else {
270        return Ok(None);
271    };
272    if !indirect || !extension.is_critical() {
273        return Err(Error::new(
274            ErrorCode::MalformedValue,
275            FIELD,
276            extension.span().start,
277        ));
278    }
279    let mut reader = extension::sequence(extension, FIELD, execution)?;
280    let names = general_name::read_list(&mut reader, FIELD, extension.span().start)?;
281    let mut directory = false;
282    for name in &names {
283        execution
284            .charge_work(1)
285            .map_err(|reason| Error::resource(reason, FIELD, extension.span().start))?;
286        if matches!(name, GeneralName::Directory(name) if !name.is_empty()) {
287            directory = true;
288        }
289    }
290    if !directory {
291        return Err(Error::new(
292            ErrorCode::MalformedValue,
293            FIELD,
294            extension.span().start,
295        ));
296    }
297    Ok(Some(names))
298}
299
300fn equal_bytes(left: &[u8], right: &[u8], execution: &Memory, at: u64) -> Result<bool> {
301    let work = (left.len().max(right.len()) as u64)
302        .checked_mul(2)
303        .and_then(|n| n.checked_add(1))
304        .ok_or_else(|| {
305            Error::resource(
306                structured_asn1::memory::MemoryError::WorkExceeded,
307                "certificateList.entryLookup",
308                at,
309            )
310        })?;
311    execution
312        .charge_work(work)
313        .map_err(|reason| Error::resource(reason, "certificateList.entryLookup", at))?;
314    Ok(left == right)
315}
316
317/// RFC 5280 §5.3.1 reasonCode values. Enumeration numbers differ from
318/// `ReasonFlags` bit positions for privilegeWithdrawn and aACompromise.
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum Reason {
321    /// No more specific reason was stated.
322    Unspecified,
323    /// Subject private key compromise.
324    KeyCompromise,
325    /// Certification authority private key compromise.
326    AuthorityCompromise,
327    /// Subject affiliation changed.
328    AffiliationChanged,
329    /// Certificate replaced.
330    Superseded,
331    /// The certified operation ceased.
332    CessationOfOperation,
333    /// Temporary suspension.
334    CertificateHold,
335    /// Delta CRL removes a prior hold or expired entry; not a new revocation.
336    RemoveFromCrl,
337    /// Privilege withdrawn (enumeration 9, reason flag 7).
338    PrivilegeWithdrawn,
339    /// Attribute authority compromise (enumeration 10, reason flag 8).
340    AttributeAuthorityCompromise,
341}
342
343impl<'a> RevokedCertificate<'a> {
344    /// Stated reason, independently of whether this CRL is a valid complete or
345    /// delta list. An omitted extension stays absent rather than rewritten.
346    pub fn reason(&self, execution: &Memory) -> Result<Option<Reason>> {
347        self.extensions
348            .get(oid::CRL_REASON, execution)?
349            .map(|extension| read_reason(extension, execution))
350            .transpose()
351    }
352
353    /// When the issuer believes invalidity began, distinct from revocationDate.
354    pub fn invalidity_date(&self, execution: &Memory) -> Result<Option<Time>> {
355        const FIELD: &str = "extensions.invalidityDate";
356        let Some(extension) = self.extensions.get(oid::INVALIDITY_DATE, execution)? else {
357            return Ok(None);
358        };
359        let mut reader = extension.reader(execution).field(FIELD)?;
360        let element = reader
361            .expect(Tag::primitive(universal::GENERALIZED_TIME))
362            .field(FIELD)?;
363        super::end(&reader, FIELD, extension.span().start)?;
364        crate::time::read_in(&reader, &element, FIELD).map(Some)
365    }
366
367    /// Explicit issuer names on this entry. None means inheritance from the
368    /// previous entry (or CRL issuer on the first), never an absent identity.
369    pub fn certificate_issuer(
370        &self,
371        execution: &Memory,
372    ) -> Result<Option<OwnedVec<GeneralName<'a>>>> {
373        const FIELD: &str = "extensions.certificateIssuer";
374        let Some(extension) = self.extensions.get(oid::CERTIFICATE_ISSUER, execution)? else {
375            return Ok(None);
376        };
377        let mut reader = extension::sequence(extension, FIELD, execution)?;
378        general_name::read_list(&mut reader, FIELD, extension.span().start).map(Some)
379    }
380}
381
382fn read_reason(extension: &Extension<'_>, execution: &Memory) -> Result<Reason> {
383    const FIELD: &str = "extensions.reasonCode";
384    let mut reader = extension.reader(execution).field(FIELD)?;
385    let element = reader.expect(Tag::primitive(10)).field(FIELD)?;
386    super::end(&reader, FIELD, extension.span().start)?;
387    reason_element(&reader, &element, FIELD)
388}
389
390/// Shared reasonCode schema for CRL entries and OCSP `RevokedInfo`.
391pub(crate) fn reason_element(
392    parent: &Reader<'_>,
393    element: &structured_asn1::Element<'_>,
394    field: &'static str,
395) -> Result<Reason> {
396    parent.charge(9).field(field)?;
397    let value = parent.integer(element).field(field)?;
398    // ENUMERATED content uses the same two's-complement/minimal encoding as
399    // INTEGER (X.690 §8.4); the reason schema selects the permitted values.
400    // https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1
401    match value.as_u64() {
402        Some(0) => Ok(Reason::Unspecified),
403        Some(1) => Ok(Reason::KeyCompromise),
404        Some(2) => Ok(Reason::AuthorityCompromise),
405        Some(3) => Ok(Reason::AffiliationChanged),
406        Some(4) => Ok(Reason::Superseded),
407        Some(5) => Ok(Reason::CessationOfOperation),
408        Some(6) => Ok(Reason::CertificateHold),
409        Some(8) => Ok(Reason::RemoveFromCrl),
410        Some(9) => Ok(Reason::PrivilegeWithdrawn),
411        Some(10) => Ok(Reason::AttributeAuthorityCompromise),
412        _ => Err(Error::new(
413            ErrorCode::MalformedValue,
414            field,
415            element.span().start,
416        )),
417    }
418}