Skip to main content

structured_cms/
attribute.rs

1//! Original CMS attribute values and the exact retagged signed range.
2
3use crate::{Error, ErrorCode, Result};
4use structured_asn1::memory::{Execution, Memory, OwnedVec, Work};
5use structured_asn1::{Element, Encoding, Oid, Parser, Reader, Span, Tag, universal};
6
7const SIGNED_SINGLETONS: [Oid<'static>; 4] = [
8    crate::oid::CONTENT_TYPE,
9    crate::oid::MESSAGE_DIGEST,
10    crate::oid::SIGNING_TIME,
11    crate::oid::ALGORITHM_PROTECTION,
12];
13
14/// The schema placement of an attribute set, not deployment policy.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Placement {
17    /// `[0] IMPLICIT SignedAttributes`, required to be DER.
18    Signed,
19    /// `[1] IMPLICIT UnsignedAttributes`, permitted to use BER.
20    Unsigned,
21}
22
23impl Placement {
24    fn field(self) -> &'static str {
25        match self {
26            Self::Signed => "signerInfo.signedAttrs",
27            Self::Unsigned => "signerInfo.unsignedAttrs",
28        }
29    }
30    fn tag(self) -> Tag {
31        Tag::context(
32            true,
33            match self {
34                Self::Signed => 0,
35                Self::Unsigned => 1,
36            },
37        )
38    }
39}
40
41/// One original attribute, preserving all values without interpreting unknown syntax.
42#[derive(Debug)]
43pub struct Attribute<'a> {
44    kind: Oid<'a>,
45    kind_element: Element<'a>,
46    values: OwnedVec<Element<'a>>,
47    element: Element<'a>,
48    parser: &'a Parser,
49    depth: u32,
50    encoding: Encoding,
51}
52
53impl<'a> Attribute<'a> {
54    fn read(parent: &Reader<'a>, element: &Element<'a>, field: &'static str) -> Result<Self> {
55        if element.tag() != Tag::constructed(universal::SEQUENCE) {
56            return Err(Error::schema(
57                ErrorCode::UnexpectedTag,
58                field,
59                element.span().start,
60            ));
61        }
62        let mut inside = parent
63            .enter(element)
64            .map_err(|error| Error::encoding(error, field))?;
65        let kind_element = inside
66            .expect(Tag::primitive(universal::OID))
67            .map_err(|error| Error::encoding(error, field))?;
68        let kind = inside
69            .oid(&kind_element)
70            .map_err(|error| Error::encoding(error, field))?;
71        let set = inside
72            .expect(Tag::constructed(universal::SET))
73            .map_err(|error| Error::encoding(error, field))?;
74        let mut members = inside
75            .enter(&set)
76            .map_err(|error| Error::encoding(error, field))?;
77        let execution = parent.execution();
78        let mut values = execution.vector();
79        let mut previous = None;
80        // RFC5652 §§5.3/12.1: attrValues is SET OF AttributeValue without SIZE.
81        // The nonempty bound belongs to SignedAttributes/UnsignedAttributes,
82        // not this inner set. Registered attribute rules impose their own
83        // cardinality in binding/counter/ESS processing; unknown syntax cannot
84        // inherit a blanket lower bound from the outer collection.
85        // https://www.rfc-editor.org/rfc/rfc5652#section-5.3
86        while let Some(value) = members
87            .next_element()
88            .map_err(|error| Error::encoding(error, field))?
89        {
90            if members.encoding().is_der() {
91                structured_pkix::der::conforms(&members, &value, field).map_err(Error::pkix)?;
92                if let Some(earlier) = previous
93                    && !structured_pkix::der::set_ordered(&members, earlier, &value, field)
94                        .map_err(Error::pkix)?
95                {
96                    return Err(Error::schema(
97                        ErrorCode::AttributeOrder,
98                        field,
99                        value.span().start,
100                    ));
101                }
102                previous = Some(value.as_bytes());
103            }
104            values
105                .try_push(value)
106                .map_err(|reason| Error::resource(reason, field, value.span().start))?;
107        }
108        if !inside.is_empty() {
109            return Err(Error::schema(
110                ErrorCode::TrailingData,
111                field,
112                inside.offset(),
113            ));
114        }
115        Ok(Self {
116            kind,
117            kind_element,
118            values: values.into_owned(),
119            element: *element,
120            parser: members.parser(),
121            depth: members.depth(),
122            encoding: members.encoding(),
123        })
124    }
125
126    /// The original attribute identifier.
127    #[must_use]
128    pub fn kind(&self) -> Oid<'a> {
129        self.kind
130    }
131
132    /// The original `attrType` encoding, including its tag and length octets.
133    #[must_use]
134    pub fn kind_encoding(&self) -> &'a [u8] {
135        self.kind_element.as_bytes()
136    }
137
138    /// Every original value, including unknown or generically empty value sets.
139    #[must_use]
140    pub fn values(&self) -> &[Element<'a>] {
141        &self.values
142    }
143
144    /// The original attribute source range.
145    #[must_use]
146    pub fn span(&self) -> Span {
147        self.element.span()
148    }
149
150    /// The original encoded attribute without normalization.
151    #[must_use]
152    pub fn as_bytes(&self) -> &'a [u8] {
153        self.element.as_bytes()
154    }
155
156    pub(crate) fn element(&self) -> &Element<'a> {
157        &self.element
158    }
159
160    /// Opens one original value in the current account and inherited parser depth.
161    ///
162    /// The index selects this attribute's catalogue, so a foreign element cannot
163    /// be rebound to its provenance. No serialized bridge or replacement parser
164    /// is used. Missing indices are absence; resource refusal remains an error.
165    pub fn value_reader(
166        &self,
167        index: usize,
168        execution: &Memory,
169        field: &'static str,
170    ) -> Result<Option<Reader<'a>>> {
171        let Some(value) = self.selected_value(index, execution, field)? else {
172            return Ok(None);
173        };
174        debug_assert!(self.depth > 0);
175        self.parser
176            .read_nested_in(
177                value.as_bytes(),
178                value.span().start,
179                self.encoding,
180                self.depth - 1,
181                execution,
182            )
183            .map(Some)
184            .map_err(|error| Error::encoding(error, field))
185    }
186
187    /// Opens one original value with producing local and current aggregate work.
188    /// A foreign parser session refuses before lookup/allocation; it cannot
189    /// silently replace the current producing ceiling with this value's old one.
190    pub fn value_reader_in<'query>(
191        &self,
192        index: usize,
193        parent: &Reader<'query>,
194        field: &'static str,
195    ) -> Result<Option<Reader<'query>>>
196    where
197        'a: 'query,
198    {
199        if !core::ptr::eq(self.parser, parent.parser()) {
200            return Err(Error::pkix(structured_pkix::Error::new(
201                structured_pkix::ErrorCode::ForeignReader,
202                field,
203                self.element.span().start,
204            )));
205        }
206        let Some(value) = self.selected_value(index, &parent.execution(), field)? else {
207            return Ok(None);
208        };
209        debug_assert!(self.depth > 0);
210        self.parser
211            .read_nested_query_in(
212                parent,
213                value.as_bytes(),
214                value.span().start,
215                self.encoding,
216                self.depth - 1,
217            )
218            .map(Some)
219            .map_err(|error| Error::encoding(error, field))
220    }
221
222    fn selected_value<W: Work>(
223        &self,
224        index: usize,
225        execution: &W,
226        field: &'static str,
227    ) -> Result<Option<Element<'a>>> {
228        execution
229            .charge_work(1)
230            .map_err(|reason| Error::resource(reason, field, self.element.span().start))?;
231        Ok(self.values.get(index).copied())
232    }
233
234    /// Requires a singleton for attribute types whose specific schema demands it.
235    pub fn single(&self, field: &'static str) -> Result<&Element<'a>> {
236        match &*self.values {
237            [value] => Ok(value),
238            _ => Err(Error::schema(
239                ErrorCode::AttributeCardinality,
240                field,
241                self.element.span().start,
242            )),
243        }
244    }
245}
246
247/// An original attribute set with independently retained catalogue storage.
248#[derive(Debug)]
249pub struct Attributes<'a> {
250    items: OwnedVec<Attribute<'a>>,
251    element: Element<'a>,
252    placement: Placement,
253    parser: &'a Parser,
254}
255
256/// Inspection selection among original byte-identical attribute instances.
257///
258/// Repetition is a conformance violation for a registered singleton, not a
259/// successful strict binding. Signer inspection retains its located diagnostics
260/// independently; this borrowed selection does not allocate another report.
261#[derive(Debug)]
262#[must_use]
263pub struct SingletonReading<'s, 'a> {
264    attribute: Option<&'s Attribute<'a>>,
265    repeats: usize,
266}
267
268impl<'s, 'a> SingletonReading<'s, 'a> {
269    /// The original first instance, or absence after the complete paid query.
270    #[must_use]
271    pub fn attribute(&self) -> Option<&'s Attribute<'a>> {
272        self.attribute
273    }
274
275    /// Number of later byte-identical instances, excluding the first one.
276    ///
277    /// Both absence and one instance have zero repeats. This describes
278    /// instances, not the cardinality of values inside one Attribute.
279    #[must_use]
280    pub fn repeats(&self) -> usize {
281        self.repeats
282    }
283}
284
285impl<'a> Attributes<'a> {
286    /// Reads nonempty attributes in their trusted schema placement.
287    ///
288    /// RFC5652 §5.3 requires nonempty Signed/UnsignedAttributes. Signed sets
289    /// are reread under DER through the same parser, not repaired or re-encoded.
290    /// Generic attributes preserve unknown instances; per-type cardinality and
291    /// placement checks are separate (§§11.1–11.4, RFC6211 §2).
292    /// <https://www.rfc-editor.org/rfc/rfc5652#section-5.3>
293    pub fn read(parent: &Reader<'a>, element: &Element<'a>, placement: Placement) -> Result<Self> {
294        let reading = Self::read_inspection(parent, element, placement)?;
295        if let Some(fault) = reading.fault() {
296            return Err(fault);
297        }
298        Ok(reading)
299    }
300
301    /// Reads original attributes while retaining a uniquely bounded empty set.
302    ///
303    /// An empty outer set violates RFC5652 §5.3 and is exposed by [`Self::fault`].
304    /// This inspection recovery does not relax signed DER, member syntax or
305    /// binding requirements. It preserves presence rather than treating an
306    /// invalid written set as absent.
307    pub fn read_inspection(
308        parent: &Reader<'a>,
309        element: &Element<'a>,
310        placement: Placement,
311    ) -> Result<Self> {
312        if element.tag() != placement.tag() {
313            return Err(Error::schema(
314                ErrorCode::UnexpectedTag,
315                placement.field(),
316                element.span().start,
317            ));
318        }
319        if placement == Placement::Signed {
320            let mut strict = parent
321                .reread(element, Encoding::Der)
322                .map_err(|error| Error::encoding(error, placement.field()))?;
323            let original = strict
324                .expect(placement.tag())
325                .map_err(|error| Error::encoding(error, placement.field()))?;
326            Self::read_set(&strict, &original, placement)
327        } else {
328            Self::read_set(parent, element, placement)
329        }
330    }
331
332    fn read_set(parent: &Reader<'a>, element: &Element<'a>, placement: Placement) -> Result<Self> {
333        let field = placement.field();
334        let mut inside = parent
335            .enter(element)
336            .map_err(|error| Error::encoding(error, field))?;
337        let execution = parent.execution();
338        let mut items = execution.vector();
339        let mut previous = None;
340        while let Some(member) = inside
341            .next_element()
342            .map_err(|error| Error::encoding(error, field))?
343        {
344            if inside.encoding().is_der() {
345                if let Some(earlier) = previous
346                    && !structured_pkix::der::set_ordered(&inside, earlier, &member, field)
347                        .map_err(Error::pkix)?
348                {
349                    return Err(Error::schema(
350                        ErrorCode::AttributeOrder,
351                        field,
352                        member.span().start,
353                    ));
354                }
355                previous = Some(member.as_bytes());
356            }
357            let attribute = Attribute::read(&inside, &member, field)?;
358            items
359                .try_push(attribute)
360                .map_err(|reason| Error::resource(reason, field, member.span().start))?;
361        }
362        Ok(Self {
363            items: items.into_owned(),
364            element: *element,
365            placement,
366            parser: parent.parser(),
367        })
368    }
369
370    /// The located nonempty-bound violation, without allocating diagnostics.
371    ///
372    /// Original signed bytes remain available for inspection; this fault must
373    /// be refused before preparing a cryptographic operation.
374    #[must_use]
375    pub fn fault(&self) -> Option<Error> {
376        if self.items.is_empty() {
377            return Some(Error::schema(
378                ErrorCode::EmptyAttributes,
379                self.placement.field(),
380                self.element.span().start,
381            ));
382        }
383        None
384    }
385
386    /// The original attribute catalogue in encoded order.
387    #[must_use]
388    pub fn items(&self) -> &[Attribute<'a>] {
389        &self.items
390    }
391
392    /// The exact range and placement supplied by the schema.
393    #[must_use]
394    pub fn element(&self) -> &Element<'a> {
395        &self.element
396    }
397
398    /// Searches every instance with the current retained-query account.
399    ///
400    /// Lookup errors are yielded once, then iteration stops. Absence is returned
401    /// only after the whole admitted search, including its terminal step.
402    pub fn all<'s>(&'s self, kind: Oid<'s>, execution: &Memory) -> Matching<'s, 'a> {
403        Matching {
404            attributes: self,
405            kind,
406            execution: execution.clone(),
407            next: 0,
408            stopped: false,
409        }
410    }
411
412    /// Searches through the producing reader's local and aggregate ceilings.
413    pub fn all_in<'s>(
414        &'s self,
415        kind: Oid<'s>,
416        parent: &'s Reader<'_>,
417    ) -> Matching<'s, 'a, Execution<'s>> {
418        Matching {
419            attributes: self,
420            kind,
421            execution: parent.execution(),
422            next: 0,
423            stopped: false,
424        }
425    }
426
427    /// Requires at most one instance for a specifically singleton attribute type.
428    ///
429    /// Unknown generic types must use [`Self::all_in`] instead: RFC5652 §5.3
430    /// does not forbid every repeated attribute identifier.
431    pub fn unique_in<'s>(
432        &'s self,
433        kind: Oid<'s>,
434        parent: &'s Reader<'_>,
435    ) -> Result<Option<&'s Attribute<'a>>> {
436        let mut found = None;
437        for result in self.all_in(kind, parent) {
438            let attribute = result?;
439            if found.is_some() {
440                return Err(Error::schema(
441                    ErrorCode::DuplicateAttribute,
442                    self.placement.field(),
443                    attribute.span().start,
444                ));
445            }
446            found = Some(attribute);
447        }
448        Ok(found)
449    }
450
451    /// Selects one readable original singleton while reporting copied instances.
452    ///
453    /// Only byte-identical copies share one inspection reading. Conflicts
454    /// refuse rather than choosing a claim; original signed bytes are never
455    /// normalized. This does not validate the type's values or relax strict
456    /// [`Self::unique_in`] / binding. Located registered-type violations are
457    /// retained by [`crate::signer::SignerInfo::faults`].
458    ///
459    /// The producing session is checked before query work. Scanning and byte
460    /// comparisons use both its local and aggregate current ceilings. The
461    /// returned borrowing facts retain no query execution or scratch storage.
462    pub fn singleton_inspection_in<'s>(
463        &'s self,
464        kind: Oid<'_>,
465        parent: &Reader<'_>,
466    ) -> Result<SingletonReading<'s, 'a>> {
467        self.require_session(parent)?;
468        let mut first: Option<&Attribute<'a>> = None;
469        let mut repeats = 0;
470        let mut next = 0;
471        let mut stopped = false;
472        let execution = parent.execution();
473        while let Some(result) = self.next_matching(kind, &execution, &mut next, &mut stopped) {
474            let attribute = result?;
475            if let Some(original) = first {
476                if !same_encoding(
477                    parent,
478                    original.as_bytes(),
479                    attribute.as_bytes(),
480                    self.placement.field(),
481                    attribute.span().start,
482                )? {
483                    return Err(Error::schema(
484                        ErrorCode::DuplicateAttribute,
485                        self.placement.field(),
486                        attribute.span().start,
487                    ));
488                }
489                debug_assert!(repeats < self.items.len());
490                repeats += 1;
491            } else {
492                first = Some(attribute);
493            }
494        }
495        Ok(SingletonReading {
496            attribute: first,
497            repeats,
498        })
499    }
500
501    fn require_session(&self, parent: &Reader<'_>) -> Result<()> {
502        if !core::ptr::eq(self.parser, parent.parser()) {
503            return Err(Error::pkix(structured_pkix::Error::new(
504                structured_pkix::ErrorCode::ForeignReader,
505                self.placement.field(),
506                self.element.span().start,
507            )));
508        }
509        Ok(())
510    }
511
512    pub(crate) fn placement_faults_in(
513        &self,
514        parent: &Reader<'_>,
515        mut report: impl FnMut(Error) -> Result<()>,
516    ) -> Result<()> {
517        self.require_session(parent)?;
518        if self.items.is_empty() {
519            return Ok(());
520        }
521        // One placement kernel serves retained inspection and strict binding.
522        // RFC5652 §§11.1–11.4 and RFC6211 §2 constrain these schema roles.
523        // https://www.rfc-editor.org/rfc/rfc5652#section-11
524        // https://www.rfc-editor.org/rfc/rfc6211#section-2
525        if self.placement == Placement::Unsigned {
526            for kind in SIGNED_SINGLETONS {
527                for result in self.all_in(kind, parent) {
528                    let attribute = result?;
529                    report(Error::schema(
530                        ErrorCode::AttributePlacement,
531                        self.placement.field(),
532                        attribute.span().start,
533                    ))?;
534                }
535            }
536        } else {
537            // A signed counter is misplaced even if its child cannot be read.
538            // Unsigned repetition remains legal in the generic CMS schema.
539            for result in self.all_in(crate::oid::COUNTERSIGNATURE, parent) {
540                let attribute = result?;
541                report(Error::schema(
542                    ErrorCode::AttributePlacement,
543                    "signerInfo.signedAttrs.counterSignature",
544                    attribute.span().start,
545                ))?;
546            }
547        }
548        Ok(())
549    }
550
551    pub(crate) fn inspect_registered<W: Work>(
552        &self,
553        parent: &Reader<'_>,
554        faults: &mut OwnedVec<Error, W>,
555    ) -> Result<()> {
556        self.placement_faults_in(parent, |fault| retain(faults, fault))?;
557        if self.placement != Placement::Signed || self.items.is_empty() {
558            return Ok(());
559        }
560        // Registered CMS rules, not a blanket ban on generic repeated OIDs.
561        // RFC5652 §§11.1–11.3; RFC6211 §2. ESS retains its independent profile.
562        // https://www.rfc-editor.org/rfc/rfc5652#section-11
563        // https://www.rfc-editor.org/rfc/rfc6211#section-2
564        for kind in SIGNED_SINGLETONS {
565            let mut seen = false;
566            for result in self.all_in(kind, parent) {
567                let attribute = result?;
568                if !seen {
569                    seen = true;
570                    continue;
571                }
572                retain(
573                    faults,
574                    Error::schema(
575                        ErrorCode::DuplicateAttribute,
576                        self.placement.field(),
577                        attribute.span().start,
578                    ),
579                )?;
580            }
581        }
582        Ok(())
583    }
584
585    /// Original signed octets with only the implicit identifier replaced by SET.
586    ///
587    /// RFC5652 §5.4 requires the SET OF identifier, preserving original DER
588    /// length/content. Unsigned sets have no such signed range.
589    #[must_use]
590    pub fn signed_bytes(&self) -> Option<[&'a [u8]; 2]> {
591        if self.placement != Placement::Signed {
592            return None;
593        }
594        Some([&[0x31], &self.element.as_bytes()[1..]])
595    }
596}
597
598fn retain<W: Work>(faults: &mut OwnedVec<Error, W>, fault: Error) -> Result<()> {
599    let field = fault.field();
600    let at = fault.offset();
601    faults
602        .try_push(fault)
603        .map_err(|reason| Error::resource(reason, field, at))
604}
605
606pub(crate) fn same_encoding(
607    parent: &Reader<'_>,
608    left: &[u8],
609    right: &[u8],
610    field: &'static str,
611    at: u64,
612) -> Result<bool> {
613    let execution = parent.execution();
614    execution
615        .charge_work(1)
616        .map_err(|reason| Error::resource(reason, field, at))?;
617    if left.len() != right.len() {
618        return Ok(false);
619    }
620    execution
621        .charge_work(left.len() as u64)
622        .map_err(|reason| Error::resource(reason, field, at))?;
623    Ok(left == right)
624}
625
626/// Accounted matching instances, with one terminal resource refusal.
627pub struct Matching<'s, 'a, W: Work = Memory> {
628    attributes: &'s Attributes<'a>,
629    kind: Oid<'s>,
630    execution: W,
631    next: usize,
632    stopped: bool,
633}
634
635impl<'s, 'a, W: Work> Iterator for Matching<'s, 'a, W> {
636    type Item = Result<&'s Attribute<'a>>;
637
638    fn next(&mut self) -> Option<Self::Item> {
639        self.attributes.next_matching(
640            self.kind,
641            &self.execution,
642            &mut self.next,
643            &mut self.stopped,
644        )
645    }
646}
647
648impl<'a> Attributes<'a> {
649    pub(crate) fn next_matching<'s>(
650        &'s self,
651        kind: Oid<'_>,
652        execution: &impl Work,
653        next: &mut usize,
654        stopped: &mut bool,
655    ) -> Option<Result<&'s Attribute<'a>>> {
656        if *stopped {
657            return None;
658        }
659        let field = self.placement.field();
660        loop {
661            if let Err(reason) = execution.charge_work(1) {
662                *stopped = true;
663                let at = self
664                    .items
665                    .get(*next)
666                    .map_or(self.element.span().start, |attribute| {
667                        attribute.span().start
668                    });
669                return Some(Err(Error::resource(reason, field, at)));
670            }
671            let Some(attribute) = self.items.get(*next) else {
672                *stopped = true;
673                return None;
674            };
675            *next += 1;
676            if attribute.kind.as_bytes().len() != kind.as_bytes().len() {
677                continue;
678            }
679            if let Err(reason) = execution.charge_work(attribute.kind.as_bytes().len() as u64) {
680                *stopped = true;
681                return Some(Err(Error::resource(reason, field, attribute.span().start)));
682            }
683            if attribute.kind == kind {
684                return Some(Ok(attribute));
685            }
686        }
687    }
688}
689
690impl<W: Work> core::iter::FusedIterator for Matching<'_, '_, W> {}
691
692#[cfg(test)]
693mod tests;