Skip to main content

structured_cms/attribute_certificate/
proxy.rs

1//! Accounted proxy-set checks (RFC5755 §7.2), separate from AC authentication.
2//!
3//! Sender/recipient/hop identities and the trustworthy complete forwarding path
4//! come from the application protocol. This checks no signature or AA trust.
5
6use super::target::{CheckError, Membership, TargetInformation};
7use crate::{Error, Result};
8use structured_asn1::memory::{Execution, Work};
9use structured_asn1::{Element, Oid, Parser};
10use structured_pkix::Extension;
11use structured_pkix::general_name::GeneralName;
12
13const FIELD: &str = "attributeCertificate.proxyInfo";
14/// `id-pe-ac-proxying`, 1.3.6.1.5.5.7.1.10.
15pub const IDENTIFIER: Oid<'static> = Oid::from_bytes(&[43, 6, 1, 5, 5, 7, 1, 10]);
16
17/// Already-authenticated sender facts supplied by the caller, not this reader.
18#[derive(Debug, Clone, Copy)]
19pub enum Sender<'a, 'n> {
20    /// Caller has established the sender matches the AC Holder.
21    Holder,
22    /// Sender is a proxy with these independently established identities.
23    Proxy(&'n [GeneralName<'a>]),
24}
25
26/// Identity whose group membership the callback must establish.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Subject {
29    /// Authenticated immediate sender.
30    Sender,
31    /// Current verifier.
32    Recipient,
33    /// Earlier proxy on the trustworthy forwarding path, in supplied order.
34    Intermediate(usize),
35}
36
37/// Complete proxy constraints, sharing the targeting decoder and original groups.
38#[derive(Debug)]
39pub struct ProxyInfo<'a> {
40    parser: &'a Parser,
41    targets: TargetInformation<'a>,
42}
43
44/// Completed proxy-set fact, never an application authorization verdict.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Decision {
47    /// Every required participant matched the same original proxy set.
48    Matches {
49        /// Original group ordinal, not an index into a merged union.
50        group: usize,
51    },
52    /// Each group had a completed negative participant check.
53    Differs,
54    /// A group could qualify but at least one participant remained unknown.
55    Unproven,
56    /// `ProxyInfo` must be critical (RFC5755 §7.2).
57    NonCritical,
58    /// targetCert is forbidden anywhere, even after an otherwise matching set.
59    ProhibitedTargetCertificate {
60        /// Original flat target ordinal.
61        target: usize,
62    },
63}
64
65impl<'a> ProxyInfo<'a> {
66    /// Reads the registered extension using the existing complete Target kernel.
67    /// Late malformed or prohibited alternatives cannot disappear on early match.
68    pub fn read<W: Work>(extension: &Extension<'a>, work: &W) -> Result<Self> {
69        Ok(Self {
70            parser: extension.parser(),
71            targets: TargetInformation::read_as(extension, work, IDENTIFIER, FIELD)?,
72        })
73    }
74
75    /// Original whole extension-value SEQUENCE.
76    #[must_use]
77    pub fn element(&self) -> Element<'a> {
78        self.targets.element()
79    }
80
81    /// Original sets with flat target ranges. Proxy matching preserves these
82    /// boundaries, unlike recipient-only targeting's union.
83    #[must_use]
84    pub fn groups(&self) -> &[super::target::Group<'a>] {
85        self.targets.groups()
86    }
87
88    /// Original flat target catalogue; reading it is not a proxy check.
89    #[must_use]
90    pub fn targets(&self) -> &[super::target::Target<'a>] {
91        self.targets.targets()
92    }
93
94    /// Original prohibited alternatives, retained for inspection.
95    #[must_use]
96    pub fn certificates(&self) -> &[super::target::TargetCertificate<'a>] {
97        self.targets.certificates()
98    }
99
100    /// Written extension criticality, not inferred from its OID.
101    #[must_use]
102    pub fn is_critical(&self) -> bool {
103        self.targets.is_critical()
104    }
105
106    /// Checks RFC5755 §7.2's common-set rule for all supplied participants.
107    /// The protocol must supply a trustworthy COMPLETE preceding-hop list;
108    /// omitting a hop here cannot prove that it was allowed. Holder bypasses
109    /// only the sender membership check, never recipient or earlier proxies.
110    /// Unknown membership differs from a completed negative; processing and
111    /// callback errors remain terminal, with their original causes.
112    /// The membership callback must admit its own data-dependent work through
113    /// the supplied execution. Its external side effects cannot be rolled back
114    /// if a later participant or set refuses.
115    pub fn check<W: Work, E>(
116        &self,
117        sender: Sender<'_, '_>,
118        recipient: &[GeneralName<'_>],
119        intermediates: &[&[GeneralName<'_>]],
120        work: &W,
121        mut membership: impl FnMut(
122            Subject,
123            &GeneralName<'a>,
124            &Execution<'_>,
125        ) -> core::result::Result<Membership, E>,
126    ) -> core::result::Result<Decision, CheckError<E>> {
127        let execution = work.execution_for(self.parser);
128        let pay = |at| {
129            execution
130                .charge_work(1)
131                .map_err(|e| CheckError::Processing(Error::resource(e, FIELD, at)))
132        };
133        pay(self.targets.element().span().start)?;
134        if !self.targets.is_critical() {
135            return Ok(Decision::NonCritical);
136        }
137        if let Some(entry) = self.targets.certificates().first() {
138            return Ok(Decision::ProhibitedTargetCertificate {
139                target: entry.target,
140            });
141        }
142        let mut unknown = false;
143        // RFC5755 §7.2 requires all proxies on the path to be members of
144        // the SAME set. Never use TargetInformation::check's union here.
145        // https://www.rfc-editor.org/rfc/rfc5755#section-7.2
146        for (index, group) in self.targets.groups().iter().enumerate() {
147            pay(group.element.span().start)?;
148            let mut undecided = false;
149            match self.matches(
150                index,
151                Subject::Recipient,
152                recipient,
153                &execution,
154                &mut membership,
155            )? {
156                Membership::NotMember => continue,
157                Membership::Unknown => undecided = true,
158                Membership::Member => {}
159            }
160            if let Sender::Proxy(names) = sender {
161                match self.matches(index, Subject::Sender, names, &execution, &mut membership)? {
162                    Membership::NotMember => continue,
163                    Membership::Unknown => undecided = true,
164                    Membership::Member => {}
165                }
166            }
167            let mut excluded = false;
168            for (ordinal, names) in intermediates.iter().enumerate() {
169                pay(group.element.span().start)?;
170                match self.matches(
171                    index,
172                    Subject::Intermediate(ordinal),
173                    names,
174                    &execution,
175                    &mut membership,
176                )? {
177                    Membership::NotMember => {
178                        excluded = true;
179                        break;
180                    }
181                    Membership::Unknown => undecided = true,
182                    Membership::Member => {}
183                }
184            }
185            if excluded {
186                continue;
187            }
188            if !undecided {
189                return Ok(Decision::Matches { group: index });
190            }
191            unknown = true;
192        }
193        Ok(if unknown {
194            Decision::Unproven
195        } else {
196            Decision::Differs
197        })
198    }
199
200    fn matches<E>(
201        &self,
202        group: usize,
203        subject: Subject,
204        names: &[GeneralName<'_>],
205        execution: &Execution<'_>,
206        membership: &mut impl FnMut(
207            Subject,
208            &GeneralName<'a>,
209            &Execution<'_>,
210        ) -> core::result::Result<Membership, E>,
211    ) -> core::result::Result<Membership, CheckError<E>> {
212        let mut unknown = false;
213        for target in &self.targets.targets()[self.targets.groups()[group].targets.clone()] {
214            match super::target::matches_target(
215                target,
216                names,
217                execution,
218                FIELD,
219                &mut |name, work| membership(subject, name, work),
220            )? {
221                Membership::Member => return Ok(Membership::Member),
222                Membership::Unknown => unknown = true,
223                Membership::NotMember => {}
224            }
225        }
226        Ok(if unknown {
227            Membership::Unknown
228        } else {
229            Membership::NotMember
230        })
231    }
232}