Skip to main content

structured_pades/timestamp/
requirements.rs

1//! Complete CMS and PDF timestamp inputs, including unsuccessful occurrences.
2
3use super::{ISSUER, Node, Pades};
4use crate::{
5    basic::Basic,
6    document::Document,
7    error::{Error, ErrorKind},
8    model::admit,
9    range::RangeSource,
10    signature::{Candidate, Coverage, Failure, Signature, SubFilter},
11};
12use structured_asn1::{
13    Span,
14    memory::{OwnedVec, Work},
15};
16use structured_ltv::{
17    pki::CounterReceipts,
18    source::Location as Original,
19    timestamp::{CheckedActor, Purpose, RequiredToken, TimestampRequirements, TimestampScope},
20};
21
22/// Complete original timestamp census or the original PDF/source refusal.
23pub type RequirementsRead<'a, 's, D, G, E> =
24    Result<Requirements<'a, 's, D, G>, crate::error::ReadFailure<E>>;
25
26/// The whole actual root/counter token census followed by every document
27/// timestamp in the original document. Earlier DTS authorities remain
28/// preceding-material obligations even when they cannot prove this root.
29pub struct Requirements<'a, 's, D, G> {
30    all: TimestampRequirements<Node, D, G, Pades>,
31    pdf: OwnedVec<DocumentTimestamp<'a, 's, D, G>>,
32    counters: OwnedVec<Node>,
33    pub(super) root: Signature<'a, 's, D, G>,
34    revision: Option<usize>,
35    vri: OwnedVec<VriTimestamp>,
36    total_pdf: usize,
37}
38
39/// One actual VRI TS occurrence in its historical DSS interpretation window.
40#[derive(Debug, Clone, Copy)]
41pub struct VriTimestamp {
42    /// Original ordinal across all historical VRI timestamp occurrences.
43    pub ordinal: usize,
44    /// Historical DSS window, not the latest file revision.
45    pub revision: usize,
46    /// VRI dictionary ordinal in that same original Validation owner.
47    pub vri: usize,
48    /// TS stream ordinal in that Validation owner's retained item pool.
49    pub item: usize,
50}
51
52/// One actual PDF timestamp candidate, including an unreadable source input.
53pub enum DocumentTimestamp<'a, 's, D, G> {
54    /// Original dictionary has readable exact ranges and Contents.
55    Readable(Signature<'a, 's, D, G>),
56    /// Original candidate has no usable replacement signature handle.
57    Unreadable(&'a Failure),
58}
59impl<'a, 's, D: Copy + Eq, G: Copy + Eq> DocumentTimestamp<'a, 's, D, G> {
60    /// Exact source dictionary, independently of readable required fields.
61    #[must_use]
62    pub fn location(&self) -> crate::source::Location {
63        match self {
64            Self::Readable(value) => value.location(),
65            Self::Unreadable(value) => value.location,
66        }
67    }
68    /// Actual signature handle only when its required fields were readable.
69    #[must_use]
70    pub fn signature(&self) -> Option<Signature<'a, 's, D, G>> {
71        match self {
72            Self::Readable(value) => Some(*value),
73            Self::Unreadable(_) => None,
74        }
75    }
76    /// Original required-value failure; never a successful timestamp absence.
77    #[must_use]
78    pub fn failure(&self) -> Option<&'a Failure> {
79        match self {
80            Self::Readable(_) => None,
81            Self::Unreadable(value) => Some(value),
82        }
83    }
84    pub(crate) fn token(&self) -> Node {
85        match self {
86            Self::Readable(value) => Node::of(value.contents()),
87            Self::Unreadable(value) => match value.contents {
88                Some(location) => Node::of(location),
89                None => Node::missing_contents(value.location),
90            },
91        }
92    }
93}
94impl<'a, 's, D: Copy + Eq, G: Copy + Eq> Requirements<'a, 's, D, G> {
95    /// Actual original PDF signature from which this entire census was derived.
96    #[must_use]
97    pub fn signature(&self) -> Signature<'a, 's, D, G> {
98        self.root
99    }
100    /// Entire original set accepted by the common all-token TSA path binder.
101    #[must_use]
102    pub fn all(&self) -> &TimestampRequirements<Node, D, G, Pades> {
103        &self.all
104    }
105    /// Source-derived PDF timestamp dictionaries in physical source order.
106    /// These are not selected by successful cryptographic checks.
107    #[must_use]
108    pub fn document_timestamps(&self) -> &[DocumentTimestamp<'a, 's, D, G>] {
109        &self.pdf
110    }
111
112    /// Explicit historical assessment window, absent for the current census.
113    /// The source ordinal remains its position in the complete original set.
114    #[must_use]
115    pub fn revision(&self) -> Option<usize> {
116        self.revision
117    }
118    /// Every original VRI TS, including an unreadable carried token. The
119    /// matching source model belongs to this exact historical owner.
120    #[must_use]
121    pub fn vri_timestamps(&self) -> &[VriTimestamp] {
122        &self.vri
123    }
124    pub(crate) fn total_pdf(&self) -> usize {
125        self.total_pdf
126    }
127
128    /// Derives the exact timestamp census visible at one original DTS revision.
129    /// A malformed selected occurrence refuses this projection; it is never
130    /// replaced with an earlier readable or successful occurrence. The caller
131    /// retains this complete source census independently of the historical view.
132    pub fn at_source_revision(&self, ordinal: usize, operation: &impl Work) -> crate::Result<Self> {
133        let location = self.root.location();
134        let work = self.root.owner().value.session.query(operation);
135        admit(&work, location, 1)?;
136        if self.revision.is_some() {
137            return Err(Error {
138                kind: ErrorKind::Foreign,
139                location,
140            });
141        }
142        let selected = self
143            .pdf
144            .get(ordinal)
145            .and_then(DocumentTimestamp::signature)
146            .ok_or(Error {
147                kind: ErrorKind::Foreign,
148                location,
149            })?;
150        let Coverage::Revision(revision) = selected.coverage() else {
151            return Err(Error {
152                kind: ErrorKind::Syntax(crate::error::Syntax::Container),
153                location: selected.location(),
154            });
155        };
156        if !matches!(self.root.coverage(), Coverage::Revision(root) if root <= revision) {
157            return Err(Error {
158                kind: ErrorKind::Foreign,
159                location: selected.location(),
160            });
161        }
162        let mut entries = work.vector();
163        let mut pdf = work.vector();
164        // The CMS source is immutable inside the selected root; the following
165        // PDF entries preserve every occurrence, including unreadable siblings.
166        let prefix = self.all.entries().len() - self.pdf.len() - self.vri.len();
167        for entry in &self.all.entries()[..=prefix + ordinal] {
168            admit(&work, location, 1)?;
169            entries.try_push(*entry).map_err(|cause| Error {
170                kind: ErrorKind::Resource(cause),
171                location,
172            })?;
173        }
174        for original in &self.pdf[..=ordinal] {
175            admit(&work, original.location(), 1)?;
176            let original = match original {
177                DocumentTimestamp::Readable(value) => DocumentTimestamp::Readable(*value),
178                DocumentTimestamp::Unreadable(value) => DocumentTimestamp::Unreadable(value),
179            };
180            pdf.try_push(original).map_err(|cause| Error {
181                kind: ErrorKind::Resource(cause),
182                location,
183            })?;
184        }
185        let mut vri = work.vector();
186        for (index, original) in self.vri.iter().enumerate() {
187            admit(&work, location, 1)?;
188            if original.revision > revision {
189                continue;
190            }
191            entries
192                .try_push(self.all.entries()[prefix + self.pdf.len() + index])
193                .map_err(|cause| Error {
194                    kind: ErrorKind::Resource(cause),
195                    location,
196                })?;
197            vri.try_push(*original).map_err(|cause| Error {
198                kind: ErrorKind::Resource(cause),
199                location,
200            })?;
201        }
202        let mut counters = work.vector();
203        let mut kept = work.vector();
204        for counter in &self.counters {
205            admit(&work, location, 1)?;
206            counters.try_push(*counter).map_err(|cause| Error {
207                kind: ErrorKind::Resource(cause),
208                location,
209            })?;
210            kept.try_push(*counter).map_err(|cause| Error {
211                kind: ErrorKind::Resource(cause),
212                location,
213            })?;
214        }
215        let all = TimestampRequirements::with_counters(
216            ISSUER,
217            self.all.signature(),
218            self.all.scope(),
219            entries.into_owned(),
220            counters.into_owned(),
221            None,
222        );
223        Ok(Self {
224            all,
225            pdf: pdf.into_owned(),
226            counters: kept.into_owned(),
227            root: self.root,
228            revision: Some(revision),
229            vri: vri.into_owned(),
230            total_pdf: self.total_pdf,
231        })
232    }
233
234    pub(crate) fn first_existence_timestamp(
235        &self,
236        operation: &impl Work,
237    ) -> crate::Result<Option<Node>> {
238        let work = self.root.owner().value.session.query(operation);
239        for original in &self.pdf {
240            admit(&work, original.location(), 1)?;
241            let Some(signature) = original.signature() else {
242                continue;
243            };
244            if matches!(signature.coverage(), Coverage::Revision(_))
245                && covers(signature, self.root.contents().enclosing(), &work)?
246            {
247                return Ok(Some(Node::of(signature.location())));
248            }
249        }
250        Ok(None)
251    }
252    /// Complete earlier-token scope for one actual document timestamp.
253    /// Its own and later tokens are excluded by exact original input ranges;
254    /// no current-time success count or caller-provided mask selects actors.
255    pub fn before_archive(
256        &self,
257        ordinal: usize,
258        actor: &CheckedActor<'_, '_, Node, D, G, Pades>,
259        operation: &impl Work,
260    ) -> crate::Result<TimestampRequirements<Node, D, G, Pades>> {
261        let at = self.archive(ordinal, actor, operation)?;
262        let location = at.location();
263        let work = self.root.owner().value.session.query(operation);
264        let proof = actor.verified();
265        let mut entries = work.vector();
266        for entry in self.all.entries() {
267            admit(&work, location, 1)?;
268            if entry.token == proof.token().node {
269                continue;
270            }
271            if covers_node(at, entry.token, &work)? {
272                entries.try_push(*entry).map_err(|cause| Error {
273                    kind: ErrorKind::Resource(cause),
274                    location,
275                })?;
276            }
277        }
278        let mut counters = work.vector();
279        let mut included = work.vector();
280        for &counter in &self.counters {
281            admit(&work, location, 1)?;
282            counters.try_push(counter).map_err(|cause| Error {
283                kind: ErrorKind::Resource(cause),
284                location,
285            })?;
286            included
287                .try_push(covers(at, counter.span(), &work)?)
288                .map_err(|cause| Error {
289                    kind: ErrorKind::Resource(cause),
290                    location,
291                })?;
292        }
293        Ok(TimestampRequirements::with_counters(
294            ISSUER,
295            self.all.signature(),
296            TimestampScope::BeforeArchive(Original {
297                snapshot: proof.token().snapshot,
298                node: proof.token().node,
299            }),
300            entries.into_owned(),
301            counters.into_owned(),
302            Some(included.into_owned()),
303        ))
304    }
305
306    pub(super) fn archive(
307        &self,
308        ordinal: usize,
309        actor: &CheckedActor<'_, '_, Node, D, G, Pades>,
310        operation: &impl Work,
311    ) -> crate::Result<Signature<'a, 's, D, G>> {
312        let work = self.root.owner().value.session.query(operation);
313        admit(&work, self.root.location(), 1)?;
314        let at = self
315            .pdf
316            .get(ordinal)
317            .and_then(DocumentTimestamp::signature)
318            .ok_or(Error {
319                kind: ErrorKind::Foreign,
320                location: self.root.location(),
321            })?;
322        let proof = actor.verified();
323        if proof.token().snapshot != self.root.owner().value.stamp.snapshot
324            || proof.signature() != self.all.signature()
325            || proof.property() != Node::of(at.location())
326            || proof.token().node != Node::of(at.contents())
327            || proof.purpose() != Purpose::Archive
328        {
329            return Err(Error {
330                kind: ErrorKind::Foreign,
331                location: at.location(),
332            });
333        }
334        Ok(at)
335    }
336}
337
338pub(crate) fn covers_node<D: Copy + Eq, G: Copy + Eq>(
339    signature: Signature<'_, '_, D, G>,
340    node: Node,
341    work: &impl Work,
342) -> crate::Result<bool> {
343    admit(work, signature.location(), 1)?;
344    if let Some(material) = node.material_revision() {
345        // EN319142-1 ยงยง5.4.2/5.4.3 bind DSS interpretation and all its
346        // references/decoding controls to the authenticated revision. A full
347        // revision proof excludes only its own isolated Contents string, not
348        // a DSS stream. Using only the stream body's old span could accept a
349        // late xref/DSS/filter reassociation as already protected material.
350        // https://www.etsi.org/deliver/etsi_EN/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf
351        return Ok(
352            matches!(signature.coverage(), Coverage::Revision(revision) if material <= revision),
353        );
354    }
355    if node.is_missing_contents() {
356        return Ok(false);
357    }
358    covers(signature, node.span(), work)
359}
360
361pub(crate) fn covers<D: Copy + Eq, G: Copy + Eq>(
362    signature: Signature<'_, '_, D, G>,
363    item: Span,
364    work: &impl Work,
365) -> crate::Result<bool> {
366    for range in signature.ranges() {
367        admit(work, signature.location(), 1)?;
368        if range.start <= item.start
369            && item.start - range.start <= range.length
370            && item.length <= range.length - (item.start - range.start)
371        {
372            return Ok(true);
373        }
374    }
375    Ok(false)
376}
377
378fn document_timestamps<'a, 's, D: Copy + Eq, G: Copy + Eq, W: Work>(
379    root: Signature<'a, 's, D, G>,
380    mapping: crate::binding::Mapping<'_, '_, D, G>,
381    entries: &mut OwnedVec<RequiredToken<Node>, W>,
382    work: &W,
383) -> crate::Result<OwnedVec<DocumentTimestamp<'a, 's, D, G>, W>> {
384    let location = root.location();
385    let mut pdf = work.vector();
386    for index in 0..root.catalogue().candidates() {
387        admit(work, location, 1)?;
388        let timestamp = match root
389            .catalogue()
390            .candidate(index)
391            .expect("bounded complete census")
392        {
393            Candidate::Readable(timestamp) => {
394                if !timestamp.is_document_timestamp()
395                    && timestamp.sub_filter() != Some(SubFilter::Rfc3161)
396                {
397                    continue;
398                }
399                DocumentTimestamp::Readable(timestamp)
400            }
401            Candidate::Unreadable(timestamp) => {
402                if !timestamp.document_timestamp {
403                    continue;
404                }
405                DocumentTimestamp::Unreadable(timestamp)
406            }
407        };
408        let property = Node::of(timestamp.location());
409        entries
410            .try_push(RequiredToken {
411                signature: mapping.root,
412                property,
413                token: timestamp.token(),
414                purpose: Purpose::Archive,
415            })
416            .map_err(|cause| Error {
417                kind: ErrorKind::Resource(cause),
418                location: timestamp.location(),
419            })?;
420        let at = timestamp.location();
421        pdf.try_push(timestamp).map_err(|cause| Error {
422            kind: ErrorKind::Resource(cause),
423            location: at,
424        })?;
425    }
426    Ok(pdf)
427}
428
429fn project_requirements<D: Copy, G: Copy, W: Work>(
430    mapping: crate::binding::Mapping<'_, '_, D, G>,
431    source: &[RequiredToken<structured_cades::source::Node>],
432    entries: &mut OwnedVec<RequiredToken<Node>, W>,
433    location: crate::source::Location,
434    work: &impl Work,
435) -> crate::Result<()> {
436    for original in source {
437        admit(work, location, 1)?;
438        entries
439            .try_push(RequiredToken {
440                signature: mapping.node(original.signature),
441                property: mapping.node(original.property),
442                token: mapping.node(original.token),
443                purpose: original.purpose,
444            })
445            .map_err(|cause| Error {
446                kind: ErrorKind::Resource(cause),
447                location,
448            })?;
449    }
450    Ok(())
451}
452
453impl<'s, S: RangeSource> Document<'s, S> {
454    /// Derives all timestamp obligations from the sealed original preparation
455    /// and the same complete physical signature census as its selected root.
456    ///
457    /// # Panics
458    /// Panics if an internal candidate ordinal leaves the complete source census.
459    pub fn timestamp_requirements<'a, 'r, 'v, 'p, 'm, 'd, 'path, W: Work>(
460        &self,
461        basic: &Basic<'r, 'v, 'p, 'm, 'd, 'a, 's, 'path, S::Document, S::Generation>,
462        history: &crate::material::History<'_, 's, S::Document, S::Generation>,
463        operation: &W,
464    ) -> RequirementsRead<'a, 's, S::Document, S::Generation, S::Error> {
465        let bound = basic.binding();
466        let mapping = bound.mapping;
467        let prepared = bound.prepared();
468        let root = prepared.container.signature();
469        let location = root.location();
470        let work = self.session.query(operation);
471        if mapping.snapshot != self.stamp.snapshot
472            || !core::ptr::eq(root.owner().value.session, self.session)
473            || history.root.location() != root.location()
474            || history.root.owner().value.stamp != self.stamp
475            || !core::ptr::eq(history.root.owner().value.session, self.session)
476        {
477            return Err(Error {
478                kind: ErrorKind::Foreign,
479                location,
480            }
481            .into());
482        }
483        self.check(&work, location)?;
484        let mut entries = work.vector();
485        project_requirements(
486            mapping,
487            prepared.cms.timestamp_requirements().entries(),
488            &mut entries,
489            location,
490            &work,
491        )?;
492        let pdf = document_timestamps(root, mapping, &mut entries, &work)?;
493        let mut vri = work.vector();
494        for window in history.revisions() {
495            for (index, value) in window.vri().iter().enumerate() {
496                admit(&work, value.location(), 1)?;
497                let Some(item) = value.timestamp() else {
498                    continue;
499                };
500                let property = Node::dss(value.location(), window.revision());
501                let token = Node::dss(window.items()[item].location(), window.revision());
502                entries
503                    .try_push(RequiredToken {
504                        signature: mapping.root,
505                        property,
506                        token,
507                        purpose: Purpose::ValidationData,
508                    })
509                    .map_err(|cause| Error {
510                        kind: ErrorKind::Resource(cause),
511                        location: value.location(),
512                    })?;
513                vri.try_push(VriTimestamp {
514                    ordinal: vri.len(),
515                    revision: window.revision(),
516                    vri: index,
517                    item,
518                })
519                .map_err(|cause| Error {
520                    kind: ErrorKind::Resource(cause),
521                    location: value.location(),
522                })?;
523            }
524        }
525        let mut counters = work.vector();
526        let mut kept = work.vector();
527        for ordinal in 0..basic.counters().len() {
528            admit(&work, location, 1)?;
529            let node = basic.counters().counter(ordinal).signature;
530            counters.try_push(node).map_err(|cause| Error {
531                kind: ErrorKind::Resource(cause),
532                location,
533            })?;
534            kept.try_push(node).map_err(|cause| Error {
535                kind: ErrorKind::Resource(cause),
536                location,
537            })?;
538        }
539        self.check(&work, location)?;
540        let all = TimestampRequirements::with_counters(
541            ISSUER,
542            mapping.root,
543            TimestampScope::All(Original {
544                snapshot: mapping.snapshot,
545                node: mapping.root,
546            }),
547            entries.into_owned(),
548            counters.into_owned(),
549            None,
550        );
551        let total_pdf = pdf.len();
552        Ok(Requirements {
553            all,
554            pdf: pdf.into_owned(),
555            counters: kept.into_owned(),
556            root,
557            revision: None,
558            vri: vri.into_owned(),
559            total_pdf,
560        })
561    }
562}