Skip to main content

structured_cades/evidence/
poe.rs

1//! TS101733 ยง6.5.1: algorithm-dependent eight-hash and multiset-tree inputs.
2//!
3//! This is private: only the original-format recipe can construct components.
4//! No arbitrary raw digest or caller-written successful receipt is accepted.
5
6use crate::Error;
7use core::cmp::Ordering;
8use structured_asn1::memory::{OwnedVec, SortError, Work};
9use structured_cms::{
10    digest::{DigestBatch, DigestError, DigestMemo, PassResult, RequestSource},
11    stream::{Receiver, Refused, StreamError},
12};
13use structured_pkix::algorithm::digest::{Algorithm, AsnDigest};
14
15const FIELD: &str = "cades.longTermValidation.input";
16
17/// Every actual derivation/refusal remains distinct from a completed mismatch.
18#[derive(Debug)]
19pub enum InputError<SE, PE> {
20    /// Original source/schema/local/aggregate processing refusal.
21    Processing(Error),
22    /// Selected provider refused, or contradicted its promised output width.
23    Digest(DigestError<PE>),
24    /// Actual carried/detached content failed before complete EOF.
25    Content(crate::timestamp::InputError<SE>),
26    /// The required algorithm has no identity in this selected provider.
27    Unsupported,
28    /// Hs has no original signed-attribute encoding; no empty input is made.
29    MissingSignedAttributes,
30    /// Original unsigned date ordering uncertainty or resource refusal.
31    Date(structured_pkix::temporal::CompareError),
32    /// Qualified later archive/source/coverage admission refused.
33    Augmentation(crate::timestamp::historical::SelectionError),
34    /// Components belong to a different provider, identity or original request.
35    Foreign,
36    /// The common sticky receiver retains its original provider/driver cause.
37    Receiver(Refused),
38}
39
40pub(super) fn batch<'p, P: AsnDigest<'p>, W: Work, SE>(
41    provider: &'p P,
42    algorithms: &[Algorithm],
43    work: &W,
44    at: u64,
45) -> Result<DigestBatch<'p, P>, InputError<SE, P::Error>> {
46    let mut identities = work.vector();
47    identities
48        .try_reserve_exact(algorithms.len())
49        .map_err(|cause| InputError::Processing(Error::resource(cause, FIELD, at)))?;
50    for algorithm in algorithms {
51        work.charge_work(1)
52            .map_err(|cause| InputError::Processing(Error::resource(cause, FIELD, at)))?;
53        identities
54            .try_push(
55                provider
56                    .identity(*algorithm)
57                    .ok_or(InputError::Unsupported)?,
58            )
59            .map_err(|cause| InputError::Processing(Error::resource(cause, FIELD, at)))?;
60    }
61    DigestBatch::prepare(provider, identities.into_owned(), work, FIELD, at)
62        .map_err(InputError::Digest)
63}
64
65/// Exactly eight independently typed completed components. Construction is
66/// limited to the source-bound schema recipe in this module's parent.
67pub(super) struct Eight<'p, P: AsnDigest<'p>> {
68    parts: OwnedVec<DigestMemo<'p, P>>,
69    covered: OwnedVec<structured_asn1::Span>,
70    algorithms: OwnedVec<Algorithm>,
71    at: u64,
72}
73impl<'p, P: AsnDigest<'p>> Eight<'p, P> {
74    pub(super) fn build<W: Work, SE>(
75        provider: &'p P,
76        algorithms: &[Algorithm],
77        work: &W,
78        at: u64,
79        covered: OwnedVec<structured_asn1::Span>,
80        mut component: impl FnMut(
81            usize,
82            DigestBatch<'p, P>,
83            &W,
84        ) -> Result<DigestMemo<'p, P>, InputError<SE, P::Error>>,
85    ) -> Result<Self, InputError<SE, P::Error>> {
86        let resource = |cause| InputError::Processing(Error::resource(cause, FIELD, at));
87        let mut parts = work.vector();
88        parts.try_reserve_exact(8).map_err(resource)?;
89        let mut saved = work.vector();
90        saved.try_extend_from_slice(algorithms).map_err(resource)?;
91        for index in 0..8 {
92            work.charge_work(1).map_err(resource)?;
93            let pending = batch(provider, algorithms, work, at)?;
94            let completed = component(index, pending, work)?;
95            parts.try_push(completed).map_err(resource)?;
96        }
97        Ok(Self {
98            parts: parts.into_owned(),
99            covered,
100            algorithms: saved.into_owned(),
101            at,
102        })
103    }
104
105    pub(super) fn stream<W: Work, SE>(
106        &self,
107        ordinal: usize,
108        provider: &'p P,
109        identity: &P::Identity,
110        out: &mut impl Receiver,
111        work: &W,
112    ) -> Result<(), InputError<SE, P::Error>> {
113        work.charge_work(1)
114            .map_err(|cause| InputError::Processing(Error::resource(cause, FIELD, self.at)))?;
115        let algorithm = self.algorithms.get(ordinal).ok_or(InputError::Foreign)?;
116        // Admit and validate ALL eight before the first byte reaches the
117        // outer hash. A late mismatch cannot silently mix provider identities.
118        debug_assert_eq!(self.parts.len(), 8);
119        // All eight slots are assigned by the private constructor invariant
120        // before any slot is observed; initial slices are not missing values.
121        let mut outputs = [&[][..]; 8];
122        for (slot, part) in outputs.iter_mut().zip(&*self.parts) {
123            *slot = checked(
124                part,
125                ordinal,
126                provider,
127                identity,
128                algorithm.width(),
129                work,
130                self.at,
131            )?;
132        }
133        for bytes in outputs {
134            out.write(bytes).map_err(InputError::Receiver)?;
135        }
136        Ok(())
137    }
138}
139
140fn checked<'m, 'p, P: AsnDigest<'p>, W: Work, SE>(
141    memo: &'m DigestMemo<'p, P>,
142    ordinal: usize,
143    provider: &'p P,
144    identity: &P::Identity,
145    width: usize,
146    work: &W,
147    at: u64,
148) -> Result<&'m [u8], InputError<SE, P::Error>> {
149    work.charge_work(1)
150        .map_err(|cause| InputError::Processing(Error::resource(cause, FIELD, at)))?;
151    let value = memo
152        .for_request(ordinal, work)
153        .map_err(InputError::Digest)?
154        .ok_or(InputError::Foreign)?;
155    if !value.finished().from_provider(provider) {
156        return Err(InputError::Foreign);
157    }
158    if provider
159        .compare(value.identity(), identity, work)
160        .map_err(|cause| InputError::Digest(DigestError::Callback(cause)))?
161        != Ordering::Equal
162    {
163        return Err(InputError::Foreign);
164    }
165    if value.as_bytes().len() != width {
166        return Err(InputError::Digest(DigestError::OutputWidth {
167            expected: width,
168            actual: value.as_bytes().len(),
169        }));
170    }
171    Ok(value.as_bytes())
172}
173
174/// Per-algorithm treehash preimage. Distinct encoded items are hashed first;
175/// their COMPLETE outputs are sorted and streamed, preserving duplicates.
176pub(super) struct Tree<'alg, 'p, P: AsnDigest<'p>, SE> {
177    provider: &'p P,
178    items: OwnedVec<DigestMemo<'p, P>>,
179    algorithms: &'alg [Algorithm],
180    at: u64,
181    error: core::marker::PhantomData<fn() -> SE>,
182}
183impl<'alg, 'p, P: AsnDigest<'p>, SE> Tree<'alg, 'p, P, SE> {
184    pub(super) fn new(
185        provider: &'p P,
186        items: OwnedVec<DigestMemo<'p, P>>,
187        algorithms: &'alg [Algorithm],
188        at: u64,
189    ) -> Self {
190        Self {
191            provider,
192            items,
193            algorithms,
194            at,
195            error: core::marker::PhantomData,
196        }
197    }
198}
199impl<'p, P: AsnDigest<'p>, SE> RequestSource<'p, P> for Tree<'_, 'p, P, SE> {
200    type Error = InputError<SE, P::Error>;
201    fn stream_request<W: Work>(
202        &mut self,
203        ordinal: usize,
204        provider: &'p P,
205        identity: &P::Identity,
206        out: &mut impl Receiver,
207        work: &W,
208    ) -> Result<(), Self::Error> {
209        let resource = |cause| InputError::Processing(Error::resource(cause, FIELD, self.at));
210        work.charge_work(1).map_err(resource)?;
211        if !core::ptr::eq(provider, self.provider) {
212            return Err(InputError::Foreign);
213        }
214        let width = self
215            .algorithms
216            .get(ordinal)
217            .ok_or(InputError::Foreign)?
218            .width();
219        let mut hashes = work.vector();
220        hashes
221            .try_reserve_exact(self.items.len())
222            .map_err(resource)?;
223        for item in &self.items {
224            let value = checked(item, ordinal, provider, identity, width, work, self.at)?;
225            hashes.try_push(value).map_err(resource)?;
226        }
227        hashes
228            .try_sort_unstable_by(|left, right| {
229                work.charge_work(1 + left.len().min(right.len()) as u64)?;
230                Ok::<_, structured_asn1::memory::MemoryError>(left.cmp(right))
231            })
232            .map_err(|cause| match cause {
233                SortError::Resource(cause) | SortError::Comparison(cause) => resource(cause),
234            })?;
235        for value in &hashes {
236            work.charge_work(1).map_err(resource)?;
237            out.write(value).map_err(InputError::Receiver)?;
238        }
239        // Empty treehash is H(empty), not an absent or invented zero digest.
240        Ok(())
241    }
242}
243
244pub(super) fn derived_result<T, SE, PE>(
245    result: PassResult<T, InputError<SE, PE>, PE>,
246) -> Result<T, InputError<SE, PE>> {
247    result.map_err(|cause| match cause {
248        StreamError::Source(cause) => cause,
249        StreamError::Callback(cause) => InputError::Digest(cause),
250        StreamError::Processing(cause) => InputError::Processing(Error::Cms(cause)),
251    })
252}
253
254mod check;
255mod recipe;
256pub use check::{CheckError, CheckResult};
257
258/// Construct components only when the actual common token check requests
259/// covered input, after syntax, selected-key and signed-profile admission.
260struct Lazy<'s, 'a, 'm, 'd, 'p, 'utc, P: AsnDigest<'p>, S> {
261    view: &'s super::View<'a, 'm, 'd>,
262    later: &'s [crate::timestamp::historical::Later<'s, 'm, 'd>],
263    provider: &'p P,
264    algorithms: &'s [Algorithm],
265    operation: &'s structured_asn1::Reader<'d>,
266    utc: &'s structured_pkix::temporal::LeapTable<'utc>,
267    detached: Option<&'s mut S>,
268    ready: Option<Eight<'p, P>>,
269}
270impl<'p, P: AsnDigest<'p>, S: structured_cms::stream::Source> Lazy<'_, '_, '_, '_, 'p, '_, P, S> {
271    fn ready<W: Work>(
272        &mut self,
273        work: &W,
274    ) -> Result<&Eight<'p, P>, InputError<S::Error, P::Error>> {
275        if self.ready.is_none() {
276            self.ready = Some(self.view.components_in(
277                self.provider,
278                self.algorithms,
279                self.operation,
280                self.detached.take(),
281                self.utc,
282                self.later,
283                work,
284            )?);
285        }
286        Ok(self
287            .ready
288            .as_ref()
289            .expect("successful component construction"))
290    }
291}
292impl<'p, P: AsnDigest<'p>, S: structured_cms::stream::Source> RequestSource<'p, P>
293    for Lazy<'_, '_, '_, '_, 'p, '_, P, S>
294{
295    type Error = InputError<S::Error, P::Error>;
296    fn stream_request<W: Work>(
297        &mut self,
298        ordinal: usize,
299        provider: &'p P,
300        identity: &P::Identity,
301        out: &mut impl Receiver,
302        work: &W,
303    ) -> Result<(), Self::Error> {
304        self.ready(work)?
305            .stream(ordinal, provider, identity, out, work)
306    }
307}
308impl<'p, P: AsnDigest<'p>, S: structured_cms::stream::Source> structured_cms::stream::Source
309    for Lazy<'_, '_, '_, '_, 'p, '_, P, S>
310{
311    type Error = InputError<S::Error, P::Error>;
312    fn stream<W: Work>(&mut self, out: &mut impl Receiver, work: &W) -> Result<(), Self::Error> {
313        work.charge_work(1).map_err(|cause| {
314            InputError::Processing(Error::resource(
315                cause,
316                FIELD,
317                self.view.required().property.span().start,
318            ))
319        })?;
320        if self.algorithms.len() != 1 {
321            return Err(InputError::Foreign);
322        }
323        let provider = self.provider;
324        let identity = provider
325            .identity(self.algorithms[0])
326            .ok_or(InputError::Unsupported)?;
327        self.ready(work)?.stream(0, provider, &identity, out, work)
328    }
329}
330
331#[cfg(test)]
332#[path = "poe/tests.rs"]
333mod tests;