Skip to main content

structured_pkix/
policy.rs

1//! The policies a certificate was issued under.
2//!
3//! A policy identifier is what a trusted list points at when it says which
4//! service issued a certificate, so these are identity rather than
5//! description. The qualifiers beside them are kept as they were encoded: one
6//! of them is a location, and a location is data here.
7
8use crate::error::{Error, ErrorCode, Located, Result, gate};
9use crate::extension::{self, Extension};
10use structured_asn1::memory::{OwnedVec, Work};
11use structured_asn1::{Oid, Parser, Tag, universal};
12
13/// One policy, and whatever was written beside it.
14#[derive(Debug)]
15pub struct PolicyInformation<'a> {
16    policy: Oid<'a>,
17    qualifiers: OwnedVec<PolicyQualifier<'a>>,
18    at: u64,
19}
20
21impl<'a> PolicyInformation<'a> {
22    /// Which policy.
23    #[must_use]
24    pub fn policy(&self) -> Oid<'a> {
25        self.policy
26    }
27
28    /// What was written beside it.
29    #[must_use]
30    pub fn qualifiers(&self) -> &[PolicyQualifier<'a>] {
31        &self.qualifiers
32    }
33}
34
35/// One qualifier: what kind it is, and its value as encoded.
36#[derive(Debug, Clone, Copy)]
37pub struct PolicyQualifier<'a> {
38    kind: Oid<'a>,
39    value: &'a [u8],
40    /// Where the value sits in the document, so an error found while
41    /// re-parsing it points at the qualifier rather than at the front of the
42    /// certificate.
43    at: u64,
44    /// Depth above the qualifier pair, retained for its later decode.
45    outer: u32,
46}
47
48impl<'a> PolicyQualifier<'a> {
49    /// What kind of qualifier this is.
50    #[must_use]
51    pub fn kind(self) -> Oid<'a> {
52        self.kind
53    }
54
55    /// The value, as it was encoded. A practice statement qualifier holds a
56    /// location, which stays a string of bytes here.
57    #[must_use]
58    pub fn value(self) -> &'a [u8] {
59        self.value
60    }
61}
62
63/// The policies of one certificate.
64#[derive(Debug)]
65pub struct CertificatePolicies<'a> {
66    policies: OwnedVec<PolicyInformation<'a>>,
67}
68
69impl<'a> CertificatePolicies<'a> {
70    /// Every policy, in order.
71    #[must_use]
72    pub fn policies(&self) -> &[PolicyInformation<'a>] {
73        &self.policies
74    }
75
76    /// Whether one policy is among them.
77    ///
78    /// The identifier that stands for any policy is not treated as matching
79    /// everything: whether it does depends on where in a path this certificate
80    /// sits, which is not a question this layer can answer.
81    pub fn has<W: Work>(&self, policy: Oid<'_>, execution: &W) -> Result<bool> {
82        for information in &self.policies {
83            let work = 1 + information
84                .policy
85                .as_bytes()
86                .len()
87                .max(policy.as_bytes().len()) as u64;
88            execution.charge_work(work).map_err(|reason| {
89                Error::resource(reason, "extensions.certificatePolicies", information.at)
90            })?;
91            if information.policy == policy {
92                return Ok(true);
93            }
94        }
95        Ok(false)
96    }
97
98    /// Reads the value of a certificate policies extension.
99    pub fn read<W: Work>(extension: &Extension<'a>, execution: &W) -> Result<Self> {
100        const FIELD: &str = "extensions.certificatePolicies";
101        let fail = |code| Error::new(code, FIELD, extension.span().start);
102
103        let mut inside = extension::sequence(extension, FIELD, execution)?;
104        let work = inside.execution_in(execution.memory());
105        let mut policies = work.vector();
106        let mut identifiers = work.map::<Oid<'a>, ()>();
107
108        while let Some(entry) = inside.next_element().field(FIELD)? {
109            let at = |code| Error::new(code, FIELD, entry.span().start);
110            if entry.tag() != Tag::constructed(universal::SEQUENCE) {
111                return Err(at(ErrorCode::Encoding(
112                    structured_asn1::ErrorCode::UnexpectedTag,
113                )));
114            }
115
116            let mut information = inside.enter(&entry).field(FIELD)?;
117            let policy = information.read_oid().field(FIELD)?;
118            let mut qualifiers = work.vector();
119
120            if let Some(list) = information.next_element().field(FIELD)? {
121                if list.tag() != Tag::constructed(universal::SEQUENCE) {
122                    return Err(at(ErrorCode::Encoding(
123                        structured_asn1::ErrorCode::UnexpectedTag,
124                    )));
125                }
126                let mut each = information.enter(&list).field(FIELD)?;
127                while let Some(qualifier) = each.next_element().field(FIELD)? {
128                    if qualifier.tag() != Tag::constructed(universal::SEQUENCE) {
129                        return Err(at(ErrorCode::Encoding(
130                            structured_asn1::ErrorCode::UnexpectedTag,
131                        )));
132                    }
133                    let mut pair = each.enter(&qualifier).field(FIELD)?;
134                    let kind = pair.read_oid().field(FIELD)?;
135                    let value = pair
136                        .next_element()
137                        .field(FIELD)?
138                        .ok_or_else(|| at(ErrorCode::MissingField))?;
139                    if !pair.is_empty() {
140                        return Err(at(ErrorCode::TrailingData));
141                    }
142                    // The value is preserved for whoever knows the kind, and
143                    // a policy is what a trusted list points at when it names
144                    // the service that issued a certificate, so what is kept
145                    // has to be the encoding the document claims all the way
146                    // down. The outer header alone would leave a second
147                    // spelling of a value one level lower, arriving at the
148                    // same reader by the same route.
149                    crate::name::walk_der(&pair, &value, FIELD, crate::name::Strings::Checked)?;
150                    qualifiers
151                        .try_push(PolicyQualifier {
152                            kind,
153                            value: value.as_bytes(),
154                            at: value.span().start,
155                            outer: each.depth(),
156                        })
157                        .map_err(|reason| Error::resource(reason, FIELD, qualifier.span().start))?;
158                }
159                if qualifiers.is_empty() {
160                    return Err(at(ErrorCode::EmptySequence));
161                }
162            }
163
164            if !information.is_empty() {
165                return Err(at(ErrorCode::TrailingData));
166            }
167
168            // One policy written twice leaves no answer to which qualifiers
169            // belong to it. A set rather than a scan, so a long list costs
170            // the work its size says and not its square.
171            if identifiers
172                .try_insert(policy, ())
173                .map_err(|reason| Error::resource(reason, FIELD, entry.span().start))?
174                .is_some()
175            {
176                return Err(at(ErrorCode::MalformedValue));
177            }
178
179            policies
180                .try_push(PolicyInformation {
181                    policy,
182                    qualifiers: qualifiers.into_owned(),
183                    at: entry.span().start,
184                })
185                .map_err(|reason| Error::resource(reason, FIELD, entry.span().start))?;
186        }
187
188        if policies.is_empty() {
189            return Err(fail(ErrorCode::EmptySequence));
190        }
191        Ok(Self {
192            policies: policies.into_owned(),
193        })
194    }
195}
196
197/// A policy of one domain treated as equivalent to a policy of another.
198#[derive(Debug, Clone, Copy)]
199pub struct PolicyMapping<'a> {
200    issuer_domain: Oid<'a>,
201    subject_domain: Oid<'a>,
202}
203
204impl<'a> PolicyMapping<'a> {
205    /// The policy as the issuer of this certificate names it.
206    #[must_use]
207    pub fn issuer_domain(self) -> Oid<'a> {
208        self.issuer_domain
209    }
210
211    /// The policy it stands for below this certificate.
212    #[must_use]
213    pub fn subject_domain(self) -> Oid<'a> {
214        self.subject_domain
215    }
216}
217
218/// The mappings of one certificate.
219#[derive(Debug)]
220pub struct PolicyMappings<'a> {
221    mappings: OwnedVec<PolicyMapping<'a>>,
222}
223
224impl<'a> PolicyMappings<'a> {
225    /// Every mapping, in order.
226    #[must_use]
227    pub fn mappings(&self) -> &[PolicyMapping<'a>] {
228        &self.mappings
229    }
230
231    /// Reads the value of a policy mappings extension.
232    pub fn read<W: Work>(extension: &Extension<'a>, execution: &W) -> Result<Self> {
233        const FIELD: &str = "extensions.policyMappings";
234        let fail = |code| Error::new(code, FIELD, extension.span().start);
235
236        let mut inside = extension::sequence(extension, FIELD, execution)?;
237        let work = inside.execution_in(execution.memory());
238        let mut mappings = work.vector();
239        let mut seen = work.map::<(Oid<'a>, Oid<'a>), ()>();
240
241        while let Some(entry) = inside.next_element().field(FIELD)? {
242            let at = |code| Error::new(code, FIELD, entry.span().start);
243            if entry.tag() != Tag::constructed(universal::SEQUENCE) {
244                return Err(at(ErrorCode::Encoding(
245                    structured_asn1::ErrorCode::UnexpectedTag,
246                )));
247            }
248
249            let mut pair = inside.enter(&entry).field(FIELD)?;
250            let issuer_domain = pair.read_oid().field(FIELD)?;
251            let subject_domain = pair.read_oid().field(FIELD)?;
252            if !pair.is_empty() {
253                return Err(at(ErrorCode::TrailingData));
254            }
255
256            // Mapping the identifier that stands for any policy would make
257            // every policy below this certificate equivalent to one of them.
258            if issuer_domain == crate::oid::ANY_POLICY || subject_domain == crate::oid::ANY_POLICY {
259                return Err(at(ErrorCode::MalformedValue));
260            }
261
262            // A repeated issuer domain with distinct subjects is legitimate:
263            // path processing takes the set of subject policies equivalent to
264            // one issuer policy (RFC 5280 6.1.4 (b)(1)), and repeating the
265            // issuer is how that set is written. The exact pair written twice
266            // maps what one occurrence maps, and the list is a SEQUENCE OF
267            // with no uniqueness constraint: the repeat is folded away rather
268            // than refused, so a valid certificate is not blocked over it.
269            if seen
270                .try_insert((issuer_domain, subject_domain), ())
271                .map_err(|reason| Error::resource(reason, FIELD, entry.span().start))?
272                .is_none()
273            {
274                mappings
275                    .try_push(PolicyMapping {
276                        issuer_domain,
277                        subject_domain,
278                    })
279                    .map_err(|reason| Error::resource(reason, FIELD, entry.span().start))?;
280            }
281        }
282
283        if mappings.is_empty() {
284            return Err(fail(ErrorCode::EmptySequence));
285        }
286        Ok(Self {
287            mappings: mappings.into_owned(),
288        })
289    }
290}
291
292impl CertificatePolicies<'_> {
293    /// Whether every qualifier was actually interpreted, for the gate that
294    /// decides what a critical extension blocked.
295    ///
296    /// RFC 5280 4.2.1.4 defines two qualifiers, the practice-statement
297    /// pointer and the user notice. A kind outside those, or a known kind
298    /// whose payload does not have its shape, is a critical qualifier this
299    /// run never understood. An error is a spent budget and only that.
300    pub(crate) fn understood<W: Work>(&self, parser: &Parser, execution: &W) -> Result<bool> {
301        for qualifier in self.policies.iter().flat_map(PolicyInformation::qualifiers) {
302            execution
303                .charge_work(1)
304                .map_err(|reason| Error::resource(reason, QUALIFIER_FIELD, qualifier.at))?;
305            if !retained_qualifier_understood(parser, *qualifier, execution)? {
306                return Ok(false);
307            }
308        }
309        Ok(true)
310    }
311}
312
313/// The field the qualifier gates below report a spent budget in.
314const QUALIFIER_FIELD: &str = "extensions.certificatePolicies";
315
316fn retained_qualifier_understood<W: Work>(
317    parser: &Parser,
318    qualifier: PolicyQualifier<'_>,
319    execution: &W,
320) -> Result<bool> {
321    // Re-parsed from where the value sits, so the one error that leaves
322    // here, a spent budget, carries a document offset as the error type
323    // promises. Malformation still answers no.
324    let Some(mut reader) = gate(
325        execution
326            .read_nested(
327                parser,
328                qualifier.value,
329                qualifier.at,
330                structured_asn1::Encoding::Der,
331                qualifier.outer,
332            )
333            .field(QUALIFIER_FIELD),
334    )?
335    else {
336        return Ok(false);
337    };
338    let Some(Some(value)) = gate(reader.next_element().field(QUALIFIER_FIELD))? else {
339        return Ok(false);
340    };
341    if !reader.is_empty() {
342        return Ok(false);
343    }
344
345    qualifier_understood(&reader, qualifier.kind, &value)
346}
347
348#[derive(Clone, Copy)]
349enum QualifierKind {
350    Cps,
351    Notice,
352}
353
354/// Checks a DER policy qualifier's selected schema (RFC5280 ยง4.2.1.4).
355///
356/// `parent` is the producing reader, with the current execution account and
357/// original parser-local cumulative work/depth ceilings. A known value from
358/// a BER envelope is reread under DER with those same ceilings and offsets.
359/// No value bytes are copied, no URI is fetched and no application policy is selected.
360/// Unknown kinds and malformed known payloads return `false`; callers decide
361/// independently whether an unknown qualifier is permitted or preserved.
362///
363/// # Errors
364/// Resource refusals remain located errors, separately from `false`.
365pub fn qualifier_understood<'a>(
366    parent: &structured_asn1::ScopedReader<'a, '_>,
367    kind: Oid<'_>,
368    value: &structured_asn1::Element<'a>,
369) -> Result<bool> {
370    let work = (kind
371        .as_bytes()
372        .len()
373        .max(crate::oid::CPS_QUALIFIER.as_bytes().len()) as u64)
374        .checked_mul(2)
375        .and_then(|cost| cost.checked_add(1))
376        .ok_or_else(|| {
377            Error::resource(
378                structured_asn1::memory::MemoryError::WorkExceeded,
379                QUALIFIER_FIELD,
380                value.span().start,
381            )
382        })?;
383    qualifier_charge(parent, value, work)?;
384    let kind = if kind == crate::oid::CPS_QUALIFIER {
385        QualifierKind::Cps
386    } else if kind == crate::oid::USER_NOTICE_QUALIFIER {
387        QualifierKind::Notice
388    } else {
389        return Ok(false);
390    };
391    if parent.encoding().is_der() {
392        return qualifier_schema(parent, kind, value);
393    }
394    let Some(mut reader) = gate(
395        parent
396            .reread(value, structured_asn1::Encoding::Der)
397            .field(QUALIFIER_FIELD),
398    )?
399    else {
400        return Ok(false);
401    };
402    let Some(Some(value)) = gate(reader.next_element().field(QUALIFIER_FIELD))? else {
403        return Ok(false);
404    };
405    if !reader.is_empty() {
406        return Ok(false);
407    }
408    qualifier_schema(&reader, kind, &value)
409}
410
411fn qualifier_charge(
412    reader: &structured_asn1::ScopedReader<'_, '_>,
413    value: &structured_asn1::Element<'_>,
414    work: u64,
415) -> Result<()> {
416    reader.charge(work).map_err(|error| {
417        Error::new(
418            ErrorCode::Encoding(error.code()),
419            QUALIFIER_FIELD,
420            value.span().start,
421        )
422    })
423}
424
425fn qualifier_schema(
426    reader: &structured_asn1::ScopedReader<'_, '_>,
427    kind: QualifierKind,
428    value: &structured_asn1::Element<'_>,
429) -> Result<bool> {
430    Ok(match kind {
431        QualifierKind::Cps => {
432            // A pointer to the practice statement: one IA5String holding a URI
433            // (RFC 5280 4.2.1.4, CPSuri). Text that is not a URI points at
434            // nothing while wearing the qualifier. Read, never fetched. The
435            // scans below touch every content byte and CPSuri has no local size
436            // ceiling, so the length is charged to the shared work budget first.
437            if value.tag() != Tag::primitive(universal::IA5_STRING) {
438                return Ok(false);
439            }
440            // ASCII/UTF8 checks and all URI component/authority scans are linear;
441            // admit their complete possible passes before examining the contents.
442            let work = (value.contents().len() as u64)
443                .checked_mul(12)
444                .and_then(|cost| cost.checked_add(1))
445                .ok_or_else(|| {
446                    Error::resource(
447                        structured_asn1::memory::MemoryError::WorkExceeded,
448                        QUALIFIER_FIELD,
449                        value.span().start,
450                    )
451                })?;
452            qualifier_charge(reader, value, work)?;
453            crate::name::is_ia5(value.contents())
454                && core::str::from_utf8(value.contents()).is_ok_and(crate::general_name::is_cps_uri)
455        }
456        QualifierKind::Notice => user_notice_understood(reader, value)?,
457    })
458}
459
460/// Whether a user notice has its shape: an optional notice reference, then
461/// optional explicit text. Both members are optional (RFC 5280 4.2.1.4):
462/// the empty sequence says nothing, but it is inside the schema, and
463/// refusing it would block an otherwise valid critical policy.
464fn user_notice_understood(
465    parent: &structured_asn1::ScopedReader<'_, '_>,
466    value: &structured_asn1::Element<'_>,
467) -> Result<bool> {
468    if value.tag() != Tag::constructed(universal::SEQUENCE) {
469        return Ok(false);
470    }
471    let Some(mut inside) = gate(parent.enter(value).field(QUALIFIER_FIELD))? else {
472        return Ok(false);
473    };
474    let Some(mut next) = gate(inside.next_element().field(QUALIFIER_FIELD))? else {
475        return Ok(false);
476    };
477    if let Some(reference) = next
478        && reference.tag() == Tag::constructed(universal::SEQUENCE)
479    {
480        if !notice_reference_understood(&inside, &reference)? {
481            return Ok(false);
482        }
483        let Some(following) = gate(inside.next_element().field(QUALIFIER_FIELD))? else {
484            return Ok(false);
485        };
486        next = following;
487    }
488    if let Some(text) = next
489        && !is_display_text(&inside, &text)?
490    {
491        return Ok(false);
492    }
493    Ok(inside.is_empty())
494}
495
496/// The reference half of a user notice: the organization's display text,
497/// then a sequence of notice numbers (which may be empty).
498fn notice_reference_understood(
499    parent: &structured_asn1::ScopedReader<'_, '_>,
500    reference: &structured_asn1::Element<'_>,
501) -> Result<bool> {
502    let Some(mut inside) = gate(parent.enter(reference).field(QUALIFIER_FIELD))? else {
503        return Ok(false);
504    };
505    let Some(Some(organization)) = gate(inside.next_element().field(QUALIFIER_FIELD))? else {
506        return Ok(false);
507    };
508    if !is_display_text(&inside, &organization)? {
509        return Ok(false);
510    }
511    let Some(Some(numbers)) = gate(inside.next_element().field(QUALIFIER_FIELD))? else {
512        return Ok(false);
513    };
514    if numbers.tag() != Tag::constructed(universal::SEQUENCE) || !inside.is_empty() {
515        return Ok(false);
516    }
517    let Some(mut each) = gate(inside.enter(&numbers).field(QUALIFIER_FIELD))? else {
518        return Ok(false);
519    };
520    loop {
521        match gate(each.next_element().field(QUALIFIER_FIELD))? {
522            Some(Some(number)) => {
523                // The schema says SEQUENCE OF INTEGER (RFC 5280 4.2.1.4);
524                // the decoding helper is tag-agnostic for implicitly tagged
525                // fields, so the tag is required here.
526                if number.tag() != Tag::primitive(universal::INTEGER)
527                    || gate(each.integer(&number).field(QUALIFIER_FIELD))?.is_none()
528                {
529                    return Ok(false);
530                }
531            }
532            // noticeNumbers is SEQUENCE OF INTEGER with no minimum size
533            // (RFC 5280 4.2.1.4): reaching the end is understood even when
534            // nothing was inside, the way an empty UserNotice is.
535            Some(None) => return Ok(true),
536            None => return Ok(false),
537        }
538    }
539}
540
541/// Whether an element is one of the display text types of RFC 5280 4.2.1.4,
542/// decoding under its declared type to between one and two hundred
543/// characters, the SIZE (1..200) the schema puts on every choice. Bytes that
544/// do not decode wear the tag without being the type.
545fn is_display_text(
546    reader: &structured_asn1::ScopedReader<'_, '_>,
547    element: &structured_asn1::Element<'_>,
548) -> Result<bool> {
549    let tag = element.tag();
550    let contents = element.contents();
551    // The ceiling is checked on the encoded bytes before any text is
552    // materialized: no choice here spends more than four bytes per
553    // character, so contents past four times the character bound can never
554    // fit it and are refused without walking the value.
555    if contents.len() > 200 * 4 {
556        return Ok(false);
557    }
558    // The per-value ceiling does not bound the aggregate: a policy may carry
559    // as many notices as the document has room for, so each scan is charged
560    // to the shared work budget before it runs.
561    // UTF8 validation and scalar counting are separate passes. The other
562    // choices fit this same bound without constructing decoded text.
563    qualifier_charge(reader, element, (contents.len() as u64) * 2 + 1)?;
564    let characters = if tag == Tag::primitive(universal::IA5_STRING) {
565        // The 128-character IRV (X.680, 41.4).
566        contents.iter().all(u8::is_ascii).then_some(contents.len())
567    } else if tag == Tag::primitive(universal::UTF8_STRING) {
568        core::str::from_utf8(contents)
569            .ok()
570            .map(|text| text.chars().count())
571    } else if tag == Tag::primitive(26) {
572        // VisibleString, whose number this schema layer names itself since
573        // nothing below it needs it: the printing characters and space.
574        contents
575            .iter()
576            .all(|byte| matches!(byte, 0x20..=0x7e))
577            .then_some(contents.len())
578    } else if tag == Tag::primitive(30) {
579        // BMPString, two bytes a character, so the size constraint counts
580        // characters rather than bytes. Counted rather than decoded: the
581        // gate asks how many characters there are, and building the text to
582        // answer that would allocate a copy of the value.
583        crate::name::wide_char_count(contents, 2)
584    } else {
585        return Ok(false);
586    };
587    Ok(characters.is_some_and(|count| (1..=200).contains(&count)))
588}
589
590#[cfg(test)]
591mod tests;