Skip to main content

structured_ltv/timestamp/
signature.rs

1//! A signature timestamp's association with the signer's checked path.
2
3use super::{Interval, Purpose, TemporalError, Verified};
4use crate::pki::{SignerReceipt, TimestampPath};
5use crate::source::{Query, ScopeError};
6use structured_pkix::temporal::LeapTable;
7
8#[derive(Clone, Copy)]
9enum Input {
10    SignatureValue,
11    Existence,
12}
13
14/// A completed mathematical/static association, still requiring authenticated
15/// TSA path/status and signer revocation/algorithm/policy processing.
16pub struct SignatureTime<'receipt, S, N, D, G, I> {
17    signer: &'receipt S,
18    timestamp: &'receipt Verified<N, D, G, I>,
19    generation: Interval,
20}
21
22impl<'receipt, S, N, D, G, I> SignatureTime<'receipt, S, N, D, G, I> {
23    /// Same original selected signature/certificate and checked static path.
24    #[must_use]
25    pub fn signer(&self) -> &'receipt S {
26        self.signer
27    }
28    /// Same complete token check over the selected original signature value.
29    #[must_use]
30    pub fn timestamp(&self) -> &'receipt Verified<N, D, G, I> {
31        self.timestamp
32    }
33    /// Entire stated generation interval, never a bare claimed signing time.
34    #[must_use]
35    pub fn generation(&self) -> Interval {
36        self.generation
37    }
38}
39
40/// Missing association facts do not assert that a certificate has expired.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum SignatureTimeFailure {
43    /// Selected key/certificate or an original path digest is not established.
44    Certificate,
45    /// Static path was not checked over the complete generation interval.
46    Interval,
47}
48
49/// Original execution/context error, not a completed negative time claim.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum SignatureTimeError<N> {
52    /// Original current-source/generation/resource refusal.
53    Scope(ScopeError<N>),
54    /// The receipt belongs to another selected signature or source generation.
55    Foreign {
56        /// Original timestamp property.
57        property: N,
58    },
59    /// The timestamp covers another kind of input; it cannot replace a
60    /// signature timestamp simply because it also covers some signature bytes.
61    Purpose {
62        /// Original timestamp property.
63        property: N,
64    },
65    /// Full stated accuracy/UTC interval cannot be established.
66    Time {
67        /// Original timestamp property.
68        property: N,
69        /// Original UTC uncertainty or execution refusal.
70        cause: TemporalError,
71    },
72}
73
74/// Mathematical/static binding or explicit insufficient facts, not a B-T grade.
75pub enum SignatureTimeBinding<'receipt, S, N, D, G, I> {
76    /// Same selected signature, with full generation-time static path coverage.
77    Bound(SignatureTime<'receipt, S, N, D, G, I>),
78    /// Required certificate/interval facts have not been established.
79    Unproven(SignatureTimeFailure),
80}
81
82/// Association result in the query's original identity domain.
83pub type SignatureTimeResult<'receipt, S, Q, I> = Result<
84    SignatureTimeBinding<
85        'receipt,
86        S,
87        <Q as Query>::Id,
88        <Q as Query>::Document,
89        <Q as Query>::Generation,
90        I,
91    >,
92    SignatureTimeError<<Q as Query>::Id>,
93>;
94
95/// Associate a complete original signature timestamp with the actual signing
96/// certificate/path, preserving source generation and its full accuracy.
97///
98/// EN319132-1 V1.3.1 §§5.3/6.3(o) timestamp the original signature value and
99/// require creation before signing-certificate expiry/revocation. A checked
100/// path interval containing the entire generation interval supplies static
101/// validity; revocation and TSA authority remain separate necessary facts.
102/// Neither an archive timestamp nor a claimed signing time can substitute here.
103/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
104pub fn signature_time<'receipt, 'path, 'document, S, Q, I>(
105    signer: &'receipt S,
106    timestamp: &'receipt Verified<Q::Id, Q::Document, Q::Generation, I>,
107    utc: &LeapTable<'_>,
108    query: &Q,
109) -> SignatureTimeResult<'receipt, S, Q, I>
110where
111    'document: 'path,
112    S: SignerReceipt<
113            'path,
114            'document,
115            Id = Q::Id,
116            Document = Q::Document,
117            Generation = Q::Generation,
118        >,
119    Q: Query,
120{
121    bind(signer, timestamp, query, Input::SignatureValue, || {
122        timestamp.interval(utc, query.execution())
123    })
124}
125
126/// Reuse the complete generation interval already established by the actual
127/// checked TSA path for this SAME token receipt. This has the same
128/// source/purpose/certificate/static interval semantics as [`signature_time`],
129/// without repeating UTC lookup or arithmetic.
130pub fn signature_time_path<'receipt, 'path, 'document, S, Q, I>(
131    signer: &'receipt S,
132    path: &'receipt TimestampPath<'_, '_, '_, Q::Id, Q::Document, Q::Generation, I>,
133    query: &Q,
134) -> SignatureTimeResult<'receipt, S, Q, I>
135where
136    'document: 'path,
137    S: SignerReceipt<
138            'path,
139            'document,
140            Id = Q::Id,
141            Document = Q::Document,
142            Generation = Q::Generation,
143        >,
144    Q: Query,
145{
146    bind(
147        signer,
148        path.actor().verified(),
149        query,
150        Input::SignatureValue,
151        || Ok(path.generation()),
152    )
153}
154
155/// Associates an actual checked signature OR archive input with the selected
156/// signer's static path interval. A format must already have bound that input
157/// to this signature; this does not turn an archive into a signature property.
158/// EN319142-1 §6.3 permits a covering document timestamp to supply `PAdES` B-T.
159/// Other formats retain their independent signature-property requirements.
160/// <https://www.etsi.org/deliver/etsi_EN/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf>
161pub fn existence_time_path<'receipt, 'path, 'document, S, Q, I>(
162    signer: &'receipt S,
163    path: &'receipt TimestampPath<'_, '_, '_, Q::Id, Q::Document, Q::Generation, I>,
164    query: &Q,
165) -> SignatureTimeResult<'receipt, S, Q, I>
166where
167    'document: 'path,
168    S: SignerReceipt<
169            'path,
170            'document,
171            Id = Q::Id,
172            Document = Q::Document,
173            Generation = Q::Generation,
174        >,
175    Q: Query,
176{
177    bind(
178        signer,
179        path.actor().verified(),
180        query,
181        Input::Existence,
182        || Ok(path.generation()),
183    )
184}
185
186fn bind<'receipt, 'path, 'document, S, Q, I>(
187    signer: &'receipt S,
188    timestamp: &'receipt Verified<Q::Id, Q::Document, Q::Generation, I>,
189    query: &Q,
190    input: Input,
191    interval: impl FnOnce() -> Result<Interval, TemporalError>,
192) -> SignatureTimeResult<'receipt, S, Q, I>
193where
194    'document: 'path,
195    S: SignerReceipt<
196            'path,
197            'document,
198            Id = Q::Id,
199            Document = Q::Document,
200            Generation = Q::Generation,
201        >,
202    Q: Query,
203{
204    let selected = signer.signature();
205    let property = timestamp.property();
206    if selected.snapshot != query.snapshot() || timestamp.token().snapshot != selected.snapshot {
207        return Err(SignatureTimeError::Foreign { property });
208    }
209    if !query
210        .equal(timestamp.signature(), selected.node)
211        .map_err(|cause| {
212            SignatureTimeError::Scope(ScopeError::Resource {
213                node: property,
214                cause,
215            })
216        })?
217    {
218        return Err(SignatureTimeError::Foreign { property });
219    }
220    query
221        .admit(selected.node, 1)
222        .map_err(SignatureTimeError::Scope)?;
223    if timestamp.purpose() != Purpose::Signature
224        && !(matches!(input, Input::Existence) && timestamp.purpose() == Purpose::Archive)
225    {
226        return Err(SignatureTimeError::Purpose { property });
227    }
228    let Some(path) = signer.path() else {
229        return Ok(SignatureTimeBinding::Unproven(
230            SignatureTimeFailure::Certificate,
231        ));
232    };
233    let generation = interval().map_err(|cause| SignatureTimeError::Time { property, cause })?;
234    query
235        .admit(selected.node, 1)
236        .map_err(SignatureTimeError::Scope)?;
237    let checked = path.interval();
238    if checked.from_instant() > generation.earliest || checked.to_instant() < generation.latest {
239        return Ok(SignatureTimeBinding::Unproven(
240            SignatureTimeFailure::Interval,
241        ));
242    }
243    Ok(SignatureTimeBinding::Bound(SignatureTime {
244        signer,
245        timestamp,
246        generation,
247    }))
248}