Skip to main content

structured_xades/timestamp/
tokens.rs

1//! One frozen original source set for simultaneous timestamp/material checks.
2
3use super::Role;
4use crate::binary::{
5    self, EncodingDeclaration, Location, OwnedBinaryXml, OwnedDecoded, PkiEncoding,
6};
7use crate::prepare::Prepared;
8use crate::xml::{Access, AccessError, Snapshot, Xml};
9use structured_asn1::{
10    Encoding, Reader,
11    memory::{MemoryError, Work},
12};
13use structured_cms::timestamp::Token;
14
15/// Explicit envelope admission, selected by the caller, never by XML content.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TokenProfile {
18    /// RFC5652 BER envelope, independently reporting the XML declaration.
19    Cms,
20    /// EN319132-1 §5.1.3 declaration-driven admission; absence selects DER.
21    /// CER, PER and XER remain unsupported by this parser.
22    Etsi,
23}
24
25/// Declaration admission is independent of timestamp mathematics and trust.
26/// `Matches` means the existing CMS Token reader accepts that encoding; it
27/// does not clear readable token faults or validate every opaque ANY value.
28#[derive(Debug)]
29pub enum EncodingConformance {
30    /// The declared encoding's CMS envelope reader accepted the original bytes.
31    Matches,
32    /// That reader refused syntax; the CMS profile still read the BER envelope.
33    Mismatch(structured_cms::Error),
34    /// No canonical checker exists for this encoding. BER success is not CER
35    /// conformance (X.690 §9); callers must not count this as a match.
36    Unchecked(PkiEncoding),
37}
38
39/// Independently retained original XML token source. There are no internal
40/// borrowed pointers, re-encoding, copied parser or reset source ceilings.
41pub struct TokenSource<N, D, G> {
42    location: Location<N, D, G>,
43    signature: N,
44    property: N,
45    role: Role,
46    stamp: usize,
47    ordinal: usize,
48    encoding: Encoding,
49    declaration: EncodingDeclaration,
50    conformance: EncodingConformance,
51    source: OwnedDecoded,
52}
53impl<N: Copy, D: Copy, G: Copy> TokenSource<N, D, G> {
54    /// Original absent or explicit XML Encoding declaration.
55    #[must_use]
56    pub fn declaration(&self) -> EncodingDeclaration {
57        self.declaration
58    }
59    /// Separate envelope declaration result. No resource refusal is retained
60    /// here: it terminates the complete pool before the visitor is called.
61    #[must_use]
62    pub fn encoding_conformance(&self) -> &EncodingConformance {
63        &self.conformance
64    }
65    /// Original root or nested signature owning this timestamp input.
66    #[must_use]
67    pub fn signature(&self) -> N {
68        self.signature
69    }
70    /// Actual original token enclosure and source generation.
71    #[must_use]
72    pub fn location(&self) -> Location<N, D, G> {
73        self.location
74    }
75    /// Original timestamp property, separately from its encoded token element.
76    #[must_use]
77    pub fn property(&self) -> N {
78        self.property
79    }
80    /// Actual common input role of the enclosing timestamp property.
81    #[must_use]
82    pub fn role(&self) -> Role {
83        self.role
84    }
85    /// Ordinal in the frozen property set: prepared root properties followed by
86    /// child properties in original depth-first counter order.
87    #[must_use]
88    pub fn stamp_index(&self) -> usize {
89        self.stamp
90    }
91    /// Original token ordinal within that timestamp property.
92    #[must_use]
93    pub fn token_index(&self) -> usize {
94        self.ordinal
95    }
96    /// Original decoded buffer, unchanged and not a physical XML byte range.
97    #[must_use]
98    pub fn bytes(&self) -> &[u8] {
99        &self.source.bytes
100    }
101}
102
103/// Scoped complete CMS token together with its producing query reader. This
104/// is inspection only; readable diagnostics never establish positive evidence.
105pub struct TokenReading<'source> {
106    reader: Reader<'source>,
107    token: Token<'source>,
108}
109
110/// One inseparable original source/parsed-token pair from a frozen pool.
111/// Callers cannot attach a parsed token to a different XML enclosure.
112#[derive(Clone, Copy)]
113pub struct TokenEntry<'view, 'source, N, D, G> {
114    pub(super) source: &'view TokenSource<N, D, G>,
115    pub(super) reading: &'view TokenReading<'source>,
116}
117impl<'source> TokenReading<'source> {
118    /// Current operation plus original parser/depth/source coordinate context.
119    #[must_use]
120    pub fn reader(&self) -> &Reader<'source> {
121        &self.reader
122    }
123    /// Original envelope, retained faults, signer and carried material.
124    #[must_use]
125    pub fn token(&self) -> &Token<'source> {
126        &self.token
127    }
128}
129
130/// All sources are frozen before any parsed token borrows them. Both slices
131/// have identical ordinals; no successfully parsed prefix is returned on failure.
132pub struct TokenView<'source, N, D, G> {
133    sources: &'source [TokenSource<N, D, G>],
134    readings: &'source [TokenReading<'source>],
135    snapshot: Snapshot<D, G>,
136    signature: N,
137}
138impl<'source, N, D, G> TokenView<'source, N, D, G> {
139    /// Select one original token pair in constant time. This is not a positive
140    /// proof or a complete-token census; requirements still bind EVERY token.
141    #[must_use]
142    pub fn entry(&self, index: usize) -> Option<TokenEntry<'_, 'source, N, D, G>> {
143        Some(TokenEntry {
144            source: self.sources.get(index)?,
145            reading: self.readings.get(index)?,
146        })
147    }
148    /// Independent original source ownership for each corresponding reading.
149    #[must_use]
150    pub fn sources(&self) -> &'source [TokenSource<N, D, G>] {
151        self.sources
152    }
153    /// All parsed tokens, preserving prepared-property and encapsulated-token order.
154    #[must_use]
155    pub fn readings(&self) -> &'source [TokenReading<'source>] {
156        self.readings
157    }
158}
159
160impl<'source, N: Copy, D: Copy, G: Copy> TokenView<'source, N, D, G> {
161    /// Original source generation, including an empty token-source set.
162    #[must_use]
163    pub fn snapshot(&self) -> Snapshot<D, G> {
164        self.snapshot
165    }
166    /// Original XML signature whose unsigned properties contain these tokens.
167    #[must_use]
168    pub fn signature(&self) -> N {
169        self.signature
170    }
171
172    pub(super) fn reborrow<'query>(&self) -> TokenView<'query, N, D, G>
173    where
174        'source: 'query,
175    {
176        TokenView {
177            sources: self.sources,
178            readings: self.readings,
179            snapshot: self.snapshot,
180            signature: self.signature,
181        }
182    }
183}
184
185/// Source/parse/visitor refusal, preserving original coordinate classes.
186#[derive(Debug)]
187pub enum TokenPipelineError<E, VE, N, D, G> {
188    /// Child catalogue belongs to another root or source, before any decoding.
189    Foreign {
190        /// Expected original root signature.
191        node: N,
192    },
193    /// XML/generation/current-operation failure.
194    Access(AccessError<E, N>),
195    /// Original Encoding selection refused through the shared binary kernel;
196    /// unsupported/unknown forms are not silently replaced by another codec.
197    Encoding(binary::Error<E, core::convert::Infallible, N, D, G>),
198    /// Original source Reader could not be opened under both active ceilings.
199    Reader {
200        /// Original XML token.
201        location: Location<N, D, G>,
202        /// Original decoded-coordinate cause.
203        cause: structured_asn1::Error,
204    },
205    /// Complete CMS timestamp envelope schema/inspection failure.
206    Token {
207        /// Original XML token.
208        location: Location<N, D, G>,
209        /// Unchanged decoded-coordinate CMS cause.
210        cause: structured_cms::Error,
211    },
212    /// Source/parsed catalogue admission failed before growth.
213    Resource {
214        /// Actual original enclosing property/token.
215        node: N,
216        /// Unchanged resource refusal.
217        cause: MemoryError,
218    },
219    /// Exact visitor failure; external visitor side effects are not rolled back.
220    Visitor(VE),
221}
222
223/// Pipeline result using this XML owner's original cause and identity domain.
224pub type TokenPipelineResult<T, X, VE> = Result<
225    T,
226    TokenPipelineError<
227        <X as Xml>::Error,
228        VE,
229        <X as Xml>::Id,
230        <X as Xml>::Document,
231        <X as Xml>::Generation,
232    >,
233>;
234
235/// Retains each actual token once, freezes ownership, then parses through the
236/// same CMS Token kernel used by standalone checks. A scoped visitor may inspect
237/// all carried certificate/status sources and perform common complete checks
238/// without retaining temporary XML/binary borrows. CMS mode additionally probes
239/// DER declarations through the same strict CMS kernel, under both ceilings;
240/// its temporary catalogue drops before the actual BER model is retained.
241/// Original decoded bytes are owned once; no XML or signed bytes are rewritten.
242/// No provider, source acquisition, TSA selection, trust or B level is implied.
243///
244/// ```compile_fail
245/// use structured_asn1::Reader;
246/// use structured_cms::timestamp::Token;
247/// use structured_xades::{binary::OwnedBinaryXml,prepare::Prepared,timestamp};
248/// fn escape<'a,X:OwnedBinaryXml>(xml:&X,prepared:&Prepared<X>,operation:&Reader<'_>)
249///   -> timestamp::TokenPipelineResult<&'a Token<'a>,X,()> {
250///   timestamp::with_tokens(xml,prepared,operation,timestamp::TokenProfile::Cms,|_,view|Ok(view.readings()[0].token()))
251/// }
252/// ```
253pub fn with_tokens<X: OwnedBinaryXml, R, VE>(
254    xml: &X,
255    prepared: &Prepared<X>,
256    operation: &Reader<'_>,
257    profile: TokenProfile,
258    visitor: impl for<'source> FnOnce(
259        &Reader<'source>,
260        TokenView<'source, X::Id, X::Document, X::Generation>,
261    ) -> Result<R, VE>,
262) -> TokenPipelineResult<R, X, VE> {
263    with_inputs(xml, prepared, None, operation, profile, visitor)
264}
265
266/// Freeze root AND every nested counter's timestamp tokens in one pool.
267/// The complete child census is source-bound before any binary decoding; the
268/// visitor sees one source/reading pool, with each token's actual owning signature.
269/// No token is omitted because it lacks positive evidence or usable TSA material.
270///
271/// ```compile_fail
272/// use structured_asn1::Reader;
273/// use structured_xades::{binary::OwnedBinaryXml, prepare::Prepared, timestamp};
274/// fn escape<'a,X:OwnedBinaryXml>(xml:&X, prepared:&Prepared<X>, operation:&Reader<'_>,
275///     counters:&timestamp::CounterTimestamps<X::Id,X::Document,X::Generation>)
276///     -> timestamp::TokenPipelineResult<&'a [u8],X,()> {
277///     timestamp::with_counter_tokens(xml,prepared,counters,operation,timestamp::TokenProfile::Cms,|_,view|Ok(view.sources()[0].bytes()))
278/// }
279/// ```
280pub fn with_counter_tokens<X: OwnedBinaryXml, R, VE>(
281    xml: &X,
282    prepared: &Prepared<X>,
283    counters: &super::CounterTimestamps<X::Id, X::Document, X::Generation>,
284    operation: &Reader<'_>,
285    profile: TokenProfile,
286    visitor: impl for<'source> FnOnce(
287        &Reader<'source>,
288        TokenView<'source, X::Id, X::Document, X::Generation>,
289    ) -> Result<R, VE>,
290) -> TokenPipelineResult<R, X, VE> {
291    with_inputs(xml, prepared, Some(counters), operation, profile, visitor)
292}
293
294impl<N: Copy, D: Copy, G: Copy> TokenEntry<'_, '_, N, D, G> {
295    pub(super) fn reader_in<'query>(
296        &'query self,
297        operation: &Reader<'query>,
298    ) -> structured_asn1::Result<Reader<'query>> {
299        self.source
300            .source
301            .reader_in(operation, self.source.encoding)
302    }
303}
304
305fn with_inputs<X: OwnedBinaryXml, R, VE>(
306    xml: &X,
307    prepared: &Prepared<X>,
308    counters: Option<&super::CounterTimestamps<X::Id, X::Document, X::Generation>>,
309    operation: &Reader<'_>,
310    profile: TokenProfile,
311    visitor: impl for<'source> FnOnce(
312        &Reader<'source>,
313        TokenView<'source, X::Id, X::Document, X::Generation>,
314    ) -> Result<R, VE>,
315) -> TokenPipelineResult<R, X, VE> {
316    let layout = prepared.bound().containers();
317    let execution = operation.execution();
318    let access = Access::new(xml, layout.snapshot(), &execution);
319    admit(&access, layout.signature())?;
320    if let Some(counters) = counters
321        && (counters.snapshot != layout.snapshot()
322            || !access
323                .equal_id(counters.signature, layout.signature())
324                .map_err(|cause| TokenPipelineError::Resource {
325                    node: layout.signature(),
326                    cause,
327                })?)
328    {
329        return Err(TokenPipelineError::Foreign {
330            node: layout.signature(),
331        });
332    }
333    let mut sources = execution.vector();
334    let children = counters
335        .into_iter()
336        .flat_map(|counters| counters.entries.iter().map(super::CounterStamp::stamp));
337    let mut timestamps = prepared.timestamps().iter().chain(children).enumerate();
338    loop {
339        admit(&access, layout.signature())?;
340        let Some((stamp, timestamp)) = timestamps.next() else {
341            break;
342        };
343        for ordinal in 0..timestamp.tokens().len() {
344            admit(&access, timestamp.node())?;
345            let node = timestamp.tokens()[ordinal];
346            let location = Location {
347                snapshot: layout.snapshot(),
348                node,
349            };
350            let (declaration, encoding) = select_encoding(profile, &access, location)?;
351            let source = binary::owned_source(xml, location, operation)
352                .map_err(TokenPipelineError::Access)?
353                .value;
354            let conformance = declaration_conformance::<X, VE>(
355                profile,
356                declaration.encoding(),
357                &source,
358                operation,
359                location,
360            )?;
361            sources
362                .try_push(TokenSource {
363                    location,
364                    signature: timestamp.signature,
365                    property: timestamp.node(),
366                    role: timestamp.role(),
367                    stamp,
368                    ordinal,
369                    encoding,
370                    declaration,
371                    conformance,
372                    source,
373                })
374                .map_err(|cause| TokenPipelineError::Resource { node, cause })?;
375        }
376    }
377    let mut readings = execution.vector();
378    for index in 0..sources.len() {
379        admit(&access, layout.signature())?;
380        let source = &sources[index];
381        let mut reader = source
382            .source
383            .reader_in(operation, source.encoding)
384            .map_err(|cause| TokenPipelineError::Reader {
385                location: source.location,
386                cause,
387            })?;
388        let token = Token::read_from(&mut reader).map_err(|cause| TokenPipelineError::Token {
389            location: source.location,
390            cause,
391        })?;
392        readings
393            .try_push(TokenReading { reader, token })
394            .map_err(|cause| TokenPipelineError::Resource {
395                node: source.location.node,
396                cause,
397            })?;
398    }
399    admit(&access, layout.signature())?;
400    let result = visitor(
401        operation,
402        TokenView {
403            sources: &sources,
404            readings: &readings,
405            snapshot: layout.snapshot(),
406            signature: layout.signature(),
407        },
408    )
409    .map_err(TokenPipelineError::Visitor)?;
410    admit(&access, layout.signature())?;
411    Ok(result)
412}
413
414fn select_encoding<X: Xml, W: Work, VE>(
415    profile: TokenProfile,
416    access: &Access<'_, X, W>,
417    location: Location<X::Id, X::Document, X::Generation>,
418) -> TokenPipelineResult<(EncodingDeclaration, Encoding), X, VE> {
419    let declaration = binary::pki_encoding(access, location.node)
420        .map_err(|cause| TokenPipelineError::Encoding(binary::Error::Access { location, cause }))?
421        .ok_or(TokenPipelineError::Encoding(
422            binary::Error::UnknownEncoding { location },
423        ))?;
424    let encoding = match (profile, declaration.encoding()) {
425        (TokenProfile::Cms, PkiEncoding::Der | PkiEncoding::Ber | PkiEncoding::Cer)
426        | (TokenProfile::Etsi, PkiEncoding::Ber) => Encoding::Ber,
427        (TokenProfile::Etsi, PkiEncoding::Der) => Encoding::Der,
428        (_, encoding) => {
429            return Err(TokenPipelineError::Encoding(
430                binary::Error::UnsupportedEncoding { location, encoding },
431            ));
432        }
433    };
434    Ok((declaration, encoding))
435}
436
437fn declaration_conformance<X: Xml, VE>(
438    profile: TokenProfile,
439    declared: PkiEncoding,
440    source: &OwnedDecoded,
441    operation: &Reader<'_>,
442    location: Location<X::Id, X::Document, X::Generation>,
443) -> TokenPipelineResult<EncodingConformance, X, VE> {
444    if profile == TokenProfile::Cms && declared == PkiEncoding::Der {
445        // RFC5652 §§1/5.3 allows BER envelopes with DER signedAttrs.
446        // Reuse the strict schema reader, including IMPLICIT SET order,
447        // rather than guessing conformance from BER framing success.
448        // https://www.rfc-editor.org/rfc/rfc5652#section-5.3
449        let mut strict = source
450            .reader_in(operation, Encoding::Der)
451            .map_err(|cause| TokenPipelineError::Reader { location, cause })?;
452        match Token::read_from(&mut strict) {
453            Ok(_) => Ok(EncodingConformance::Matches),
454            Err(cause) if cause.is_terminal() => Err(TokenPipelineError::Token { location, cause }),
455            Err(cause) => Ok(EncodingConformance::Mismatch(cause)),
456        }
457    } else if declared == PkiEncoding::Cer {
458        Ok(EncodingConformance::Unchecked(declared))
459    } else {
460        // Only exposed after the entire actual pool has parsed.
461        Ok(EncodingConformance::Matches)
462    }
463}
464
465fn admit<X: Xml, W: Work, VE>(
466    access: &Access<'_, X, W>,
467    node: X::Id,
468) -> TokenPipelineResult<(), X, VE> {
469    access
470        .query(node, |_, _, work| Ok(work.charge_work(1)))
471        .map_err(TokenPipelineError::Access)?
472        .map_err(|cause| TokenPipelineError::Resource { node, cause })
473}