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