Skip to main content

structured_ltv/timestamp/
obligation.rs

1//! Complete original timestamp obligations for one validation-data scope.
2
3use super::Purpose;
4use crate::pki::{CounterReceipts, TimestampPath};
5use crate::source::{Location, Query, ScopeError};
6use core::marker::PhantomData;
7use structured_asn1::memory::OwnedVec;
8
9/// Original source boundary of a set of timestamp obligations.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TimestampScope<N, D, G> {
12    /// Whole selected signature; no archive-precedence assertion is made.
13    All(Location<N, D, G>),
14    /// Original archive timestamp, excluding its own and later unsigned tokens.
15    BeforeArchive(Location<N, D, G>),
16}
17
18impl<N: Copy, D: Copy, G: Copy> TimestampScope<N, D, G> {
19    /// Real enclosing signature/archive location for source checks and errors.
20    #[must_use]
21    pub fn location(self) -> Location<N, D, G> {
22        match self {
23            Self::All(location) | Self::BeforeArchive(location) => location,
24        }
25    }
26}
27
28/// One original token obligation; this is not a successful timestamp receipt.
29#[derive(Debug, Clone, Copy)]
30pub struct RequiredToken<N> {
31    /// Actual root or nested signature whose input this token covers.
32    pub signature: N,
33    /// Original timestamp property.
34    pub property: N,
35    /// Original encapsulated token, independently of its decoded bytes.
36    pub token: N,
37    /// Kind of input selected for the token, never inferred from success.
38    pub purpose: Purpose,
39}
40
41/// Complete original token set for one explicit protocol scope. Failed or
42/// unreadable tokens cannot disappear behind a caller's successful subset:
43/// only the holder of issuer `I` derives it from the original source.
44pub struct TimestampRequirements<N, D, G, I> {
45    signature: N,
46    scope: TimestampScope<N, D, G>,
47    entries: OwnedVec<RequiredToken<N>>,
48    counters: Option<Counters<N>>,
49    issuer: PhantomData<I>,
50}
51
52// The complete counter-source census and, for an archive scope, the aligned
53// mask of the counters that archive's own input covers.
54struct Counters<N> {
55    signatures: OwnedVec<N>,
56    covered: Option<OwnedVec<bool>>,
57}
58
59impl<N: Copy, D: Copy, G: Copy, I> TimestampRequirements<N, D, G, I> {
60    /// The complete token obligations of one signature without countersignatures.
61    ///
62    /// The format layer derives `entries` from every original timestamp in
63    /// `scope`, in source order, failed and unsupported ones included.
64    #[must_use]
65    pub fn new(
66        _issuer: I,
67        signature: N,
68        scope: TimestampScope<N, D, G>,
69        entries: OwnedVec<RequiredToken<N>>,
70    ) -> Self {
71        Self {
72            signature,
73            scope,
74            entries,
75            counters: None,
76            issuer: PhantomData,
77        }
78    }
79
80    /// The same obligations including every original countersignature's tokens.
81    ///
82    /// `signatures` lists every countersignature in source order. For an
83    /// archive scope, `covered` marks the ones whose whole element that
84    /// archive's own input covers; a later countersignature is not an earlier
85    /// actor. A whole-signature scope takes `None`.
86    #[must_use]
87    pub fn with_counters(
88        _issuer: I,
89        signature: N,
90        scope: TimestampScope<N, D, G>,
91        entries: OwnedVec<RequiredToken<N>>,
92        signatures: OwnedVec<N>,
93        covered: Option<OwnedVec<bool>>,
94    ) -> Self {
95        Self {
96            signature,
97            scope,
98            entries,
99            counters: Some(Counters {
100                signatures,
101                covered,
102            }),
103            issuer: PhantomData,
104        }
105    }
106
107    /// Original selected signature.
108    #[must_use]
109    pub fn signature(&self) -> N {
110        self.signature
111    }
112    /// Actual whole-signature or preceding-archive scope.
113    #[must_use]
114    pub fn scope(&self) -> TimestampScope<N, D, G> {
115        self.scope
116    }
117    /// Every original token in prepared property/token order.
118    #[must_use]
119    pub fn entries(&self) -> &[RequiredToken<N>] {
120        &self.entries
121    }
122}
123
124/// Checked TSA paths for every token in the SAME original scope. No token
125/// authentication, temporal acceptance or application trust is repeated here.
126pub struct TimestampPaths<'view, 'certificate, 'document, N, D, G, I> {
127    required: &'view TimestampRequirements<N, D, G, I>,
128    paths: &'view [&'view TimestampPath<'view, 'certificate, 'document, N, D, G, I>],
129}
130
131impl<'view, 'certificate, 'document, N, D, G, I>
132    TimestampPaths<'view, 'certificate, 'document, N, D, G, I>
133{
134    /// Complete original token obligations; their scope cannot be relabelled.
135    #[must_use]
136    pub fn required(&self) -> &'view TimestampRequirements<N, D, G, I> {
137        self.required
138    }
139    /// Actual paths in original obligation order, without copying.
140    #[must_use]
141    pub fn paths(
142        &self,
143    ) -> &'view [&'view TimestampPath<'view, 'certificate, 'document, N, D, G, I>] {
144        self.paths
145    }
146}
147
148/// Missing/wrong proofs cannot establish a successful partial token set.
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum TimestampPathFailure {
151    /// Every original token requires one actual checked TSA path.
152    Count,
153    /// A supplied path belongs to a different original token or source.
154    Source {
155        /// Original obligation ordinal, not an ASN.1 or source offset.
156        index: usize,
157    },
158}
159
160/// Complete path association or explicit insufficient original proofs.
161pub enum TimestampPathBinding<'view, 'certificate, 'document, N, D, G, I> {
162    /// Every required token has its actual original TSA path.
163    Bound(TimestampPaths<'view, 'certificate, 'document, N, D, G, I>),
164    /// No complete proof set exists; no successful prefix is returned.
165    Unproven(TimestampPathFailure),
166}
167
168/// A source, scope or census refusal, not a failed token assertion.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum ObligationError<N> {
171    /// Current source generation or operation refused.
172    Scope(ScopeError<N>),
173    /// The query, counter census or mask belongs to another source or scope.
174    Foreign {
175        /// Original enclosing signature/archive.
176        node: N,
177    },
178}
179
180impl<N> From<ScopeError<N>> for ObligationError<N> {
181    fn from(cause: ScopeError<N>) -> Self {
182        Self::Scope(cause)
183    }
184}
185
186/// Association of complete paths with these obligations.
187pub type PathBindingResult<'view, 'certificate, 'document, N, D, G, I> =
188    Result<TimestampPathBinding<'view, 'certificate, 'document, N, D, G, I>, ObligationError<N>>;
189
190impl<N: Copy + Eq, D: Copy + Eq, G: Copy + Eq, I> TimestampRequirements<N, D, G, I> {
191    /// Whether the countersignature at this census ordinal is an actor of this
192    /// scope: every counter for the whole signature, only covered ones before
193    /// an archive.
194    pub fn counter_in_scope<Q: Query<Id = N, Document = D, Generation = G>>(
195        &self,
196        ordinal: usize,
197        query: &Q,
198    ) -> Result<bool, ObligationError<N>> {
199        let node = self.scope.location().node;
200        if self.scope.location().snapshot != query.snapshot() {
201            return Err(ObligationError::Foreign { node });
202        }
203        query.admit(node, 2)?;
204        match self.scope {
205            TimestampScope::All(_) => Ok(true),
206            TimestampScope::BeforeArchive(_) => {
207                let Some(covered) = self.counters.as_ref().and_then(|c| c.covered.as_ref()) else {
208                    return Err(ObligationError::Foreign { node });
209                };
210                covered
211                    .get(ordinal)
212                    .copied()
213                    .ok_or(ObligationError::Foreign { node })
214            }
215        }
216    }
217
218    /// Whether these obligations were derived from exactly this counter census.
219    pub fn includes_counters<
220        'p,
221        'd,
222        C: CounterReceipts<'p, 'd, Id = N>,
223        Q: Query<Id = N, Document = D, Generation = G>,
224    >(
225        &self,
226        counters: &C,
227        query: &Q,
228    ) -> Result<bool, ScopeError<N>>
229    where
230        'd: 'p,
231    {
232        let Some(expected) = &self.counters else {
233            return Ok(counters.len() == 0);
234        };
235        if expected.signatures.len() != counters.len() {
236            return Ok(false);
237        }
238        for (index, expected) in expected.signatures.iter().enumerate() {
239            let node = counters.counter(index).signature;
240            if !query
241                .equal(*expected, node)
242                .map_err(|cause| ScopeError::Resource { node, cause })?
243            {
244                return Ok(false);
245            }
246        }
247        Ok(true)
248    }
249
250    /// Associate actual paths once per original token. The SAME source/purpose
251    /// comparison governs all-token LT and preceding-archive requirements.
252    /// Paths of another issuer cannot be associated.
253    pub fn bind_paths<'view, 'certificate, 'document, Q>(
254        &'view self,
255        paths: &'view [&'view TimestampPath<'view, 'certificate, 'document, N, D, G, I>],
256        query: &Q,
257    ) -> PathBindingResult<'view, 'certificate, 'document, N, D, G, I>
258    where
259        Q: Query<Id = N, Document = D, Generation = G>,
260    {
261        let location = self.scope.location();
262        if location.snapshot != query.snapshot() {
263            return Err(ObligationError::Foreign {
264                node: location.node,
265            });
266        }
267        // One dispatch plus one unit per pass, as every other scoped query pays.
268        query.admit(self.signature, 2)?;
269        if paths.len() != self.entries.len() {
270            return Ok(TimestampPathBinding::Unproven(TimestampPathFailure::Count));
271        }
272        let resource = |cause| ScopeError::Resource {
273            node: location.node,
274            cause,
275        };
276        for (index, path) in paths.iter().enumerate() {
277            query.admit(self.signature, 2)?;
278            let expected = &self.entries[index];
279            let actual = path.actor().verified();
280            if !query
281                .equal(actual.signature(), expected.signature)
282                .map_err(resource)?
283                || !query
284                    .equal(actual.property(), expected.property)
285                    .map_err(resource)?
286                || actual.token().snapshot != location.snapshot
287                || !query
288                    .equal(actual.token().node, expected.token)
289                    .map_err(resource)?
290                || actual.purpose() != expected.purpose
291            {
292                return Ok(TimestampPathBinding::Unproven(
293                    TimestampPathFailure::Source { index },
294                ));
295            }
296        }
297        Ok(TimestampPathBinding::Bound(TimestampPaths {
298            required: self,
299            paths,
300        }))
301    }
302}