Skip to main content

structured_cades/
prepare.rs

1//! Source-derived baseline obligations, before keys, provider states or content.
2
3use crate::properties::legacy::LEGACY;
4use crate::source::{Document, DocumentId, Node, Original};
5use crate::{Cades, Error, ISSUER, Level, Result};
6use structured_asn1::memory::{OwnedMap, OwnedVec, Work};
7use structured_asn1::{Element, Reader, Span};
8use structured_cms::attribute::{Attribute, Attributes};
9use structured_cms::cades::{Profile, Role};
10use structured_cms::counter::{self, Visit, WalkError};
11use structured_cms::signer::SignerInfo;
12use structured_cms::{ErrorCode, oid};
13use structured_ltv::baseline::ArchiveCensus;
14use structured_ltv::timestamp::{Purpose, RequiredToken, TimestampRequirements, TimestampScope};
15
16const FIELD: &str = "cades";
17pub(crate) mod encoding;
18pub(crate) mod tokens;
19pub use tokens::ReadError;
20
21/// One located mandatory rule or advisory. A SHOULD is not a failed SHALL.
22#[derive(Debug)]
23pub struct Finding<'model, 'document> {
24    /// Original attribute/container coordinates and identity.
25    pub location: Original<'model, 'document>,
26    /// Normative strength of this finding.
27    pub severity: Severity,
28    /// The format requirement, distinct from a processing refusal.
29    pub rule: Rule,
30}
31
32/// Strength of a format requirement, without application severity policy.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Severity {
35    /// A normative SHALL/SHALL NOT is not met.
36    Required,
37    /// A normative SHOULD/SHOULD NOT is not met.
38    Advisory,
39}
40
41/// Format-specific findings preserve their cause instead of a blanket invalid.
42#[derive(Debug)]
43pub enum Rule {
44    /// A present original value fails its attribute's selected schema.
45    Value {
46        /// Original attribute type, not a guessed value interpretation.
47        attribute: structured_asn1::Oid<'static>,
48        /// Field of the retained cause on the original property reading.
49        field: &'static str,
50    },
51    /// Existing CMS/ESS/CAdES schema diagnostic.
52    Schema {
53        /// Exact original CMS/encoding/PKIX diagnostic category.
54        code: ErrorCode,
55        /// Original protocol field; the finding carries its source location.
56        field: &'static str,
57    },
58    /// Mandatory original attribute is absent.
59    Missing(structured_asn1::Oid<'static>),
60    /// This original attribute is forbidden or discouraged at the requested
61    /// level; the finding severity distinguishes SHALL NOT from SHOULD NOT.
62    Forbidden(structured_asn1::Oid<'static>),
63    /// No signing-certificate protection service exists.
64    SigningCertificate,
65    /// Credential references lack this signer's protected certified attribute
66    /// or signed assertion (Table1 requirement n).
67    CredentialReferences(structured_asn1::Oid<'static>),
68    /// The requested level needs a root signature timestamp.
69    SignatureTimestamp,
70    /// The requested archival level needs a root `ATSv3`.
71    ArchiveTimestamp,
72    /// A baseline original container needs the signer's certificate.
73    Certificate,
74    /// CMS content must be id-data for this root baseline signature.
75    ContentType,
76    /// A data-type description is recommended, not mandatory.
77    DataTypeDescription,
78    /// A written store lacks its protected, nonzero policy document digest.
79    PolicyStore,
80    /// Actual use of MD5 violates the format, independently of backend support.
81    Md5,
82    /// Known archive-index and timestamp-imprint digest identities disagree
83    /// (EN319122-1 §5.5.2); no provider or mathematical comparison is implied.
84    ArchiveAlgorithm,
85    /// A V2 certificate reference uses SHA-1 instead of a different hash.
86    SigningCertificateVersion,
87    /// Optional issuerSerial hint SHOULD be omitted (§6.3(g)).
88    IssuerHint,
89}
90
91/// Complete original timestamp occurrence, including unreadable token values.
92/// Each carries its actual owning signer, independently of whether it checks.
93#[derive(Debug, Clone, Copy)]
94pub struct Timestamp<'a> {
95    owner: Node,
96    property: Node,
97    value: Option<crate::source::Value<'a>>,
98    purpose: Purpose,
99    preservation_kind: Option<crate::preservation::Kind>,
100}
101
102impl<'a> Timestamp<'a> {
103    /// Original owning root or nested countersignature.
104    #[must_use]
105    pub fn owner(&self) -> Node {
106        self.owner
107    }
108    /// Original whole timestamp attribute.
109    #[must_use]
110    pub fn property(&self) -> Node {
111        self.property
112    }
113    /// Original carrier value, not a successful decoded token.
114    #[must_use]
115    pub fn value(&self) -> Option<Element<'a>> {
116        self.value.map(|value| value.element())
117    }
118    /// Original parser/encoding/depth for a later current-operation query.
119    #[must_use]
120    pub fn source(&self) -> Option<crate::source::Value<'a>> {
121        self.value
122    }
123    /// Normative input kind, selected from the original attribute.
124    #[must_use]
125    pub fn purpose(&self) -> Purpose {
126        self.purpose
127    }
128    /// Historical mechanism, when this is not a modern content/signature/ATSv3
129    /// occurrence. An archive purpose alone never selects an `ATSHashIndexV3`.
130    #[must_use]
131    pub fn preservation_kind(&self) -> Option<crate::preservation::Kind> {
132        self.preservation_kind
133    }
134}
135
136/// One original counter value. Malformed values retain a real location and
137/// diagnostic and cannot disappear from a later participant census.
138pub struct Counter<'a> {
139    parent: Node,
140    node: Node,
141    malformed: Option<structured_cms::Error>,
142    signature: core::ops::Range<usize>,
143    properties: Option<crate::properties::Properties<'a>>,
144    pub(crate) encoding: Option<encoding::SignerEncoding<'a>>,
145}
146
147impl<'a> Counter<'a> {
148    /// Original direct parent; the child covers its signature content octets.
149    #[must_use]
150    pub fn parent(&self) -> Node {
151        self.parent
152    }
153    /// Original child node, or the located unreadable-value start.
154    #[must_use]
155    pub fn node(&self) -> Node {
156        self.node
157    }
158    /// Read failure, separate from successful mathematical verification.
159    #[must_use]
160    pub fn malformed(&self) -> Option<&structured_cms::Error> {
161        self.malformed.as_ref()
162    }
163    /// Optional original property reading; absent only for an unreadable child.
164    #[must_use]
165    pub fn properties(&self) -> Option<&crate::properties::Properties<'a>> {
166        self.properties.as_ref()
167    }
168}
169
170/// Immutable source-derived root profile and COMPLETE nested timestamp census.
171/// Findings prevent a positive predicate; processing refusal returns no prefix.
172pub struct Preparation<'model, 'document> {
173    document: &'model Document<'model, 'document>,
174    signer: &'model SignerInfo<'document>,
175    requested: Level,
176    profile: Profile<'document>,
177    properties: crate::properties::Properties<'document>,
178    findings: OwnedVec<Finding<'model, 'document>>,
179    counters: OwnedVec<Counter<'document>>,
180    parallel: Option<Parallel<'document>>,
181    counter_index: OwnedMap<Node, usize>,
182    signatures: OwnedVec<structured_cms::octets::Piece<'document>>,
183    timestamps: OwnedVec<Timestamp<'document>>,
184    pub(crate) timestamp_ranges: Option<OwnedMap<Node, core::ops::Range<usize>>>,
185    required: TimestampRequirements<Node, DocumentId<'model, 'document>, (), Cades>,
186    conforming: bool,
187    basic_conforming: bool,
188    preservation: OwnedVec<crate::preservation::Required<'document>>,
189    pub(crate) parsed: tokens::Cache<'model, 'document>,
190}
191
192struct Parallel<'d> {
193    counters: Option<OwnedVec<Counter<'d>>>,
194    index: Option<OwnedMap<Node, usize>>,
195    timestamps: Option<OwnedVec<Timestamp<'d>>>,
196}
197
198impl<'m, 'd> Preparation<'m, 'd> {
199    /// Builds format obligations from the original selected root. All nested
200    /// countersignatures are traversed iteratively by the existing CMS walker.
201    /// No key lookup, provider, source acquisition or signature check occurs.
202    ///
203    /// EN319122-1 V1.3.1 §§6.2.2/6.3 table 1 makes the requested level explicit;
204    /// '*' is advisory and an optional archive never promotes a lower request.
205    /// <https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf>
206    #[expect(
207        clippy::result_large_err,
208        reason = "keep original timestamp syntax failures and their independent source maps without refusal-time allocation"
209    )]
210    pub fn read(
211        document: &'m Document<'m, 'd>,
212        signer: &'m SignerInfo<'d>,
213        requested: Level,
214        parent: &Reader<'d>,
215    ) -> core::result::Result<Self, ReadError> {
216        let selected = document.signer(signer)?;
217        document.message().require_session(parent, FIELD)?;
218        let execution = parent.execution();
219        let profile = structured_cms::cades::inspect_profile(signer, Role::Signature, parent)?;
220        let mut findings = execution.vector();
221        let properties =
222            root_properties(document, signer, requested, &profile, parent, &mut findings)?;
223        let mut timestamps = execution.vector();
224        collect_timestamps(signer, parent, &mut timestamps)?;
225        let mut signatures = execution.vector();
226        let mut preservation = crate::preservation::roots(document.message(), parent, &execution)?;
227        let counters = read_counters(
228            document,
229            signer,
230            parent,
231            &mut findings,
232            &mut timestamps,
233            &mut signatures,
234            &mut preservation,
235        )?;
236        let mut parallel =
237            read_parallel(document, signer, parent, &mut signatures, &mut preservation)?;
238        let timestamp_ranges = history_timestamps(
239            &preservation,
240            &mut timestamps,
241            &mut parallel,
242            parent,
243            Node::of(signer.span()),
244        )?;
245        let tokens = token_obligations(&timestamps, &execution, selected.node.span().start)?;
246        let CounterCatalogue {
247            children,
248            index: counter_index,
249            mut conforming,
250        } = counter_catalogue(&counters, parent, selected.node)?;
251        let mut basic_conforming = conforming;
252        let mut parsed = tokens::read(
253            document,
254            &timestamps,
255            selected.node,
256            &counter_index,
257            parent,
258            tokens::Findings {
259                rows: &mut findings,
260                work: &execution,
261            },
262        )?;
263        parallel_findings(document, parallel.as_ref(), &mut parsed, parent)?;
264        apply_findings(
265            &findings,
266            &execution,
267            &mut conforming,
268            &mut basic_conforming,
269        )?;
270        let required = TimestampRequirements::with_counters(
271            ISSUER,
272            selected.node,
273            TimestampScope::All(selected),
274            tokens.into_owned(),
275            children.into_owned(),
276            None,
277        );
278        Ok(Self {
279            document,
280            signer,
281            requested,
282            profile,
283            properties,
284            findings: findings.into_owned(),
285            counters,
286            parallel,
287            counter_index: counter_index.into_owned(),
288            signatures: signatures.into_owned(),
289            timestamps: timestamps.into_owned(),
290            timestamp_ranges,
291            required,
292            conforming,
293            basic_conforming,
294            preservation: preservation.into_owned(),
295            parsed,
296        })
297    }
298
299    /// Explicit profile requested before interpreting unsigned augmentation.
300    #[must_use]
301    pub fn requested(&self) -> Level {
302        self.requested
303    }
304    /// Every historical preservation obligation from ALL parallel root signers
305    /// and selected counters. These require their actual older proof processors;
306    /// absence of a qualified receipt cannot be replaced with `ATSv3` evidence.
307    #[must_use]
308    pub fn preservation(&self) -> &[crate::preservation::Required<'d>] {
309        &self.preservation
310    }
311    /// Located preservation findings on OTHER parallel roots/counters. These
312    /// do not change the selected root's B-B syntax facts; their TSA actors
313    /// are nevertheless mandatory in the full historical timestamp census.
314    pub fn preservation_findings(&self) -> &[Finding<'m, 'd>] {
315        self.parsed.related.as_deref().unwrap_or(&[])
316    }
317    /// Original selected root signer.
318    #[must_use]
319    pub fn signer(&self) -> &'m SignerInfo<'d> {
320        self.signer
321    }
322    /// Full original root `SignedData`, including parallel signer/material fields.
323    #[must_use]
324    pub fn document(&self) -> &'m Document<'m, 'd> {
325        self.document
326    }
327    /// Already parsed ESS restrictions and profile findings; no second read.
328    #[must_use]
329    pub fn profile(&self) -> &Profile<'d> {
330        &self.profile
331    }
332    /// Original optional property syntax, open-format obligations and causes.
333    #[must_use]
334    pub fn properties(&self) -> &crate::properties::Properties<'d> {
335        &self.properties
336    }
337    /// Every original nested counter value in original depth-first order.
338    #[must_use]
339    pub fn counters(&self) -> &[Counter<'d>] {
340        &self.counters
341    }
342    /// Original counters of OTHER parallel roots, retained only as source and
343    /// preservation metadata. Their presence does not assert their signature
344    /// math or add their signer paths to this selected root's counter census.
345    pub fn parallel_counters(&self) -> &[Counter<'d>] {
346        self.parallel
347            .as_ref()
348            .and_then(|parallel| parallel.counters.as_deref())
349            .unwrap_or(&[])
350    }
351    pub(crate) fn source_timestamps(&self) -> impl Iterator<Item = &Timestamp<'d>> {
352        self.timestamps.iter().chain(
353            self.parallel
354                .as_ref()
355                .and_then(|parallel| parallel.timestamps.as_deref())
356                .unwrap_or(&[])
357                .iter(),
358        )
359    }
360    /// Original signature CONTENT octets of a timestamp's actual owner. A
361    /// counter timestamp covers that counter, not the counter's parent/root.
362    pub fn signature_input(
363        &self,
364        timestamp: usize,
365        parent: &Reader<'_>,
366    ) -> Result<Option<&[structured_cms::octets::Piece<'d>]>> {
367        self.document.message().require_session(parent, FIELD)?;
368        parent
369            .charge(1)
370            .map_err(|cause| Error::encoding(cause, FIELD))?;
371        let Some(token) = self.timestamps.get(timestamp) else {
372            return Ok(None);
373        };
374        self.signature_of(token.owner, parent).map(Some)
375    }
376    /// A content timestamp on a counter covers that counter's original input:
377    /// its PARENT signature. The root's content timestamp uses eContent and
378    /// consequently returns None here, never the root signature as a fallback.
379    pub fn counter_content_input(
380        &self,
381        timestamp: usize,
382        parent: &Reader<'_>,
383    ) -> Result<Option<&[structured_cms::octets::Piece<'d>]>> {
384        self.document.message().require_session(parent, FIELD)?;
385        parent
386            .charge(1)
387            .map_err(|cause| Error::encoding(cause, FIELD))?;
388        let Some(token) = self.timestamps.get(timestamp) else {
389            return Ok(None);
390        };
391        match self.counter_parent(token.owner, parent)? {
392            None => Ok(None),
393            Some(owner) => self.signature_of(owner, parent).map(Some),
394        }
395    }
396    pub(crate) fn signature_of(
397        &self,
398        owner: Node,
399        parent: &Reader<'_>,
400    ) -> Result<&[structured_cms::octets::Piece<'d>]> {
401        self.signature_of_in(owner, parent, parent)
402    }
403    pub(crate) fn signature_of_in(
404        &self,
405        owner: Node,
406        origin: &Reader<'_>,
407        operation: &Reader<'_>,
408    ) -> Result<&[structured_cms::octets::Piece<'d>]> {
409        self.document.message().require_session(origin, FIELD)?;
410        let work = origin.execution_query(operation);
411        work.charge_work(1)
412            .map_err(|cause| Error::resource(cause, FIELD, owner.span().start))?;
413        if owner == Node::of(self.signer.span()) {
414            return Ok(self.signer.signature().pieces());
415        }
416        for signer in self.document.message().signers() {
417            work.charge_work(1)
418                .map_err(|cause| Error::resource(cause, FIELD, owner.span().start))?;
419            if owner == Node::of(signer.span()) {
420                return Ok(signer.signature().pieces());
421            }
422        }
423        Ok(&self.signatures[self.counter_record(owner, &work)?.signature.clone()])
424    }
425    pub(crate) fn signer_encoding(
426        &self,
427        owner: Node,
428        parent: &Reader<'_>,
429    ) -> Result<encoding::SignerView<'_, 'd>> {
430        self.document.message().require_session(parent, FIELD)?;
431        for signer in self.document.message().signers() {
432            parent
433                .charge(1)
434                .map_err(|cause| Error::encoding(cause, FIELD))?;
435            if owner == Node::of(signer.span()) {
436                return Ok(encoding::SignerView::Root(signer));
437            }
438        }
439        let work = parent.execution();
440        self.counter_record(owner, &work)?
441            .encoding
442            .as_ref()
443            .map(encoding::SignerView::Counter)
444            .ok_or(Error::Foreign {
445                at: owner.span().start,
446            })
447    }
448    pub(crate) fn counter_parent(&self, owner: Node, parent: &Reader<'_>) -> Result<Option<Node>> {
449        self.counter_parent_in(owner, parent, parent)
450    }
451    pub(crate) fn counter_parent_in(
452        &self,
453        owner: Node,
454        origin: &Reader<'_>,
455        operation: &Reader<'_>,
456    ) -> Result<Option<Node>> {
457        self.document.message().require_session(origin, FIELD)?;
458        let work = origin.execution_query(operation);
459        for signer in self.document.message().signers() {
460            work.charge_work(1)
461                .map_err(|cause| Error::resource(cause, FIELD, owner.span().start))?;
462            if owner == Node::of(signer.span()) {
463                return Ok(None);
464            }
465        }
466        Ok(Some(self.counter_record(owner, &work)?.parent))
467    }
468    fn counter_record(&self, owner: Node, work: &impl Work) -> Result<&Counter<'d>> {
469        let resource = |cause| Error::resource(cause, FIELD, owner.span().start);
470        if let Some(index) = self.counter_index.get(&owner, work).map_err(resource)? {
471            return Ok(&self.counters[*index]);
472        }
473        if let Some(parallel) = &self.parallel
474            && let Some(index) = &parallel.index
475            && let Some(ordinal) = index.get(&owner, work).map_err(resource)?
476        {
477            return Ok(&parallel
478                .counters
479                .as_ref()
480                .expect("index belongs to its counter catalogue")[*ordinal]);
481        }
482        Err(Error::Foreign {
483            at: owner.span().start,
484        })
485    }
486    /// All original root/counter timestamp occurrences, including failed ones.
487    #[must_use]
488    pub fn timestamps(&self) -> &[Timestamp<'d>] {
489        &self.timestamps
490    }
491    /// Complete source-derived TSA obligations for the common LTV core.
492    #[must_use]
493    pub fn timestamp_requirements(
494        &self,
495    ) -> &TimestampRequirements<Node, DocumentId<'m, 'd>, (), Cades> {
496        &self.required
497    }
498    /// Located mandatory and advisory findings, without product severity policy.
499    #[must_use]
500    pub fn findings(&self) -> &[Finding<'m, 'd>] {
501        &self.findings
502    }
503    /// Only format preparation; no signature, ESS hash, path or trust assertion.
504    #[must_use]
505    pub fn all_format_requirements_met(&self) -> bool {
506        self.conforming
507    }
508    /// Mandatory root/counter B-B format conditions only. A missing timestamp
509    /// at a higher requested level does not retroactively erase a B-B fact.
510    #[must_use]
511    pub fn basic_format_requirements_met(&self) -> bool {
512        self.basic_conforming
513    }
514}
515
516impl<'m, 'd> ArchiveCensus for Preparation<'m, 'd> {
517    type Id = Node;
518    type Document = DocumentId<'m, 'd>;
519    type Generation = ();
520    fn signature(&self) -> Original<'m, 'd> {
521        self.document.locate(self.signer.span())
522    }
523    fn timestamps(&self) -> usize {
524        self.timestamps.len()
525    }
526    fn archive(&self, ordinal: usize) -> Option<Node> {
527        let token = &self.timestamps[ordinal];
528        if token.owner == Node::of(self.signer.span()) && token.purpose == Purpose::Archive {
529            Some(match token.value {
530                Some(value) => Node::of(value.element().span()),
531                None => token.property,
532            })
533        } else {
534            None
535        }
536    }
537}
538
539fn root_properties<'m, 'd, W: Work>(
540    document: &Document<'m, 'd>,
541    signer: &SignerInfo<'d>,
542    requested: Level,
543    profile: &structured_cms::cades::Profile<'d>,
544    parent: &Reader<'d>,
545    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
546) -> Result<crate::properties::Properties<'d>> {
547    root_rules(document, signer, requested, profile, parent, findings)?;
548    let properties = crate::properties::Properties::read(signer, parent)?;
549    property_findings(document, &properties, parent, findings)?;
550    mechanism_findings(document, signer, profile, &properties, parent, findings)?;
551    legacy_rules(
552        document,
553        signer,
554        &properties,
555        Some(requested),
556        parent,
557        findings,
558    )?;
559    Ok(properties)
560}
561
562fn apply_findings<W: Work>(
563    findings: &[Finding<'_, '_>],
564    work: &W,
565    conforming: &mut bool,
566    basic_conforming: &mut bool,
567) -> Result<()> {
568    for finding in findings {
569        work.charge_work(1)
570            .map_err(|cause| Error::resource(cause, FIELD, finding.location.node.span().start))?;
571        if finding.severity == Severity::Required {
572            *conforming = false;
573            if !matches!(
574                finding.rule,
575                Rule::SignatureTimestamp | Rule::ArchiveTimestamp | Rule::Forbidden(_)
576            ) {
577                *basic_conforming = false;
578            }
579        }
580    }
581    Ok(())
582}
583
584fn schema_findings<'m, 'd, W: Work>(
585    document: &Document<'m, 'd>,
586    signer: &SignerInfo<'_>,
587    profile: &Profile<'_>,
588    parent: &Reader<'_>,
589    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
590) -> Result<()> {
591    if let Some(signed) = signer.signed_attributes() {
592        for attribute in signed.items() {
593            parent
594                .charge(1 + attribute.kind().as_bytes().len().min(11) as u64)
595                .map_err(|cause| Error::encoding(cause, FIELD))?;
596            if crate::preservation::Kind::from_oid(attribute.kind())
597                .is_some_and(crate::preservation::Kind::is_archive)
598            {
599                // RFC5126 §6.4.1 (and RFC3126 §4.4.1) define an UNSIGNED
600                // archive attribute. Retain readable values for inspection,
601                // but never turn this placement into format conformance.
602                // https://www.rfc-editor.org/rfc/rfc5126#section-6.4.1
603                finding(
604                    document,
605                    attribute.span(),
606                    Severity::Required,
607                    Rule::Schema {
608                        code: ErrorCode::AttributePlacement,
609                        field: "cades.historical",
610                    },
611                    findings,
612                )?;
613            }
614        }
615    }
616    for error in signer
617        .faults()
618        .iter()
619        .chain(profile.faults())
620        .chain(profile.signing().faults())
621    {
622        parent.charge(1).map_err(|e| Error::encoding(e, FIELD))?;
623        finding(
624            document,
625            Span {
626                start: error.offset(),
627                length: 0,
628            },
629            Severity::Required,
630            Rule::Schema {
631                code: error.code(),
632                field: error.field(),
633            },
634            findings,
635        )?;
636    }
637    for certificate in [profile.signing().v1(), profile.signing().v2()]
638        .into_iter()
639        .flatten()
640    {
641        for error in certificate.faults() {
642            parent.charge(1).map_err(|e| Error::encoding(e, FIELD))?;
643            finding(
644                document,
645                Span {
646                    start: error.offset(),
647                    length: 0,
648                },
649                Severity::Required,
650                Rule::Schema {
651                    code: error.code(),
652                    field: error.field(),
653                },
654                findings,
655            )?;
656        }
657    }
658    Ok(())
659}
660
661fn property_findings<'m, 'd, W: Work>(
662    document: &Document<'m, 'd>,
663    properties: &crate::properties::Properties<'_>,
664    parent: &Reader<'_>,
665    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
666) -> Result<()> {
667    for hint in properties.issuer_hints() {
668        parent
669            .charge(1)
670            .map_err(|cause| Error::encoding(cause, FIELD))?;
671        finding(
672            document,
673            *hint,
674            Severity::Advisory,
675            Rule::IssuerHint,
676            findings,
677        )?;
678    }
679    for fault in properties.faults() {
680        parent
681            .charge(1)
682            .map_err(|cause| Error::encoding(cause, FIELD))?;
683        finding(
684            document,
685            Span {
686                start: fault.cause.offset(),
687                length: 0,
688            },
689            Severity::Required,
690            match &fault.cause {
691                Error::Syntax {
692                    code: ErrorCode::AttributeCardinality,
693                    field,
694                    ..
695                } => Rule::Schema {
696                    code: ErrorCode::AttributeCardinality,
697                    field,
698                },
699                _ => Rule::Value {
700                    attribute: fault.attribute,
701                    field: fault.field,
702                },
703            },
704            findings,
705        )?;
706    }
707    Ok(())
708}
709
710fn mechanism_findings<'m, 'd, W: Work>(
711    document: &Document<'m, 'd>,
712    signer: &SignerInfo<'_>,
713    profile: &Profile<'_>,
714    properties: &crate::properties::Properties<'_>,
715    parent: &Reader<'_>,
716    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
717) -> Result<()> {
718    let md5 = structured_asn1::Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 2, 5]);
719    parent
720        .charge(2 + signer.digest_algorithm().algorithm().as_bytes().len() as u64)
721        .map_err(|cause| Error::encoding(cause, FIELD))?;
722    // §6.2.1 is a format prohibition, independent of the selected provider's
723    // capabilities or application algorithm policy. It applies to actual use.
724    if signer.digest_algorithm().algorithm() == md5 {
725        finding(
726            document,
727            signer.span(),
728            Severity::Required,
729            Rule::Md5,
730            findings,
731        )?;
732    }
733    for attribute in [profile.signing().v1(), profile.signing().v2()]
734        .into_iter()
735        .flatten()
736    {
737        for certificate in attribute.identifiers() {
738            parent
739                .charge(1)
740                .map_err(|cause| Error::encoding(cause, FIELD))?;
741            if let structured_cms::provider::DigestAlgorithm::Written(identifier) =
742                certificate.digest_algorithm()
743            {
744                parent
745                    .charge(2 * (1 + identifier.algorithm().as_bytes().len() as u64))
746                    .map_err(|cause| Error::encoding(cause, FIELD))?;
747                if identifier.algorithm() == md5 {
748                    finding(
749                        document,
750                        certificate.element().span(),
751                        Severity::Required,
752                        Rule::Md5,
753                        findings,
754                    )?;
755                }
756                // EN319122-1 §5.2.2.3 narrows RFC5035: every CAdES V2
757                // reference uses a non-SHA1 hash, whether V1 coexists or not.
758                // https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf
759                if attribute.version() == structured_cms::ess::Version::V2
760                    && identifier.algorithm() == oid::SHA1
761                {
762                    finding(
763                        document,
764                        certificate.element().span(),
765                        Severity::Required,
766                        Rule::SigningCertificateVersion,
767                        findings,
768                    )?;
769                }
770            }
771            if !matches!(
772                certificate.issuer_serial(),
773                structured_cms::ess::IssuerReading::Absent
774            ) {
775                finding(
776                    document,
777                    certificate.element().span(),
778                    Severity::Advisory,
779                    Rule::IssuerHint,
780                    findings,
781                )?;
782            }
783        }
784    }
785    if let Some(policy) = properties.policy() {
786        parent
787            .charge(1 + policy.algorithm.algorithm().as_bytes().len() as u64)
788            .map_err(|cause| Error::encoding(cause, FIELD))?;
789        if policy.algorithm.algorithm() == md5 {
790            finding(
791                document,
792                signer.span(),
793                Severity::Required,
794                Rule::Md5,
795                findings,
796            )?;
797        }
798    }
799    // §6.3(k): an unsigned store is allowed ONLY with the actual protected
800    // policy digest. Zero-hash explicitly means unknown (§5.2.9.1), any width.
801    if properties.store().is_some() && properties.policy().is_none_or(|policy| policy.hash_unknown)
802    {
803        finding(
804            document,
805            signer.span(),
806            Severity::Required,
807            Rule::PolicyStore,
808            findings,
809        )?;
810    }
811    Ok(())
812}
813
814fn legacy_rules<'m, 'd, W: Work>(
815    document: &Document<'m, 'd>,
816    signer: &SignerInfo<'_>,
817    properties: &crate::properties::Properties<'_>,
818    level: Option<Level>,
819    parent: &Reader<'_>,
820    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
821) -> Result<()> {
822    let mut credentials = None;
823    let mut seen = 0u8;
824    for (protected, set) in [
825        (true, signer.signed_attributes()),
826        (false, signer.unsigned_attributes()),
827    ] {
828        let Some(set) = set else {
829            continue;
830        };
831        for attribute in set.items() {
832            let Some((ordinal, kind)) = legacy_kind(attribute.kind(), parent)? else {
833                continue;
834            };
835            legacy_cardinality(
836                document,
837                attribute,
838                ordinal,
839                &mut seen,
840                level.is_some() && !protected,
841                parent,
842                findings,
843            )?;
844            // AnnexA defines these as unsigned. Placement is mandatory in
845            // root and counter signers, regardless of the baseline-level row.
846            // https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf
847            if protected {
848                finding(
849                    document,
850                    attribute.span(),
851                    Severity::Required,
852                    Rule::Schema {
853                        code: ErrorCode::AttributePlacement,
854                        field: "cades.legacy.unsignedAttrs",
855                    },
856                    findings,
857                )?;
858            }
859            // The requested Table1 row belongs to the selected baseline root;
860            // counters still receive the same placement and credential rules.
861            if let Some(level) = level {
862                finding(
863                    document,
864                    attribute.span(),
865                    if level >= Level::LongTerm {
866                        Severity::Required
867                    } else {
868                        Severity::Advisory
869                    },
870                    Rule::Forbidden(kind),
871                    findings,
872                )?;
873            }
874            if kind != LEGACY[6] && kind != LEGACY[7] {
875                continue;
876            }
877            let present = if let Some(present) = credentials {
878                present
879            } else {
880                let present = protected_credential(signer, properties, parent)?;
881                credentials = Some(present);
882                present
883            };
884            // §6.3 Table1(n) is SHALL NOT without this protected service,
885            // even where the separate '*' presence rule is only advisory.
886            if !present {
887                finding(
888                    document,
889                    attribute.span(),
890                    Severity::Required,
891                    Rule::CredentialReferences(kind),
892                    findings,
893                )?;
894            }
895        }
896    }
897    Ok(())
898}
899
900fn legacy_cardinality<'m, 'd, W: Work>(
901    document: &Document<'m, 'd>,
902    attribute: &Attribute<'_>,
903    ordinal: u8,
904    seen: &mut u8,
905    root: bool,
906    parent: &Reader<'_>,
907    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
908) -> Result<()> {
909    parent
910        .charge(2)
911        .map_err(|cause| Error::encoding(cause, FIELD))?;
912    // Properties::read retains AnnexA.1 value-cardinality faults for EVERY
913    // signer, including timestamp signers. This phase only adds Table1's
914    // attribute-occurrence rule: repeated timestamp rows, other rows 0 or 1.
915    // https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf
916    if root && ordinal != 4 && ordinal != 5 {
917        debug_assert!(ordinal < 8);
918        let bit = 1u8 << ordinal;
919        if *seen & bit != 0 {
920            finding(
921                document,
922                attribute.span(),
923                Severity::Required,
924                Rule::Schema {
925                    code: ErrorCode::DuplicateAttribute,
926                    field: "cades.legacy",
927                },
928                findings,
929            )?;
930        }
931        *seen |= bit;
932    }
933    Ok(())
934}
935
936fn legacy_kind(
937    kind: structured_asn1::Oid<'_>,
938    parent: &Reader<'_>,
939) -> Result<Option<(u8, structured_asn1::Oid<'static>)>> {
940    for (ordinal, expected) in (0u8..).zip(LEGACY) {
941        parent
942            .charge(1 + kind.as_bytes().len().min(expected.as_bytes().len()) as u64)
943            .map_err(|cause| Error::encoding(cause, FIELD))?;
944        if kind == expected {
945            return Ok(Some((ordinal, expected)));
946        }
947    }
948    Ok(None)
949}
950
951fn protected_credential(
952    signer: &SignerInfo<'_>,
953    properties: &crate::properties::Properties<'_>,
954    parent: &Reader<'_>,
955) -> Result<bool> {
956    let Some(protected_set) = signer.signed_attributes() else {
957        return Ok(false);
958    };
959    let protected = protected_set.element().span();
960    for value in properties.open_values() {
961        parent
962            .charge(1)
963            .map_err(|cause| Error::encoding(cause, FIELD))?;
964        let span = value.enclosure.span();
965        if span.start >= protected.start
966            && span.end() <= protected.end()
967            && matches!(
968                value.role,
969                crate::properties::OpenRole::AttributeCertificate
970                    | crate::properties::OpenRole::OtherAttributeCertificate
971                    | crate::properties::OpenRole::SignedAssertion
972            )
973        {
974            return Ok(true);
975        }
976    }
977    Ok(false)
978}
979
980fn root_rules<'m, 'd, W: Work>(
981    document: &Document<'m, 'd>,
982    signer: &SignerInfo<'_>,
983    requested: Level,
984    profile: &Profile<'_>,
985    parent: &Reader<'_>,
986    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
987) -> Result<()> {
988    schema_findings(document, signer, profile, parent, findings)?;
989    for error in document.message().faults() {
990        parent.charge(1).map_err(|e| Error::encoding(e, FIELD))?;
991        finding(
992            document,
993            Span {
994                start: error.offset(),
995                length: 0,
996            },
997            Severity::Required,
998            Rule::Schema {
999                code: error.code(),
1000                field: error.field(),
1001            },
1002            findings,
1003        )?;
1004    }
1005    parent
1006        .charge(1 + document.message().content().kind().as_bytes().len() as u64)
1007        .map_err(|e| Error::encoding(e, FIELD))?;
1008    if document.message().content().kind() != oid::DATA {
1009        finding(
1010            document,
1011            document.message().span(),
1012            Severity::Required,
1013            Rule::ContentType,
1014            findings,
1015        )?;
1016    }
1017    binding_findings(
1018        document,
1019        signer,
1020        structured_cms::binding::Scope::Content(document.message().content().kind()),
1021        parent,
1022        findings,
1023    )?;
1024    protected_rules(document, signer, profile, parent, findings)?;
1025    unsigned_rules(document, signer, requested, parent, findings)
1026}
1027
1028fn binding_findings<'m, 'd, 'a, W: Work>(
1029    document: &Document<'m, 'd>,
1030    signer: &SignerInfo<'a>,
1031    scope: structured_cms::binding::Scope<'a>,
1032    parent: &Reader<'a>,
1033    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
1034) -> Result<()> {
1035    match structured_cms::binding::inspect(signer, parent, scope) {
1036        Ok(_) => {}
1037        Err(error) if error.is_terminal() => return Err(Error::Cms(error)),
1038        Err(error) => finding(
1039            document,
1040            Span {
1041                start: error.offset(),
1042                length: 0,
1043            },
1044            Severity::Required,
1045            Rule::Schema {
1046                code: error.code(),
1047                field: error.field(),
1048            },
1049            findings,
1050        )?,
1051    }
1052    Ok(())
1053}
1054
1055fn protected_rules<'m, 'd, W: Work>(
1056    document: &Document<'m, 'd>,
1057    signer: &SignerInfo<'_>,
1058    profile: &Profile<'_>,
1059    parent: &Reader<'_>,
1060    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
1061) -> Result<()> {
1062    let protected = signer.signed_attributes();
1063    for kind in [oid::CONTENT_TYPE, oid::MESSAGE_DIGEST, oid::SIGNING_TIME] {
1064        if !present(document, protected, kind, parent, findings)? {
1065            finding(
1066                document,
1067                signer.span(),
1068                Severity::Required,
1069                Rule::Missing(kind),
1070                findings,
1071            )?;
1072        }
1073    }
1074    if profile.signing().v1().is_none() && profile.signing().v2().is_none() {
1075        finding(
1076            document,
1077            signer.span(),
1078            Severity::Required,
1079            Rule::SigningCertificate,
1080            findings,
1081        )?;
1082    }
1083    if document.message().certificates().is_empty() {
1084        finding(
1085            document,
1086            document.message().span(),
1087            Severity::Required,
1088            Rule::Certificate,
1089            findings,
1090        )?;
1091    }
1092    let hints = present(document, protected, oid::CONTENT_HINTS, parent, findings)?;
1093    let mime = present(document, protected, oid::MIME_TYPE, parent, findings)?;
1094    if !hints && !mime {
1095        finding(
1096            document,
1097            signer.span(),
1098            Severity::Advisory,
1099            Rule::DataTypeDescription,
1100            findings,
1101        )?;
1102    }
1103    // Table 1 narrows the generic attribute rules to at most one instance.
1104    for kind in [
1105        oid::COMMITMENT_TYPE_INDICATION,
1106        oid::SIGNER_LOCATION,
1107        oid::SIGNER_ATTRIBUTES_V2,
1108        oid::SIGNATURE_POLICY_IDENTIFIER,
1109        oid::CONTENT_REFERENCE,
1110        oid::CONTENT_IDENTIFIER,
1111        oid::SIGNATURE_POLICY_STORE,
1112    ] {
1113        let set = if kind == oid::SIGNATURE_POLICY_STORE {
1114            signer.unsigned_attributes()
1115        } else {
1116            protected
1117        };
1118        present(document, set, kind, parent, findings)?;
1119    }
1120    Ok(())
1121}
1122
1123fn unsigned_rules<'m, 'd, W: Work>(
1124    document: &Document<'m, 'd>,
1125    signer: &SignerInfo<'_>,
1126    requested: Level,
1127    parent: &Reader<'_>,
1128    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
1129) -> Result<()> {
1130    let mut signature_times = 0;
1131    let mut archives = 0;
1132    if let Some(unsigned) = signer.unsigned_attributes() {
1133        for attribute in unsigned.items() {
1134            parent
1135                .charge(1 + attribute.kind().as_bytes().len() as u64)
1136                .map_err(|e| Error::encoding(e, FIELD))?;
1137            if attribute.kind() == oid::SIGNATURE_TIMESTAMP {
1138                signature_times += 1;
1139            }
1140            if attribute.kind() == oid::ARCHIVE_TIMESTAMP_V3 {
1141                archives += 1;
1142            }
1143            // Table1 '*' is SHOULD NOT, never a failed SHALL. Count these
1144            // same original occurrences for mandatory higher levels too.
1145            if (attribute.kind() == oid::SIGNATURE_TIMESTAMP && requested == Level::Basic)
1146                || (attribute.kind() == oid::ARCHIVE_TIMESTAMP_V3 && requested < Level::Archival)
1147            {
1148                finding(
1149                    document,
1150                    attribute.span(),
1151                    Severity::Advisory,
1152                    Rule::Forbidden(if attribute.kind() == oid::SIGNATURE_TIMESTAMP {
1153                        oid::SIGNATURE_TIMESTAMP
1154                    } else {
1155                        oid::ARCHIVE_TIMESTAMP_V3
1156                    }),
1157                    findings,
1158                )?;
1159            }
1160        }
1161    }
1162    if requested >= Level::Timestamped && signature_times == 0 {
1163        finding(
1164            document,
1165            signer.span(),
1166            Severity::Required,
1167            Rule::SignatureTimestamp,
1168            findings,
1169        )?;
1170    }
1171    if requested == Level::Archival && archives == 0 {
1172        finding(
1173            document,
1174            signer.span(),
1175            Severity::Required,
1176            Rule::ArchiveTimestamp,
1177            findings,
1178        )?;
1179    }
1180    Ok(())
1181}
1182
1183fn present<'m, 'd, W: Work>(
1184    document: &Document<'m, 'd>,
1185    set: Option<&Attributes<'_>>,
1186    kind: structured_asn1::Oid<'static>,
1187    parent: &Reader<'_>,
1188    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
1189) -> Result<bool> {
1190    let Some(set) = set else {
1191        return Ok(false);
1192    };
1193    match set.unique_in(kind, parent) {
1194        Ok(value) => Ok(value.is_some()),
1195        Err(cause) if cause.is_terminal() => Err(Error::Cms(cause)),
1196        Err(cause) => {
1197            // unique_in's nonterminal refusal proves repeated presence, not
1198            // absence. Retain the violation without truncating the original
1199            // counter/timestamp census or accepting either duplicate value.
1200            finding(
1201                document,
1202                Span {
1203                    start: cause.offset(),
1204                    length: 0,
1205                },
1206                Severity::Required,
1207                Rule::Schema {
1208                    code: cause.code(),
1209                    field: cause.field(),
1210                },
1211                findings,
1212            )?;
1213            Ok(true)
1214        }
1215    }
1216}
1217
1218fn finding<'m, 'd, W: Work>(
1219    document: &Document<'m, 'd>,
1220    span: Span,
1221    severity: Severity,
1222    rule: Rule,
1223    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
1224) -> Result<()> {
1225    findings
1226        .try_push(Finding {
1227            location: document.locate(span),
1228            severity,
1229            rule,
1230        })
1231        .map_err(|cause| Error::resource(cause, FIELD, span.start))
1232}
1233
1234fn collect_timestamps<'a, W: Work>(
1235    signer: &SignerInfo<'a>,
1236    parent: &Reader<'a>,
1237    out: &mut OwnedVec<Timestamp<'a>, W>,
1238) -> Result<()> {
1239    for set in [signer.signed_attributes(), signer.unsigned_attributes()]
1240        .into_iter()
1241        .flatten()
1242    {
1243        for attribute in set.items() {
1244            parent
1245                .charge(3 * (1 + attribute.kind().as_bytes().len().min(12) as u64))
1246                .map_err(|e| Error::encoding(e, FIELD))?;
1247            let purpose = if attribute.kind() == oid::CONTENT_TIMESTAMP {
1248                Purpose::Content
1249            } else if attribute.kind() == oid::SIGNATURE_TIMESTAMP {
1250                Purpose::Signature
1251            } else if attribute.kind() == oid::ARCHIVE_TIMESTAMP_V3 {
1252                Purpose::Archive
1253            } else {
1254                continue;
1255            };
1256            if attribute.values().is_empty() {
1257                out.try_push(Timestamp {
1258                    owner: Node::of(signer.span()),
1259                    property: Node::of(attribute.span()),
1260                    value: None,
1261                    purpose,
1262                    preservation_kind: None,
1263                })
1264                .map_err(|cause| Error::resource(cause, FIELD, attribute.span().start))?;
1265            }
1266            for (index, value) in attribute.values().iter().enumerate() {
1267                let source =
1268                    attribute
1269                        .value_reader_in(index, parent, FIELD)?
1270                        .ok_or(Error::Foreign {
1271                            at: value.span().start,
1272                        })?;
1273                out.try_push(Timestamp {
1274                    owner: Node::of(signer.span()),
1275                    property: Node::of(attribute.span()),
1276                    value: Some(crate::source::Value::of(&source, *value, FIELD)),
1277                    purpose,
1278                    preservation_kind: None,
1279                })
1280                .map_err(|cause| Error::resource(cause, FIELD, value.span().start))?;
1281            }
1282        }
1283    }
1284    Ok(())
1285}
1286
1287fn collect_historical_timestamps<'d, W: Work>(
1288    required: &[crate::preservation::Required<'d>],
1289    parent: &Reader<'d>,
1290    timestamps: &mut OwnedVec<Timestamp<'d>, W>,
1291) -> Result<()> {
1292    for entry in required {
1293        parent
1294            .charge(1)
1295            .map_err(|cause| Error::encoding(cause, FIELD))?;
1296        if !entry.kind.is_archive() && !entry.kind.is_reference_timestamp() {
1297            continue;
1298        }
1299        timestamps
1300            .try_push(Timestamp {
1301                owner: entry.signature,
1302                property: entry.property,
1303                value: entry.value,
1304                purpose: if entry.kind.is_archive() {
1305                    Purpose::Archive
1306                } else {
1307                    Purpose::ValidationData
1308                },
1309                preservation_kind: Some(entry.kind),
1310            })
1311            .map_err(|cause| Error::resource(cause, FIELD, entry.property.span().start))?;
1312    }
1313    Ok(())
1314}
1315
1316struct CounterCatalogue {
1317    children: OwnedVec<Node>,
1318    index: OwnedMap<Node, usize>,
1319    conforming: bool,
1320}
1321fn counter_catalogue(
1322    counters: &[Counter<'_>],
1323    parent: &Reader<'_>,
1324    signature: Node,
1325) -> Result<CounterCatalogue> {
1326    let work = parent.execution();
1327    let mut children = work.vector();
1328    children
1329        .try_reserve_exact(counters.len())
1330        .map_err(|cause| Error::resource(cause, FIELD, signature.span().start))?;
1331    let mut index = work.map();
1332    let mut conforming = true;
1333    for (ordinal, counter) in counters.iter().enumerate() {
1334        work.charge_work(1)
1335            .map_err(|cause| Error::resource(cause, FIELD, counter.node.span().start))?;
1336        if counter.malformed.is_some() {
1337            conforming = false;
1338        }
1339        children
1340            .try_push(counter.node)
1341            .map_err(|cause| Error::resource(cause, FIELD, counter.node.span().start))?;
1342        if index
1343            .try_insert(counter.node, ordinal)
1344            .map_err(|cause| Error::resource(cause, FIELD, counter.node.span().start))?
1345            .is_some()
1346        {
1347            return Err(Error::Foreign {
1348                at: counter.node.span().start,
1349            });
1350        }
1351    }
1352    Ok(CounterCatalogue {
1353        children: children.into_owned(),
1354        index: index.into_owned(),
1355        conforming,
1356    })
1357}
1358
1359fn timestamp_ranges(
1360    timestamps: &[Timestamp<'_>],
1361    parent: &Reader<'_>,
1362) -> Result<OwnedMap<Node, core::ops::Range<usize>>> {
1363    let work = parent.execution();
1364    let mut ranges = work.map();
1365    for (ordinal, entry) in timestamps.iter().enumerate() {
1366        let resource = |cause| Error::resource(cause, FIELD, entry.property.span().start);
1367        work.charge_work(1).map_err(resource)?;
1368        if let Some(previous) = ranges.get_mut(&entry.property, &work).map_err(resource)? {
1369            let previous: &mut core::ops::Range<usize> = previous;
1370            // Each source attribute is collected once, in its original value
1371            // order. A range must never include another attribute's values.
1372            if previous.end != ordinal {
1373                return Err(Error::Foreign {
1374                    at: entry.property.span().start,
1375                });
1376            }
1377            previous.end += 1;
1378        } else {
1379            ranges
1380                .try_insert(entry.property, ordinal..ordinal + 1)
1381                .map_err(resource)?;
1382        }
1383    }
1384    Ok(ranges.into_owned())
1385}
1386
1387fn history_timestamps<'d, W: Work>(
1388    preservation: &[crate::preservation::Required<'d>],
1389    timestamps: &mut OwnedVec<Timestamp<'d>, W>,
1390    parallel: &mut Option<Parallel<'d>>,
1391    parent: &Reader<'d>,
1392    signer: Node,
1393) -> Result<Option<OwnedMap<Node, core::ops::Range<usize>>>> {
1394    let execution = parent.execution();
1395    let mut historical = false;
1396    for required in preservation {
1397        execution
1398            .charge_work(1)
1399            .map_err(|cause| Error::resource(cause, FIELD, required.property.span().start))?;
1400        if required.kind.is_archive() || required.kind.is_reference_timestamp() {
1401            historical = true;
1402            break;
1403        }
1404    }
1405    if historical
1406        && let Some(parallel) = parallel
1407        && let Some(extra) = parallel.timestamps.take()
1408    {
1409        execution
1410            .charge_work(extra.len() as u64)
1411            .map_err(|cause| Error::resource(cause, FIELD, signer.span().start))?;
1412        for entry in extra {
1413            timestamps
1414                .try_push(entry)
1415                .map_err(|cause| Error::resource(cause, FIELD, signer.span().start))?;
1416        }
1417    }
1418    collect_historical_timestamps(preservation, parent, timestamps)?;
1419    if historical {
1420        Ok(Some(timestamp_ranges(timestamps, parent)?))
1421    } else {
1422        Ok(None)
1423    }
1424}
1425
1426fn parallel_lookup(
1427    counters: Option<&[Counter<'_>]>,
1428    parent: &Reader<'_>,
1429) -> Result<Option<OwnedMap<Node, usize>>> {
1430    let Some(counters) = counters else {
1431        return Ok(None);
1432    };
1433    let work = parent.execution();
1434    let mut lookup = work.map();
1435    for (ordinal, counter) in counters.iter().enumerate() {
1436        if lookup
1437            .try_insert(counter.node, ordinal)
1438            .map_err(|cause| Error::resource(cause, FIELD, counter.node.span().start))?
1439            .is_some()
1440        {
1441            return Err(Error::Foreign {
1442                at: counter.node.span().start,
1443            });
1444        }
1445    }
1446    Ok(Some(lookup.into_owned()))
1447}
1448
1449fn read_parallel<'d, W: Work>(
1450    document: &Document<'_, 'd>,
1451    selected: &SignerInfo<'d>,
1452    parent: &Reader<'d>,
1453    signatures: &mut OwnedVec<structured_cms::octets::Piece<'d>, W>,
1454    preservation: &mut OwnedVec<crate::preservation::Required<'d>, W>,
1455) -> Result<Option<Parallel<'d>>> {
1456    let work = parent.execution();
1457    let mut counters = None;
1458    let mut timestamps = None;
1459    for root in document.message().signers() {
1460        work.charge_work(1)
1461            .map_err(|cause| Error::resource(cause, FIELD, root.span().start))?;
1462        if core::ptr::eq(root, selected) {
1463            continue;
1464        }
1465        collect_timestamps(
1466            root,
1467            parent,
1468            timestamps.get_or_insert_with(|| work.vector()),
1469        )?;
1470        let visited = counter::walk(root, parent, |visit, reader| -> Result<()> {
1471            let entry = match visit {
1472                Visit::Signer {
1473                    signer,
1474                    parent: owner,
1475                    ..
1476                } => {
1477                    crate::preservation::collect(signer, reader, preservation)?;
1478                    collect_timestamps(
1479                        signer,
1480                        reader,
1481                        timestamps.get_or_insert_with(|| work.vector()),
1482                    )?;
1483                    let start = signatures.len();
1484                    signatures
1485                        .try_extend_from_slice(signer.signature().pieces())
1486                        .map_err(|cause| Error::resource(cause, FIELD, signer.span().start))?;
1487                    let properties =
1488                        if crate::properties::Properties::has_legacy_values(signer, reader)? {
1489                            Some(crate::properties::Properties::read(signer, reader)?)
1490                        } else {
1491                            None
1492                        };
1493                    Counter {
1494                        parent: Node::of(owner),
1495                        node: Node::of(signer.span()),
1496                        malformed: None,
1497                        signature: start..signatures.len(),
1498                        properties,
1499                        encoding: Some(encoding::SignerEncoding::read(signer, reader)?),
1500                    }
1501                }
1502                Visit::Malformed {
1503                    source,
1504                    parent: owner,
1505                } => Counter {
1506                    parent: Node::of(owner),
1507                    node: Node::of(Span {
1508                        start: source.offset(),
1509                        length: 0,
1510                    }),
1511                    malformed: Some(source),
1512                    signature: signatures.len()..signatures.len(),
1513                    properties: None,
1514                    encoding: None,
1515                },
1516            };
1517            let at = entry.node.span().start;
1518            counters
1519                .get_or_insert_with(|| work.vector())
1520                .try_push(entry)
1521                .map_err(|cause| Error::resource(cause, FIELD, at))
1522        })
1523        .map_err(|cause| match cause {
1524            WalkError::Processing(cause) => Error::Cms(cause),
1525            WalkError::Callback { source, .. } => source,
1526        })?;
1527        drop(visited);
1528    }
1529    let timestamps = timestamps
1530        .filter(|rows| !rows.is_empty())
1531        .map(OwnedVec::into_owned);
1532    let index = parallel_lookup(counters.as_deref(), parent)?;
1533    let counters = counters.map(OwnedVec::into_owned);
1534    if counters.is_none() && timestamps.is_none() {
1535        return Ok(None);
1536    }
1537    Ok(Some(Parallel {
1538        counters,
1539        index,
1540        timestamps,
1541    }))
1542}
1543
1544fn parallel_findings<'m, 'd>(
1545    document: &Document<'m, 'd>,
1546    parallel: Option<&Parallel<'d>>,
1547    parsed: &mut tokens::Cache<'m, 'd>,
1548    parent: &Reader<'d>,
1549) -> Result<()> {
1550    let Some(counters) = parallel.and_then(|parallel| parallel.counters.as_deref()) else {
1551        return Ok(());
1552    };
1553    let work = parent.execution();
1554    let mut related = None;
1555    for counter in counters {
1556        work.charge_work(1)
1557            .map_err(|cause| Error::resource(cause, FIELD, counter.node.span().start))?;
1558        let Some(cause) = counter.malformed() else {
1559            continue;
1560        };
1561        if related.is_none() {
1562            related = Some(match parsed.related.take() {
1563                Some(saved) => saved.with_execution(&work).map_err(|_| Error::Foreign {
1564                    at: counter.node.span().start,
1565                })?,
1566                None => work.vector(),
1567            });
1568        }
1569        // The full history census cannot treat an unreadable source as an
1570        // empty set of historical properties. Keep the original cause on the
1571        // counter and its negative finding SEPARATE from selected B-B syntax.
1572        finding(
1573            document,
1574            counter.node.span(),
1575            Severity::Required,
1576            Rule::Schema {
1577                code: cause.code(),
1578                field: cause.field(),
1579            },
1580            related.as_mut().expect("created for this original finding"),
1581        )?;
1582    }
1583    if let Some(related) = related {
1584        parsed.related = Some(related.into_owned());
1585    }
1586    Ok(())
1587}
1588
1589fn read_counters<'m, 'd, W: Work>(
1590    document: &Document<'m, 'd>,
1591    signer: &SignerInfo<'d>,
1592    parent: &Reader<'d>,
1593    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
1594    timestamps: &mut OwnedVec<Timestamp<'d>, W>,
1595    signatures: &mut OwnedVec<structured_cms::octets::Piece<'d>, W>,
1596    preservation: &mut OwnedVec<crate::preservation::Required<'d>, W>,
1597) -> Result<OwnedVec<Counter<'d>>> {
1598    counter::walk(signer, parent, |visit, reader| -> Result<Counter<'d>> {
1599        match visit {
1600            Visit::Signer {
1601                signer: child,
1602                parent: owner,
1603                ..
1604            } => {
1605                crate::preservation::collect(child, reader, preservation)?;
1606                let profile =
1607                    structured_cms::cades::inspect_profile(child, Role::Countersignature, reader)?;
1608                schema_findings(document, child, &profile, reader, findings)?;
1609                // RFC5652 §11.4 gives counters their own protected-field
1610                // rules. Inspect them before keys/math, using the same kernel
1611                // as actual counter verification; preserve readable siblings.
1612                // https://www.rfc-editor.org/rfc/rfc5652#section-11.4
1613                binding_findings(
1614                    document,
1615                    child,
1616                    structured_cms::binding::Scope::CounterSignature,
1617                    reader,
1618                    findings,
1619                )?;
1620                let properties = crate::properties::Properties::read(child, reader)?;
1621                property_findings(document, &properties, reader, findings)?;
1622                mechanism_findings(document, child, &profile, &properties, reader, findings)?;
1623                legacy_rules(document, child, &properties, None, reader, findings)?;
1624                collect_timestamps(child, reader, timestamps)?;
1625                let start = signatures.len();
1626                signatures
1627                    .try_extend_from_slice(child.signature().pieces())
1628                    .map_err(|cause| Error::resource(cause, FIELD, child.span().start))?;
1629                let end = signatures.len();
1630                Ok(Counter {
1631                    parent: Node::of(owner),
1632                    node: Node::of(child.span()),
1633                    malformed: None,
1634                    signature: start..end,
1635                    properties: Some(properties),
1636                    encoding: Some(encoding::SignerEncoding::read(child, reader)?),
1637                })
1638            }
1639            Visit::Malformed {
1640                source,
1641                parent: owner,
1642            } => {
1643                let at = source.offset();
1644                Ok(Counter {
1645                    parent: Node::of(owner),
1646                    node: Node::of(Span {
1647                        start: at,
1648                        length: 0,
1649                    }),
1650                    malformed: Some(source),
1651                    signature: signatures.len()..signatures.len(),
1652                    properties: None,
1653                    encoding: None,
1654                })
1655            }
1656        }
1657    })
1658    .map_err(|error| match error {
1659        WalkError::Processing(error) => Error::Cms(error),
1660        WalkError::Callback { source: error, .. } => error,
1661    })
1662}
1663
1664fn token_obligations<W: Work>(
1665    timestamps: &[Timestamp<'_>],
1666    execution: &W,
1667    at: u64,
1668) -> Result<OwnedVec<RequiredToken<Node>, W>> {
1669    let mut tokens = execution.vector();
1670    tokens
1671        .try_reserve_exact(timestamps.len())
1672        .map_err(|cause| Error::resource(cause, FIELD, at))?;
1673    for entry in timestamps {
1674        tokens
1675            .try_push(RequiredToken {
1676                signature: entry.owner,
1677                property: entry.property,
1678                token: entry
1679                    .value
1680                    .map_or(entry.property, |value| Node::of(value.element().span())),
1681                purpose: entry.purpose,
1682            })
1683            .map_err(|cause| Error::resource(cause, FIELD, entry.property.span().start))?;
1684    }
1685    Ok(tokens)
1686}