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}