Skip to main content

structured_cms/timestamp/
info.rs

1//! `TSTInfo` schema views, original source fields and explicit recovery faults.
2
3use super::{AccuracyReading, MessageImprint, TokenExtensions, TsaReading, record};
4use crate::{Error, ErrorCode, Result};
5use structured_asn1::memory::{OwnedVec, Work};
6use structured_asn1::{Element, Encoding, Integer, Oid, Reader, Span, Tag, universal};
7use structured_pkix::temporal::Reading;
8
9const FIELD: &str = "tstInfo";
10
11/// Original integer spelling and the common decoder's minimal numeric view.
12#[derive(Debug, Clone, Copy)]
13pub struct IntegerField<'a> {
14    element: Element<'a>,
15    value: Integer<'a>,
16}
17impl<'a> IntegerField<'a> {
18    fn read<W: Work>(
19        parent: &Reader<'a>,
20        element: &Element<'a>,
21        field: &'static str,
22        faults: &mut OwnedVec<Error, W>,
23    ) -> Result<Self> {
24        let (value, fault) = parent
25            .integer_reading(element)
26            .map_err(|error| Error::encoding(error, field))?
27            .into_parts();
28        if let Some(fault) = fault {
29            record(faults, Error::encoding(fault, field))?;
30        }
31        Ok(Self {
32            element: *element,
33            value,
34        })
35    }
36    /// Whole original INTEGER TLV; no minimal re-encoding is substituted.
37    #[must_use]
38    pub fn element(self) -> Element<'a> {
39        self.element
40    }
41    /// Shared minimal numeric view, still borrowing the original contents.
42    #[must_use]
43    pub fn value(self) -> Integer<'a> {
44        self.value
45    }
46}
47
48/// Required timestamp facts and optional values, independently of acceptance.
49/// Original bytes are borrowed; diagnostics/catalogues own their accounted capacity.
50#[derive(Debug)]
51pub struct TokenInfo<'a> {
52    policy: Oid<'a>,
53    imprint: MessageImprint<'a>,
54    serial: IntegerField<'a>,
55    gen_time: Reading,
56    accuracy: Option<AccuracyReading<'a>>,
57    ordering: bool,
58    nonce: Option<IntegerField<'a>>,
59    tsa: Option<TsaReading<'a>>,
60    extensions: Option<TokenExtensions<'a>>,
61    faults: OwnedVec<Error>,
62    span: Span,
63}
64
65impl<'a> TokenInfo<'a> {
66    pub(super) fn read_carried(parent: &mut Reader<'a>) -> Result<Self> {
67        let element = parent
68            .expect(Tag::constructed(universal::SEQUENCE))
69            .map_err(|error| Error::encoding(error, FIELD))?;
70        let info = Self::read(parent, &element)?;
71        if parent.is_empty() {
72            return Ok(info);
73        }
74        info.with_tail(parent)
75    }
76
77    fn with_tail(mut self, parent: &Reader<'_>) -> Result<Self> {
78        let at = parent.offset();
79        let work = parent.execution();
80        // Recovery retains the uniquely bounded first TSTInfo and its source
81        // map. RFC3161 §2.4.2 does not permit trailing content: this fault must
82        // still prevent complete checking/evidence. It is inspection recovery,
83        // not a second TSTInfo or silently consumed bytes.
84        // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
85        let mut faults = self.faults.with_execution(&work).map_err(|_| {
86            Error::pkix(structured_pkix::Error::new(
87                structured_pkix::ErrorCode::ForeignReader,
88                FIELD,
89                at,
90            ))
91        })?;
92        record(
93            &mut faults,
94            Error::schema(ErrorCode::TrailingData, FIELD, at),
95        )?;
96        self.faults = faults.into_owned();
97        Ok(self)
98    }
99
100    /// Reads RFC3161 §2.4.2 fields under the original producing reader.
101    ///
102    /// A BER inspection reader retains DER/schema faults instead of repairing
103    /// source bytes. This is not token acceptance: every nested fault remains
104    /// visible and must be considered before successful proof extraction.
105    /// INTEGER roles follow required/optional schema order; competing serial/
106    /// nonce assignments refuse. Unknown bounded fields remain diagnostics.
107    /// <https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2>
108    pub fn read(parent: &Reader<'a>, element: &Element<'a>) -> Result<Self> {
109        if element.tag() != Tag::constructed(universal::SEQUENCE) {
110            return Err(Error::schema(
111                ErrorCode::UnexpectedTag,
112                FIELD,
113                element.span().start,
114            ));
115        }
116        let execution = parent.execution();
117        let mut faults = execution.vector();
118        distinguished(parent, element, &mut faults)?;
119        let mut inside = parent
120            .enter(element)
121            .map_err(|error| Error::encoding(error, FIELD))?;
122        let mut fields = Fields::default();
123        // Only first successful schema fields require duplicate records: ten
124        // fixed slots, never an unaccounted input-driven Vec of encodings.
125        let mut seen: [Option<Element<'a>>; 10] = [None; 10];
126        let mut last = None;
127        while let Some(element) = inside
128            .next_element()
129            .map_err(|error| Error::encoding(error, FIELD))?
130        {
131            execution
132                .charge_work(1)
133                .map_err(|reason| Error::resource(reason, FIELD, element.span().start))?;
134            let Some(slot) = fields.slot(&inside, &element)? else {
135                if carries(element.tag()) {
136                    if !same_duplicate(&inside, &fields, &seen, &element)? {
137                        return Err(Error::schema(
138                            ErrorCode::DuplicateField,
139                            FIELD,
140                            element.span().start,
141                        ));
142                    }
143                    record(
144                        &mut faults,
145                        Error::schema(ErrorCode::DuplicateField, FIELD, element.span().start),
146                    )?;
147                } else {
148                    record(
149                        &mut faults,
150                        Error::schema(ErrorCode::UnknownField, FIELD, element.span().start),
151                    )?;
152                }
153                continue;
154            };
155            if (slot == Slot::Serial && fields.gen_time.is_some())
156                || (slot == Slot::Nonce && (fields.serial_displaced || fields.gen_time.is_none()))
157            {
158                return Err(Error::schema(
159                    ErrorCode::DuplicateField,
160                    FIELD,
161                    element.span().start,
162                ));
163            }
164            if last.is_some_and(|last| slot < last) {
165                record(
166                    &mut faults,
167                    Error::schema(ErrorCode::FieldOrder, FIELD, element.span().start),
168                )?;
169            }
170            last = Some(last.map_or(slot, |last| last.max(slot)));
171            fields.read(slot, &inside, &element, &mut faults)?;
172            seen[slot as usize] = Some(element);
173        }
174        fields.finish(element.span(), faults.into_owned())
175    }
176
177    /// Original policy OID; acceptable policy selection belongs to the caller.
178    #[must_use]
179    pub fn policy(&self) -> Oid<'a> {
180        self.policy
181    }
182    /// Original imprint fields, including explicit malformed algorithm syntax.
183    #[must_use]
184    pub fn imprint(&self) -> &MessageImprint<'a> {
185        &self.imprint
186    }
187    /// Original serial; RFC3161 imposes no positivity or 160-bit maximum.
188    #[must_use]
189    pub fn serial(&self) -> IntegerField<'a> {
190        self.serial
191    }
192    /// Shared UTC label/precision reading; supplied UTC facts remain independent.
193    #[must_use]
194    pub fn gen_time(&self) -> Reading {
195        self.gen_time
196    }
197    /// Absent complete Accuracy differs from a present empty/invalid reading.
198    #[must_use]
199    pub fn accuracy(&self) -> Option<&AccuracyReading<'a>> {
200        self.accuracy.as_ref()
201    }
202    /// Original ordering BOOLEAN, with FALSE only for its normative absent default.
203    #[must_use]
204    pub fn ordering(&self) -> bool {
205        self.ordering
206    }
207    /// Original optional nonce, without a fixed width or sign policy.
208    #[must_use]
209    pub fn nonce(&self) -> Option<IntegerField<'a>> {
210        self.nonce
211    }
212    /// Original explicit wrapper and common `GeneralName` reading or malformation.
213    #[must_use]
214    pub fn tsa(&self) -> Option<&TsaReading<'a>> {
215        self.tsa.as_ref()
216    }
217    /// Original optional extension catalogue and its own recovery diagnostics.
218    #[must_use]
219    pub fn extensions(&self) -> Option<&TokenExtensions<'a>> {
220        self.extensions.as_ref()
221    }
222    /// Root schema/DER faults; nested imprint/accuracy/extension faults stay there.
223    #[must_use]
224    pub fn faults(&self) -> &[Error] {
225        &self.faults
226    }
227    /// Any retained root or nested conformance fault, without scanning collections.
228    #[must_use]
229    pub fn has_faults(&self) -> bool {
230        !self.faults.is_empty()
231            || !self.imprint.faults().is_empty()
232            || self
233                .accuracy
234                .as_ref()
235                .is_some_and(|reading| !reading.faults().is_empty())
236            || self
237                .extensions
238                .as_ref()
239                .is_some_and(|reading| !reading.faults().is_empty())
240    }
241    /// Whole original `TSTInfo` range, never reconstructed from scalar fields.
242    #[must_use]
243    pub fn span(&self) -> Span {
244        self.span
245    }
246}
247
248fn same_duplicate(
249    parent: &Reader<'_>,
250    fields: &Fields<'_>,
251    seen: &[Option<Element<'_>>; 10],
252    element: &Element<'_>,
253) -> Result<bool> {
254    let execution = parent.execution();
255    let refusal = |reason| Error::resource(reason, FIELD, element.span().start);
256    if element.tag() == Tag::primitive(universal::OID)
257        && let Some(policy) = fields.policy
258    {
259        // RFC3161 §2.4.2 requires one DER policy; duplicate recovery is our
260        // inspection contract, not conformance. X.690 §8.1.3 length spelling
261        // does not change the decoded OID. Shared decoding validates both.
262        let other = parent
263            .oid(element)
264            .map_err(|error| Error::encoding(error, "tstInfo.policy"))?;
265        execution.charge_work(1).map_err(refusal)?;
266        if policy.as_bytes().len() != other.as_bytes().len() {
267            return Ok(false);
268        }
269        execution
270            .charge_work(policy.as_bytes().len() as u64)
271            .map_err(refusal)?;
272        return Ok(policy.as_bytes() == other.as_bytes());
273    }
274    // RFC3161 §2.4.2 orders the INTEGER roles. Here all three roles are
275    // populated, so another INTEGER can only repeat nonce. Standard-silent
276    // recovery must not rescue a conflicting nonce by matching version/serial.
277    // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
278    let applicable = if element.tag() == Tag::primitive(universal::INTEGER) {
279        &seen[Slot::Nonce as usize..=Slot::Nonce as usize]
280    } else {
281        &seen[..]
282    };
283    for prior in applicable.iter().flatten() {
284        execution.charge_work(1).map_err(refusal)?;
285        if prior.as_bytes().len() != element.as_bytes().len() {
286            continue;
287        }
288        execution
289            .charge_work(element.as_bytes().len() as u64)
290            .map_err(refusal)?;
291        if prior.as_bytes() == element.as_bytes() {
292            return Ok(true);
293        }
294    }
295    Ok(false)
296}
297
298#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
299#[repr(usize)]
300enum Slot {
301    Version,
302    Policy,
303    Imprint,
304    Serial,
305    GenTime,
306    Accuracy,
307    Ordering,
308    Nonce,
309    Tsa,
310    Extensions,
311}
312
313#[derive(Default)]
314struct Fields<'a> {
315    version: bool,
316    policy: Option<Oid<'a>>,
317    imprint: Option<MessageImprint<'a>>,
318    serial: Option<IntegerField<'a>>,
319    serial_displaced: bool,
320    gen_time: Option<Reading>,
321    accuracy: Option<AccuracyReading<'a>>,
322    ordering: Option<bool>,
323    nonce: Option<IntegerField<'a>>,
324    tsa: Option<TsaReading<'a>>,
325    extensions: Option<TokenExtensions<'a>>,
326}
327
328impl<'a> Fields<'a> {
329    fn slot(&self, parent: &Reader<'a>, element: &Element<'a>) -> Result<Option<Slot>> {
330        let tag = element.tag();
331        let slot = if tag == Tag::primitive(universal::INTEGER) {
332            if !self.version {
333                // RFC3161 §2.4.2 orders version before policy and imprint.
334                // Recovery is not specified: once those anchors occurred,
335                // this INTEGER also admits serial with missing version. Never
336                // assign one of the competing readings merely by first slot.
337                // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
338                if self.policy.is_some() || self.imprint.is_some() || self.gen_time.is_some() {
339                    return Err(Error::schema(
340                        ErrorCode::DuplicateField,
341                        FIELD,
342                        element.span().start,
343                    ));
344                }
345                Slot::Version
346            } else if self.serial.is_none() {
347                Slot::Serial
348            } else if self.nonce.is_none() {
349                Slot::Nonce
350            } else {
351                return Ok(None);
352            }
353        } else if tag == Tag::primitive(universal::OID) {
354            Slot::Policy
355        } else if tag == Tag::constructed(universal::SEQUENCE) {
356            let mut probe = parent
357                .enter(element)
358                .map_err(|error| Error::encoding(error, FIELD))?;
359            let first = probe
360                .next_element()
361                .map_err(|error| Error::encoding(error, FIELD))?;
362            // A bare OID + hash has one malformed imprint reading; it must not
363            // occupy Accuracy and erase required serial/genTime at finish.
364            if first.is_some_and(|first| {
365                first.tag() == Tag::constructed(universal::SEQUENCE)
366                    || first.tag() == Tag::primitive(universal::OID)
367            }) {
368                Slot::Imprint
369            } else {
370                Slot::Accuracy
371            }
372        } else if tag == Tag::primitive(universal::GENERALIZED_TIME)
373            || tag == Tag::constructed(universal::GENERALIZED_TIME)
374        {
375            Slot::GenTime
376        } else if tag == Tag::primitive(universal::BOOLEAN) {
377            Slot::Ordering
378        } else if tag == Tag::context(true, 0) {
379            Slot::Tsa
380        } else if tag == Tag::context(true, 1) {
381            Slot::Extensions
382        } else {
383            return Ok(None);
384        };
385        let found = match slot {
386            Slot::Version => self.version,
387            Slot::Policy => self.policy.is_some(),
388            Slot::Imprint => self.imprint.is_some(),
389            Slot::Serial => self.serial.is_some(),
390            Slot::GenTime => self.gen_time.is_some(),
391            Slot::Accuracy => self.accuracy.is_some(),
392            Slot::Ordering => self.ordering.is_some(),
393            Slot::Nonce => self.nonce.is_some(),
394            Slot::Tsa => self.tsa.is_some(),
395            Slot::Extensions => self.extensions.is_some(),
396        };
397        Ok(if found { None } else { Some(slot) })
398    }
399
400    fn read<W: Work>(
401        &mut self,
402        slot: Slot,
403        parent: &Reader<'a>,
404        element: &Element<'a>,
405        faults: &mut OwnedVec<Error, W>,
406    ) -> Result<()> {
407        match slot {
408            Slot::Version => {
409                let version = IntegerField::read(parent, element, "tstInfo.version", faults)?;
410                if version.value.as_u64() != Some(1) {
411                    return Err(Error::schema(
412                        ErrorCode::UnsupportedVersion,
413                        "tstInfo.version",
414                        element.span().start,
415                    ));
416                }
417                self.version = true;
418            }
419            Slot::Policy => {
420                self.policy = Some(
421                    parent
422                        .oid(element)
423                        .map_err(|error| Error::encoding(error, "tstInfo.policy"))?,
424                );
425            }
426            Slot::Imprint => self.imprint = Some(MessageImprint::read(parent, element)?),
427            Slot::Serial => {
428                let serial = IntegerField::read(parent, element, "tstInfo.serialNumber", faults)?;
429                // RFC3161 §2.4.2 places serial after messageImprint. Before
430                // that anchor, value 1 also admits a repeated version with
431                // serial absent. Recovery must not select a competing reading.
432                // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
433                if self.imprint.is_none() && serial.value.as_u64() == Some(1) {
434                    return Err(Error::schema(
435                        ErrorCode::DuplicateField,
436                        FIELD,
437                        element.span().start,
438                    ));
439                }
440                self.serial_displaced = self.imprint.is_none() || self.gen_time.is_some();
441                self.serial = Some(serial);
442            }
443            Slot::GenTime => {
444                self.gen_time = Some(
445                    structured_pkix::temporal::read_generalized(parent, element, "tstInfo.genTime")
446                        .map_err(Error::pkix)?,
447                );
448            }
449            Slot::Accuracy => self.accuracy = Some(AccuracyReading::read(parent, element)?),
450            Slot::Ordering => {
451                let ordering = parent
452                    .boolean(element)
453                    .map_err(|error| Error::encoding(error, "tstInfo.ordering"))?;
454                if !ordering {
455                    record(
456                        faults,
457                        Error::schema(
458                            ErrorCode::DefaultWritten,
459                            "tstInfo.ordering",
460                            element.span().start,
461                        ),
462                    )?;
463                }
464                self.ordering = Some(ordering);
465            }
466            Slot::Nonce => {
467                self.nonce = Some(IntegerField::read(
468                    parent,
469                    element,
470                    "tstInfo.nonce",
471                    faults,
472                )?);
473            }
474            Slot::Tsa => self.tsa = Some(TsaReading::read(parent, element, faults)?),
475            Slot::Extensions => self.extensions = Some(TokenExtensions::read(parent, element)?),
476        }
477        Ok(())
478    }
479
480    fn finish(self, span: Span, faults: OwnedVec<Error>) -> Result<TokenInfo<'a>> {
481        let missing = |field| Error::schema(ErrorCode::MissingField, field, span.start);
482        if !self.version {
483            return Err(missing("tstInfo.version"));
484        }
485        Ok(TokenInfo {
486            policy: self.policy.ok_or_else(|| missing("tstInfo.policy"))?,
487            imprint: self
488                .imprint
489                .ok_or_else(|| missing("tstInfo.messageImprint"))?,
490            serial: self.serial.ok_or_else(|| missing("tstInfo.serialNumber"))?,
491            gen_time: self.gen_time.ok_or_else(|| missing("tstInfo.genTime"))?,
492            accuracy: self.accuracy,
493            ordering: self.ordering.unwrap_or(false),
494            nonce: self.nonce,
495            tsa: self.tsa,
496            extensions: self.extensions,
497            faults,
498            span,
499        })
500    }
501}
502
503fn carries(tag: Tag) -> bool {
504    tag == Tag::primitive(universal::INTEGER)
505        || tag == Tag::primitive(universal::OID)
506        || tag == Tag::constructed(universal::SEQUENCE)
507        || tag == Tag::primitive(universal::GENERALIZED_TIME)
508        || tag == Tag::constructed(universal::GENERALIZED_TIME)
509        || tag == Tag::primitive(universal::BOOLEAN)
510        || tag == Tag::context(true, 0)
511        || tag == Tag::context(true, 1)
512}
513
514fn distinguished<W: Work>(
515    parent: &Reader<'_>,
516    element: &Element<'_>,
517    faults: &mut OwnedVec<Error, W>,
518) -> Result<()> {
519    let mut strict = parent
520        .reread(element, Encoding::Der)
521        .map_err(|error| Error::encoding(error, FIELD))?;
522    let result = strict
523        .expect(element.tag())
524        .map_err(|error| Error::encoding(error, FIELD))
525        .and_then(|again| {
526            structured_pkix::der::conforms(&strict, &again, FIELD).map_err(Error::pkix)
527        });
528    match result {
529        Ok(()) => Ok(()),
530        Err(error) => record(faults, error),
531    }
532}
533
534#[cfg(test)]
535mod tail_tests {
536    use super::*;
537    use alloc::sync::Arc;
538    use structured_asn1::{
539        Parser,
540        memory::{Memory, MemoryAccount, MemoryError},
541    };
542
543    fn carried_bytes() -> [u8; 42] {
544        [
545            0x30, 37, 2, 1, 1, 6, 1, 43, 0x30, 9, 0x30, 3, 6, 1, 42, 4, 2, b'a', b'b', 2, 1, 7,
546            0x18, 15, b'2', b'0', b'2', b'6', b'0', b'1', b'0', b'1', b'0', b'0', b'0', b'0', b'0',
547            b'0', b'Z', 0xff, 0xfe, 0xfd,
548        ]
549    }
550
551    #[test]
552    fn tail_capacity_refusal_rolls_back_without_funding_a_successful_reading() {
553        // Fill capacity only after schema reading, isolating the fault admission
554        // from temporary DER-walk frames and proving refusal at the tail itself.
555        let bytes = carried_bytes();
556        let memory = Memory::new(Arc::new(MemoryAccount::new(65_536, 1_000_000)));
557        let parser = Parser::ber(memory.clone());
558        let mut parent = parser.read(&bytes, 70).unwrap();
559        let element = parent
560            .expect(Tag::constructed(universal::SEQUENCE))
561            .unwrap();
562        let info = TokenInfo::read(&parent, &element).unwrap();
563        assert_eq!(info.faults().len(), 0);
564        let mut held = memory.vector::<u8>();
565        held.try_reserve_exact(memory.limit() - memory.used())
566            .unwrap();
567        assert_eq!(memory.used(), memory.limit());
568        let error = info.with_tail(&parent).unwrap_err();
569        assert!(error.is_terminal());
570        assert_eq!(error.offset(), 109);
571        assert_eq!(error.field(), "tstInfo");
572        assert_eq!(
573            error.code(),
574            ErrorCode::Encoding(structured_asn1::ErrorCode::from(MemoryError::LimitExceeded))
575        );
576        drop(error);
577        drop(held);
578        assert_eq!(memory.used(), 0);
579    }
580
581    #[test]
582    fn retained_tail_diagnostic_keeps_charge_after_original_parser_drops() {
583        // Moving the independently owned catalogue preserves its allocation and
584        // source location; releasing the parser is not a storage refund.
585        let bytes = carried_bytes();
586        let memory = Memory::new(Arc::new(MemoryAccount::new(65_536, 1_000_000)));
587        let faults = {
588            let parser = Parser::ber(memory.clone());
589            let mut reader = parser.read(&bytes, 70).unwrap();
590            let TokenInfo { faults, .. } = TokenInfo::read_carried(&mut reader).unwrap();
591            faults
592        };
593        assert_eq!(faults.len(), 1);
594        assert_eq!(faults[0].code(), ErrorCode::TrailingData);
595        assert_eq!(faults[0].offset(), 109);
596        assert_eq!(faults[0].field(), "tstInfo");
597        assert!(memory.used() >= core::mem::size_of::<Error>());
598        drop(faults);
599        assert_eq!(memory.used(), 0);
600    }
601}