Skip to main content

structured_pkix/
access.rs

1//! Where things related to a certificate are published.
2//!
3//! Every location here is text. Nothing in this crate resolves one, and
4//! nothing above it may resolve one without a person choosing to: a parser
5//! that fetched a location a document named would ask a server chosen by
6//! whoever wrote the document to confirm what the document says, and would
7//! tell that server which document is being looked at.
8
9use crate::error::{Error, ErrorCode, Located, Result};
10use crate::extension::{self, Extension};
11use crate::general_name::{self, GeneralName};
12use crate::name::{self, RelativeName};
13use crate::oid;
14use structured_asn1::memory::{OwnedVec, Work};
15use structured_asn1::{BitString, Element, Oid, ScopedReader as Reader, Tag, universal};
16
17/// One published service or document, and where it is.
18#[derive(Debug)]
19pub struct AccessDescription<'a> {
20    method: Oid<'a>,
21    location: GeneralName<'a>,
22    at: u64,
23}
24
25impl<'a> AccessDescription<'a> {
26    /// What sits at the location.
27    #[must_use]
28    pub fn method(&self) -> Oid<'a> {
29        self.method
30    }
31
32    /// Where it is, unresolved.
33    #[must_use]
34    pub fn location(&self) -> &GeneralName<'a> {
35        &self.location
36    }
37}
38
39/// The access descriptions of one extension, whether about the issuer or about
40/// the subject.
41#[derive(Debug)]
42pub struct InfoAccess<'a> {
43    descriptions: OwnedVec<AccessDescription<'a>>,
44    field: &'static str,
45}
46
47impl<'a> InfoAccess<'a> {
48    /// Every description, in order.
49    #[must_use]
50    pub fn descriptions(&self) -> &[AccessDescription<'a>] {
51        &self.descriptions
52    }
53
54    /// The locations published for one method. Each comparison is admitted
55    /// in the current execution domain; an error is yielded once and ends
56    /// the iterator. Returned locations borrow this collection.
57    pub fn locations_for<'n, 'q, W: Work>(
58        &'n self,
59        method: Oid<'q>,
60        execution: &'q W,
61    ) -> impl Iterator<Item = Result<&'n GeneralName<'a>>> + 'q
62    where
63        'n: 'q,
64    {
65        let mut descriptions = self.descriptions.iter();
66        let mut failed = false;
67        core::iter::from_fn(move || {
68            if failed {
69                return None;
70            }
71            for description in descriptions.by_ref() {
72                let work = 1 + description
73                    .method
74                    .as_bytes()
75                    .len()
76                    .max(method.as_bytes().len()) as u64;
77                if let Err(reason) = execution.charge_work(work) {
78                    failed = true;
79                    return Some(Err(Error::resource(reason, self.field, description.at)));
80                }
81                if description.method == method {
82                    return Some(Ok(description.location()));
83                }
84            }
85            None
86        })
87    }
88
89    /// Reads the value of an authority or subject information access
90    /// extension, which have the same shape.
91    pub fn read<W: Work>(extension: &Extension<'a>, execution: &W) -> Result<Self> {
92        let field = if extension.kind() == oid::SUBJECT_INFO_ACCESS {
93            "extensions.subjectInfoAccess"
94        } else {
95            "extensions.authorityInfoAccess"
96        };
97
98        let mut inside = extension::sequence(extension, field, execution)?;
99        let work = inside.execution_in(execution.memory());
100        let mut descriptions = work.vector();
101
102        while let Some(entry) = inside.next_element().field(field)? {
103            let at = |code| Error::new(code, field, entry.span().start);
104            if entry.tag() != Tag::constructed(universal::SEQUENCE) {
105                return Err(at(ErrorCode::Encoding(
106                    structured_asn1::ErrorCode::UnexpectedTag,
107                )));
108            }
109
110            let mut pair = inside.enter(&entry).field(field)?;
111            let method = pair.read_oid().field(field)?;
112            let location = pair
113                .next_element()
114                .field(field)?
115                .ok_or_else(|| at(ErrorCode::MissingField))?;
116            let location = general_name::read(&pair, &location, field)?;
117            if !pair.is_empty() {
118                return Err(at(ErrorCode::TrailingData));
119            }
120
121            descriptions
122                .try_push(AccessDescription {
123                    method,
124                    location,
125                    at: entry.span().start,
126                })
127                .map_err(|reason| Error::resource(reason, field, entry.span().start))?;
128        }
129
130        if descriptions.is_empty() {
131            return Err(Error::new(
132                ErrorCode::EmptySequence,
133                field,
134                extension.span().start,
135            ));
136        }
137        Ok(Self {
138            descriptions: descriptions.into_owned(),
139            field,
140        })
141    }
142}
143
144/// One place a revocation list covering this certificate is published.
145#[derive(Debug)]
146pub struct DistributionPoint<'a> {
147    full_name: OwnedVec<GeneralName<'a>>,
148    name_relative_to_issuer: Option<RelativeName<'a>>,
149    reasons: Option<BitString<'a>>,
150    issuer: OwnedVec<GeneralName<'a>>,
151}
152
153impl<'a> DistributionPoint<'a> {
154    /// Where the list is, where the point names it outright.
155    #[must_use]
156    pub fn full_name(&self) -> &[GeneralName<'a>] {
157        &self.full_name
158    }
159
160    /// Where the list is, where the point names it relative to whoever issues
161    /// it rather than outright.
162    #[must_use]
163    pub fn name_relative_to_issuer(&self) -> Option<&RelativeName<'a>> {
164        self.name_relative_to_issuer.as_ref()
165    }
166
167    /// Which reasons for revocation this list covers, where the point says.
168    /// Absent means all of them, and a point that names a few is not a list a
169    /// complete answer can be built from on its own.
170    #[must_use]
171    pub fn reasons(&self) -> Option<BitString<'a>> {
172        self.reasons
173    }
174
175    /// Who issues the list, where that is not the issuer of the certificate.
176    #[must_use]
177    pub fn issuer(&self) -> &[GeneralName<'a>] {
178        &self.issuer
179    }
180}
181
182/// The places revocation lists covering this certificate are published.
183#[derive(Debug)]
184pub struct DistributionPoints<'a> {
185    points: OwnedVec<DistributionPoint<'a>>,
186}
187
188impl<'a> DistributionPoints<'a> {
189    /// Every point, in order.
190    #[must_use]
191    pub fn points(&self) -> &[DistributionPoint<'a>] {
192        &self.points
193    }
194
195    /// Reads the value of a distribution points extension, which the freshest
196    /// revocation list is published in the same shape as.
197    pub fn read<W: Work>(extension: &Extension<'a>, execution: &W) -> Result<Self> {
198        let field = if extension.kind() == oid::FRESHEST_CRL {
199            "extensions.freshestCRL"
200        } else {
201            "extensions.cRLDistributionPoints"
202        };
203
204        let mut inside = extension::sequence(extension, field, execution)?;
205        let work = inside.execution_in(execution.memory());
206        let mut points = work.vector();
207        while let Some(entry) = inside.next_element().field(field)? {
208            points
209                .try_push(read_point(&inside, &entry, field)?)
210                .map_err(|reason| Error::resource(reason, field, entry.span().start))?;
211        }
212
213        if points.is_empty() {
214            return Err(Error::new(
215                ErrorCode::EmptySequence,
216                field,
217                extension.span().start,
218            ));
219        }
220        Ok(Self {
221            points: points.into_owned(),
222        })
223    }
224}
225
226fn read_point<'a>(
227    parent: &Reader<'a, '_>,
228    element: &Element<'a>,
229    field: &'static str,
230) -> Result<DistributionPoint<'a>> {
231    let fail = |code| Error::new(code, field, element.span().start);
232
233    if element.tag() != Tag::constructed(universal::SEQUENCE) {
234        return Err(fail(ErrorCode::Encoding(
235            structured_asn1::ErrorCode::UnexpectedTag,
236        )));
237    }
238
239    let mut inside = parent.enter(element).field(field)?;
240    let mut point = DistributionPoint {
241        full_name: parent.memory().vector(),
242        name_relative_to_issuer: None,
243        reasons: None,
244        issuer: parent.memory().vector(),
245    };
246
247    // The three fields are written in the order of their numbers, once each:
248    // a tag at or below the last one read is not this schema.
249    let mut last_number = None;
250    while let Some(entry) = inside.next_element().field(field)? {
251        let tag = extension::context_of(entry.tag());
252        match tag {
253            Some((0, true)) if last_number.is_none() => {
254                read_point_name(&inside, &entry, field, &mut point)?;
255            }
256            Some((1, false)) if last_number < Some(1) => {
257                let reasons = inside.bit_string(&entry).field(field)?;
258                // The zero walk below touches every content byte, so it is
259                // charged to the shared work budget first.
260                inside
261                    .charge(reasons.as_bytes().len() as u64)
262                    .field(field)?;
263                // Present while naming no reason, the field covers none while
264                // claiming to narrow the list: a reader would take the point
265                // for a partial list and a full one at once. Absent is how
266                // "all reasons" is said. Written as zero bits, as zero bytes,
267                // or with only bit zero, which the schema names "unused" and
268                // gives no meaning (RFC 5280 4.2.1.13), it says the same
269                // nothing: a scope whose only member means nothing has no
270                // members.
271                if !(1..=8).any(|bit| reasons.bit(bit)) {
272                    return Err(Error::new(
273                        ErrorCode::MalformedValue,
274                        field,
275                        entry.span().start,
276                    ));
277                }
278                // A named bit list under DER: X.690 11.2.2 trims trailing
279                // zero bits, and RFC 5280 4.2.1.13 names bits 0 through 8, so
280                // a longer string carries a bit with no name. unused(0) is
281                // deliberately readable: it is a named bit of the schema, and
282                // RFC 5280 gives it no meaning but no prohibition either;
283                // what a set bit means is the revocation evidence's question.
284                if inside.encoding().is_der()
285                    && (reasons.len() > 9
286                        || (!reasons.is_empty() && !reasons.bit(reasons.len() - 1)))
287                {
288                    return Err(Error::new(
289                        ErrorCode::MalformedValue,
290                        field,
291                        entry.span().start,
292                    ));
293                }
294                point.reasons = Some(reasons);
295            }
296            Some((2, true)) if last_number < Some(2) => {
297                let issuer = general_name::read_all(&inside, &entry, field)?;
298                // The profile is explicit that this is exclusive: a present
299                // cRLIssuer "MUST only contain the distinguished name (DN)
300                // from the issuer field of the CRL" (RFC 5280 4.2.1.13), so
301                // every member is required to be a directory name, not
302                // merely one of them. Any other form cannot be matched
303                // against a list's issuer field anyway. Several directory
304                // names stay permitted: the same section contemplates them
305                // by forbidding only the relative alternative "when
306                // cRLIssuer contains more than one distinguished name", the
307                // check below, because the relative fragment appends to
308                // exactly one name and with two there is no answer to which
309                // list the point names.
310                if !issuer
311                    .iter()
312                    .all(|name| matches!(name, general_name::GeneralName::Directory(_)))
313                    || (point.name_relative_to_issuer.is_some() && issuer.len() > 1)
314                {
315                    return Err(Error::new(
316                        ErrorCode::MalformedValue,
317                        field,
318                        entry.span().start,
319                    ));
320                }
321                point.issuer = issuer;
322            }
323            _ => {
324                return Err(Error::new(
325                    ErrorCode::UnexpectedField,
326                    field,
327                    entry.span().start,
328                ));
329            }
330        }
331        last_number = tag.map(|(number, _)| number);
332    }
333
334    // A point must say where the list is or who issues it. With neither, it
335    // names no source a list could ever be resolved from, and reporting it as
336    // parsed would count a malformed extension as a working one. A relative
337    // name WITHOUT a cRLIssuer stays valid on its own: the profile appends
338    // the fragment to the certificate issuer's distinguished name when the
339    // issuer field is absent (RFC 5280 4.2.1.13), so the point still names a
340    // resolvable source. Reasons beside a cRLIssuer with no point name stay
341    // valid too: the only shape rule the profile states is that a point
342    // "MUST NOT consist of only the reasons field; either distributionPoint
343    // or cRLIssuer MUST be present" (RFC 5280 4.2.1.13), and no sentence
344    // requires a point name beside reasons; the list is then located
345    // through the issuer's directory entry, its reason scope intact.
346    if point.full_name.is_empty()
347        && point.name_relative_to_issuer.is_none()
348        && point.issuer.is_empty()
349    {
350        return Err(fail(ErrorCode::MalformedValue));
351    }
352
353    Ok(point)
354}
355
356/// Reads the name of a point, which is a choice between naming the list
357/// outright and naming it under whoever issues it. A choice cannot take the
358/// tag of its position, so the field wraps it.
359#[derive(Debug)]
360pub(crate) enum PointName<'a> {
361    Full(OwnedVec<GeneralName<'a>>),
362    Relative(RelativeName<'a>),
363}
364
365/// Shared `DistributionPointName` grammar for certificate DPs and CRL IDPs.
366pub(crate) fn read_distribution_name<'a>(
367    parent: &Reader<'a, '_>,
368    element: &Element<'a>,
369    field: &'static str,
370) -> Result<PointName<'a>> {
371    let fail = |code| Error::new(code, field, element.span().start);
372    let mut inside = parent.enter(element).field(field)?;
373    let chosen = inside
374        .next_element()
375        .field(field)?
376        .ok_or_else(|| fail(ErrorCode::MissingField))?;
377    let name = match extension::context_of(chosen.tag()) {
378        Some((0, true)) => PointName::Full(general_name::read_locations(&inside, &chosen, field)?),
379        Some((1, true)) => {
380            let relative = name::read_relative(&inside, &chosen, field)?;
381            for attribute in relative.attributes() {
382                if !name::well_formed_in(attribute, &inside, field)? {
383                    return Err(fail(ErrorCode::MalformedValue));
384                }
385            }
386            PointName::Relative(relative)
387        }
388        _ => {
389            return Err(fail(ErrorCode::Encoding(
390                structured_asn1::ErrorCode::UnexpectedTag,
391            )));
392        }
393    };
394    if !inside.is_empty() {
395        return Err(fail(ErrorCode::TrailingData));
396    }
397    Ok(name)
398}
399
400fn read_point_name<'a>(
401    parent: &Reader<'a, '_>,
402    element: &Element<'a>,
403    field: &'static str,
404    point: &mut DistributionPoint<'a>,
405) -> Result<()> {
406    match read_distribution_name(parent, element, field)? {
407        PointName::Full(names) => point.full_name = names,
408        PointName::Relative(name) => point.name_relative_to_issuer = Some(name),
409    }
410    Ok(())
411}
412
413#[cfg(test)]
414mod tests;