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