Skip to main content

structured_xades/timestamp/
selection.rs

1//! Complete original timestamp requirements for LT and one archive boundary.
2
3use super::{InputError, Role};
4use crate::binary::Location;
5use crate::prepare::Prepared;
6use crate::xml::{Access, Xml};
7use crate::{ISSUER, Xades};
8use structured_asn1::memory::{MemoryError, Work};
9use structured_ltv::timestamp::{RequiredToken, TimestampRequirements, TimestampScope};
10
11/// Protocol scope to derive from complete original preparation, not a policy
12/// switch or permission to omit failed timestamps from an attained level.
13#[derive(Debug, Clone, Copy)]
14pub enum TimestampSelection {
15    /// Every incorporated token, including archive tokens and later timestamps.
16    All,
17    /// Tokens preceding this archive ordinal in the prepared timestamp set.
18    BeforeArchive(usize),
19}
20
21impl TimestampSelection {
22    fn includes(self, ordinal: usize, role: Role) -> bool {
23        match self {
24            Self::All => true,
25            Self::BeforeArchive(archive) => {
26                ordinal < archive
27                    || matches!(role, Role::AllDataObjects | Role::IndividualDataObjects)
28            }
29        }
30    }
31}
32
33/// Source refusal and caller selection errors are not failed token assertions.
34#[derive(Debug)]
35pub enum RequirementError<E, N> {
36    /// Original source, generation or current-operation refusal.
37    Input(InputError<E, N>),
38    /// Original archive input coverage could not be selected completely.
39    Coverage(super::CoverageError<E, N>),
40    /// Archive coverage of a countersignature could not be tested.
41    Selection(structured_ltv::material::CoverageError<N>),
42    /// No timestamp exists at this prepared catalogue ordinal.
43    Index {
44        /// Original selected signature.
45        signature: N,
46        /// Caller-provided ordinal, not an XML offset.
47        index: usize,
48    },
49    /// The selected original timestamp is not an archive.
50    Role {
51        /// Original selected property.
52        property: N,
53    },
54}
55
56impl<E, N> From<InputError<E, N>> for RequirementError<E, N> {
57    fn from(cause: InputError<E, N>) -> Self {
58        Self::Input(cause)
59    }
60}
61
62/// Timestamp census using this XML owner's original identity/error domain.
63pub type RequirementResult<X> = Result<
64    TimestampRequirements<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation, Xades>,
65    RequirementError<<X as Xml>::Error, <X as Xml>::Id>,
66>;
67
68type Entries<X, W> = structured_asn1::memory::OwnedVec<RequiredToken<<X as Xml>::Id>, W>;
69
70/// Derive this selected signature's OWN original token set without parsing or
71/// cloning token bytes. Use [`requirements_with_counters`] for a whole actor
72/// census; the participant census refuses this root-only set when counters
73/// are present, even when their timestamps have no successful proof yet.
74/// EN319132-1 ยง6.3(x) requires incorporated timestamp validation data;6.3(aa)
75/// requires earlier material before EACH archive, excluding its own tokens.
76/// Signed data-object timestamps precede unsigned archives even though they
77/// are prepared after the unsigned-property pass. Both scopes use ONE selector,
78/// two paid metadata passes and one admitted exact output allocation.
79/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
80///
81/// This derives obligations only, not a baseline grade, temporal/trust verdict
82/// or usable material. An unsupported/failed token must still be accounted for.
83pub fn requirements<X: Xml, W: Work>(
84    access: &Access<'_, X, W>,
85    prepared: &Prepared<X>,
86    selection: TimestampSelection,
87) -> RequirementResult<X> {
88    let (scope, entries) = root_entries(access, prepared, selection)?;
89    let signature = prepared.bound().containers().signature();
90    Ok(TimestampRequirements::new(
91        ISSUER,
92        signature,
93        scope,
94        entries.into_owned(),
95    ))
96}
97
98type RootEntries<X, W> = Result<
99    (
100        TimestampScope<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
101        Entries<X, W>,
102    ),
103    RequirementError<<X as Xml>::Error, <X as Xml>::Id>,
104>;
105
106fn root_entries<X: Xml, W: Work>(
107    access: &Access<'_, X, W>,
108    prepared: &Prepared<X>,
109    selection: TimestampSelection,
110) -> RootEntries<X, W> {
111    let layout = prepared.bound().containers();
112    let signature = layout.signature();
113    if layout.snapshot() != access.snapshot() {
114        return Err(InputError::Foreign { node: signature }.into());
115    }
116    let pay = || {
117        access
118            .query(signature, |_, _, work| Ok(work.charge_work(1)))
119            .map_err(InputError::Access)?
120            .map_err(|cause| InputError::Resource {
121                node: signature,
122                cause,
123            })
124    };
125    pay()?;
126    let scope = match selection {
127        TimestampSelection::All => TimestampScope::All(Location {
128            snapshot: access.snapshot(),
129            node: signature,
130        }),
131        TimestampSelection::BeforeArchive(index) => {
132            let Some(selected) = prepared.timestamps().get(index) else {
133                return Err(RequirementError::Index { signature, index });
134            };
135            if selected.role() != Role::Archive {
136                return Err(RequirementError::Role {
137                    property: selected.node(),
138                });
139            }
140            TimestampScope::BeforeArchive(Location {
141                snapshot: access.snapshot(),
142                node: selected.node(),
143            })
144        }
145    };
146    let resource = |cause| InputError::Resource {
147        node: scope.location().node,
148        cause,
149    };
150    let mut count = 0usize;
151    for (ordinal, stamp) in prepared.timestamps().iter().enumerate() {
152        pay()?;
153        if selection.includes(ordinal, stamp.role()) {
154            count = count
155                .checked_add(stamp.tokens().len())
156                .ok_or_else(|| resource(MemoryError::WorkExceeded))?;
157        }
158    }
159    let mut entries = access.execution().vector();
160    entries.try_reserve_exact(count).map_err(resource)?;
161    for (ordinal, stamp) in prepared.timestamps().iter().enumerate() {
162        pay()?;
163        if !selection.includes(ordinal, stamp.role()) {
164            continue;
165        }
166        for token in stamp.tokens() {
167            pay()?;
168            entries
169                .try_push(RequiredToken {
170                    signature,
171                    property: stamp.node(),
172                    token: *token,
173                    purpose: stamp.role().purpose(),
174                })
175                .map_err(resource)?;
176        }
177    }
178    access
179        .query(signature, |_, _, _| Ok(()))
180        .map_err(InputError::Access)?;
181    Ok((scope, entries))
182}
183
184/// Derive complete root AND nested-counter token obligations. For an archive,
185/// child properties enter only when its actual original input selects their
186/// whole element; a later countersignature cannot become an earlier TSA actor.
187/// This reuses the root input/coverage kernel, not XML ordinal arithmetic or a
188/// caller-written list of successful timestamps. No canonicalization is run.
189pub fn requirements_with_counters<X: super::TimestampXml, W: Work>(
190    access: &Access<'_, X, W>,
191    prepared: &Prepared<X>,
192    counters: &super::CounterTimestamps<X::Id, X::Document, X::Generation>,
193    selection: TimestampSelection,
194) -> RequirementResult<X> {
195    let signature = prepared.bound().containers().signature();
196    let resource = |cause| InputError::Resource {
197        node: signature,
198        cause,
199    };
200    if counters.snapshot != access.snapshot()
201        || !access
202            .equal_id(counters.signature, signature)
203            .map_err(resource)?
204    {
205        return Err(InputError::Foreign { node: signature }.into());
206    }
207    let (scope, mut entries) = root_entries(access, prepared, selection)?;
208    let mut signatures = access.execution().vector();
209    signatures
210        .try_extend_from_slice(&counters.signatures)
211        .map_err(resource)?;
212    let coverage = match selection {
213        TimestampSelection::All => None,
214        TimestampSelection::BeforeArchive(index) => {
215            // The root kernel above has already checked ordinal and role.
216            let input = super::Input::prepare(
217                access.owner(),
218                prepared.bound(),
219                &prepared.timestamps()[index],
220                access.execution(),
221            )?;
222            Some(
223                input
224                    .element_coverage(access.execution())
225                    .map_err(RequirementError::Coverage)?,
226            )
227        }
228    };
229    // EN319132-1 6.3(aa) describes material present BEFORE generating this
230    // archive. 5.5.2.1 note4 permits later countersignatures. Their own actors
231    // and credentials are not antecedents merely because the final LT census
232    // contains them. Reuse exactly the existing original element coverage.
233    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
234    let covered = if let Some(coverage) = &coverage {
235        let mut mask = access.execution().vector();
236        let mut index = 0;
237        while index < signatures.len() {
238            access
239                .query(signature, |_, _, work| Ok(work.charge_work(1)))
240                .map_err(InputError::Access)?
241                .map_err(resource)?;
242            let included = coverage
243                .selects(signatures[index], access)
244                .map_err(RequirementError::Selection)?;
245            mask.try_push(included).map_err(resource)?;
246            index += 1;
247        }
248        Some(mask.into_owned())
249    } else {
250        None
251    };
252    for child in counters.entries() {
253        let stamp = child.stamp();
254        access
255            .query(stamp.node(), |_, _, work| Ok(work.charge_work(1)))
256            .map_err(InputError::Access)?
257            .map_err(resource)?;
258        if let Some(coverage) = &coverage
259            && !coverage
260                .selects(stamp.node(), access)
261                .map_err(RequirementError::Selection)?
262        {
263            continue;
264        }
265        for token in stamp.tokens() {
266            access
267                .query(*token, |_, _, work| Ok(work.charge_work(1)))
268                .map_err(InputError::Access)?
269                .map_err(resource)?;
270            entries
271                .try_push(RequiredToken {
272                    signature: stamp.signature,
273                    property: stamp.node(),
274                    token: *token,
275                    purpose: stamp.role().purpose(),
276                })
277                .map_err(resource)?;
278        }
279    }
280    access
281        .query(signature, |_, _, _| Ok(()))
282        .map_err(InputError::Access)?;
283    Ok(TimestampRequirements::with_counters(
284        ISSUER,
285        signature,
286        scope,
287        entries.into_owned(),
288        signatures.into_owned(),
289        covered,
290    ))
291}