Skip to main content

structured_cms/attribute_certificate/validation/
aa_path.rs

1//! RFC5755 §7.4 restrictions along one explicitly supplied AA certificate path.
2//!
3//! [`AaPath::read`] collects restrictions from an already authenticated
4//! leaf-first path. [`validate`] combines that same census with the common
5//! PKIX validator and a separately selected trusted AA CA. Its returned
6//! [`ValidatedPath`] can authenticate AC issuance without repeating path
7//! signatures. Trust selection, revocation and application authorization
8//! remain caller obligations.
9//! <https://www.rfc-editor.org/rfc/rfc5755#section-7.4>
10
11use super::{Check, Reason, charge, refused};
12use crate::attribute_certificate::controls::{self, AaControls};
13use crate::{Error, Result};
14use structured_asn1::memory::{OwnedVec, Work};
15use structured_asn1::{Oid, Span};
16use structured_pkix::Certificate;
17
18pub(super) mod validated;
19pub use validated::{
20    Issued, PathCheck, PathError, PathResult, ValidatedPath, validate, validate_with_key,
21};
22
23/// One original control and its position in the supplied leaf-first path.
24#[derive(Debug)]
25struct Control<'a> {
26    position: usize,
27    value: AaControls<'a>,
28}
29
30/// Complete restriction census from the AA issuer through the chosen AA CA.
31#[derive(Debug)]
32pub struct AaPath<'p, 'a> {
33    certificates: &'p [&'p Certificate<'a>],
34    controls: OwnedVec<Control<'a>>,
35}
36
37/// Completed per-attribute intersection, not an application permission verdict.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Permission {
40    /// Every applicable control permits this attribute type.
41    Permitted,
42    /// The attribute must be ignored for authorization under this AA path.
43    Excluded {
44        /// Original certificate position carrying the first excluding control.
45        certificate: usize,
46        /// Original `AAControls` extension value, not the AC attribute's location.
47        source: Span,
48    },
49}
50
51impl<'p, 'a> AaPath<'p, 'a> {
52    /// Collect every control before returning a usable attribute query.
53    ///
54    /// Every PKC below the selected final AA CA must contain `AAControls`; the AA
55    /// CA's own control is optional but applies if present. An array containing
56    /// just the directly trusted AA issuer therefore need not have a control.
57    /// The distance excludes the target AA issuer and self-issued intermediate
58    /// PKCs, as RFC5280 §4.2.1.9 prescribes for pathLenConstraint.
59    ///
60    /// Malformed/control-resource failures are terminal `Result` errors. A missing
61    /// control or excessive distance produces a completed located rejection;
62    /// no successful prefix census is returned. Certificate path authenticity
63    /// and final-AA selection are preconditions supplied by the caller.
64    pub fn read<W: Work>(certificates: &'p [&'p Certificate<'a>], work: &W) -> Result<Check<Self>> {
65        let Some(last) = certificates.len().checked_sub(1) else {
66            // No source exists for an empty caller-supplied path.
67            return Ok(Check::Rejected(super::Rejection {
68                reason: Reason::EmptyPath,
69                source: None,
70            }));
71        };
72        charge(work, 1, certificates[0].span().start)?;
73        let mut controls = work.vector().into_owned();
74        let mut distance = 0;
75        for (position, certificate) in certificates.iter().enumerate() {
76            let execution = certificate.execution_in(work);
77            charge(&execution, 1, certificate.span().start)?;
78            let extension = certificate
79                .tbs()
80                .extensions()
81                .get(controls::IDENTIFIER, &execution)
82                .map_err(Error::pkix)?;
83            if let Some(extension) = extension {
84                // Reserve output before decoding the nested variable-sized
85                // catalogue; a capacity failure cannot leave an unretained read.
86                // Both owners originate in this same sealed Work account;
87                // rebinding can refuse only an account mismatch, not resources.
88                let Ok(mut scoped) = controls.with_execution(&execution) else {
89                    unreachable!("control storage retains this operation's account");
90                };
91                scoped
92                    .try_reserve(1)
93                    .map_err(|e| Error::resource(e, super::FIELD, extension.span().start))?;
94                let value = AaControls::read_extension(extension, &execution)?;
95                if !value.permits_distance(distance, &execution)? {
96                    return Ok(refused(Reason::ControlDistance, value.element.span()));
97                }
98                scoped
99                    .try_push(Control { position, value })
100                    .map_err(|e| Error::resource(e, super::FIELD, extension.span().start))?;
101                controls = scoped.into_owned();
102            } else if position != last {
103                return Ok(refused(Reason::MissingControls, certificate.span()));
104            }
105            if position != 0
106                && position != last
107                && !certificate
108                    .tbs()
109                    .subject()
110                    .matches(certificate.tbs().issuer(), &execution)
111                    .map_err(Error::pkix)?
112            {
113                // At most one increment per supplied slice entry, bounded by
114                // its representable length, without a new caller policy limit.
115                distance += 1;
116            }
117        }
118        Ok(Check::Accepted(Self {
119            certificates,
120            controls: controls.into_owned(),
121        }))
122    }
123
124    /// Exact supplied path, including its explicitly chosen final AA CA.
125    #[must_use]
126    pub fn certificates(&self) -> &[&Certificate<'a>] {
127        self.certificates
128    }
129
130    /// Intersect all controls for one attribute, retaining the denying source.
131    ///
132    /// Call this for every actual attribute after decryption, including encAttrs
133    /// itself (§7.4). An exclusion means ignore this attribute for authorization,
134    /// not that every other attribute or the complete AC must be discarded.
135    /// Original stored controls keep their capacity charges; current Work pays
136    /// the traversal and each deferred comparison. No result is cached across
137    /// requests and no failed query becomes a successful permission.
138    pub fn allows<W: Work>(&self, attribute: Oid<'_>, work: &W) -> Result<Permission> {
139        charge(work, 1, self.certificates[0].span().start)?;
140        for control in &self.controls {
141            charge(work, 1, control.value.element.span().start)?;
142            if !control.value.allows(attribute, work)? {
143                return Ok(Permission::Excluded {
144                    certificate: control.position,
145                    source: control.value.element.span(),
146                });
147            }
148        }
149        Ok(Permission::Permitted)
150    }
151}