Skip to main content

native_ossl/
digest.rs

1//! `DigestAlg` — `EVP_MD` algorithm descriptor, and `DigestCtx` — stateful context.
2//!
3//! Phase 3.1 delivers `DigestAlg`; Phase 4.1 extends this module with `DigestCtx`.
4
5use crate::error::ErrorStack;
6use native_ossl_sys as sys;
7use std::ffi::CStr;
8use std::sync::Arc;
9
10// ── DigestAlg — algorithm descriptor ─────────────────────────────────────────
11
12/// An OpenSSL digest algorithm descriptor (`EVP_MD*`).
13///
14/// Fetched once and reused.  Implements `Clone` via `EVP_MD_up_ref`.
15#[derive(Debug)]
16pub struct DigestAlg {
17    ptr: *mut sys::EVP_MD,
18    /// Keeps the library context alive while this descriptor is in use.
19    lib_ctx: Option<Arc<crate::lib_ctx::LibCtx>>,
20}
21
22impl DigestAlg {
23    /// Fetch a digest algorithm from the global default library context.
24    ///
25    /// # Errors
26    ///
27    /// Returns `Err` if the algorithm is not available.
28    pub fn fetch(name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack> {
29        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
30        let ptr = unsafe { sys::EVP_MD_fetch(std::ptr::null_mut(), name.as_ptr(), props_ptr) };
31        if ptr.is_null() {
32            return Err(ErrorStack::drain());
33        }
34        Ok(DigestAlg { ptr, lib_ctx: None })
35    }
36
37    /// Fetch a digest algorithm from an explicit library context.
38    ///
39    /// The `Arc` is cloned and held so the context outlives this descriptor.
40    ///
41    /// # Errors
42    pub fn fetch_in(
43        ctx: &Arc<crate::lib_ctx::LibCtx>,
44        name: &CStr,
45        props: Option<&CStr>,
46    ) -> Result<Self, ErrorStack> {
47        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
48        let ptr = unsafe { sys::EVP_MD_fetch(ctx.as_ptr(), name.as_ptr(), props_ptr) };
49        if ptr.is_null() {
50            return Err(ErrorStack::drain());
51        }
52        Ok(DigestAlg {
53            ptr,
54            lib_ctx: Some(Arc::clone(ctx)),
55        })
56    }
57
58    /// Digest output size in bytes (e.g. 32 for SHA-256).
59    #[must_use]
60    pub fn output_len(&self) -> usize {
61        usize::try_from(unsafe { sys::EVP_MD_get_size(self.ptr) }).unwrap_or(0)
62    }
63
64    /// Block size in bytes (e.g. 64 for SHA-256).
65    #[must_use]
66    pub fn block_size(&self) -> usize {
67        usize::try_from(unsafe { sys::EVP_MD_get_block_size(self.ptr) }).unwrap_or(0)
68    }
69
70    /// NID (numeric identifier) of the digest algorithm.
71    #[must_use]
72    pub fn nid(&self) -> i32 {
73        unsafe { sys::EVP_MD_get_type(self.ptr) }
74    }
75
76    /// Return the raw `EVP_MD*` pointer.  Valid for the lifetime of `self`.
77    #[must_use]
78    pub fn as_ptr(&self) -> *const sys::EVP_MD {
79        self.ptr
80    }
81}
82
83impl Clone for DigestAlg {
84    fn clone(&self) -> Self {
85        unsafe { sys::EVP_MD_up_ref(self.ptr) };
86        DigestAlg {
87            ptr: self.ptr,
88            lib_ctx: self.lib_ctx.clone(),
89        }
90    }
91}
92
93impl Drop for DigestAlg {
94    fn drop(&mut self) {
95        unsafe { sys::EVP_MD_free(self.ptr) };
96    }
97}
98
99// SAFETY: `EVP_MD*` is reference-counted and immutable after fetch.
100unsafe impl Send for DigestAlg {}
101unsafe impl Sync for DigestAlg {}
102
103// ── DigestCtx — stateful context (Phase 4.1) ─────────────────────────────────
104
105/// Stateful hash context (`EVP_MD_CTX*`).
106///
107/// `!Clone` — use `fork()` to duplicate mid-stream state.
108/// All stateful operations require `&mut self` (exclusive ownership).
109#[derive(Debug)]
110pub struct DigestCtx {
111    ptr: *mut sys::EVP_MD_CTX,
112}
113
114impl DigestCtx {
115    /// Feed data into the ongoing hash computation.
116    ///
117    /// # Errors
118    ///
119    /// Returns `Err` if `EVP_DigestUpdate` fails.
120    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
121        crate::ossl_call!(sys::EVP_DigestUpdate(
122            self.ptr,
123            data.as_ptr().cast(),
124            data.len()
125        ))
126    }
127
128    /// Finalise the hash and write the result into `out`.
129    ///
130    /// `out` must be at least `alg.output_len()` bytes.
131    /// Returns the number of bytes written.
132    ///
133    /// # Errors
134    ///
135    /// Returns `Err` if `EVP_DigestFinal_ex` fails.
136    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack> {
137        let mut len: u32 = 0;
138        crate::ossl_call!(sys::EVP_DigestFinal_ex(
139            self.ptr,
140            out.as_mut_ptr(),
141            std::ptr::addr_of_mut!(len)
142        ))?;
143        Ok(usize::try_from(len).unwrap_or(0))
144    }
145
146    /// Finalise with XOF (extendable-output) mode.
147    ///
148    /// Used by SHAKE-128, SHAKE-256.  `out.len()` determines the output length.
149    ///
150    /// # Errors
151    pub fn finish_xof(&mut self, out: &mut [u8]) -> Result<(), ErrorStack> {
152        crate::ossl_call!(sys::EVP_DigestFinalXOF(
153            self.ptr,
154            out.as_mut_ptr(),
155            out.len()
156        ))
157    }
158
159    /// Fork the current mid-stream state into a new context.
160    ///
161    /// Equivalent to `EVP_MD_CTX_copy_ex` — a deep copy of the context state.
162    /// Named `fork` (not `clone`) to signal the operation is potentially expensive.
163    ///
164    /// # Errors
165    pub fn fork(&self) -> Result<DigestCtx, ErrorStack> {
166        let new_ctx = unsafe { sys::EVP_MD_CTX_new() };
167        if new_ctx.is_null() {
168            return Err(ErrorStack::drain());
169        }
170        crate::ossl_call!(sys::EVP_MD_CTX_copy_ex(new_ctx, self.ptr))?;
171        Ok(DigestCtx { ptr: new_ctx })
172    }
173
174    /// Query the byte count needed to serialise this context's mid-stream state.
175    ///
176    /// Calls `EVP_MD_CTX_serialize` with a null output pointer; the provider
177    /// writes the required size to `*outlen` and returns 1.
178    ///
179    /// Only available when built against OpenSSL ≥ 4.0.
180    ///
181    /// # Errors
182    ///
183    /// Returns `Err` if the algorithm or provider does not support serialisation.
184    #[cfg(ossl_v400)]
185    pub fn serialize_size(&self) -> Result<usize, ErrorStack> {
186        let mut outlen: usize = 0;
187        crate::ossl_call!(sys::EVP_MD_CTX_serialize(
188            self.ptr,
189            std::ptr::null_mut(),
190            std::ptr::addr_of_mut!(outlen)
191        ))?;
192        Ok(outlen)
193    }
194
195    /// Serialise the current mid-stream hash state into `out`.
196    ///
197    /// `out.len()` must be ≥ `serialize_size()`.  Returns the number of bytes
198    /// written.  The serialised bytes can be passed to `deserialize()` on a
199    /// freshly initialised context to restore the exact mid-stream state.
200    ///
201    /// Only available when built against OpenSSL ≥ 4.0.
202    ///
203    /// # Errors
204    ///
205    /// Returns `Err` if serialisation fails (e.g. buffer too small, or
206    /// algorithm does not support serialisation).
207    #[cfg(ossl_v400)]
208    pub fn serialize(&self, out: &mut [u8]) -> Result<usize, ErrorStack> {
209        let mut outlen: usize = out.len();
210        crate::ossl_call!(sys::EVP_MD_CTX_serialize(
211            self.ptr,
212            out.as_mut_ptr(),
213            std::ptr::addr_of_mut!(outlen)
214        ))?;
215        Ok(outlen)
216    }
217
218    /// Restore mid-stream hash state from bytes produced by `serialize()`.
219    ///
220    /// The context must already be initialised with the same algorithm before
221    /// calling `deserialize()`.  After a successful call the context is in
222    /// exactly the same state as when `serialize()` was called.
223    ///
224    /// Only available when built against OpenSSL ≥ 4.0.
225    ///
226    /// # Errors
227    ///
228    /// Returns `Err` if the data is malformed or the algorithm does not
229    /// support deserialisation.
230    #[cfg(ossl_v400)]
231    pub fn deserialize(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
232        crate::ossl_call!(sys::EVP_MD_CTX_deserialize(
233            self.ptr,
234            data.as_ptr(),
235            data.len()
236        ))
237    }
238
239    /// Allocate an uninitialised `EVP_MD_CTX`.
240    ///
241    /// The context is not associated with any algorithm yet.  Use this when a
242    /// raw context handle is needed before a higher-level init call (e.g.
243    /// `EVP_DigestSignInit_ex`).
244    ///
245    /// # Errors
246    ///
247    /// Returns `Err` if `EVP_MD_CTX_new` fails.
248    pub fn new_empty() -> Result<Self, ErrorStack> {
249        let ptr = unsafe { sys::EVP_MD_CTX_new() };
250        if ptr.is_null() {
251            return Err(ErrorStack::drain());
252        }
253        Ok(DigestCtx { ptr })
254    }
255
256    /// Reinitialise this context for reuse with the given algorithm.
257    ///
258    /// Calls `EVP_DigestInit_ex2`.  Pass `None` for `params` when no extra
259    /// initialisation parameters are required.
260    ///
261    /// # Errors
262    ///
263    /// Returns `Err` if `EVP_DigestInit_ex2` fails.
264    pub fn reinit(
265        &mut self,
266        alg: &DigestAlg,
267        params: Option<&crate::params::Params<'_>>,
268    ) -> Result<(), ErrorStack> {
269        crate::ossl_call!(sys::EVP_DigestInit_ex2(
270            self.ptr,
271            alg.ptr,
272            params.map_or(std::ptr::null(), super::params::Params::as_ptr),
273        ))
274    }
275
276    /// Construct a `DigestCtx` from a raw, owned `EVP_MD_CTX*`.
277    ///
278    /// # Safety
279    ///
280    /// `ptr` must be a valid, non-null `EVP_MD_CTX*` that the caller is giving up ownership of.
281    /// The context need not be initialised with a digest algorithm yet.
282    pub unsafe fn from_ptr(ptr: *mut sys::EVP_MD_CTX) -> Self {
283        DigestCtx { ptr }
284    }
285
286    /// Return the raw `EVP_MD_CTX*` pointer.  Valid for the lifetime of `self`.
287    ///
288    /// Used by `Signer`/`Verifier` which call `EVP_DigestSign*`
289    /// on this context directly.  Returns a mutable pointer because most
290    /// OpenSSL EVP functions require `EVP_MD_CTX*` even for logically
291    /// read-only operations.
292    #[must_use]
293    pub fn as_ptr(&self) -> *mut sys::EVP_MD_CTX {
294        self.ptr
295    }
296}
297
298impl Drop for DigestCtx {
299    fn drop(&mut self) {
300        unsafe { sys::EVP_MD_CTX_free(self.ptr) };
301    }
302}
303
304// `EVP_MD_CTX` has no `up_ref` — it is `!Clone` and exclusively owned.
305// `Send` is safe because no thread-local state is stored in the context.
306// `Sync` is safe because Rust's &self/&mut self discipline prevents concurrent
307// mutation; read-only access from multiple threads is safe for EVP_MD_CTX.
308unsafe impl Send for DigestCtx {}
309unsafe impl Sync for DigestCtx {}
310
311impl DigestAlg {
312    /// Create a new digest context initialised with this algorithm.
313    ///
314    /// # Errors
315    ///
316    /// Returns `Err` if context allocation or init fails.
317    pub fn new_context(&self) -> Result<DigestCtx, ErrorStack> {
318        let ctx_ptr = unsafe { sys::EVP_MD_CTX_new() };
319        if ctx_ptr.is_null() {
320            return Err(ErrorStack::drain());
321        }
322        crate::ossl_call!(sys::EVP_DigestInit_ex2(ctx_ptr, self.ptr, std::ptr::null())).map_err(
323            |e| {
324                unsafe { sys::EVP_MD_CTX_free(ctx_ptr) };
325                e
326            },
327        )?;
328        Ok(DigestCtx { ptr: ctx_ptr })
329    }
330
331    /// Compute a digest in a single call (one-shot path).
332    ///
333    /// Zero-copy: reads from `data`, writes into `out`.
334    /// `out` must be at least `self.output_len()` bytes.
335    ///
336    /// # Errors
337    pub fn digest(&self, data: &[u8], out: &mut [u8]) -> Result<usize, ErrorStack> {
338        let mut len: u32 = 0;
339        crate::ossl_call!(sys::EVP_Digest(
340            data.as_ptr().cast(),
341            data.len(),
342            out.as_mut_ptr(),
343            std::ptr::addr_of_mut!(len),
344            self.ptr,
345            std::ptr::null_mut()
346        ))?;
347        Ok(usize::try_from(len).unwrap_or(0))
348    }
349
350    /// Compute a digest and return it in a freshly allocated `Vec<u8>`.
351    ///
352    /// # Errors
353    pub fn digest_to_vec(&self, data: &[u8]) -> Result<Vec<u8>, ErrorStack> {
354        let mut out = vec![0u8; self.output_len()];
355        let len = self.digest(data, &mut out)?;
356        out.truncate(len);
357        Ok(out)
358    }
359}
360
361// ── Tests ─────────────────────────────────────────────────────────────────────
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn fetch_sha256_properties() {
369        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
370        assert_eq!(alg.output_len(), 32);
371        assert_eq!(alg.block_size(), 64);
372    }
373
374    #[test]
375    fn fetch_nonexistent_fails() {
376        assert!(DigestAlg::fetch(c"NONEXISTENT_DIGEST_XYZ", None).is_err());
377    }
378
379    #[test]
380    fn clone_then_drop_both() {
381        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
382        let alg2 = alg.clone();
383        // Drop both — must not double-free.
384        drop(alg);
385        drop(alg2);
386    }
387
388    /// SHA-256("abc") known-answer test (verified against OpenSSL CLI + Python hashlib).
389    #[test]
390    fn sha256_known_answer() {
391        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
392        let mut ctx = alg.new_context().unwrap();
393        ctx.update(b"abc").unwrap();
394        let mut out = [0u8; 32];
395        let n = ctx.finish(&mut out).unwrap();
396        assert_eq!(n, 32);
397        assert_eq!(
398            hex::encode(out),
399            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
400        );
401    }
402
403    /// Same vector via oneshot path.
404    #[test]
405    fn sha256_oneshot() {
406        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
407        let got = alg.digest_to_vec(b"abc").unwrap();
408        let expected =
409            hex::decode("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
410                .unwrap();
411        assert_eq!(got, expected);
412    }
413
414    /// Fork mid-stream — two independent suffix completions.
415    #[test]
416    fn fork_mid_stream() {
417        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
418        let mut ctx = alg.new_context().unwrap();
419        ctx.update(b"common prefix").unwrap();
420
421        let mut fork = ctx.fork().unwrap();
422
423        ctx.update(b" A").unwrap();
424        fork.update(b" B").unwrap();
425
426        let mut out_a = [0u8; 32];
427        let mut out_b = [0u8; 32];
428        ctx.finish(&mut out_a).unwrap();
429        fork.finish(&mut out_b).unwrap();
430
431        // Different suffixes → different digests.
432        assert_ne!(out_a, out_b);
433    }
434
435    /// Verify that serialize/deserialize round-trip preserves mid-stream state.
436    ///
437    /// Strategy: hash "hello" in context A, serialize, restore into context B,
438    /// then append " world" in both and confirm identical final digests.
439    #[cfg(ossl_v400)]
440    #[test]
441    fn serialize_deserialize_roundtrip() {
442        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
443
444        let mut ctx_a = alg.new_context().unwrap();
445        ctx_a.update(b"hello").unwrap();
446
447        // Query then serialize mid-stream state.
448        let size = ctx_a.serialize_size().unwrap();
449        assert!(size > 0, "serialized state must be non-empty");
450
451        let mut state = vec![0u8; size];
452        let written = ctx_a.serialize(&mut state).unwrap();
453        assert_eq!(written, size, "serialize wrote unexpected byte count");
454
455        // Finish context A normally.
456        ctx_a.update(b" world").unwrap();
457        let mut out_a = [0u8; 32];
458        ctx_a.finish(&mut out_a).unwrap();
459
460        // Restore mid-stream state into context B, finish identically.
461        let mut ctx_b = alg.new_context().unwrap();
462        ctx_b.deserialize(&state).unwrap();
463        ctx_b.update(b" world").unwrap();
464        let mut out_b = [0u8; 32];
465        ctx_b.finish(&mut out_b).unwrap();
466
467        assert_eq!(out_a, out_b, "restored context produced different digest");
468    }
469
470    /// Changing the suffix after restore produces a different digest.
471    #[cfg(ossl_v400)]
472    #[test]
473    fn serialize_different_suffix_differs() {
474        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
475        let mut ctx = alg.new_context().unwrap();
476        ctx.update(b"hello").unwrap();
477
478        let size = ctx.serialize_size().unwrap();
479        let mut state = vec![0u8; size];
480        ctx.serialize(&mut state).unwrap();
481
482        let mut ctx_a = alg.new_context().unwrap();
483        ctx_a.deserialize(&state).unwrap();
484        ctx_a.update(b" world").unwrap();
485        let mut out_a = [0u8; 32];
486        ctx_a.finish(&mut out_a).unwrap();
487
488        let mut ctx_b = alg.new_context().unwrap();
489        ctx_b.deserialize(&state).unwrap();
490        ctx_b.update(b" WORLD").unwrap();
491        let mut out_b = [0u8; 32];
492        ctx_b.finish(&mut out_b).unwrap();
493
494        assert_ne!(
495            out_a, out_b,
496            "different suffixes must produce different digests"
497        );
498    }
499}