Skip to main content

structured_cms/attribute_certificate/validation/
usage.rs

1//! Relying-party AC processing over authenticated issuance and original evidence.
2//!
3//! The protocol supplies authenticated participants and selected trust. This
4//! module binds those inputs, enforces all known critical effects and records
5//! usable attribute types. It does not authenticate a protocol, discover trust,
6//! decrypt attributes, select freshness policy or grant application permission.
7//! RFC5755 §§5–7: <https://www.rfc-editor.org/rfc/rfc5755#section-5>
8
9use super::aa_path::validated::{ScopedVerifier, VerificationError};
10use super::aa_path::{Issued, Permission};
11use super::authentication::Authenticated;
12use super::digest::BoundObject;
13use super::{Check, HolderMethod, Rejection, charge};
14use crate::attribute_certificate::controls::{self, AaControls};
15use crate::attribute_certificate::{proxy, target};
16use crate::{Error, Result};
17use structured_asn1::Oid;
18use structured_asn1::memory::{Execution, OwnedVec, Work};
19use structured_pkix::Certificate;
20use structured_pkix::extension::Recognizer;
21use structured_pkix::general_name::GeneralName;
22use structured_pkix::path::{self, EffectiveAnchor, Interval, Verify, Watch};
23use structured_pkix::temporal::{LeapTable, Reading, Uncertainty};
24
25mod status;
26pub use status::{Evidence, NoStatus, Status};
27
28/// One supplied holder PKC path, checked against its explicitly chosen anchor.
29/// Protocol proof of possession and PKC revocation remain caller obligations.
30#[derive(Debug)]
31pub struct HolderPath<'p, 'a> {
32    certificates: &'p [&'a Certificate<'a>],
33    facts: path::ValidationFacts<'a>,
34    anchor: EffectiveAnchor<'a>,
35    interval: Interval,
36}
37
38/// The complete holder-path operation, without a usable partial key.
39#[derive(Debug)]
40pub enum HolderValidation<T, P> {
41    /// Complete common PKIX validation.
42    Accepted(T),
43    /// Original common path rejection, including an empty supplied path.
44    Rejected(P),
45    /// Caller-selected processing bound prevented complete validation.
46    Incomplete(path::Truncation),
47}
48
49/// Complete holder path result, retaining verifier and recognizer causes.
50pub type HolderResult<'p, 'a, V, R> = core::result::Result<
51    HolderValidation<HolderPath<'p, 'a>, path::Rejection<'a>>,
52    HolderError<V, R>,
53>;
54
55/// Located processing and independent holder-path callback causes.
56#[derive(Debug)]
57pub enum HolderError<V, R> {
58    /// Original shared processing/resource error.
59    Processing(Error),
60    /// Original selected verifier cause.
61    Verification(V),
62    /// Original extension recognizer cause.
63    Recognition(R),
64}
65
66impl<'p, 'a> HolderPath<'p, 'a> {
67    /// Original leaf-first path; its first element is the holder PKC.
68    #[must_use]
69    pub fn certificates(&self) -> &[&Certificate<'a>] {
70        self.certificates
71    }
72    /// Actual effective key and independently owned policy facts.
73    #[must_use]
74    pub fn facts(&self) -> &path::ValidationFacts<'a> {
75        &self.facts
76    }
77    /// Explicitly selected trust boundary, not a discovered self-signed root.
78    #[must_use]
79    pub fn anchor(&self) -> EffectiveAnchor<'a> {
80        self.anchor
81    }
82    /// Complete interval checked by this original operation.
83    #[must_use]
84    pub fn interval(&self) -> Interval {
85        self.interval
86    }
87    /// Validate a supplied holder path through the same PKIX core as AA paths.
88    /// The anchor is outside the supplied prospective path. Every provider call
89    /// receives the original signature ceiling as well as the current Work.
90    pub fn validate<V, T, R, W>(
91        certificates: &'p [&'a Certificate<'a>],
92        anchor: EffectiveAnchor<'a>,
93        context: &path::ValidationContext<'a, '_, V, T, R, W>,
94    ) -> HolderResult<'p, 'a, <V as Verify<W>>::Error, R::Error>
95    where
96        W: Work,
97        V: Verify<W>,
98        for<'op> V: Verify<Execution<'op>, Error = <V as Verify<W>>::Error>,
99        T: Watch,
100        R: Recognizer<W>,
101    {
102        let verifier = ScopedVerifier(context.verifier);
103        let scoped = path::ValidationContext {
104            execution: context.execution,
105            interval: context.interval,
106            policy: context.policy,
107            bounds: context.bounds,
108            verifier: &verifier,
109            watch: context.watch,
110            recognizer: context.recognizer,
111        };
112        let checked =
113            path::validate_effective(certificates, anchor, &scoped).map_err(
114                |error| match error {
115                    path::BuildError::Processing(e) => HolderError::Processing(Error::pkix(e)),
116                    path::BuildError::Recognition(e) => HolderError::Recognition(e),
117                    path::BuildError::Verification(VerificationError::Callback(e)) => {
118                        HolderError::Verification(e)
119                    }
120                    path::BuildError::Verification(VerificationError::Processing(e)) => {
121                        HolderError::Processing(e)
122                    }
123                },
124            )?;
125        Ok(match checked {
126            path::Validation::Accepted(facts) => HolderValidation::Accepted(Self {
127                certificates,
128                facts,
129                anchor,
130                interval: context.interval,
131            }),
132            path::Validation::Rejected(error) => HolderValidation::Rejected(error),
133            path::Validation::Incomplete(reason) => HolderValidation::Incomplete(reason),
134        })
135    }
136}
137
138/// Explicit caller-authenticated holder context. Selecting an alternative never
139/// tries another written Holder identity as a fallback. Supplying this input
140/// asserts that the protocol established the holder's possession/identity and,
141/// for a certificate, its revocation status. The library checks the PKIX path.
142#[derive(Debug)]
143pub enum Holder<'r, 'p, 'a, 'c> {
144    /// An authenticated PKC with a completed original path.
145    Certificate {
146        /// Original checked path whose first PKC authenticated the holder.
147        path: &'r HolderPath<'p, 'a>,
148        /// Explicit baseCertificateID or entityName interpretation.
149        method: HolderMethod,
150    },
151    /// Already completed object-digest comparison for this AC. For a PKC object,
152    /// its complete path is required; bare keys use the protocol's independent
153    /// authentication scheme (RFC5755 §5(1), §7.3).
154    Object {
155        /// Original digest comparison, never just an asserted digest match.
156        binding: &'r BoundObject<'r, 'c, 'a>,
157        /// Required for certificate objects; optional for bare-key protocols.
158        path: Option<&'r HolderPath<'p, 'a>>,
159    },
160}
161
162/// Authenticated protocol identities for all proxy participants. The caller
163/// supplies the trustworthy complete preceding-hop list, never an untrusted
164/// list from the AC. A missing context cannot bypass a present `ProxyInfo`.
165#[derive(Debug, Clone, Copy)]
166pub struct Proxy<'n, 'a> {
167    /// Holder or independently authenticated immediate proxy.
168    pub sender: proxy::Sender<'a, 'n>,
169    /// Every earlier proxy, in protocol order.
170    pub intermediates: &'n [&'n [GeneralName<'a>]],
171}
172
173/// A completed negative or unproven use requirement. No variant is a resource
174/// or callback failure; those remain terminal errors.
175#[derive(Debug)]
176pub enum Failure {
177    /// Direct trust did not select the actual authenticated AA object.
178    AuthorityTrust,
179    /// Requested time is outside a checked issuance or holder-path interval.
180    Interval,
181    /// Supplied UTC facts cannot establish the requested precise label.
182    Time(Uncertainty),
183    /// Common Holder comparison refused, retaining its original source.
184    Holder(Rejection),
185    /// Object-digest facts refer to another original AC.
186    HolderObject,
187    /// A PKC digest object lacks its own path or names another path's PKC.
188    HolderPath,
189    /// Present targeting completed without a match or with unknown membership.
190    Target(target::Decision),
191    /// A present proxy restriction lacks complete authenticated participants.
192    ProxyContext,
193    /// Present proxy restriction did not establish a same-set match.
194    Proxy(proxy::Decision),
195    /// Original evidence does not establish status for this target/time/scheme.
196    Status(Status),
197}
198
199/// Complete use outcome. Only Checked retains a usable attribute catalogue.
200#[derive(Debug)]
201pub enum Outcome<T> {
202    /// All mechanism requirements completed relative to explicit trusted inputs.
203    Checked(T),
204    /// Completed negative or unproven requirement.
205    Rejected(Failure),
206}
207
208/// Trusted group-membership query role, with the original target callback error.
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum GroupRole {
211    /// Current recipient's group membership for targetInformation.
212    Target,
213    /// Sender, recipient or original preceding proxy for `ProxyInfo`.
214    Proxy(proxy::Subject),
215}
216
217/// All operation facts. No configuration discovery, clock, default status or
218/// fallback holder method is inferred. Additional OCSP age/nonce/recipient and
219/// responder-trust policy belongs to the caller before supplying evidence.
220pub struct Context<'r, 'p, 'a, 'c, 'op, S: Evidence, W: Work> {
221    /// Caller-authenticated holder, with any required checked certificate path.
222    pub holder: Holder<'r, 'p, 'a, 'c>,
223    /// Known identities of the current recipient, not AC-supplied claims.
224    pub recipient: &'r [GeneralName<'a>],
225    /// Complete authenticated proxy context when `ProxyInfo` is present.
226    pub proxy: Option<Proxy<'r, 'a>>,
227    /// Previously processed original status evidence or explicit `NoStatus`.
228    /// The caller establishes trust/path/status of each signing authority,
229    /// including the issuer/same-anchor facts required for indirect CRLs.
230    pub status: &'r S,
231    /// Exact evaluation label used by the status processor.
232    pub now: Reading,
233    /// Authenticated UTC coverage, including precise upward rounding facts.
234    pub utc: &'op LeapTable<'op>,
235    /// Current operation pays every new query, callback and retained result.
236    pub execution: &'op W,
237}
238
239/// One original attribute's AA-control result. Excluded attributes must be
240/// ignored for authorization, without suppressing unrelated permitted types.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub struct AttributeUse {
243    /// Original attribute ordinal, without sorting/deduplication or byte changes.
244    pub index: usize,
245    /// Complete applicable AA-control intersection for this actual type.
246    pub permission: Permission,
247}
248
249/// Issuance and trust sources retained without cloning profile/path storage.
250#[derive(Debug)]
251pub enum Authority<'r, 'p, 'a, 'c> {
252    /// The separately selected directly trusted AA is the authenticated AA.
253    Direct(&'r Authenticated<'r, 'c, 'a>),
254    /// Existing authenticated path and issuance, with its complete controls.
255    Path(&'r Issued<'r, 'p, 'c, 'a>),
256}
257impl<'r, 'a, 'c> Authority<'r, '_, 'a, 'c> {
258    fn facts(&self) -> &'r Authenticated<'r, 'c, 'a> {
259        match self {
260            Self::Direct(facts) => facts,
261            Self::Path(issued) => issued.facts(),
262        }
263    }
264    fn allows<W: Work>(
265        &self,
266        kind: Oid<'_>,
267        controls: Option<&AaControls<'_>>,
268        work: &W,
269    ) -> Result<Permission> {
270        match self {
271            Self::Direct(facts) => {
272                charge(
273                    &work.execution_for(facts.certificate().parser),
274                    1,
275                    facts.certificate().element().span().start,
276                )?;
277                if let Some(control) = controls
278                    && !control.allows(kind, work)?
279                {
280                    return Ok(Permission::Excluded {
281                        certificate: 0,
282                        source: control.element.span(),
283                    });
284                }
285                Ok(Permission::Permitted)
286            }
287            Self::Path(issued) => issued.allows(kind, work),
288        }
289    }
290}
291
292/// Completed mechanism facts relative to the caller's authenticated context.
293/// Original issuance/holder/status references remain required; only the new
294/// eligibility catalogue is independently owned. This is not an application
295/// authorization grant or proof that opaque callback/backend costs are bounded.
296#[derive(Debug)]
297pub struct Checked<'r, 'p, 'a, 'c, S> {
298    authority: Authority<'r, 'p, 'a, 'c>,
299    direct_controls: Option<AaControls<'a>>,
300    holder: Holder<'r, 'p, 'a, 'c>,
301    status: &'r S,
302    now: Reading,
303    target: Option<usize>,
304    proxy: Option<usize>,
305    attributes: OwnedVec<AttributeUse>,
306}
307impl<'r, 'p, 'a, 'c, S> Checked<'r, 'p, 'a, 'c, S> {
308    /// Exact original AC whose complete use requirements produced this receipt.
309    /// This does not authorize substitution of another equal-looking AC.
310    #[must_use]
311    pub fn certificate(&self) -> &'r crate::attribute_certificate::AttributeCertificate<'c> {
312        self.authority.facts().certificate()
313    }
314    /// Original authenticated issuance and selected trust route.
315    #[must_use]
316    pub fn authority(&self) -> &Authority<'r, 'p, 'a, 'c> {
317        &self.authority
318    }
319    /// Actual authenticated holder input, not another later certificate.
320    #[must_use]
321    pub fn holder(&self) -> &Holder<'r, 'p, 'a, 'c> {
322        &self.holder
323    }
324    /// Original evidence whose association/time/status checks completed.
325    #[must_use]
326    pub fn status(&self) -> &'r S {
327        self.status
328    }
329    /// Exact evaluation label, including subnanosecond precision.
330    #[must_use]
331    pub fn evaluated_at(&self) -> Reading {
332        self.now
333    }
334    /// Original matching target ordinal; None means no targeting was written.
335    #[must_use]
336    pub fn target(&self) -> Option<usize> {
337        self.target
338    }
339    /// Original common proxy-set ordinal; None means no `ProxyInfo` was written.
340    #[must_use]
341    pub fn proxy(&self) -> Option<usize> {
342        self.proxy
343    }
344    /// Complete original attribute catalogue and per-type restriction results.
345    /// Caller traversal/output must be admitted by its own operation.
346    #[must_use]
347    pub fn attributes(&self) -> &[AttributeUse] {
348        &self.attributes
349    }
350    /// Apply the same authenticated controls to an actual caller-decrypted type.
351    /// This does not prove decryption or `ACClearAttrs` issuer/serial binding.
352    pub fn allows<W: Work>(&self, kind: Oid<'_>, work: &W) -> Result<Permission> {
353        self.authority
354            .allows(kind, self.direct_controls.as_ref(), work)
355    }
356}
357
358/// Terminal source/current processing failure or original group callback cause.
359#[derive(Debug)]
360pub enum UseError<E> {
361    /// Located source/query error, distinct from a completed unmet requirement.
362    Processing(Error),
363    /// Original membership callback cause and target range.
364    Membership(target::CheckError<E>),
365}
366impl<E> From<Error> for UseError<E> {
367    fn from(error: Error) -> Self {
368        Self::Processing(error)
369    }
370}
371
372/// Complete processing using an explicitly selected directly trusted AA.
373/// Equal bytes from another untrusted object do not silently select trust.
374pub fn direct<'r, 'p, 'a, 'c, S: Evidence, W: Work, E>(
375    facts: &'r Authenticated<'r, 'c, 'a>,
376    trusted_aa: &Certificate<'_>,
377    context: Context<'r, 'p, 'a, 'c, '_, S, W>,
378    membership: impl FnMut(
379        GroupRole,
380        &GeneralName<'c>,
381        &Execution<'_>,
382    ) -> core::result::Result<target::Membership, E>,
383) -> core::result::Result<Outcome<Checked<'r, 'p, 'a, 'c, S>>, UseError<E>> {
384    let work = facts.authority().execution_in(context.execution);
385    charge(&work, 1, facts.authority().span().start)?;
386    if !core::ptr::eq(facts.authority(), trusted_aa) {
387        return Ok(Outcome::Rejected(Failure::AuthorityTrust));
388    }
389    // A directly trusted AA is a one-position restriction path: its control
390    // is optional, but written exclusions still apply (RFC5755 §7.4).
391    let controls = facts
392        .authority()
393        .tbs()
394        .extensions()
395        .get(controls::IDENTIFIER, &work)
396        .map_err(Error::pkix)?
397        .map(|extension| AaControls::read_extension(extension, &work))
398        .transpose()?;
399    check(Authority::Direct(facts), controls, context, membership)
400}
401
402/// Complete processing under one existing constrained, authenticated AA path.
403/// Path and issuance signatures are not repeated by this composition.
404pub fn issued<'r, 'p, 'a, 'c, S: Evidence, W: Work, E>(
405    facts: &'r Issued<'r, 'p, 'c, 'a>,
406    context: Context<'r, 'p, 'a, 'c, '_, S, W>,
407    membership: impl FnMut(
408        GroupRole,
409        &GeneralName<'c>,
410        &Execution<'_>,
411    ) -> core::result::Result<target::Membership, E>,
412) -> core::result::Result<Outcome<Checked<'r, 'p, 'a, 'c, S>>, UseError<E>> {
413    check(Authority::Path(facts), None, context, membership)
414}
415
416fn holder_binding<W: Work>(
417    ac: &crate::attribute_certificate::AttributeCertificate<'_>,
418    holder: &Holder<'_, '_, '_, '_>,
419    inside: impl Fn(Interval) -> bool,
420    execution: &W,
421) -> Result<Option<Failure>> {
422    match holder {
423        Holder::Certificate { path, method } => {
424            if !inside(path.interval) {
425                return Ok(Some(Failure::Interval));
426            }
427            if let Check::Rejected(reason) =
428                ac.bind_holder(path.certificates[0], *method, execution)?
429            {
430                return Ok(Some(Failure::Holder(reason)));
431            }
432        }
433        Holder::Object { binding, path } => {
434            let original = binding.certificate();
435            charge(
436                &execution.execution_for(original.parser),
437                1,
438                original.element().span().start,
439            )?;
440            if !core::ptr::eq(binding.certificate(), ac) {
441                return Ok(Some(Failure::HolderObject));
442            }
443            if let Some(path) = path
444                && !inside(path.interval)
445            {
446                return Ok(Some(Failure::Interval));
447            }
448            match binding.object() {
449                super::digest::Object::Certificate(certificate) => {
450                    charge(
451                        &certificate.execution_in(execution),
452                        1,
453                        certificate.span().start,
454                    )?;
455                    if let Some(path) = path {
456                        let actual = path.certificates[0];
457                        charge(&actual.execution_in(execution), 1, actual.span().start)?;
458                    }
459                    if !path.is_some_and(|path| core::ptr::eq(path.certificates[0], certificate)) {
460                        return Ok(Some(Failure::HolderPath));
461                    }
462                }
463                super::digest::Object::PublicKey(key) => {
464                    charge(
465                        &key.key().execution_in(execution),
466                        1,
467                        key.key().algorithm().offset(),
468                    )?;
469                    if let Some(path) = path {
470                        let actual = path.facts.key;
471                        charge(
472                            &actual.key().execution_in(execution),
473                            1,
474                            actual.key().algorithm().offset(),
475                        )?;
476                        let parameters = match (actual.parameters(), key.parameters()) {
477                            (None, None) => true,
478                            (Some(left), Some(right)) => core::ptr::eq(left, right),
479                            _ => false,
480                        };
481                        if !core::ptr::eq(actual.key().as_bytes(), key.key().as_bytes())
482                            || !parameters
483                        {
484                            return Ok(Some(Failure::HolderPath));
485                        }
486                    }
487                }
488            }
489        }
490    }
491    Ok(None)
492}
493
494fn check<'r, 'p, 'a, 'c, S: Evidence, W: Work, E>(
495    authority: Authority<'r, 'p, 'a, 'c>,
496    direct_controls: Option<AaControls<'a>>,
497    context: Context<'r, 'p, 'a, 'c, '_, S, W>,
498    mut membership: impl FnMut(
499        GroupRole,
500        &GeneralName<'c>,
501        &Execution<'_>,
502    ) -> core::result::Result<target::Membership, E>,
503) -> core::result::Result<Outcome<Checked<'r, 'p, 'a, 'c, S>>, UseError<E>> {
504    let facts = authority.facts();
505    let ac = facts.certificate();
506    let execution = context.execution.execution_for(ac.parser);
507    let at = ac.element().span().start;
508    // LeapTable::shift performs three binary searches. As in the common
509    // CRL update-window check, fund the actual table-dependent search bound
510    // plus scalar arithmetic before validating/rounding the label.
511    let searches = u64::from(usize::BITS - context.utc.transitions().len().leading_zeros() + 1);
512    charge(&execution, 6 + 3 * searches + 64, at)?;
513    let upper = match context.now.upper_bound(context.utc) {
514        Ok(value) => value,
515        Err(reason) => return Ok(Outcome::Rejected(Failure::Time(reason))),
516    };
517    let inside = |interval: Interval| {
518        context.now.start() >= interval.from_instant() && upper <= interval.to_instant()
519    };
520    if !inside(facts.interval()) {
521        return Ok(Outcome::Rejected(Failure::Interval));
522    }
523    if let Some(reason) = holder_binding(ac, &context.holder, inside, &execution)? {
524        return Ok(Outcome::Rejected(reason));
525    }
526    let target = if let Some(targets) = facts.profile().targeting() {
527        match targets
528            .check(context.recipient, &execution, |name, work| {
529                membership(GroupRole::Target, name, work)
530            })
531            .map_err(UseError::Membership)?
532        {
533            target::Decision::Matches { target } => Some(target),
534            other => return Ok(Outcome::Rejected(Failure::Target(other))),
535        }
536    } else {
537        None
538    };
539    let proxy = if let Some(proxies) = facts.profile().proxying() {
540        let Some(proxy) = context.proxy else {
541            return Ok(Outcome::Rejected(Failure::ProxyContext));
542        };
543        match proxies
544            .check(
545                proxy.sender,
546                context.recipient,
547                proxy.intermediates,
548                &execution,
549                |subject, name, work| membership(GroupRole::Proxy(subject), name, work),
550            )
551            .map_err(UseError::Membership)?
552        {
553            proxy::Decision::Matches { group } => Some(group),
554            other => return Ok(Outcome::Rejected(Failure::Proxy(other))),
555        }
556    } else {
557        None
558    };
559    let status = context
560        .status
561        .check(facts.profile(), context.now, &execution)?;
562    if !matches!(status, Status::Good | Status::NoRevAvailable) {
563        return Ok(Outcome::Rejected(Failure::Status(status)));
564    }
565    let mut attributes = execution.vector();
566    let original = &ac.information().attributes;
567    attributes
568        .try_reserve(original.len())
569        .map_err(|e| Error::resource(e, super::FIELD, at))?;
570    for (index, attribute) in original.iter().enumerate() {
571        charge(&execution, 1, attribute.element().span().start)?;
572        let permission =
573            authority.allows(attribute.kind(), direct_controls.as_ref(), &execution)?;
574        attributes
575            .try_push(AttributeUse { index, permission })
576            .map_err(|e| Error::resource(e, super::FIELD, at))?;
577    }
578    Ok(Outcome::Checked(Checked {
579        authority,
580        direct_controls,
581        holder: context.holder,
582        status: context.status,
583        now: context.now,
584        target,
585        proxy,
586        attributes: attributes.into_owned(),
587    }))
588}