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}