Skip to main content

structured_cms/attribute_certificate/validation/aa_path/
validated.rs

1//! Authenticated AA-CA paths and their constrained AC issuance facts.
2use super::{AaPath, Permission};
3use crate::attribute_certificate::AttributeCertificate;
4use crate::attribute_certificate::controls;
5use crate::attribute_certificate::validation::authentication::{self, AlgorithmAgreement};
6use crate::attribute_certificate::validation::{Check, Rejection, charge};
7use crate::{Error, Result};
8use structured_asn1::memory::{Execution, Work};
9use structured_asn1::{Oid, Span};
10use structured_pkix::Certificate;
11use structured_pkix::extension::Recognizer;
12use structured_pkix::path::{self, Anchor, EffectiveAnchor, Interval, Verify, Watch, WorkingKey};
13
14/// Completed AA path result. No rejected or incomplete path carries a usable key.
15#[derive(Debug)]
16pub enum PathCheck<T, P> {
17    /// Static PKIX validation and the complete `AAControls` census/distance
18    /// checks completed. Attribute use still requires the retained intersection.
19    Accepted(T),
20    /// The caller supplied no AA issuer followed by a distinct AA CA position.
21    /// Direct-AA trust uses the separate AC authentication API.
22    MissingAaCa,
23    /// The final path position is not the explicitly selected trusted AA CA
24    /// object. Equal encodings from an untrusted pool are not substituted.
25    TrustBoundary,
26    /// The supplied effective anchor key does not belong to the selected AA CA.
27    AnchorKey,
28    /// A control was absent or its distance was exceeded.
29    Controls(Rejection),
30    /// Completed common PKIX path refusal, preserving its source position.
31    Path(P),
32    /// A caller-selected PKIX processing bound prevented a complete path check.
33    Incomplete(path::Truncation),
34}
35
36/// Execution failures keep their original processing or callback cause.
37#[derive(Debug)]
38pub enum PathError<V, R> {
39    /// Original AC/PKIX decoding or resource failure.
40    Processing(Error),
41    /// Original path-verifier error; not an invalid-signature answer.
42    Verification(V),
43    /// Original caller recognizer error; not an unknown-extension answer.
44    Recognition(R),
45}
46impl<V, R> From<Error> for PathError<V, R> {
47    fn from(error: Error) -> Self {
48        Self::Processing(error)
49    }
50}
51
52/// Complete AA-path operation result with independent verifier/recognizer errors.
53pub type PathResult<'p, 'a, V, R> =
54    core::result::Result<PathCheck<ValidatedPath<'p, 'a>, path::Rejection<'a>>, PathError<V, R>>;
55
56/// A caller-selected AA CA, its authenticated path, effective AA key and controls.
57///
58/// The last supplied certificate is explicitly trusted as the AA CA and is not
59/// part of the RFC5280 prospective path. This capability establishes neither
60/// revocation status nor application authorization, holder proof, or targeting.
61/// Policy storage and control catalogues retain their accounts after the query.
62#[derive(Debug)]
63pub struct ValidatedPath<'p, 'a> {
64    controls: AaPath<'p, 'a>,
65    facts: path::ValidationFacts<'a>,
66    anchor: EffectiveAnchor<'a>,
67    interval: Interval,
68}
69
70impl<'p, 'a> ValidatedPath<'p, 'a> {
71    /// Original leaf-first certificates, including the explicitly trusted AA CA.
72    #[must_use]
73    pub fn certificates(&self) -> &[&Certificate<'a>] {
74        self.controls.certificates()
75    }
76    /// Selected AA CA and its bound effective key, without root re-verification.
77    #[must_use]
78    pub fn anchor(&self) -> EffectiveAnchor<'a> {
79        self.anchor
80    }
81    /// Completed static PKIX facts, including the actual AA effective key.
82    #[must_use]
83    pub fn facts(&self) -> &path::ValidationFacts<'a> {
84        &self.facts
85    }
86    /// The same complete interval used by path and later AC authentication.
87    #[must_use]
88    pub fn interval(&self) -> Interval {
89        self.interval
90    }
91    /// Intersect every control, including for decrypted types and encAttrs.
92    /// The caller owns attribute decryption and application permission decisions.
93    pub fn allows<W: Work>(&self, kind: Oid<'_>, work: &W) -> Result<Permission> {
94        self.controls.allows(kind, work)
95    }
96    /// Authenticate an AC with this path's AA key and interval, without repeating
97    /// any path signature. The supplied verifier enforces its selected algorithm
98    /// policy and accounts its backend; no engine is selected by this module.
99    /// Holder proof, status, target/proxy effects and authorization remain separate.
100    pub fn authenticate<'r, 'c, V, W: Work>(
101        &'r self,
102        certificate: &'r AttributeCertificate<'c>,
103        verifier: &V,
104        work: &W,
105    ) -> core::result::Result<
106        authentication::Outcome<Issued<'r, 'p, 'c, 'a>>,
107        authentication::AuthenticationError<<V as AlgorithmAgreement>::Error>,
108    >
109    where
110        V: AlgorithmAgreement,
111        for<'op> V: Verify<Execution<'op>, Error = <V as AlgorithmAgreement>::Error>,
112    {
113        Ok(
114            match certificate.authenticate(
115                self.controls.certificates[0],
116                self.facts.key,
117                self.interval,
118                verifier,
119                work,
120            )? {
121                authentication::Outcome::Authenticated(facts) => {
122                    authentication::Outcome::Authenticated(Issued { path: self, facts })
123                }
124                authentication::Outcome::Rejected(error) => {
125                    authentication::Outcome::Rejected(error)
126                }
127                authentication::Outcome::Unsupported(stage) => {
128                    authentication::Outcome::Unsupported(stage)
129                }
130            },
131        )
132    }
133}
134
135/// Original AC issuance authenticated under one checked AA-CA path.
136/// This is not an authorization grant or proof that other AC obligations passed.
137#[derive(Debug)]
138pub struct Issued<'r, 'p, 'c, 'a> {
139    path: &'r ValidatedPath<'p, 'a>,
140    facts: authentication::Authenticated<'r, 'c, 'a>,
141}
142impl<'r, 'p, 'c, 'a> Issued<'r, 'p, 'c, 'a> {
143    /// Actual checked path and selected trust boundary.
144    #[must_use]
145    pub fn path(&self) -> &'r ValidatedPath<'p, 'a> {
146        self.path
147    }
148    /// Exact AC, AA, effective key, interval and profile obligations.
149    #[must_use]
150    pub fn facts(&self) -> &authentication::Authenticated<'r, 'c, 'a> {
151        &self.facts
152    }
153    /// Attribute eligibility under this authenticated issuance's complete path.
154    /// Excluded types must be ignored for authorization (RFC5755 §7.4), rather
155    /// than causing other permitted attributes to disappear. This also applies
156    /// to caller-decrypted types and to id-aca-encAttrs itself.
157    pub fn allows<W: Work>(&self, kind: Oid<'_>, work: &W) -> Result<Permission> {
158        self.path.allows(kind, work)
159    }
160}
161
162/// Validate one full AA path ending in a caller-selected, directly trusted AA CA.
163///
164/// Input includes the AA issuer first and the selected AA CA last. The final
165/// position must be the exact `trusted_aa_ca` object supplied separately from
166/// the path; trust is never inferred from the final element of an input pool.
167/// Equal encodings can be represented by using that trusted object in the
168/// final position, without copying or re-parsing its contents. All lower
169/// certificates must have `AAControls`; the AA CA's optional control also applies.
170/// One position alone is not an AA-CA path: use explicit direct-AA authentication
171/// instead. No self-signed certificate discovered in a pool becomes trusted.
172///
173/// The existing PKIX context controls policy, interval and path processing bounds.
174/// Its Work bounds the entire operation, including the control census. Only the
175/// PKIX phase consumes ValidationBounds.work; no new policy settings are added.
176/// All control syntax/distance checks run before provider callbacks. Critical
177/// `AAControls` is understood only because this operation enforces its complete
178/// census; the caller's remaining recognizer capabilities are forwarded unchanged.
179/// No successful prefix or key is returned on processing/callback refusal.
180/// RFC5755 §7.4: <https://www.rfc-editor.org/rfc/rfc5755#section-7.4>
181pub fn validate<'p, 'a, V, T, R, W>(
182    certificates: &'p [&'a Certificate<'a>],
183    trusted_aa_ca: &'a Certificate<'a>,
184    context: &path::ValidationContext<'a, '_, V, T, R, W>,
185) -> PathResult<'p, 'a, <V as Verify<W>>::Error, R::Error>
186where
187    W: Work,
188    V: Verify<W>,
189    for<'op> V: Verify<Execution<'op>, Error = <V as Verify<W>>::Error>,
190    T: Watch,
191    R: Recognizer<W>,
192{
193    validate_inner(certificates, trusted_aa_ca, None, context)
194}
195
196/// The same complete operation with an effective key supplied for the selected
197/// AA CA. Original inherited parameters are retained; a key from another SPKI
198/// refuses before control decoding or any verification callback.
199pub fn validate_with_key<'p, 'a, V, T, R, W>(
200    certificates: &'p [&'a Certificate<'a>],
201    trusted_aa_ca: &'a Certificate<'a>,
202    anchor_key: WorkingKey<'a>,
203    context: &path::ValidationContext<'a, '_, V, T, R, W>,
204) -> PathResult<'p, 'a, <V as Verify<W>>::Error, R::Error>
205where
206    W: Work,
207    V: Verify<W>,
208    for<'op> V: Verify<Execution<'op>, Error = <V as Verify<W>>::Error>,
209    T: Watch,
210    R: Recognizer<W>,
211{
212    validate_inner(certificates, trusted_aa_ca, Some(anchor_key), context)
213}
214
215enum RecognitionError<E> {
216    Processing(Error),
217    Callback(E),
218}
219
220pub(in crate::attribute_certificate::validation) enum VerificationError<E> {
221    Processing(Error),
222    Callback(E),
223}
224pub(in crate::attribute_certificate::validation) struct ScopedVerifier<'r, V>(
225    pub(in crate::attribute_certificate::validation) &'r V,
226);
227impl<W: Work, V> Verify<W> for ScopedVerifier<'_, V>
228where
229    V: Verify<W>,
230    for<'op> V: Verify<Execution<'op>, Error = <V as Verify<W>>::Error>,
231{
232    type Error = VerificationError<<V as Verify<W>>::Error>;
233    fn verify(
234        &self,
235        algorithm: &structured_pkix::AlgorithmIdentifier<'_>,
236        key: WorkingKey<'_>,
237        input: &[u8],
238        signature: structured_pkix::signature::SignatureValue<'_>,
239        work: &W,
240    ) -> core::result::Result<path::Verified, Self::Error> {
241        let execution = signature.execution_in(work);
242        charge(&execution, 1, signature.span().start).map_err(VerificationError::Processing)?;
243        self.0
244            .verify(algorithm, key, input, signature, &execution)
245            .map_err(VerificationError::Callback)
246    }
247}
248struct Recognized<'r, R> {
249    inner: &'r R,
250    source: Span,
251}
252impl<W: Work, R: Recognizer<W>> Recognizer<W> for Recognized<'_, R> {
253    type Error = RecognitionError<R::Error>;
254    fn recognizes(&self, kind: Oid<'_>, work: &W) -> core::result::Result<bool, Self::Error> {
255        charge(work, 10, self.source.start).map_err(RecognitionError::Processing)?;
256        if kind == controls::IDENTIFIER {
257            Ok(true)
258        } else {
259            self.inner
260                .recognizes(kind, work)
261                .map_err(RecognitionError::Callback)
262        }
263    }
264    fn recognizes_other_name(
265        &self,
266        kind: Oid<'_>,
267        work: &W,
268    ) -> core::result::Result<bool, Self::Error> {
269        self.inner
270            .recognizes_other_name(kind, work)
271            .map_err(RecognitionError::Callback)
272    }
273    fn recognizes_name_constraint(
274        &self,
275        kind: Oid<'_>,
276        work: &W,
277    ) -> core::result::Result<bool, Self::Error> {
278        self.inner
279            .recognizes_name_constraint(kind, work)
280            .map_err(RecognitionError::Callback)
281    }
282    fn recognizes_access_method(
283        &self,
284        kind: Oid<'_>,
285        work: &W,
286    ) -> core::result::Result<bool, Self::Error> {
287        self.inner
288            .recognizes_access_method(kind, work)
289            .map_err(RecognitionError::Callback)
290    }
291    fn recognizes_statement(
292        &self,
293        kind: Oid<'_>,
294        work: &W,
295    ) -> core::result::Result<bool, Self::Error> {
296        self.inner
297            .recognizes_statement(kind, work)
298            .map_err(RecognitionError::Callback)
299    }
300}
301
302fn validate_inner<'p, 'a, V, T, R, W>(
303    certificates: &'p [&'a Certificate<'a>],
304    trusted_aa_ca: &'a Certificate<'a>,
305    key: Option<WorkingKey<'a>>,
306    context: &path::ValidationContext<'a, '_, V, T, R, W>,
307) -> PathResult<'p, 'a, <V as Verify<W>>::Error, R::Error>
308where
309    W: Work,
310    V: Verify<W>,
311    for<'op> V: Verify<Execution<'op>, Error = <V as Verify<W>>::Error>,
312    T: Watch,
313    R: Recognizer<W>,
314{
315    let Some((aa_ca, chain)) = certificates.split_last() else {
316        return Ok(PathCheck::MissingAaCa);
317    };
318    if chain.is_empty() {
319        return Ok(PathCheck::MissingAaCa);
320    }
321    if !core::ptr::eq(*aa_ca, trusted_aa_ca) {
322        return Ok(PathCheck::TrustBoundary);
323    }
324    if chain.len() > context.bounds.depth {
325        return Ok(PathCheck::Incomplete(path::Truncation::Depth));
326    }
327    let anchor = Anchor::from_certificate(aa_ca);
328    let key = key.unwrap_or_else(|| WorkingKey::standing(anchor.key()));
329    let Some(anchor) = EffectiveAnchor::bind(anchor, key, context.execution)
330        .map_err(|e| PathError::Processing(Error::pkix(e)))?
331    else {
332        return Ok(PathCheck::AnchorKey);
333    };
334    let controls = match AaPath::read(certificates, context.execution)? {
335        Check::Accepted(value) => value,
336        Check::Rejected(error) => return Ok(PathCheck::Controls(error)),
337    };
338    let recognizer = Recognized {
339        inner: context.recognizer,
340        source: chain[0].span(),
341    };
342    // Every path callback must carry the original signed certificate's
343    // ceiling, even when the PKIX processor uses an independent current Work.
344    let verifier = ScopedVerifier(context.verifier);
345    let scoped = path::ValidationContext {
346        execution: context.execution,
347        interval: context.interval,
348        policy: context.policy,
349        bounds: context.bounds,
350        verifier: &verifier,
351        watch: context.watch,
352        recognizer: &recognizer,
353    };
354    let facts = path::validate_effective(chain, anchor, &scoped).map_err(|error| match error {
355        path::BuildError::Processing(e) => PathError::Processing(Error::pkix(e)),
356        path::BuildError::Verification(VerificationError::Callback(e)) => {
357            PathError::Verification(e)
358        }
359        path::BuildError::Verification(VerificationError::Processing(e)) => {
360            PathError::Processing(e)
361        }
362        path::BuildError::Recognition(RecognitionError::Callback(e)) => PathError::Recognition(e),
363        path::BuildError::Recognition(RecognitionError::Processing(e)) => PathError::Processing(e),
364    })?;
365    Ok(match facts {
366        path::Validation::Accepted(facts) => PathCheck::Accepted(ValidatedPath {
367            controls,
368            facts,
369            anchor,
370            interval: context.interval,
371        }),
372        path::Validation::Rejected(error) => PathCheck::Path(error),
373        path::Validation::Incomplete(reason) => PathCheck::Incomplete(reason),
374    })
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use alloc::sync::Arc;
381    use core::cell::Cell;
382    use structured_asn1::memory::{Memory, MemoryAccount};
383
384    struct Spy {
385        calls: [Cell<u32>; 5],
386        fail: bool,
387    }
388    impl Spy {
389        fn observe(&self, role: usize) -> core::result::Result<bool, usize> {
390            self.calls[role].set(self.calls[role].get() + 1);
391            if self.fail { Err(role) } else { Ok(true) }
392        }
393    }
394    impl Recognizer for Spy {
395        type Error = usize;
396        fn recognizes(&self, _: Oid<'_>, _: &Memory) -> core::result::Result<bool, usize> {
397            self.observe(0)
398        }
399        fn recognizes_other_name(
400            &self,
401            _: Oid<'_>,
402            _: &Memory,
403        ) -> core::result::Result<bool, usize> {
404            self.observe(1)
405        }
406        fn recognizes_name_constraint(
407            &self,
408            _: Oid<'_>,
409            _: &Memory,
410        ) -> core::result::Result<bool, usize> {
411            self.observe(2)
412        }
413        fn recognizes_access_method(
414            &self,
415            _: Oid<'_>,
416            _: &Memory,
417        ) -> core::result::Result<bool, usize> {
418            self.observe(3)
419        }
420        fn recognizes_statement(
421            &self,
422            _: Oid<'_>,
423            _: &Memory,
424        ) -> core::result::Result<bool, usize> {
425            self.observe(4)
426        }
427    }
428    #[test]
429    fn control_recognition_preserves_all_other_scheme_roles_and_causes() {
430        // A composition adapter must not silently replace any of the caller's five capabilities.
431        let work = Memory::new(Arc::new(MemoryAccount::new(0, 100)));
432        let kind = Oid::from_bytes(&[42]);
433        for fail in [false, true] {
434            let spy = Spy {
435                calls: core::array::from_fn(|_| Cell::new(0)),
436                fail,
437            };
438            let adapter = Recognized {
439                inner: &spy,
440                source: Span {
441                    start: 10,
442                    length: 10,
443                },
444            };
445            assert!(matches!(
446                adapter.recognizes(controls::IDENTIFIER, &work),
447                Ok(true)
448            ));
449            let outcomes = [
450                adapter.recognizes(kind, &work),
451                adapter.recognizes_other_name(kind, &work),
452                adapter.recognizes_name_constraint(kind, &work),
453                adapter.recognizes_access_method(kind, &work),
454                adapter.recognizes_statement(kind, &work),
455            ];
456            for (role, result) in outcomes.into_iter().enumerate() {
457                if fail {
458                    assert!(
459                        matches!(result, Err(RecognitionError::Callback(cause)) if cause == role)
460                    );
461                } else {
462                    assert!(matches!(result, Ok(true)));
463                }
464                assert_eq!(spy.calls[role].get(), 1);
465            }
466        }
467    }
468    #[test]
469    fn control_recognition_refuses_current_work_before_comparison_or_delegate() {
470        // A known identifier is not an unaccounted success shortcut.
471        let work = Memory::new(Arc::new(MemoryAccount::new(0, 0)));
472        let spy = Spy {
473            calls: core::array::from_fn(|_| Cell::new(0)),
474            fail: false,
475        };
476        let adapter = Recognized {
477            inner: &spy,
478            source: Span {
479                start: 10,
480                length: 10,
481            },
482        };
483        assert!(
484            matches!(adapter.recognizes(controls::IDENTIFIER, &work), Err(RecognitionError::Processing(error)) if error.is_resource() && error.offset() == 10)
485        );
486        assert_eq!(spy.calls[0].get(), 0);
487    }
488}