Skip to main content

structured_xades/material/
timestamp_placement.rs

1//! Actual per-TSA validation material, not a global timestamp-purpose union.
2use super::{Index, Placement};
3use crate::Xades;
4use crate::xml::{Access, Snapshot, Xml};
5use structured_asn1::memory::{HashWork, MemoryError, OwnedVec, Work};
6use structured_ltv::material::status::{ArtifactVisitError, member_source, visit_artifacts};
7use structured_ltv::material::{ArtifactKey, ArtifactRef, IndexError};
8use structured_ltv::pki::{
9    AuthorityVisitError, Participant, ParticipantAuthority, ParticipantClosure, UsesError,
10};
11use structured_ltv::source::ScopeError;
12use structured_ltv::timestamp::TimestampScope;
13
14/// Why required original X.509 material has no eligible copy for this TSA.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum TimestampPlacementFailure {
17    /// No exact copy was incorporated in either frozen source pool.
18    Missing,
19    /// Copies exist, but only in other containers/other timestamp tokens.
20    OutsideTimestampContainers,
21}
22
23/// One actual token's missing/misplaced artifact; no signed bytes are copied.
24pub struct TimestampPlacementFinding<'certificate, 'document, N> {
25    /// Original timestamp property requiring this artifact.
26    pub property: N,
27    /// Exact original token; material in a sibling token is not self-carried.
28    pub token: N,
29    /// Exact original certificate/whole CRL/basic-response identity.
30    pub artifact: ArtifactRef<'certificate, 'document>,
31    /// Missing and misplaced remain distinct.
32    pub cause: TimestampPlacementFailure,
33}
34
35/// ONLY §6.3(x)'s eligible placement for all actual X.509 TSA dependencies.
36/// This is not usable revocation, historical trust, chronology, format-specific
37/// material or a complete B-LT/B-LTA verdict. Original inputs remain borrowed.
38pub struct TimestampPlacement<'closure, 'requirements, 'view, 'certificate, 'document, N, D, G> {
39    inputs:
40        &'closure ParticipantClosure<'requirements, 'view, 'certificate, 'document, N, D, G, Xades>,
41    findings: OwnedVec<TimestampPlacementFinding<'certificate, 'document, N>>,
42    snapshot: Snapshot<D, G>,
43}
44impl<'closure, 'requirements, 'view, 'certificate, 'document, N, D: Copy, G: Copy>
45    TimestampPlacement<'closure, 'requirements, 'view, 'certificate, 'document, N, D, G>
46{
47    /// Same complete original participant/status input census.
48    #[must_use]
49    pub fn inputs(
50        &self,
51    ) -> &'closure ParticipantClosure<'requirements, 'view, 'certificate, 'document, N, D, G, Xades>
52    {
53        self.inputs
54    }
55    /// Every missing/misplaced exact artifact once per actual TSA root.
56    #[must_use]
57    pub fn findings(&self) -> &[TimestampPlacementFinding<'certificate, 'document, N>] {
58        &self.findings
59    }
60    /// Original immutable XML generation.
61    #[must_use]
62    pub fn snapshot(&self) -> Snapshot<D, G> {
63        self.snapshot
64    }
65}
66
67/// Input/source/current-operation refusal, never a missing-material finding.
68#[derive(Debug)]
69pub enum TimestampPlacementError<N> {
70    /// A preceding-archive subset omits the current/later TSA and is insufficient.
71    Scope,
72    /// Source generation or selected signature/index was substituted.
73    Index(IndexError<N>),
74    /// Original actor-authority traversal/current storage refusal.
75    Uses(UsesError<N>),
76}
77
78/// Placement result in the original XML identity/error domain.
79pub type TimestampPlacementResult<'closure, 'requirements, 'view, 'certificate, 'document, X> =
80    Result<
81        TimestampPlacement<
82            'closure,
83            'requirements,
84            'view,
85            'certificate,
86            'document,
87            <X as Xml>::Id,
88            <X as Xml>::Document,
89            <X as Xml>::Generation,
90        >,
91        TimestampPlacementError<<X as Xml>::Id>,
92    >;
93
94#[derive(Hash, PartialEq, Eq)]
95struct Seen<'document> {
96    root: usize,
97    artifact: ArtifactKey<'document>,
98}
99impl HashWork for Seen<'_> {
100    fn hash_work(&self) -> Result<u64, MemoryError> {
101        self.artifact
102            .hash_work()?
103            .checked_add(1)
104            .ok_or(MemoryError::WorkExceeded)
105    }
106}
107
108fn allowed<N: Copy, E>(
109    placement: Placement<N>,
110    property: N,
111    token: N,
112    equal: &mut impl FnMut(N, N) -> Result<bool, E>,
113) -> Result<bool, E> {
114    match placement {
115        // §6.3(x) names these properties; their URI is only a hint.
116        Placement::General(_) | Placement::Timestamp(_) => Ok(true),
117        Placement::TimestampToken {
118            property: actual_property,
119            token: actual_token,
120            ..
121        } => Ok(equal(actual_property, property)? && equal(actual_token, token)?),
122        _ => Ok(false),
123    }
124}
125
126fn availability<X: Xml, W: Work, Q: Work>(
127    index: &Index<'_, X::Id, X::Document, X::Generation, W>,
128    artifact: ArtifactRef<'_, '_>,
129    signature: X::Id,
130    property: X::Id,
131    token: X::Id,
132    access: &Access<'_, X, Q>,
133) -> Result<Option<TimestampPlacementFailure>, IndexError<X::Id>> {
134    let mut present = false;
135    for occurrence in index.artifact(artifact, access)? {
136        let occurrence = occurrence?;
137        present = true;
138        let eligible = allowed(
139            occurrence.placement(),
140            property,
141            token,
142            &mut |left, right| {
143                access
144                    .equal_id(left, right)
145                    .map_err(|cause| IndexError::Resource { signature, cause })
146            },
147        )?;
148        if eligible {
149            return Ok(None);
150        }
151    }
152    Ok(Some(if present {
153        TimestampPlacementFailure::OutsideTimestampContainers
154    } else {
155        TimestampPlacementFailure::Missing
156    }))
157}
158
159/// Require §6.3(x)'s actual copies in timestamp/general validation data or in
160/// THAT timestamp itself. Shared roots/status authorities follow the SAME paid
161/// iterative dependency kernel, retaining per-token attribution across cycles.
162/// Complete/delta CRLs, OCSP whole-envelope identity and embedded signers reuse
163/// the common original-artifact visitor/index. No parse/hash/crypto repeats.
164/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
165///
166/// Current Work admits identity/hash/probe/traversal/report capacity before use.
167/// A late refusal drops all partial findings/scratch, not a successful prefix.
168pub fn timestamp_placement<
169    'closure,
170    'requirements,
171    'view,
172    'certificate,
173    'document,
174    X: Xml,
175    W: Work,
176    Q: Work,
177>(
178    inputs: &'closure ParticipantClosure<
179        'requirements,
180        'view,
181        'certificate,
182        'document,
183        X::Id,
184        X::Document,
185        X::Generation,
186        Xades,
187    >,
188    index: &Index<'_, X::Id, X::Document, X::Generation, W>,
189    access: &Access<'_, X, Q>,
190) -> TimestampPlacementResult<'closure, 'requirements, 'view, 'certificate, 'document, X> {
191    let required = inputs.requirements();
192    if !matches!(required.scope(), TimestampScope::All(_)) {
193        return Err(TimestampPlacementError::Scope);
194    }
195    let signature = required.signature();
196    if required.scope().location().snapshot != access.snapshot() {
197        return Err(TimestampPlacementError::Index(IndexError::Foreign {
198            signature,
199        }));
200    }
201    index
202        .require_signature(signature, access)
203        .map_err(TimestampPlacementError::Index)?;
204    let work = access.execution();
205    let resource =
206        |cause| TimestampPlacementError::Index(IndexError::Resource { signature, cause });
207    let mut actors = work.vector();
208    for entry in required.entries() {
209        work.charge_work(1).map_err(resource)?;
210        if matches!(entry.authority(), ParticipantAuthority::Path(_)) {
211            actors
212                .try_push(match entry.participant() {
213                    Participant::Timestamp { property, token } => Some((property, token)),
214                    _ => None,
215                })
216                .map_err(resource)?;
217        }
218    }
219    let mut seen = work.map();
220    let mut findings = work.vector();
221    inputs
222        .authorities()
223        .visit_roots(work, |root, node| {
224            let Some((property, token)) = actors[root] else {
225                return Ok(());
226            };
227            let mut record = |artifact: ArtifactRef<'certificate, 'document>| {
228                let key = Seen {
229                    root,
230                    artifact: artifact.identity(),
231                };
232                if seen.try_insert(key, ()).map_err(resource)?.is_some() {
233                    return Ok(());
234                }
235                if let Some(cause) =
236                    availability(index, artifact, signature, property, token, access)
237                        .map_err(TimestampPlacementError::Index)?
238                {
239                    findings
240                        .try_push(TimestampPlacementFinding {
241                            property,
242                            token,
243                            artifact,
244                            cause,
245                        })
246                        .map_err(resource)?;
247                }
248                Ok(())
249            };
250            let path = node.statuses.path();
251            if let Some(certificate) = path.anchor().certificate() {
252                record(ArtifactRef::Certificate(certificate))?;
253            }
254            for member in path.members() {
255                work.charge_work(1).map_err(resource)?;
256                record(ArtifactRef::Certificate(member.certificate()))?;
257            }
258            for input in node.statuses.entries() {
259                work.charge_work(1).map_err(resource)?;
260                if let Some(source) = member_source(input) {
261                    visit_artifacts(source, signature, access, |artifact, _, _| record(artifact))
262                        .map_err(|cause| match cause {
263                        ArtifactVisitError::Scope(ScopeError::Resource { cause, .. }) => {
264                            resource(cause)
265                        }
266                        ArtifactVisitError::Scope(cause) => {
267                            TimestampPlacementError::Uses(UsesError::Scope(cause))
268                        }
269                        ArtifactVisitError::Visitor(cause) => cause,
270                    })?;
271                }
272            }
273            Ok(())
274        })
275        .map_err(|cause| match cause {
276            AuthorityVisitError::Processing(cause) => {
277                TimestampPlacementError::Uses(UsesError::Closure(cause))
278            }
279            AuthorityVisitError::Visitor(cause) => cause,
280        })?;
281    Ok(TimestampPlacement {
282        inputs,
283        findings: findings.into_owned(),
284        snapshot: access.snapshot(),
285    })
286}
287
288#[cfg(test)]
289mod tests;