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
29impl<'a> Token<'a> {
30    /// Reads the complete timestamp CMS envelope using the original parser.
31    /// The attached `TSTInfo` bytes are inspected by [`Self::with_info`].
32    pub fn read(parser: &'a Parser, document: &'a [u8], base: u64) -> Result<Self> {
33        let mut root = parser
34            .read(document, base)
35            .map_err(|error| Error::encoding(error, "contentInfo"))?;
36        let (envelope, parent) = ContentInfo::read(&mut root)?;
37        parent
38            .execution()
39            .charge_work(envelope.kind().as_bytes().len() as u64)
40            .map_err(|reason| {
41                Error::resource(reason, "contentInfo.contentType", envelope.span().start)
42            })?;
43        if envelope.kind() != oid::SIGNED_DATA {
44            return Err(Error::schema(
45                ErrorCode::UnexpectedContentType,
46                "contentInfo.contentType",
47                envelope.span().start,
48            ));
49        }
50        let message = SignedData::read_element(&parent, envelope.content())?;
51        Self::from_message(message, &parent)
52    }
53
54    /// Applies timestamp envelope rules to an already decoded signed message.
55    ///
56    /// RFC3161 §2.4.2 requires attached `TSTInfo` and only the TSA signature.
57    /// Inspection recovery of equal parsed signers is explicitly nonconforming:
58    /// retain the first original encoding and one `ExtraSignature` fault. The
59    /// common narrow comparer ignores BER container spellings, preserves exact
60    /// signedAttrs and opaque field identity, and never rewrites signed bytes.
61    /// Conflicting signer values refuse; this is not a trust/provider selector.
62    /// <https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2>
63    pub fn from_message(message: SignedData<'a>, parent: &Reader<'a>) -> Result<Self> {
64        let field = "signedData.signerInfos";
65        let first = message
66            .signers()
67            .first()
68            .ok_or_else(|| Error::schema(ErrorCode::SignerCount, field, message.span().start))?;
69        first.require_session(parent, field)?;
70        let execution = parent.execution();
71        let mut faults = execution.vector();
72        let mut duplicate = None;
73        for extra in &message.signers()[1..] {
74            execution
75                .charge_work(1)
76                .map_err(|reason| Error::resource(reason, field, extra.span().start))?;
77            if !first.same_value(extra, parent)? {
78                return Err(Error::schema(
79                    ErrorCode::SignerCount,
80                    field,
81                    extra.span().start,
82                ));
83            }
84            duplicate.get_or_insert(extra.span().start);
85        }
86        if let Some(at) = duplicate {
87            record(
88                &mut faults,
89                Error::schema(ErrorCode::ExtraSignature, field, at),
90            )?;
91        }
92        record_counters(first, parent, &mut faults)?;
93        let content = message.content();
94        execution
95            .charge_work(content.kind().as_bytes().len() as u64)
96            .map_err(|reason| {
97                Error::resource(
98                    reason,
99                    "signedData.encapContentInfo.eContentType",
100                    content.span().start,
101                )
102            })?;
103        if content.kind() != oid::TST_INFO {
104            return Err(Error::schema(
105                ErrorCode::UnexpectedContentType,
106                "signedData.encapContentInfo.eContentType",
107                content.span().start,
108            ));
109        }
110        if content.content().is_none() {
111            return Err(Error::schema(
112                ErrorCode::DetachedContent,
113                "signedData.encapContentInfo.eContent",
114                content.span().start,
115            ));
116        }
117        Ok(Self {
118            message,
119            faults: faults.into_owned(),
120        })
121    }
122
123    /// Original signed message, including unmodified signed attribute ranges.
124    #[must_use]
125    pub fn message(&self) -> &SignedData<'a> {
126        &self.message
127    }
128
129    /// The first original TSA signer, after nonempty/unique-reading admission.
130    #[must_use]
131    pub fn signer(&self) -> &crate::signer::SignerInfo<'a> {
132        &self.message.signers()[0]
133    }
134
135    /// Additional envelope diagnostics, separate from CMS and `TSTInfo` faults.
136    #[must_use]
137    pub fn faults(&self) -> &[Error] {
138        &self.faults
139    }
140
141    /// Reads the complete attached `TSTInfo` in a callback with honest provenance.
142    ///
143    /// Owned output/error types cannot borrow temporary joins. Located errors
144    /// use the provided map's coordinates; that map is retained with every inner
145    /// error. The caller chooses verification/trust/request context, and admits
146    /// explicit independent proof data before leaving the callback.
147    /// A uniquely readable first value followed by trailing carried bytes is
148    /// returned with a located `TrailingData` fault; those bytes are not repaired
149    /// or parsed as another token. Diagnostic retention can itself refuse
150    /// resources. Complete checking still rejects the nonconforming information.
151    #[expect(
152        clippy::result_large_err,
153        reason = "preserve both original cause and accounted source context without allocating an error envelope"
154    )]
155    pub fn with_info<R, E>(
156        &self,
157        parent: &Reader<'_>,
158        callback: impl for<'scope> FnOnce(
159            &TokenInfo<'scope>,
160            &Reader<'scope>,
161            &SourceMap,
162        ) -> core::result::Result<R, E>,
163    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
164        self.inspect(parent, None, callback)
165    }
166
167    /// Inspects a token decoded inside another scoped content source.
168    ///
169    /// The parent map is composed before inspection. Errors retain physical
170    /// range metadata independently of both joined buffers; the original
171    /// parser depth and operation budgets still govern the nested reader.
172    #[expect(
173        clippy::result_large_err,
174        reason = "retain original cause and accounted physical source context"
175    )]
176    pub fn with_info_mapped<R, E>(
177        &self,
178        parent: &Reader<'_>,
179        source: &SourceMap,
180        callback: impl for<'scope> FnOnce(
181            &TokenInfo<'scope>,
182            &Reader<'scope>,
183            &SourceMap,
184        ) -> core::result::Result<R, E>,
185    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
186        self.inspect(parent, Some(source), callback)
187    }
188
189    #[expect(
190        clippy::result_large_err,
191        reason = "retain original cause and accounted physical source context"
192    )]
193    fn inspect<R, E>(
194        &self,
195        parent: &Reader<'_>,
196        source: Option<&SourceMap>,
197        callback: impl for<'scope> FnOnce(
198            &TokenInfo<'scope>,
199            &Reader<'scope>,
200            &SourceMap,
201        ) -> core::result::Result<R, E>,
202    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
203        let Some(content) = self.message.content().content() else {
204            return Err(ContentError::Processing(Error::schema(
205                ErrorCode::DetachedContent,
206                "signedData.encapContentInfo.eContent",
207                self.message.content().span().start,
208            )));
209        };
210        let decode = |reader: &mut Reader<'_>, context: &SourceMap| {
211            let info = TokenInfo::read_carried(reader).map_err(InfoError::Processing)?;
212            callback(&info, reader, context).map_err(InfoError::Callback)
213        };
214        match source {
215            Some(source) => {
216                content.with_nested_reader_mapped(parent, source, Encoding::Ber, "tstInfo", decode)
217            }
218            None => content.with_nested_reader(parent, Encoding::Ber, "tstInfo", decode),
219        }
220    }
221}
222
223fn record_counters<W: Work>(
224    signer: &crate::signer::SignerInfo<'_>,
225    parent: &Reader<'_>,
226    faults: &mut OwnedVec<Error, W>,
227) -> Result<()> {
228    // RFC3161 §2.4.2 forbids signatures other than the TSA's. This presence
229    // check is separate from the legal unsigned countersignature walker:
230    // known OID plus unreadable/empty values or signed placement still counts.
231    // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
232    for (attributes, field) in [
233        (
234            signer.signed_attributes(),
235            "signerInfo.signedAttrs.countersignature",
236        ),
237        (
238            signer.unsigned_attributes(),
239            "signerInfo.unsignedAttrs.countersignature",
240        ),
241    ] {
242        let Some(attributes) = attributes else {
243            continue;
244        };
245        for attribute in attributes.all_in(oid::COUNTERSIGNATURE, parent) {
246            let attribute = attribute?;
247            record(
248                faults,
249                Error::schema(ErrorCode::ExtraSignature, field, attribute.span().start),
250            )?;
251        }
252    }
253    Ok(())
254}