Skip to main content

structured_cms/attribute_certificate/
target.rs

1//! Attribute-certificate recipient targeting (RFC5755 §4.3.2 / Appendix B).
2//!
3//! This predicate is separate from holder authentication, AA trust, revocation
4//! and application authorization. A positive answer only establishes targeting
5//! for the caller's already-known recipient names or group-membership facts.
6//! <https://www.rfc-editor.org/rfc/rfc5755#section-4.3.2>
7
8use super::{IssuerSerial, ObjectDigestInfo, end, issuer_serial, object_digest, sequence, tag};
9use crate::signer::required;
10use crate::{Error, ErrorCode, Result};
11use core::ops::Range;
12use structured_asn1::memory::{Execution, OwnedVec, Work};
13use structured_asn1::{Class, Element, Oid, Parser, ScopedReader, Span, Tag};
14use structured_pkix::Extension;
15use structured_pkix::general_name::{self, GeneralName};
16
17pub(super) const FIELD: &str = "attributeCertificate.targetInformation";
18
19/// `id-ce-targetInformation`, RFC5755 Appendix B.
20pub const IDENTIFIER: Oid<'static> = Oid::from_bytes(&[0x55, 0x1d, 55]);
21
22/// One original `Targets` group. Consumers treat all groups as one union.
23#[derive(Debug)]
24pub struct Group<'a> {
25    /// Original SEQUENCE, including an empty group.
26    pub element: Element<'a>,
27    /// Range in [`TargetInformation::targets`], in original source order.
28    pub targets: Range<usize>,
29}
30
31/// One original recipient or group assertion.
32#[derive(Debug)]
33pub struct Target<'a> {
34    /// Original context-tagged CHOICE, without retagging.
35    pub element: Element<'a>,
36    /// Ordinal of its original `Targets` group.
37    pub group: usize,
38    /// Decoded alternative. It is not an authenticated identity.
39    pub value: Value<'a>,
40}
41
42/// All ASN.1 alternatives remain inspectable, including the forbidden one.
43#[derive(Debug)]
44pub enum Value<'a> {
45    /// `[0] EXPLICIT GeneralName` (a tagged CHOICE is explicitly wrapped).
46    Name(GeneralName<'a>),
47    /// `[1] EXPLICIT GeneralName`; membership is supplied by the caller.
48    Group(GeneralName<'a>),
49    /// Index in [`TargetInformation::certificates`]. RFC5755 forbids its use.
50    Certificate(usize),
51}
52
53/// Original `TargetCert`, inspectable but never sufficient for a targeting match.
54#[derive(Debug)]
55pub struct TargetCertificate<'a> {
56    /// Ordinal in [`TargetInformation::targets`].
57    pub target: usize,
58    /// Original `[2] IMPLICIT TargetCert`.
59    pub element: Element<'a>,
60    /// Certificate assertion, without looking up a certificate.
61    pub certificate: IssuerSerial<'a>,
62    /// Optional original name.
63    pub name: Option<GeneralName<'a>>,
64    /// Optional original digest assertion, without hashing an object.
65    pub digest: Option<ObjectDigestInfo<'a>>,
66}
67
68/// Fully decoded extension, retaining original ranges and independently owned
69/// catalogues. The short operation used to read it need not remain alive.
70#[derive(Debug)]
71pub struct TargetInformation<'a> {
72    parser: &'a Parser,
73    element: Element<'a>,
74    critical: bool,
75    groups: OwnedVec<Group<'a>>,
76    targets: OwnedVec<Target<'a>>,
77    certificates: OwnedVec<TargetCertificate<'a>>,
78}
79
80impl<'a> TargetInformation<'a> {
81    /// Reads the complete original extension using the shared Extension reader.
82    /// All alternatives are decoded before any membership callback can run.
83    /// Catalogues and nested Name storage obey source and current work/capacity
84    /// admission; no bytes are joined, retagged or re-encoded.
85    pub fn read<W: Work>(extension: &Extension<'a>, work: &W) -> Result<Self> {
86        Self::read_as(extension, work, IDENTIFIER, FIELD)
87    }
88
89    pub(super) fn read_as<W: Work>(
90        extension: &Extension<'a>,
91        work: &W,
92        identifier: Oid<'_>,
93        field: &'static str,
94    ) -> Result<Self> {
95        let execution = work.execution_for(extension.parser());
96        execution
97            .charge_work(identifier.as_bytes().len() as u64 + 1)
98            .map_err(|e| Error::resource(e, field, extension.span().start))?;
99        if extension.kind() != identifier {
100            return Err(Error::schema(
101                ErrorCode::UnexpectedContentType,
102                field,
103                extension.span().start,
104            ));
105        }
106        let mut reader = extension
107            .reader_scoped(&execution)
108            .map_err(|e| Error::encoding(e, field))?;
109        let element = required(&mut reader, field)?;
110        tag(&element, sequence(), field)?;
111        end(&reader, field)?;
112        let mut inside = reader
113            .enter(&element)
114            .map_err(|e| Error::encoding(e, field))?;
115        let mut groups = execution.vector();
116        let mut targets = execution.vector();
117        let mut certificates = execution.vector();
118        while let Some(group) = inside
119            .next_element()
120            .map_err(|e| Error::encoding(e, field))?
121        {
122            tag(&group, sequence(), field)?;
123            groups
124                .try_reserve(1)
125                .map_err(|e| Error::resource(e, field, group.span().start))?;
126            let start = targets.len();
127            let mut entries = inside
128                .enter(&group)
129                .map_err(|e| Error::encoding(e, field))?;
130            while let Some(entry) = entries
131                .next_element()
132                .map_err(|e| Error::encoding(e, field))?
133            {
134                let at = entry.span().start;
135                targets
136                    .try_reserve(1)
137                    .map_err(|e| Error::resource(e, field, at))?;
138                let value = if entry.tag() == Tag::context(true, 0)
139                    || entry.tag() == Tag::context(true, 1)
140                {
141                    // RFC5755 Appendix B tags GeneralName, an untagged CHOICE:
142                    // its selected name tag remains inside an explicit wrapper.
143                    let mut name = entries
144                        .enter(&entry)
145                        .map_err(|e| Error::encoding(e, field))?;
146                    let encoded = required(&mut name, field)?;
147                    let decoded =
148                        general_name::read(&name, &encoded, field).map_err(Error::pkix)?;
149                    end(&name, field)?;
150                    if entry.tag() == Tag::context(true, 0) {
151                        Value::Name(decoded)
152                    } else {
153                        Value::Group(decoded)
154                    }
155                } else if entry.tag() == Tag::context(true, 2) {
156                    certificates
157                        .try_reserve(1)
158                        .map_err(|e| Error::resource(e, field, at))?;
159                    let decoded = certificate(&entries, &entry, targets.len(), field)?;
160                    let index = certificates.len();
161                    certificates
162                        .try_push(decoded)
163                        .map_err(|e| Error::resource(e, field, at))?;
164                    Value::Certificate(index)
165                } else {
166                    return Err(Error::schema(ErrorCode::UnexpectedTag, field, at));
167                };
168                targets
169                    .try_push(Target {
170                        element: entry,
171                        group: groups.len(),
172                        value,
173                    })
174                    .map_err(|e| Error::resource(e, field, at))?;
175            }
176            groups
177                .try_push(Group {
178                    element: group,
179                    targets: start..targets.len(),
180                })
181                .map_err(|e| Error::resource(e, field, group.span().start))?;
182        }
183        Ok(Self {
184            parser: extension.parser(),
185            element,
186            critical: extension.is_critical(),
187            groups: groups.into_owned(),
188            targets: targets.into_owned(),
189            certificates: certificates.into_owned(),
190        })
191    }
192
193    /// Original whole extension value SEQUENCE.
194    #[must_use]
195    pub fn element(&self) -> Element<'a> {
196        self.element
197    }
198    /// Original extension criticality, not inferred from its identifier.
199    #[must_use]
200    pub fn is_critical(&self) -> bool {
201        self.critical
202    }
203    /// Original groups and their flat target ranges. Traversal by callers must
204    /// be admitted by their own current Work; [`Self::check`] does this itself.
205    #[must_use]
206    pub fn groups(&self) -> &[Group<'a>] {
207        &self.groups
208    }
209    /// Original target order, with no implicit sorting or deduplication.
210    #[must_use]
211    pub fn targets(&self) -> &[Target<'a>] {
212        &self.targets
213    }
214    /// Typed forbidden alternatives, without treating them as accepted targets.
215    #[must_use]
216    pub fn certificates(&self) -> &[TargetCertificate<'a>] {
217        &self.certificates
218    }
219
220    /// Tests recipient targeting only. Names must be already-known identities;
221    /// successfully decode untrusted names before supplying them here.
222    ///
223    /// RFC5755 §4.3.2 requires criticality, prohibits targetCert and treats all
224    /// Targets groups as one union. These profile gates run before comparison
225    /// or callback effects. A recipient-name match OR confirmed group membership
226    /// suffices. Present empty groups match nothing. Other/X.400/EDI recipient
227    /// names remain unproven because the shared name core only knows their
228    /// encoded identity; their type-specific semantic identity is not inferred.
229    /// This is not a complete AC profile check (including `GeneralName` profile
230    /// restrictions), holder authentication or an authorization decision.
231    ///
232    /// `membership` owns group discovery and must admit its own data-dependent
233    /// work through the supplied execution. Unknown membership differs from a
234    /// completed negative answer. Errors preserve their cause and original
235    /// target span; external callback side effects cannot be rolled back.
236    pub fn check<W: Work, E>(
237        &self,
238        recipients: &[GeneralName<'_>],
239        work: &W,
240        mut membership: impl FnMut(
241            &GeneralName<'a>,
242            &Execution<'_>,
243        ) -> core::result::Result<Membership, E>,
244    ) -> core::result::Result<Decision, CheckError<E>> {
245        let execution = work.execution_for(self.parser);
246        let pay = |at| {
247            execution
248                .charge_work(1)
249                .map_err(|e| CheckError::Processing(Error::resource(e, FIELD, at)))
250        };
251        pay(self.element.span().start)?;
252        if !self.critical {
253            return Ok(Decision::NonCritical);
254        }
255        if let Some(certificate) = self.certificates.first() {
256            return Ok(Decision::ProhibitedTargetCertificate {
257                target: certificate.target,
258            });
259        }
260        let mut unknown = false;
261        for (index, target) in self.targets.iter().enumerate() {
262            match matches_target(target, recipients, &execution, FIELD, &mut membership)? {
263                Membership::Member => return Ok(Decision::Matches { target: index }),
264                Membership::NotMember => {}
265                Membership::Unknown => unknown = true,
266            }
267        }
268        Ok(if unknown {
269            Decision::Unproven
270        } else {
271            Decision::Differs
272        })
273    }
274}
275
276pub(super) fn opaque(name: &GeneralName<'_>) -> bool {
277    matches!(
278        name,
279        GeneralName::Other { .. } | GeneralName::X400(_) | GeneralName::EdiParty(_)
280    )
281}
282
283// Recipient targeting and proxy-set matching differ in their quantifiers,
284// not the meaning or funding of one target. Keep that identity kernel shared.
285pub(super) fn matches_target<'a, E>(
286    target: &Target<'a>,
287    recipients: &[GeneralName<'_>],
288    execution: &Execution<'_>,
289    field: &'static str,
290    membership: &mut impl FnMut(&GeneralName<'a>, &Execution<'_>) -> core::result::Result<Membership, E>,
291) -> core::result::Result<Membership, CheckError<E>> {
292    let at = target.element.span().start;
293    let pay = || {
294        execution
295            .charge_work(1)
296            .map_err(|e| CheckError::Processing(Error::resource(e, field, at)))
297    };
298    pay()?;
299    match &target.value {
300        Value::Name(name) => {
301            if opaque(name) {
302                return Ok(Membership::Unknown);
303            }
304            for recipient in recipients {
305                pay()?;
306                if general_name::matches_at(name, recipient, execution, at, field)
307                    .map_err(|e| CheckError::Processing(Error::pkix(e)))?
308                {
309                    return Ok(Membership::Member);
310                }
311            }
312            Ok(Membership::NotMember)
313        }
314        Value::Group(name) => membership(name, execution).map_err(|source| CheckError::Callback {
315            target: target.element.span(),
316            source,
317        }),
318        Value::Certificate(_) => unreachable!("complete certificate preflight"),
319    }
320}
321
322fn certificate<'a>(
323    parent: &ScopedReader<'a, '_>,
324    element: &Element<'a>,
325    target: usize,
326    field: &'static str,
327) -> Result<TargetCertificate<'a>> {
328    let mut inside = parent
329        .enter(element)
330        .map_err(|e| Error::encoding(e, field))?;
331    let issuer = required(&mut inside, field)?;
332    tag(&issuer, sequence(), field)?;
333    let certificate = issuer_serial(&inside, &issuer, field)?;
334    let mut next = inside
335        .next_element()
336        .map_err(|e| Error::encoding(e, field))?;
337    let name = if next
338        .as_ref()
339        .is_some_and(|e| e.tag().class() == Class::ContextSpecific)
340    {
341        let decoded = general_name::read(&inside, next.as_ref().expect("present name"), field)
342            .map_err(Error::pkix)?;
343        next = inside
344            .next_element()
345            .map_err(|e| Error::encoding(e, field))?;
346        Some(decoded)
347    } else {
348        None
349    };
350    let digest = if let Some(value) = next {
351        tag(&value, sequence(), field)?;
352        Some(object_digest(&inside, &value, field)?)
353    } else {
354        None
355    };
356    end(&inside, field)?;
357    Ok(TargetCertificate {
358        target,
359        element: *element,
360        certificate,
361        name,
362        digest,
363    })
364}
365
366/// Caller-known group membership. No group is discovered by the format reader.
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368pub enum Membership {
369    /// The recipient belongs to the named group.
370    Member,
371    /// Membership was checked and the recipient is not a member.
372    NotMember,
373    /// The caller cannot establish whether the recipient belongs to this group.
374    Unknown,
375}
376
377/// Completed targeting facts, not AA trust or application permission.
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379pub enum Decision {
380    /// The indicated original target names the recipient or one of its groups.
381    Matches {
382        /// Original flat target ordinal.
383        target: usize,
384    },
385    /// All target alternatives were checked and none matched.
386    Differs,
387    /// No match was established, with at least one undecidable name/membership.
388    Unproven,
389    /// RFC5755 §4.3.2 requires this extension to be critical.
390    NonCritical,
391    /// RFC5755 §4.3.2 prohibits targetCert, even if another target would match.
392    ProhibitedTargetCertificate {
393        /// Original flat target ordinal.
394        target: usize,
395    },
396}
397
398/// Terminal processing/callback refusal, never represented as a mismatch.
399#[derive(Debug)]
400pub enum CheckError<E> {
401    /// A source/current-operation resource or shared name processing failure.
402    Processing(Error),
403    /// The caller's original cause and the original group-target range.
404    Callback {
405        /// Original group target CHOICE range.
406        target: Span,
407        /// Original callback error, without conversion.
408        source: E,
409    },
410}