Skip to main content

structured_cades/
evidence.rs

1//! `CAdES` evidence records and historical proof-of-existence composition.
2//!
3//! RFC4998 Appendix A binds internal/external records to original CMS bytes.
4//! TS101733 V2.2.1 §6.5.1 has a different eight-hash `PoE` recipe; its unsigned
5//! `poeDate` orders instances and is never authenticated occurrence time.
6//! <https://www.rfc-editor.org/rfc/rfc4998#appendix-A>
7//! <https://www.etsi.org/deliver/etsi_ts/101700_101799/101733/02.02.01_60/ts_101733v020201p.pdf>
8
9use crate::{Error, Result};
10use structured_asn1::{Element, Encoding, Parser, Reader, Span, Tag, universal};
11use structured_cms::{
12    ErrorCode,
13    evidence::EvidenceRecord,
14    signed::{CertificateSet, RevocationSet},
15    timestamp::Token,
16};
17use structured_pkix::temporal::{self, Reading};
18
19const FIELD: &str = "cades.longTermValidation";
20mod closure;
21mod cms_object;
22mod coverage;
23mod poe;
24mod record;
25pub use closure::{Closure, ClosureError, History as PreservationHistory, close_preservation};
26pub use cms_object::{CmsObject, CmsSnapshot, ObjectError, SnapshotError};
27pub use coverage::{Archive as RecordArchive, CoverageError as RecordCoverageError};
28pub use poe::{
29    CheckError as PoeCheckError, CheckResult as PoeCheckResult, InputError as PoeInputError,
30};
31pub use record::{
32    Authorities, Authority, CheckError as RecordCheckError, CheckResult as RecordCheckResult,
33    Checked as RecordChecked, SelectionError as AuthorityError, SourceError as RecordSourceError,
34    Verified as VerifiedRecord,
35};
36
37/// Original optional `PoE` value. Absence is an unfinished obligation, not an
38/// empty successful proof. The two alternatives have different input recipes.
39#[derive(Debug)]
40#[expect(
41    clippy::large_enum_variant,
42    reason = "Both first-read alternatives retain quota-backed flat catalogues inline; boxing adds an allocation to every proof."
43)]
44pub enum PoeValue<'d> {
45    /// `[0] EXPLICIT` RFC3161 token covering the complete eight-hash value.
46    Timestamp(Token<'d>),
47    /// `[1] EXPLICIT` ERS record covering that value for each record algorithm.
48    EvidenceRecord(EvidenceRecord<'d>),
49}
50
51/// Complete original TS101733 §6.5.1 enclosure, including validation extras.
52/// Its date and unprotected material do not establish a successful `PoE`.
53#[derive(Debug)]
54pub struct LongTermValidation<'d> {
55    parser: &'d Parser,
56    element: Element<'d>,
57    encoding: Encoding,
58    depth: u32,
59    date: temporal::ExactReading,
60    date_element: Element<'d>,
61    proof: Option<PoeValue<'d>>,
62    token_source: Option<crate::source::Value<'d>>,
63    certificates: Option<CertificateSet<'d>>,
64    revocations: Option<RevocationSet<'d>>,
65}
66impl<'d> LongTermValidation<'d> {
67    /// Reads one original enclosure, using the existing CMS/PKIX field kernels.
68    /// Known explicit wrappers are distinguished from optional IMPLICIT extras
69    /// by their inner schema; an unknown ambiguous shape is never guessed.
70    pub fn read(parent: &Reader<'d>, element: &Element<'d>) -> Result<Self> {
71        if element.tag() != Tag::constructed(universal::SEQUENCE) {
72            return Err(syntax(ErrorCode::UnexpectedTag, element.span()));
73        }
74        let mut inside = parent.enter(element).map_err(encoding)?;
75        let date_element = required(&mut inside)?;
76        let date =
77            temporal::read_generalized_exact(&inside, &date_element, FIELD).map_err(Error::Pkix)?;
78        if parent.encoding().is_der() {
79            structured_pkix::der::conforms(&inside, &date_element, FIELD).map_err(Error::Pkix)?;
80        }
81        let mut next = inside.next_element().map_err(encoding)?;
82        let mut token_source = None;
83        let proof = if let Some(candidate) = next {
84            if is_proof(&inside, &candidate)? {
85                let mut wrapper = inside.enter(&candidate).map_err(encoding)?;
86                let value = required(&mut wrapper)?;
87                let proof = if candidate.tag() == Tag::context(true, 0) {
88                    token_source = Some(crate::source::Value::of(&wrapper, value, FIELD));
89                    let mut source = wrapper
90                        .reread(&value, wrapper.encoding())
91                        .map_err(encoding)?;
92                    PoeValue::Timestamp(Token::read_from(&mut source)?)
93                } else {
94                    PoeValue::EvidenceRecord(EvidenceRecord::read(&wrapper, &value)?)
95                };
96                finish(&wrapper)?;
97                next = inside.next_element().map_err(encoding)?;
98                Some(proof)
99            } else {
100                None
101            }
102        } else {
103            None
104        };
105        let certificates =
106            if let Some(candidate) = next.filter(|value| value.tag() == Tag::context(true, 0)) {
107                let value = CertificateSet::read(&inside, &candidate, FIELD)?;
108                next = inside.next_element().map_err(encoding)?;
109                Some(value)
110            } else {
111                None
112            };
113        let revocations =
114            if let Some(candidate) = next.filter(|value| value.tag() == Tag::context(true, 1)) {
115                let value = RevocationSet::read(&inside, &candidate, FIELD)?;
116                next = inside.next_element().map_err(encoding)?;
117                Some(value)
118            } else {
119                None
120            };
121        if let Some(extra) = next {
122            return Err(syntax(ErrorCode::TrailingData, extra.span()));
123        }
124        finish(&inside)?;
125        Ok(Self {
126            parser: parent.parser(),
127            element: *element,
128            encoding: parent.encoding(),
129            depth: parent.depth(),
130            date,
131            date_element,
132            proof,
133            token_source,
134            certificates,
135            revocations,
136        })
137    }
138    /// Unsigned ordering metadata, never the authenticated `PoE` time.
139    #[must_use]
140    pub fn date(&self) -> Reading {
141        self.date.reading()
142    }
143    /// Exact order of the unsigned metadata, including arbitrary decimal
144    /// precision. This does not establish authenticated `PoE` chronology.
145    pub fn compare_date<W: structured_asn1::memory::Work>(
146        &self,
147        other: &Self,
148        utc: &temporal::LeapTable<'_>,
149        work: &W,
150    ) -> core::result::Result<core::cmp::Ordering, temporal::CompareError> {
151        self.date.compare(&other.date, utc, work)
152    }
153    /// Original whole generalized-time value and physical coordinates.
154    #[must_use]
155    pub fn date_element(&self) -> Element<'d> {
156        self.date_element
157    }
158    /// Actual optional token/record; absence remains explicit.
159    #[must_use]
160    pub fn proof(&self) -> Option<&PoeValue<'d>> {
161        self.proof.as_ref()
162    }
163    pub(crate) fn timestamp_source(&self) -> Option<crate::source::Value<'d>> {
164        self.token_source
165    }
166    /// Original additional choices and first-read certificate models.
167    #[must_use]
168    pub fn certificates(&self) -> Option<&CertificateSet<'d>> {
169        self.certificates.as_ref()
170    }
171    /// Original additional CRL/other-format values, not authenticated status.
172    #[must_use]
173    pub fn revocations(&self) -> Option<&RevocationSet<'d>> {
174        self.revocations.as_ref()
175    }
176    /// Whole original signed source enclosure.
177    #[must_use]
178    pub fn element(&self) -> Element<'d> {
179        self.element
180    }
181    /// Reattaches this same source/depth to current operation admission.
182    pub fn reader_in<'q>(&self, query: &Reader<'q>) -> Result<Reader<'q>>
183    where
184        'd: 'q,
185    {
186        self.parser
187            .read_query_in(
188                query,
189                self.element.as_bytes(),
190                self.element.span().start,
191                self.encoding,
192                self.depth,
193            )
194            .map_err(encoding)
195    }
196}
197
198fn is_proof(parent: &Reader<'_>, candidate: &Element<'_>) -> Result<bool> {
199    if candidate.tag() != Tag::context(true, 0) && candidate.tag() != Tag::context(true, 1) {
200        return Ok(false);
201    }
202    let mut wrapper = parent.enter(candidate).map_err(encoding)?;
203    let Some(value) = wrapper.next_element().map_err(encoding)? else {
204        return Ok(false);
205    };
206    if value.tag() != Tag::constructed(universal::SEQUENCE) {
207        return Ok(false);
208    }
209    let mut fields = wrapper.enter(&value).map_err(encoding)?;
210    let first = required(&mut fields)?;
211    Ok(if candidate.tag() == Tag::context(true, 0) {
212        first.tag() == Tag::primitive(universal::OID)
213    } else {
214        first.tag() == Tag::primitive(universal::INTEGER)
215    })
216}
217fn required<'d>(reader: &mut Reader<'d>) -> Result<Element<'d>> {
218    reader
219        .next_element()
220        .map_err(encoding)?
221        .ok_or(Error::Syntax {
222            code: ErrorCode::MissingField,
223            field: FIELD,
224            at: reader.offset(),
225        })
226}
227fn finish(reader: &Reader<'_>) -> Result<()> {
228    if !reader.is_empty() {
229        return Err(Error::Syntax {
230            code: ErrorCode::TrailingData,
231            field: FIELD,
232            at: reader.offset(),
233        });
234    }
235    Ok(())
236}
237fn syntax(code: ErrorCode, span: Span) -> Error {
238    Error::Syntax {
239        code,
240        field: FIELD,
241        at: span.start,
242    }
243}
244fn encoding(cause: structured_asn1::Error) -> Error {
245    Error::encoding(cause, FIELD)
246}
247
248/// One immutable original preservation enclosure from a preparation. Its
249/// tokens are the SAME models used by the full TSA census and material pool.
250pub struct View<'a, 'm, 'd> {
251    pub(crate) prepared: &'a crate::prepare::Preparation<'m, 'd>,
252    pub(crate) entry: &'a crate::prepare::proofs::Entry<'d>,
253    pub(crate) ordinal: usize,
254}
255impl<'a, 'm, 'd> View<'a, 'm, 'd> {
256    /// Actual original typed mechanism/placement/attribute, not a level claim.
257    #[must_use]
258    pub fn required(&self) -> crate::preservation::Required<'d> {
259        self.entry.required
260    }
261    /// This preparation's original proof ordinal; it is not a claimed date.
262    #[must_use]
263    pub fn ordinal(&self) -> usize {
264        self.ordinal
265    }
266    /// Every actual timestamp ordinal in the SAME complete preparation.
267    #[must_use]
268    pub fn timestamps(&self) -> core::ops::Range<usize> {
269        self.entry.timestamps.clone()
270    }
271    /// Actual first-read ERS alternative, including a `PoE`-carried ERS record.
272    #[must_use]
273    pub fn record(&self) -> Option<&'a EvidenceRecord<'d>> {
274        self.entry.record()
275    }
276    /// Original `PoE` enclosure and its additional validation material.
277    #[must_use]
278    pub fn poe(&self) -> Option<&'a LongTermValidation<'d>> {
279        match &self.entry.body {
280            crate::prepare::proofs::Body::Poe(poe) => Some(poe),
281            crate::prepare::proofs::Body::Record(_) => None,
282        }
283    }
284    /// Same complete original signature preparation, with no new identity.
285    #[must_use]
286    pub fn prepared(&self) -> &'a crate::prepare::Preparation<'m, 'd> {
287        self.prepared
288    }
289}
290
291impl<'m, 'd> crate::prepare::Preparation<'m, 'd> {
292    /// Readable original ERS/PoE enclosures. Failed obligations remain in
293    /// `preservation()` and `evidence_faults()`; they cannot be omitted by a close.
294    #[must_use]
295    pub fn evidence_records(&self) -> usize {
296        self.parsed.proofs.entries.len()
297    }
298    /// Every unreadable original proof enclosure, preserving source/cause.
299    #[must_use]
300    pub fn evidence_faults(&self) -> &[(crate::source::Node, Error)] {
301        &self.parsed.proofs.faults
302    }
303    /// Paid lookup of one first-read proof under the actual current query.
304    pub fn evidence(
305        &self,
306        ordinal: usize,
307        operation: &Reader<'d>,
308    ) -> Result<Option<View<'_, 'm, 'd>>> {
309        self.document()
310            .message()
311            .require_session(operation, FIELD)?;
312        operation.charge(1).map_err(encoding)?;
313        Ok(self.parsed.proofs.entries.get(ordinal).map(|entry| View {
314            prepared: self,
315            entry,
316            ordinal,
317        }))
318    }
319}