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