Skip to main content

structured_cms/timestamp/
retained.rs

1//! Independent generation/imprint facts, not a signature or trust proof.
2
3use super::{Accuracy, EffectiveAccuracy, HashAlgorithmReading, TokenInfo};
4use crate::{Error, ErrorCode, Result};
5use structured_asn1::{
6    Oid, Reader, Span,
7    memory::{OwnedVec, Work},
8    value::OwnedOid,
9};
10use structured_pkix::temporal::Reading;
11
12/// Independently retained written hash algorithm and logical digest bytes.
13/// Parameter absence stays distinct from a written NULL; no re-encoding or
14/// generic normalization substitutes for the selected provider's semantics.
15#[derive(Debug)]
16pub struct RetainedImprint {
17    algorithm: OwnedOid,
18    parameters: Option<OwnedVec<u8>>,
19    hashed: OwnedVec<u8>,
20}
21impl RetainedImprint {
22    /// Original validated identifier arcs, now independent of the source parser.
23    #[must_use]
24    pub fn algorithm(&self) -> Oid<'_> {
25        self.algorithm.as_oid()
26    }
27    /// Whole original parameter TLV, without a fabricated absent/default value.
28    #[must_use]
29    pub fn parameters(&self) -> Option<&[u8]> {
30        self.parameters.as_deref()
31    }
32    /// Complete logical hashedMessage octets, with no fragment headers.
33    #[must_use]
34    pub fn hashed(&self) -> &[u8] {
35        &self.hashed
36    }
37}
38
39/// A conforming token's generation/imprint projection and accuracy provenance.
40///
41/// This deliberately carries no successful signature/trust verdict. Callers
42/// inspect and verify the token independently; only the complete timestamp
43/// checker may turn positive checks into evidence. Every retained allocation
44/// holds its charge after parser/token drop; no infallible deep Clone exists.
45#[derive(Debug)]
46pub struct RetainedFacts {
47    imprint: RetainedImprint,
48    policy: OwnedOid,
49    serial: OwnedVec<u8>,
50    nonce: Option<OwnedVec<u8>>,
51    gen_time: structured_pkix::temporal::ExactReading,
52    accuracy: Option<EffectiveAccuracy>,
53    ordering: bool,
54    source_span: Span,
55}
56
57impl TokenInfo<'_> {
58    /// Retains this generation/imprint projection before the scoped source ends.
59    ///
60    /// A retained malformed token is not silently turned into conforming data.
61    /// This projection refuses inspection faults; the original model and all
62    /// diagnostics remain available on refusal. It copies only declared facts,
63    /// not a certificate, source payload, provider state or a trust assertion.
64    pub fn retain_facts(&self, parent: &Reader<'_>) -> Result<RetainedFacts> {
65        self.imprint().require_session(parent)?;
66        if self.has_faults() {
67            return Err(Error::schema(
68                ErrorCode::MalformedValue,
69                "tstInfo",
70                self.span().start,
71            ));
72        }
73        let HashAlgorithmReading::Identifier(algorithm) = self.imprint().algorithm() else {
74            return Err(Error::schema(
75                ErrorCode::MalformedValue,
76                "tstInfo.messageImprint",
77                self.imprint().span().start,
78            ));
79        };
80        let accuracy = self
81            .accuracy()
82            .map(super::AccuracyReading::deviation)
83            .transpose()
84            .map_err(|error| {
85                Error::schema(
86                    ErrorCode::MalformedValue,
87                    "tstInfo.accuracy",
88                    error.span.start,
89                )
90            })?;
91        let work = parent.execution();
92        work.charge_work(8)
93            .map_err(|reason| Error::resource(reason, "tstInfo", self.span().start))?;
94        let hash_algorithm =
95            OwnedOid::from_oid_in(algorithm.algorithm(), &work).map_err(|reason| {
96                Error::resource(
97                    reason,
98                    "tstInfo.messageImprint.hashAlgorithm",
99                    algorithm.offset(),
100                )
101            })?;
102        let parameters = algorithm
103            .parameters()
104            .map(|bytes| {
105                retain(
106                    bytes,
107                    &work,
108                    "tstInfo.messageImprint.hashAlgorithm.parameters",
109                    algorithm.offset(),
110                )
111            })
112            .transpose()?;
113        let hashed = self
114            .imprint()
115            .hashed()
116            .copy_in(
117                parent,
118                "tstInfo.messageImprint.hashedMessage",
119                self.imprint().hashed().span().start,
120            )?
121            .into_owned();
122        let policy = OwnedOid::from_oid_in(self.policy(), &work)
123            .map_err(|reason| Error::resource(reason, "tstInfo.policy", self.span().start))?;
124        let serial = retain(
125            self.serial().element().contents(),
126            &work,
127            "tstInfo.serialNumber",
128            self.serial().element().span().start,
129        )?;
130        let nonce = self
131            .nonce()
132            .map(|nonce| {
133                retain(
134                    nonce.element().contents(),
135                    &work,
136                    "tstInfo.nonce",
137                    nonce.element().span().start,
138                )
139            })
140            .transpose()?;
141        let gen_time = self
142            .precise_gen_time()
143            .try_copy_in(&work, "tstInfo.genTime", self.span().start)
144            .map_err(Error::pkix)?;
145        Ok(RetainedFacts {
146            imprint: RetainedImprint {
147                algorithm: hash_algorithm,
148                parameters,
149                hashed,
150            },
151            policy,
152            serial,
153            nonce,
154            gen_time,
155            accuracy: accuracy.map(EffectiveAccuracy::Token),
156            ordering: self.ordering(),
157            source_span: self.span(),
158        })
159    }
160}
161
162fn retain<W: Work>(bytes: &[u8], work: &W, field: &'static str, at: u64) -> Result<OwnedVec<u8>> {
163    let mut owned = work.vector();
164    owned
165        .try_extend_from_slice(bytes)
166        .map_err(|reason| Error::resource(reason, field, at))?;
167    Ok(owned.into_owned())
168}
169
170impl RetainedFacts {
171    // Only the complete checker supplies this already resolved value, after
172    // policy binding and token conformance; raw retention never adds policy.
173    pub(super) fn with_effective_accuracy(mut self, accuracy: Option<EffectiveAccuracy>) -> Self {
174        self.accuracy = accuracy;
175        self
176    }
177    /// Copies facts and accuracy provenance into independently charged storage.
178    /// Moving the owner needs no copy; a genuine deep copy admits new work and
179    /// capacity before execution. Partial refusal leaves this source unchanged.
180    pub fn try_copy_in<W: Work>(&self, work: &W) -> Result<Self> {
181        work.charge_work(8)
182            .map_err(|reason| Error::resource(reason, "tstInfo", self.source_span.start))?;
183        let algorithm =
184            OwnedOid::from_oid_in(self.imprint.algorithm(), work).map_err(|reason| {
185                Error::resource(
186                    reason,
187                    "tstInfo.messageImprint.hashAlgorithm",
188                    self.source_span.start,
189                )
190            })?;
191        let parameters = self
192            .imprint
193            .parameters()
194            .map(|bytes| {
195                retain(
196                    bytes,
197                    work,
198                    "tstInfo.messageImprint.hashAlgorithm.parameters",
199                    self.source_span.start,
200                )
201            })
202            .transpose()?;
203        let hashed = retain(
204            self.imprint.hashed(),
205            work,
206            "tstInfo.messageImprint.hashedMessage",
207            self.source_span.start,
208        )?;
209        let policy = OwnedOid::from_oid_in(self.policy(), work)
210            .map_err(|reason| Error::resource(reason, "tstInfo.policy", self.source_span.start))?;
211        let serial = retain(
212            self.serial(),
213            work,
214            "tstInfo.serialNumber",
215            self.source_span.start,
216        )?;
217        let nonce = self
218            .nonce()
219            .map(|bytes| retain(bytes, work, "tstInfo.nonce", self.source_span.start))
220            .transpose()?;
221        let gen_time = self
222            .gen_time
223            .try_copy_in(work, "tstInfo.genTime", self.source_span.start)
224            .map_err(Error::pkix)?;
225        Ok(Self {
226            imprint: RetainedImprint {
227                algorithm,
228                parameters,
229                hashed,
230            },
231            policy,
232            serial,
233            nonce,
234            gen_time,
235            accuracy: self.accuracy,
236            ordering: self.ordering,
237            source_span: self.source_span,
238        })
239    }
240    /// Declared complete imprint, requiring selected-provider resolution to use.
241    #[must_use]
242    pub fn imprint(&self) -> &RetainedImprint {
243        &self.imprint
244    }
245    /// Original policy arcs; no product policy acceptance is implied.
246    #[must_use]
247    pub fn policy(&self) -> Oid<'_> {
248        self.policy.as_oid()
249    }
250    /// Original serial INTEGER contents, never a narrowing numeric conversion.
251    #[must_use]
252    pub fn serial(&self) -> &[u8] {
253        &self.serial
254    }
255    /// Original nonce INTEGER contents when one was written.
256    #[must_use]
257    pub fn nonce(&self) -> Option<&[u8]> {
258        self.nonce.as_deref()
259    }
260    /// Original common UTC reading, not a fabricated exact instant.
261    #[must_use]
262    pub fn gen_time(&self) -> Reading {
263        self.gen_time.reading()
264    }
265    /// Exact generation-time order, retaining arbitrary subnanosecond digits.
266    /// This compares authenticated facts only when called on actual evidence;
267    /// a same-TSA ordering assertion or accuracy rule is a separate precondition.
268    /// Both UTC labels and complete comparison work are admitted before lookup.
269    pub fn compare_generation<W: Work>(
270        &self,
271        other: &Self,
272        utc: &super::LeapTable<'_>,
273        work: &W,
274    ) -> core::result::Result<core::cmp::Ordering, super::GenerationOrderError> {
275        self.gen_time.compare(&other.gen_time, utc, work)
276    }
277    /// Complete Accuracy absence remains distinct from a present zero value.
278    #[must_use]
279    pub fn accuracy(&self) -> Option<Accuracy> {
280        match self.accuracy {
281            Some(EffectiveAccuracy::Token(value)) => Some(value),
282            None | Some(EffectiveAccuracy::CallerPolicy(_)) => None,
283        }
284    }
285    /// Resolved deviation and its origin. Token absence remains observable
286    /// through `accuracy()` even when an authenticated caller policy is used.
287    #[must_use]
288    pub fn effective_accuracy(&self) -> Option<EffectiveAccuracy> {
289        self.accuracy
290    }
291    /// Original ordering assertion, which does not establish chronology itself.
292    #[must_use]
293    pub fn ordering(&self) -> bool {
294        self.ordering
295    }
296    /// Range in the original callback reader's coordinates. When logical, keep
297    /// its supplied `SourceMap` separately; this is not a physical document span.
298    #[must_use]
299    pub fn source_span(&self) -> Span {
300        self.source_span
301    }
302}