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