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, Tag, universal};
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    #[expect(
148        clippy::result_large_err,
149        reason = "preserve both original cause and accounted source context without allocating an error envelope"
150    )]
151    pub fn with_info<R, E>(
152        &self,
153        parent: &Reader<'_>,
154        callback: impl for<'scope> FnOnce(
155            &TokenInfo<'scope>,
156            &Reader<'scope>,
157            &SourceMap,
158        ) -> core::result::Result<R, E>,
159    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
160        self.inspect(parent, None, callback)
161    }
162
163    /// Inspects a token decoded inside another scoped content source.
164    ///
165    /// The parent map is composed before inspection. Errors retain physical
166    /// range metadata independently of both joined buffers; the original
167    /// parser depth and operation budgets still govern the nested reader.
168    #[expect(
169        clippy::result_large_err,
170        reason = "retain original cause and accounted physical source context"
171    )]
172    pub fn with_info_mapped<R, E>(
173        &self,
174        parent: &Reader<'_>,
175        source: &SourceMap,
176        callback: impl for<'scope> FnOnce(
177            &TokenInfo<'scope>,
178            &Reader<'scope>,
179            &SourceMap,
180        ) -> core::result::Result<R, E>,
181    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
182        self.inspect(parent, Some(source), callback)
183    }
184
185    #[expect(
186        clippy::result_large_err,
187        reason = "retain original cause and accounted physical source context"
188    )]
189    fn inspect<R, E>(
190        &self,
191        parent: &Reader<'_>,
192        source: Option<&SourceMap>,
193        callback: impl for<'scope> FnOnce(
194            &TokenInfo<'scope>,
195            &Reader<'scope>,
196            &SourceMap,
197        ) -> core::result::Result<R, E>,
198    ) -> core::result::Result<R, ContentError<InfoError<E>>> {
199        let Some(content) = self.message.content().content() else {
200            return Err(ContentError::Processing(Error::schema(
201                ErrorCode::DetachedContent,
202                "signedData.encapContentInfo.eContent",
203                self.message.content().span().start,
204            )));
205        };
206        let decode = |reader: &mut Reader<'_>, context: &SourceMap| {
207            let element = reader
208                .expect(Tag::constructed(universal::SEQUENCE))
209                .map_err(|error| InfoError::Processing(Error::encoding(error, "tstInfo")))?;
210            let info = TokenInfo::read(reader, &element).map_err(InfoError::Processing)?;
211            if !reader.is_empty() {
212                return Err(InfoError::Processing(Error::schema(
213                    ErrorCode::TrailingData,
214                    "tstInfo",
215                    reader.offset(),
216                )));
217            }
218            callback(&info, reader, context).map_err(InfoError::Callback)
219        };
220        match source {
221            Some(source) => {
222                content.with_nested_reader_mapped(parent, source, Encoding::Ber, "tstInfo", decode)
223            }
224            None => content.with_nested_reader(parent, Encoding::Ber, "tstInfo", decode),
225        }
226    }
227}
228
229fn record_counters<W: Work>(
230    signer: &crate::signer::SignerInfo<'_>,
231    parent: &Reader<'_>,
232    faults: &mut OwnedVec<Error, W>,
233) -> Result<()> {
234    // RFC3161 §2.4.2 forbids signatures other than the TSA's. This presence
235    // check is separate from the legal unsigned countersignature walker:
236    // known OID plus unreadable/empty values or signed placement still counts.
237    // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
238    for (attributes, field) in [
239        (
240            signer.signed_attributes(),
241            "signerInfo.signedAttrs.countersignature",
242        ),
243        (
244            signer.unsigned_attributes(),
245            "signerInfo.unsignedAttrs.countersignature",
246        ),
247    ] {
248        let Some(attributes) = attributes else {
249            continue;
250        };
251        for attribute in attributes.all_in(oid::COUNTERSIGNATURE, parent) {
252            let attribute = attribute?;
253            record(
254                faults,
255                Error::schema(ErrorCode::ExtraSignature, field, attribute.span().start),
256            )?;
257        }
258    }
259    Ok(())
260}