Skip to main content

structured_cms/timestamp/
computed.rs

1//! Scoped computed imprints: alternatives and Merkle roots without double hashing.
2
3use super::{ImprintAgreement, ImprintError, MessageImprint};
4use crate::{Error, binding::DigestBinding, digest::DigestError, provider::Finished};
5use core::cell::{Cell, RefCell};
6use structured_asn1::{Reader, Span, digest::Resolution, memory::OwnedVec};
7use structured_pkix::algorithm::digest::AsnDigest;
8
9/// An input recipe executes under the token's one resolved digest identity.
10///
11/// A recipe can stream several normative alternatives together, or compute a
12/// reduced hash tree. It cannot construct an affirmative proof from bytes or
13/// a boolean: only this operation's completed states can be compared with the
14/// original declared hash. Provider errors cannot borrow temporary joined
15/// information. No identity/state/output cloning is required.
16pub trait ComputedInput<P, PE> {
17    /// Original source or recipe failure, independent of semantic disagreement.
18    type Error;
19
20    /// Complete the input, preserving refusal even when a callback ignores it.
21    fn compute<'context, 'reader, 'document>(
22        &mut self,
23        context: &'context ImprintContext<'reader, 'document, P, Self::Error>,
24    ) -> Result<ImprintProof<'context>, InputFailure<Self::Error>>
25    where
26        P: AsnDigest<'document, Error = PE>;
27}
28
29/// Original recipe failure or a marker whose cause the operation retains.
30#[derive(Debug)]
31pub enum InputFailure<E> {
32    /// Original source/recipe cause; no successful prefix is returned.
33    Source(E),
34    /// The context keeps the first processing/provider/acquisition refusal.
35    Refused(crate::stream::Refused),
36}
37impl<E> From<crate::stream::Refused> for InputFailure<E> {
38    fn from(value: crate::stream::Refused) -> Self {
39        Self::Refused(value)
40    }
41}
42
43/// Closed comparison receipt, tied to this non-zero-sized operation owner.
44///
45/// This is not a standalone timestamp or trust proof. It cannot outlive the
46/// computation or be substituted from another token/context.
47pub struct ImprintProof<'context> {
48    owner: &'context u8,
49    agreement: ImprintAgreement,
50}
51impl ImprintProof<'_> {
52    /// Actual completed comparison, for choosing among finished alternatives.
53    #[must_use]
54    pub fn agreement(&self) -> ImprintAgreement {
55        self.agreement
56    }
57}
58
59enum Expected<'a> {
60    Borrowed(&'a [u8]),
61    Joined(OwnedVec<u8>),
62}
63impl Expected<'_> {
64    fn bytes(&self) -> &[u8] {
65        match self {
66            Self::Borrowed(bytes) => bytes,
67            Self::Joined(bytes) => bytes,
68        }
69    }
70}
71
72/// One original imprint, one provider identity and the current producing work.
73///
74/// The context is constructed only after schema/session/width preflight. Its
75/// non-zero-sized owner distinguishes even otherwise identical operations. All state
76/// transitions retain the first refusal; no `RefCell` borrow crosses a callback.
77pub struct ImprintContext<'reader, 'document, P: AsnDigest<'document>, E> {
78    provider: &'document P,
79    identity: P::Identity,
80    reader: &'reader Reader<'document>,
81    expected: Expected<'document>,
82    field: &'static str,
83    span: Span,
84    owner: u8,
85    active: Cell<usize>,
86    abandoned: Cell<bool>,
87    failure: RefCell<Option<ImprintError<E, P::Error>>>,
88}
89
90/// One unfinished state of this operation's exact resolved identity.
91/// Dropping it unfinished prevents a successful computation, even if another
92/// alternative matched. Opaque provider state owns its admitted storage.
93pub struct ImprintState<'context, 'reader, 'document, P: AsnDigest<'document>, E> {
94    context: &'context ImprintContext<'reader, 'document, P, E>,
95    state: Option<P::State>,
96}
97
98/// The provider's own completed output, retaining its context and identity.
99/// Intermediate tree hashes can be borrowed without copying, re-resolving or
100/// reconstructing a state. There is no arbitrary-byte constructor.
101pub struct ImprintDigest<'context, 'reader, 'document, P: AsnDigest<'document>, E> {
102    context: &'context ImprintContext<'reader, 'document, P, E>,
103    output: P::Output,
104}
105impl<'document, P: AsnDigest<'document>, E> ImprintDigest<'_, '_, 'document, P, E> {
106    /// Complete output bytes, never a digest of a partial/refused state.
107    #[must_use]
108    pub fn as_bytes(&self) -> &[u8] {
109        self.output.as_ref()
110    }
111    /// Typed finalized output for existing common digest consumers.
112    #[must_use]
113    pub fn finished(&self) -> Finished<'_, P, P::Identity, P::Output> {
114        Finished::new(self.context.provider, &self.context.identity, &self.output)
115    }
116}
117
118impl<'reader, 'document, P: AsnDigest<'document>, E> ImprintContext<'reader, 'document, P, E> {
119    /// Original producing information reader and current operation ceilings.
120    #[must_use]
121    pub fn reader(&self) -> &Reader<'document> {
122        self.reader
123    }
124    /// Full selected-provider identity; output width is not an algorithm ID.
125    #[must_use]
126    pub fn identity(&self) -> &P::Identity {
127        &self.identity
128    }
129    pub(crate) fn provider(&self) -> &'document P {
130        self.provider
131    }
132    fn failed(&self) -> bool {
133        self.failure.borrow().is_some()
134    }
135    fn refuse(&self, cause: ImprintError<E, P::Error>) -> crate::stream::Refused {
136        let mut failure = self.failure.borrow_mut();
137        if failure.is_none() {
138            *failure = Some(cause);
139        }
140        crate::stream::Refused
141    }
142    fn admit(&self) -> Result<(), crate::stream::Refused> {
143        if self.failed() {
144            return Err(crate::stream::Refused);
145        }
146        self.reader.charge(1).map_err(|cause| {
147            self.refuse(ImprintError::Processing(Error::encoding(cause, self.field)))
148        })
149    }
150    fn admit_pass(&self) -> Result<(), crate::stream::Refused> {
151        if self.failed() {
152            return Err(crate::stream::Refused);
153        }
154        self.reader.charge(1).map_err(|cause| {
155            self.refuse(ImprintError::Pass(crate::stream::StreamError::Processing(
156                Error::encoding(cause, self.field),
157            )))
158        })
159    }
160    /// Starts another candidate/intermediate hash under this SAME identity.
161    pub fn start(
162        &self,
163    ) -> Result<ImprintState<'_, 'reader, 'document, P, E>, crate::stream::Refused> {
164        self.admit()?;
165        let Some(active) = self.active.get().checked_add(1) else {
166            return Err(self.refuse(ImprintError::IncompleteInput));
167        };
168        let state = self
169            .provider
170            .start(&self.identity, &self.reader.execution())
171            .map_err(|cause| self.refuse(ImprintError::Digest(DigestError::Callback(cause))))?;
172        let Resolution::Supported(state) = state else {
173            return Err(self.refuse(ImprintError::Digest(DigestError::Unstreamable)));
174        };
175        self.active.set(active);
176        Ok(ImprintState {
177            context: self,
178            state: Some(state),
179        })
180    }
181    /// Feeds every selected state in ONE complete source pass. Empty state
182    /// lists and foreign contexts refuse before source I/O. Source errors and
183    /// ignored receiver failures remain sticky across the whole recipe.
184    pub fn stream<'context, S: crate::stream::Source<Error = E>>(
185        &'context self,
186        source: &mut S,
187        states: &mut [&mut ImprintState<'context, 'reader, 'document, P, E>],
188    ) -> Result<(), crate::stream::Refused> {
189        self.stream_with(source, states, &self.reader.execution())
190    }
191
192    /// Streams another original source using ITS producing ceiling and this
193    /// operation's current root ceilings/account. This does not use the old
194    /// source's former account or widen a narrowed current query. The format
195    /// recipe supplies the actual source reader, never a replacement parser.
196    pub fn stream_from<'context, S: crate::stream::Source<Error = E>>(
197        &'context self,
198        source: &mut S,
199        origin: &Reader<'_>,
200        states: &mut [&mut ImprintState<'context, 'reader, 'document, P, E>],
201    ) -> Result<(), crate::stream::Refused> {
202        self.stream_with(source, states, &origin.execution_query(self.reader))
203    }
204
205    fn stream_with<'context, S, W>(
206        &'context self,
207        source: &mut S,
208        states: &mut [&mut ImprintState<'context, 'reader, 'document, P, E>],
209        execution: &W,
210    ) -> Result<(), crate::stream::Refused>
211    where
212        S: crate::stream::Source<Error = E>,
213        W: structured_asn1::memory::Work,
214    {
215        self.admit()?;
216        if states.is_empty() {
217            return Err(self.refuse(ImprintError::IncompleteInput));
218        }
219        for state in &*states {
220            self.admit()?;
221            if !core::ptr::eq(&raw const state.context.owner, &raw const self.owner) {
222                return Err(self.refuse(ImprintError::ForeignInput));
223            }
224        }
225        let result = crate::stream::run(
226            source,
227            &mut Fanout { states },
228            execution,
229            self.field,
230            self.span.start,
231        );
232        match result {
233            Ok(()) => Ok(()),
234            Err(crate::stream::StreamError::Source(cause)) => Err(self.refuse(ImprintError::Pass(
235                crate::stream::StreamError::Source(cause),
236            ))),
237            Err(crate::stream::StreamError::Processing(cause)) => Err(self.refuse(
238                ImprintError::Pass(crate::stream::StreamError::Processing(cause)),
239            )),
240            // State writes already retained the original callback cause.
241            Err(crate::stream::StreamError::Callback(_)) => Err(crate::stream::Refused),
242        }
243    }
244    /// Compares one completed output with the original declared hash. Other
245    /// candidates can be completed and compared without another payload pass.
246    pub fn compare<'context>(
247        &'context self,
248        digest: &ImprintDigest<'_, '_, 'document, P, E>,
249    ) -> Result<ImprintProof<'context>, crate::stream::Refused> {
250        self.admit()?;
251        if !core::ptr::eq(&raw const digest.context.owner, &raw const self.owner) {
252            return Err(self.refuse(ImprintError::ForeignInput));
253        }
254        self.compare_output(&digest.output)
255    }
256
257    // RFC4998 ยง4.2(5) permits an explicit tree identity distinct from the
258    // timestamp imprint identity. Only the ERS kernel can supply that finished
259    // provider output; callers still cannot construct a proof from raw bytes.
260    pub(crate) fn compare_output<'context>(
261        &'context self,
262        output: &P::Output,
263    ) -> Result<ImprintProof<'context>, crate::stream::Refused> {
264        self.admit()?;
265        let result = crate::binding::matches_digest(
266            self.provider,
267            Finished::new(self.provider, &self.identity, output),
268            self.expected.bytes(),
269            &self.reader.execution(),
270            self.field,
271            self.span,
272        )
273        .map_err(|cause| self.refuse(ImprintError::Comparison(cause)))?;
274        Ok(ImprintProof {
275            owner: &self.owner,
276            agreement: match result {
277                DigestBinding::Matches => ImprintAgreement::Matches,
278                DigestBinding::Differs => ImprintAgreement::Differs,
279                DigestBinding::Malformed(_) => ImprintAgreement::MalformedHash,
280            },
281        })
282    }
283
284    // A private format kernel may derive its root under an explicit different
285    // RFC4998 tree identity. Keep its completed-output comparison inside the
286    // operation's sticky refusal boundary; no public byte/proof constructor.
287    pub(crate) fn compare_derived(
288        &self,
289        compare: impl FnOnce(&[u8], &structured_asn1::memory::Execution<'_>) -> Result<bool, E>,
290    ) -> Result<ImprintProof<'_>, crate::stream::Refused> {
291        self.admit()?;
292        let matches =
293            compare(self.expected.bytes(), &self.reader.execution()).map_err(|cause| {
294                self.refuse(ImprintError::Pass(crate::stream::StreamError::Source(
295                    cause,
296                )))
297            })?;
298        Ok(ImprintProof {
299            owner: &self.owner,
300            agreement: if matches {
301                ImprintAgreement::Matches
302            } else {
303                ImprintAgreement::Differs
304            },
305        })
306    }
307}
308
309impl<'context, 'reader, 'document, P: AsnDigest<'document>, E>
310    ImprintState<'context, 'reader, 'document, P, E>
311{
312    /// Admits and hashes one original/generated chunk. An ignored failure
313    /// cannot be repaired by subsequent writes or finalization.
314    pub fn write(&mut self, bytes: &[u8]) -> Result<(), crate::stream::Refused> {
315        self.write_with(bytes, &self.context.reader.execution())
316    }
317    /// Hashes a range from another original reader, with that origin and the
318    /// current operation admitted together. The caller's format kernel binds
319    /// `bytes` to that original source before invoking this operation.
320    pub fn write_from(
321        &mut self,
322        bytes: &[u8],
323        origin: &Reader<'_>,
324    ) -> Result<(), crate::stream::Refused> {
325        self.write_with(bytes, &origin.execution_query(self.context.reader))
326    }
327    fn write_with<W: structured_asn1::memory::Work>(
328        &mut self,
329        bytes: &[u8],
330        execution: &W,
331    ) -> Result<(), crate::stream::Refused> {
332        self.context.admit_pass()?;
333        // Empty ranges still cost a producing-source/fanout visit. A digest
334        // provider correctly charges zero byte work for them; it cannot fund
335        // this structural traversal or enforce its origin ceiling for us.
336        execution.charge_work(1).map_err(|cause| {
337            self.context
338                .refuse(ImprintError::Pass(crate::stream::StreamError::Processing(
339                    Error::resource(cause, self.context.field, self.context.span.start),
340                )))
341        })?;
342        self.context
343            .provider
344            .update(
345                self.state
346                    .as_mut()
347                    .expect("unfinished state owns its backend"),
348                bytes,
349                execution,
350            )
351            .map_err(|cause| {
352                self.context
353                    .refuse(ImprintError::Pass(crate::stream::StreamError::Callback(
354                        DigestError::Callback(cause),
355                    )))
356            })
357    }
358    /// Consumes the state exactly once. A wrong provider output width refuses
359    /// before exposing it for an intermediate tree or final comparison.
360    /// # Panics
361    /// If the internal ownership invariant is violated. The consuming public
362    /// API retains its backend state until this single completion.
363    pub fn finish(
364        mut self,
365    ) -> Result<ImprintDigest<'context, 'reader, 'document, P, E>, crate::stream::Refused> {
366        self.context.admit_pass()?;
367        let output = self
368            .context
369            .provider
370            .finish(
371                self.state
372                    .take()
373                    .expect("unfinished state owns its backend"),
374                &self.context.reader.execution(),
375            )
376            .map_err(|cause| {
377                self.context
378                    .refuse(ImprintError::Pass(crate::stream::StreamError::Callback(
379                        DigestError::Callback(cause),
380                    )))
381            })?;
382        if let Some(expected) = self.context.provider.width(&self.context.identity)
383            && output.as_ref().len() != expected
384        {
385            return Err(self.context.refuse(ImprintError::Pass(
386                crate::stream::StreamError::Callback(DigestError::OutputWidth {
387                    expected,
388                    actual: output.as_ref().len(),
389                }),
390            )));
391        }
392        Ok(ImprintDigest {
393            context: self.context,
394            output,
395        })
396    }
397}
398impl<'document, P: AsnDigest<'document>, E> Drop for ImprintState<'_, '_, 'document, P, E> {
399    fn drop(&mut self) {
400        let active = self.context.active.get();
401        debug_assert!(active > 0);
402        self.context.active.set(active - 1);
403        if self.state.is_some() {
404            self.context.abandoned.set(true);
405        }
406    }
407}
408
409struct Fanout<'states, 'state, 'context, 'reader, 'document, P: AsnDigest<'document>, E> {
410    states: &'states mut [&'state mut ImprintState<'context, 'reader, 'document, P, E>],
411}
412impl<'document, P: AsnDigest<'document>, E> crate::stream::Sink
413    for Fanout<'_, '_, '_, '_, 'document, P, E>
414{
415    type Error = crate::stream::Refused;
416    fn write<W: structured_asn1::memory::Work>(
417        &mut self,
418        bytes: &[u8],
419        execution: &W,
420    ) -> Result<(), Self::Error> {
421        for state in &mut *self.states {
422            state.write_with(bytes, execution)?;
423        }
424        Ok(())
425    }
426}
427
428/// Ordinary covered bytes use the same closed computation as derived inputs.
429pub struct RawInput<'source, S>(&'source mut S);
430impl<'source, S> RawInput<'source, S> {
431    /// Borrows a source without allocation, copying or changing its errors.
432    #[must_use]
433    pub fn new(source: &'source mut S) -> Self {
434        Self(source)
435    }
436}
437impl<P, PE, S: crate::stream::Source> ComputedInput<P, PE> for RawInput<'_, S> {
438    type Error = S::Error;
439    fn compute<'context, 'reader, 'document>(
440        &mut self,
441        context: &'context ImprintContext<'reader, 'document, P, Self::Error>,
442    ) -> Result<ImprintProof<'context>, InputFailure<Self::Error>>
443    where
444        P: AsnDigest<'document, Error = PE>,
445    {
446        let mut state = context.start()?;
447        context.stream(self.0, &mut [&mut state])?;
448        let output = state.finish()?;
449        context.compare(&output).map_err(InputFailure::Refused)
450    }
451}
452
453pub(super) fn compute<'a, P, I>(
454    imprint: &MessageImprint<'a>,
455    provider: &'a P,
456    parent: &Reader<'a>,
457    input: &mut I,
458    identity: P::Identity,
459) -> Result<ImprintAgreement, ImprintError<I::Error, P::Error>>
460where
461    P: AsnDigest<'a>,
462    I: ComputedInput<P, P::Error>,
463{
464    let field = imprint.fields().hashed;
465    let span = imprint.hashed().span();
466    // Complete declared-hash materialization precedes state/provider effects.
467    let expected = match imprint.hashed().as_one() {
468        Some(bytes) => Expected::Borrowed(bytes),
469        None => Expected::Joined(
470            imprint
471                .hashed()
472                .copy_in(parent, field, span.start)
473                .map_err(ImprintError::Processing)?,
474        ),
475    };
476    let context = ImprintContext {
477        provider,
478        identity,
479        reader: parent,
480        expected,
481        field,
482        span,
483        owner: 0,
484        active: Cell::new(0),
485        abandoned: Cell::new(false),
486        failure: RefCell::new(None),
487    };
488    let result = input.compute(&context);
489    if let Some(cause) = context.failure.borrow_mut().take() {
490        return Err(cause);
491    }
492    let proof = match result {
493        Ok(proof) => proof,
494        Err(InputFailure::Source(cause)) => {
495            return Err(ImprintError::Pass(crate::stream::StreamError::Source(
496                cause,
497            )));
498        }
499        Err(InputFailure::Refused(_)) => return Err(ImprintError::IncompleteInput),
500    };
501    if !core::ptr::eq(proof.owner, &raw const context.owner) {
502        return Err(ImprintError::ForeignInput);
503    }
504    if context.active.get() != 0 || context.abandoned.get() {
505        return Err(ImprintError::IncompleteInput);
506    }
507    Ok(proof.agreement)
508}