Skip to main content

structured_cms/timestamp/
token.rs

1//! Timestamp CMS envelope and scoped original `TSTInfo` inspection.
2
3use super::{TokenInfo, record};
4use crate::content_source::{ContentError, SourceMap};
5use crate::{ContentInfo, Error, ErrorCode, Result, SignedData, oid};
6use structured_asn1::memory::{OwnedVec, Work};
7use structured_asn1::{Encoding, Parser, Reader};
8
9/// Timestamp envelope; reading does not establish signature, trust or chronology.
10///
11/// The original signed message stays borrowed. `TSTInfo` is read through a scoped
12/// callback so BER joins cannot produce dangling borrowed models or require a
13/// self-referential owned container. Independently retained results are explicit.
14#[derive(Debug)]
15pub struct Token<'a> {
16    message: SignedData<'a>,
17    faults: OwnedVec<Error>,
18}
19
20/// Failure inside the explicitly identified carried-content source.
21#[derive(Debug)]
22pub enum InfoError<E> {
23    /// Located `TSTInfo` processing failure, using the supplied source coordinates.
24    Processing(Error),
25    /// The caller's unchanged error from its inspection/verification operation.
26    Callback(E),
27}
28
29/// Asynchronous inspection keeps parsed information, its inherited reader and
30/// source map scoped across awaits. Output and errors cannot borrow a join.
31pub trait AsyncInfoVisitor {
32    /// Independently retained result that cannot borrow the joined information.
33    type Output;
34    /// Original callback failure that cannot borrow the joined information.
35    type Error;
36    /// Uses the one parsed information model and its honest source map.
37    fn visit<'data: 'scope, 'scope>(
38        &'scope mut self,
39        info: &'scope TokenInfo<'data>,
40        reader: &'scope Reader<'data>,
41        source: &'scope SourceMap,
42    ) -> impl core::future::Future<Output = core::result::Result<Self::Output, Self::Error>>;
43}
44
45struct InformationVisitor<'a, V>(&'a mut V);
46
47impl<V: AsyncInfoVisitor> crate::content_source::AsyncVisitor for InformationVisitor<'_, V> {
48    type Output = V::Output;
49    type Error = InfoError<V::Error>;
50    async fn visit<'data: 'scope, 'scope>(
51        &'scope mut self,
52        reader: &'scope mut Reader<'data>,
53        source: &'scope SourceMap,
54    ) -> core::result::Result<Self::Output, Self::Error> {
55        let info = TokenInfo::read_carried(reader).map_err(InfoError::Processing)?;
56        self.0
57            .visit(&info, reader, source)
58            .await
59            .map_err(InfoError::Callback)
60    }
61}
62
63impl<'a> Token<'a> {
64    /// Reads the complete timestamp CMS envelope using the original parser.
65    /// The attached `TSTInfo` bytes are inspected by [`Self::with_info`].
66    pub fn read(parser: &'a Parser, document: &'a [u8], base: u64) -> Result<Self> {
67        let mut root = parser
68            .read(document, base)
69            .map_err(|error| Error::encoding(error, "contentInfo"))?;
70        Self::read_from(&mut root)
71    }
72
73    /// Reads a complete token from its original scoped source reader.
74    ///
75    /// Source encoding, offsets, inherited depth, parser session and current
76    /// operation ceilings are preserved. No replacement parser, retagging or
77    /// copy is performed. Trailing bytes in this source window are refused.
78    pub fn read_from(root: &mut Reader<'a>) -> Result<Self> {
79        let (envelope, parent) = ContentInfo::read(root)?;
80        parent
81            .execution()
82            .charge_work(envelope.kind().as_bytes().len() as u64)
83            .map_err(|reason| {
84                Error::resource(reason, "contentInfo.contentType", envelope.span().start)
85            })?;
86        if envelope.kind() != oid::SIGNED_DATA {
87            return Err(Error::schema(
88                ErrorCode::UnexpectedContentType,
89                "contentInfo.contentType",
90                envelope.span().start,
91            ));
92        }
93        let message = SignedData::read_element(&parent, envelope.content())?;
94        Self::from_message(message, &parent)
95    }
96
97    /// Applies timestamp envelope rules to an already decoded signed message.
98    ///
99    /// RFC3161 §2.4.2 requires attached `TSTInfo` and only the TSA signature.
100    /// Inspection recovery of equal parsed signers is explicitly nonconforming:
101    /// retain the first original encoding and one `ExtraSignature` fault. The
102    /// common narrow comparer ignores BER container spellings, preserves exact
103    /// signedAttrs and opaque field identity, and never rewrites signed bytes.
104    /// Conflicting signer values refuse; this is not a trust/provider selector.
105    /// <https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2>
106    pub fn from_message(message: SignedData<'a>, parent: &Reader<'a>) -> Result<Self> {
107        let field = "signedData.signerInfos";
108        let first = message
109            .signers()
110            .first()
111            .ok_or_else(|| Error::schema(ErrorCode::SignerCount, field, message.span().start))?;
112        first.require_session(parent, field)?;
113        let execution = parent.execution();
114        let mut faults = execution.vector();
115        let mut duplicate = None;
116        for extra in &message.signers()[1..] {
117            execution
118                .charge_work(1)
119                .map_err(|reason| Error::resource(reason, field, extra.span().start))?;
120            if !first.same_value(extra, parent)? {
121                return Err(Error::schema(
122                    ErrorCode::SignerCount,
123                    field,
124                    extra.span().start,
125                ));
126            }
127            duplicate.get_or_insert(extra.span().start);
128        }
129        if let Some(at) = duplicate {
130            record(
131                &mut faults,
132                Error::schema(ErrorCode::ExtraSignature, field, at),
133            )?;
134        }
135        record_counters(first, parent, &mut faults)?;
136        let content = message.content();
137        execution
138            .charge_work(content.kind().as_bytes().len() as u64)
139            .map_err(|reason| {
140                Error::resource(
141                    reason,
142                    "signedData.encapContentInfo.eContentType",
143                    content.span().start,
144                )
145            })?;
146        if content.kind() != oid::TST_INFO {
147            return Err(Error::schema(
148                ErrorCode::UnexpectedContentType,
149                "signedData.encapContentInfo.eContentType",
150                content.span().start,
151            ));
152        }
153        if content.content().is_none() {
154            return Err(Error::schema(
155                ErrorCode::DetachedContent,
156                "signedData.encapContentInfo.eContent",
157                content.span().start,
158            ));
159        }
160        Ok(Self {
161            message,
162            faults: faults.into_owned(),
163        })
164    }
165
166    /// Original signed message, including unmodified signed attribute ranges.
167    #[must_use]
168    pub fn message(&self) -> &SignedData<'a> {
169        &self.message
170    }
171
172    /// The first original TSA signer, after nonempty/unique-reading admission.
173    #[must_use]
174    pub fn signer(&self) -> &crate::signer::SignerInfo<'a> {
175        &self.message.signers()[0]
176    }
177
178    /// Additional envelope diagnostics, separate from CMS and `TSTInfo` faults.
179    #[must_use]
180    pub fn faults(&self) -> &[Error] {
181        &self.faults
182    }
183
184    /// Reads the complete attached `TSTInfo` in a callback with honest provenance.
185    ///
186    /// Owned output/error types cannot borrow temporary joins. Located errors
187    /// use the provided map's coordinates; that map is retained with every inner
188    /// error. The caller chooses verification/trust/request context, and admits
189    /// explicit independent proof data before leaving the callback.
190    /// A uniquely readable first value followed by trailing carried bytes is
191    /// returned with a located `TrailingData` fault; those bytes are not repaired
192    /// or parsed as another token. Diagnostic retention can itself refuse
193    /// resources. Complete checking still rejects the nonconforming information.
194    #[expect(
195        clippy::result_large_err,
196        reason = "preserve both original cause and accounted source context without allocating an error envelope"
197    )]
198    pub fn with_info<R, E>(
199        &self,
200        parent: &Reader<'_>,
201        callback: impl for<'scope> FnOnce(
202            &TokenInfo<'scope>,
203            &Reader<'scope>,
204            &SourceMap,
205        ) -> core::result::Result<R, E>,
206    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
207        self.inspect(parent, None, callback)
208    }
209
210    /// Inspects the same complete original `TSTInfo` while a callback awaits
211    /// bounded external inputs. Joined data and source maps stay alive until
212    /// callback completion and are released on cancellation or refusal.
213    #[expect(
214        clippy::result_large_err,
215        reason = "the original source map accompanies decoding and callback failures after cancellation-safe scoped inspection"
216    )]
217    pub async fn with_info_async<V: AsyncInfoVisitor>(
218        &self,
219        parent: &Reader<'_>,
220        origin: Option<&SourceMap>,
221        visitor: &mut V,
222    ) -> core::result::Result<V::Output, ContentError<InfoError<V::Error>>> {
223        let Some(content) = self.message.content().content() else {
224            return Err(ContentError::Processing(Error::schema(
225                ErrorCode::DetachedContent,
226                "signedData.encapContentInfo.eContent",
227                self.message.content().span().start,
228            )));
229        };
230        content
231            .with_nested_reader_async(
232                parent,
233                origin,
234                Encoding::Ber,
235                "tstInfo",
236                &mut InformationVisitor(visitor),
237            )
238            .await
239    }
240
241    /// Inspects a token decoded inside another scoped content source.
242    ///
243    /// The parent map is composed before inspection. Errors retain physical
244    /// range metadata independently of both joined buffers; the original
245    /// parser depth and operation budgets still govern the nested reader.
246    #[expect(
247        clippy::result_large_err,
248        reason = "retain original cause and accounted physical source context"
249    )]
250    pub fn with_info_mapped<R, E>(
251        &self,
252        parent: &Reader<'_>,
253        source: &SourceMap,
254        callback: impl for<'scope> FnOnce(
255            &TokenInfo<'scope>,
256            &Reader<'scope>,
257            &SourceMap,
258        ) -> core::result::Result<R, E>,
259    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
260        self.inspect(parent, Some(source), callback)
261    }
262
263    #[expect(
264        clippy::result_large_err,
265        reason = "retain original cause and accounted physical source context"
266    )]
267    fn inspect<R, E>(
268        &self,
269        parent: &Reader<'_>,
270        source: Option<&SourceMap>,
271        callback: impl for<'scope> FnOnce(
272            &TokenInfo<'scope>,
273            &Reader<'scope>,
274            &SourceMap,
275        ) -> core::result::Result<R, E>,
276    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
277        let Some(content) = self.message.content().content() else {
278            return Err(ContentError::Processing(Error::schema(
279                ErrorCode::DetachedContent,
280                "signedData.encapContentInfo.eContent",
281                self.message.content().span().start,
282            )));
283        };
284        let decode = |reader: &mut Reader<'_>, context: &SourceMap| {
285            let info = TokenInfo::read_carried(reader).map_err(InfoError::Processing)?;
286            callback(&info, reader, context).map_err(InfoError::Callback)
287        };
288        match source {
289            Some(source) => {
290                content.with_nested_reader_mapped(parent, source, Encoding::Ber, "tstInfo", decode)
291            }
292            None => content.with_nested_reader(parent, Encoding::Ber, "tstInfo", decode),
293        }
294    }
295}
296
297fn record_counters<W: Work>(
298    signer: &crate::signer::SignerInfo<'_>,
299    parent: &Reader<'_>,
300    faults: &mut OwnedVec<Error, W>,
301) -> Result<()> {
302    // RFC3161 §2.4.2 forbids signatures other than the TSA's. This presence
303    // check is separate from the legal unsigned countersignature walker:
304    // known OID plus unreadable/empty values or signed placement still counts.
305    // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
306    for (attributes, field) in [
307        (
308            signer.signed_attributes(),
309            "signerInfo.signedAttrs.countersignature",
310        ),
311        (
312            signer.unsigned_attributes(),
313            "signerInfo.unsignedAttrs.countersignature",
314        ),
315    ] {
316        let Some(attributes) = attributes else {
317            continue;
318        };
319        for attribute in attributes.all_in(oid::COUNTERSIGNATURE, parent) {
320            let attribute = attribute?;
321            record(
322                faults,
323                Error::schema(ErrorCode::ExtraSignature, field, attribute.span().start),
324            )?;
325        }
326    }
327    Ok(())
328}