Skip to main content

structured_asn1/
digest.rs

1//! One incremental digest execution contract for every format.
2//!
3//! A consumer implements [`Digest`] once over its own cryptographic backend.
4//! Which algorithm a document names is a separate question answered by the
5//! format that wrote it: ASN.1 identifiers by `structured-pkix`, XML methods
6//! by `structured-xades`. Both hand their resolved identity to this contract.
7
8use crate::memory::Work;
9use core::cmp::Ordering;
10
11/// Semantic resolution, distinct from failure to execute the resolver.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Resolution<T> {
14    /// Fully resolved algorithm identity, including parameters/output semantics.
15    Supported(T),
16    /// The selected provider cannot answer this mechanism.
17    Unsupported,
18    /// The original algorithm identifier violates the selected algorithm's syntax.
19    Malformed,
20}
21
22/// Incremental digest operations of one explicitly selected provider.
23///
24/// Identities carry complete algorithm semantics, not just an identifier or a
25/// byte width. Every callback admits its complete transitive work and storage
26/// in the supplied execution before it runs. Errors are a spent ceiling or a
27/// provider fault, never a finding about the document. States may borrow the
28/// provider for `'a` and carry no `Clone`, `Send` or `Sync` requirement.
29pub trait Digest<'a> {
30    /// Fully resolved digest identity.
31    type Identity;
32    /// Digest state, independently movable from its identity object.
33    type State;
34    /// Final output, inline or retaining its own admitted allocation charge.
35    type Output: AsRef<[u8]>;
36    /// Original callback failure, including that provider's resource refusal.
37    type Error;
38
39    /// Fixed output width, or `None` for an identity without one. This is O(1)
40    /// metadata access on the resolved identity, not parsing.
41    fn width(&self, identity: &Self::Identity) -> Option<usize>;
42
43    /// Total semantic order: `Equal` means the outputs can be shared.
44    fn compare<W: Work>(
45        &self,
46        left: &Self::Identity,
47        right: &Self::Identity,
48        execution: &W,
49    ) -> Result<Ordering, Self::Error>;
50
51    /// Starts one stream for this identity without retaining a reference to the
52    /// identity object. A backend without incremental capability answers
53    /// `Unsupported`; it never buffers into a one-shot fallback.
54    fn start<W: Work>(
55        &'a self,
56        identity: &Self::Identity,
57        execution: &W,
58    ) -> Result<Resolution<Self::State>, Self::Error>;
59
60    /// Admits and processes one complete chunk exactly once.
61    fn update<W: Work>(
62        &self,
63        state: &mut Self::State,
64        bytes: &[u8],
65        execution: &W,
66    ) -> Result<(), Self::Error>;
67
68    /// Consumes a completed state once; no state cloning or repeated finish.
69    fn finish<W: Work>(
70        &self,
71        state: Self::State,
72        execution: &W,
73    ) -> Result<Self::Output, Self::Error>;
74
75    /// Compares this provider's finished output with declared bytes, admitting
76    /// the whole scan and applying the backend's comparison strategy.
77    ///
78    /// The answer is whether `expected` is exactly the bytes of `output`. A
79    /// strategy, such as a constant-time scan, governs how those bytes are
80    /// compared, never which differing byte strings count as equal; callers
81    /// rely on this to find the one output an expected value can name by its
82    /// bytes.
83    fn matches<W: Work>(
84        &self,
85        identity: &Self::Identity,
86        output: &Self::Output,
87        expected: &[u8],
88        execution: &W,
89    ) -> Result<bool, Self::Error>;
90}
91
92/// Failure of a one-shot check, apart from every completed comparison.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum OnceError<E> {
95    /// Original provider failure, including its resource refusal.
96    Provider(E),
97    /// The provider's output contradicts the fixed width of its own identity.
98    /// This is a provider contract failure, not malformed document data.
99    OutputWidth {
100        /// Width promised by the resolved identity.
101        expected: usize,
102        /// Width actually returned.
103        actual: usize,
104    },
105}
106
107/// Hashes one contiguous input and compares it with `expected`: start, one
108/// update, finish and matches over the same contract as streamed input.
109///
110/// `Unsupported` and `Malformed` are the provider's start answer, returned
111/// before any byte is hashed.
112///
113/// # Errors
114///
115/// Returns the provider's failure, or [`OnceError::OutputWidth`] when the
116/// output contradicts the identity's fixed width.
117///
118/// # Examples
119///
120/// ```
121/// # extern crate alloc;
122/// # use alloc::sync::Arc;
123/// # use core::cmp::Ordering;
124/// # use structured_asn1::digest::{self, Digest, Resolution};
125/// # use structured_asn1::memory::{Memory, MemoryAccount, MemoryError, Work};
126/// /// A one-byte additive checksum, enough to show the call sequence.
127/// struct Sum;
128/// impl Digest<'_> for Sum {
129///     type Identity = ();
130///     type State = u8;
131///     type Output = [u8; 1];
132///     type Error = MemoryError;
133///     fn width(&self, (): &()) -> Option<usize> { Some(1) }
134///     fn compare<W: Work>(&self, (): &(), (): &(), _: &W) -> Result<Ordering, MemoryError> {
135///         Ok(Ordering::Equal)
136///     }
137///     fn start<W: Work>(&self, (): &(), w: &W) -> Result<Resolution<u8>, MemoryError> {
138///         w.charge_work(1)?;
139///         Ok(Resolution::Supported(0))
140///     }
141///     fn update<W: Work>(&self, s: &mut u8, bytes: &[u8], w: &W) -> Result<(), MemoryError> {
142///         w.charge_work(bytes.len() as u64)?;
143///         *s = bytes.iter().fold(*s, |sum, byte| sum.wrapping_add(*byte));
144///         Ok(())
145///     }
146///     fn finish<W: Work>(&self, s: u8, _: &W) -> Result<[u8; 1], MemoryError> { Ok([s]) }
147///     fn matches<W: Work>(&self, (): &(), o: &[u8; 1], e: &[u8], w: &W) -> Result<bool, MemoryError> {
148///         w.charge_work(1)?;
149///         Ok(o.as_slice() == e)
150///     }
151/// }
152/// let execution = Memory::new(Arc::new(MemoryAccount::new(0, 64)));
153/// let answer = digest::matches_once(&Sum, &(), b"\x01\x02", &[3], &execution).unwrap();
154/// assert_eq!(answer, Resolution::Supported(true));
155/// ```
156pub fn matches_once<'a, D: Digest<'a> + ?Sized, W: Work>(
157    digest: &'a D,
158    identity: &D::Identity,
159    input: &[u8],
160    expected: &[u8],
161    execution: &W,
162) -> Result<Resolution<bool>, OnceError<D::Error>> {
163    let mut state = match digest
164        .start(identity, execution)
165        .map_err(OnceError::Provider)?
166    {
167        Resolution::Supported(state) => state,
168        Resolution::Unsupported => return Ok(Resolution::Unsupported),
169        Resolution::Malformed => return Ok(Resolution::Malformed),
170    };
171    digest
172        .update(&mut state, input, execution)
173        .map_err(OnceError::Provider)?;
174    let output = digest
175        .finish(state, execution)
176        .map_err(OnceError::Provider)?;
177    if let Some(expected) = digest.width(identity) {
178        let actual = output.as_ref().len();
179        if actual != expected {
180            return Err(OnceError::OutputWidth { expected, actual });
181        }
182    }
183    digest
184        .matches(identity, &output, expected, execution)
185        .map(Resolution::Supported)
186        .map_err(OnceError::Provider)
187}