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