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}