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