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    /// Serialize a live mid-computation digest context to opaque bytes.
175    ///
176    /// Calls `EVP_MD_CTX_serialize`, which allocates a buffer via
177    /// `OPENSSL_malloc` and writes the pointer and length to out-params.
178    /// The buffer is copied into a `Vec<u8>` and then freed with
179    /// `CRYPTO_free` (the real symbol behind the `OPENSSL_free` macro,
180    /// which bindgen cannot bind directly).
181    ///
182    /// The bytes are version-specific to this OpenSSL build and cannot be
183    /// exchanged between different OpenSSL versions.
184    ///
185    /// Only available when built against OpenSSL ≥ 4.0.
186    ///
187    /// # Errors
188    ///
189    /// Returns `Err` if serialization fails (e.g. the context has not been
190    /// initialised or the algorithm does not support serialization).
191    #[cfg(ossl_v400)]
192    pub fn serialize(&self) -> Result<Vec<u8>, ErrorStack> {
193        let mut ptr: *mut u8 = std::ptr::null_mut();
194        let mut len: usize = 0;
195        // SAFETY:
196        // - self.ptr is non-null (constructor invariant)
197        // - addr_of_mut!(ptr) and addr_of_mut!(len) are valid out-params;
198        //   EVP_MD_CTX_serialize writes the OPENSSL_malloc'd buffer address
199        //   and its byte count to them on success
200        // - &self ensures no concurrent mutation of the context during this call
201        let rc = unsafe {
202            sys::EVP_MD_CTX_serialize(
203                self.ptr,
204                std::ptr::addr_of_mut!(ptr).cast(),
205                std::ptr::addr_of_mut!(len),
206            )
207        };
208        if rc != 1 {
209            return Err(ErrorStack::drain());
210        }
211        // SAFETY:
212        // - ptr is non-null on success (EVP_MD_CTX_serialize guarantees this
213        //   when it returns 1)
214        // - ptr points to `len` initialised bytes allocated by OPENSSL_malloc;
215        //   they are valid and exclusively owned by us at this point
216        // - slice::from_raw_parts requires the pointer to be non-null and
217        //   valid for `len` bytes, both of which hold here
218        let vec = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec();
219        // SAFETY:
220        // - ptr was allocated by OPENSSL_malloc inside EVP_MD_CTX_serialize
221        // - CRYPTO_free is the real function that the OPENSSL_free macro expands
222        //   to; bindgen cannot bind macros, so we call CRYPTO_free directly
223        // - the cast to *mut _ (void*) is safe for any byte pointer
224        // - the file/line advisory args follow the same convention as x509.rs
225        unsafe { sys::CRYPTO_free(ptr.cast(), c"digest.rs".as_ptr(), 0) };
226        Ok(vec)
227    }
228
229    /// Restore a digest context from bytes produced by [`DigestCtx::serialize`].
230    ///
231    /// The context must already be initialised with the same algorithm before
232    /// calling `deserialize()`.  After a successful call the context is in
233    /// exactly the same state as when `serialize()` was called.
234    ///
235    /// Only available when built against OpenSSL ≥ 4.0.
236    ///
237    /// # Errors
238    ///
239    /// Returns `Err` if the data is invalid or the algorithm does not support
240    /// deserialization.
241    #[cfg(ossl_v400)]
242    pub fn deserialize(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
243        // SAFETY:
244        // - self.ptr is non-null (constructor invariant)
245        // - data.as_ptr() is valid for data.len() bytes for the duration of this call
246        // - &mut self ensures exclusive access to the context
247        crate::ossl_call!(sys::EVP_MD_CTX_deserialize(
248            self.ptr,
249            data.as_ptr(),
250            data.len()
251        ))
252    }
253
254    /// Allocate an uninitialised `EVP_MD_CTX`.
255    ///
256    /// The context is not associated with any algorithm yet.  Use this when a
257    /// raw context handle is needed before a higher-level init call (e.g.
258    /// `EVP_DigestSignInit_ex`).
259    ///
260    /// # Errors
261    ///
262    /// Returns `Err` if `EVP_MD_CTX_new` fails.
263    pub fn new_empty() -> Result<Self, ErrorStack> {
264        let ptr = unsafe { sys::EVP_MD_CTX_new() };
265        if ptr.is_null() {
266            return Err(ErrorStack::drain());
267        }
268        Ok(DigestCtx { ptr })
269    }
270
271    /// Reinitialise this context for reuse with the given algorithm.
272    ///
273    /// Calls `EVP_DigestInit_ex2`.  Pass `None` for `params` when no extra
274    /// initialisation parameters are required.
275    ///
276    /// # Errors
277    ///
278    /// Returns `Err` if `EVP_DigestInit_ex2` fails.
279    pub fn reinit(
280        &mut self,
281        alg: &DigestAlg,
282        params: Option<&crate::params::Params<'_>>,
283    ) -> Result<(), ErrorStack> {
284        crate::ossl_call!(sys::EVP_DigestInit_ex2(
285            self.ptr,
286            alg.ptr,
287            params.map_or(std::ptr::null(), super::params::Params::as_ptr),
288        ))
289    }
290
291    /// Construct a `DigestCtx` from a raw, owned `EVP_MD_CTX*`.
292    ///
293    /// # Safety
294    ///
295    /// `ptr` must be a valid, non-null `EVP_MD_CTX*` that the caller is giving up ownership of.
296    /// The context need not be initialised with a digest algorithm yet.
297    pub unsafe fn from_ptr(ptr: *mut sys::EVP_MD_CTX) -> Self {
298        DigestCtx { ptr }
299    }
300
301    /// Return the algorithm associated with this context, if any.
302    ///
303    /// Returns `None` when the context was created with [`DigestCtx::new_empty`]
304    /// and has not yet been initialised via [`DigestCtx::reinit`].
305    ///
306    /// The returned [`DigestAlg`] holds an independent reference (refcount bumped
307    /// via `EVP_MD_up_ref`) and may safely outlive `self`.
308    #[must_use]
309    pub fn alg(&self) -> Option<DigestAlg> {
310        // SAFETY:
311        // - self.ptr is non-null (constructor invariant: Err is returned on null,
312        //   null is never stored in DigestCtx)
313        // - EVP_MD_CTX_get0_md borrows the algorithm pointer; it is valid for at
314        //   least the lifetime of this context; we immediately bump the refcount
315        //   before returning so the caller's DigestAlg owns an independent reference
316        // - &self ensures no concurrent mutation of the context while we read it
317        let md_ptr = unsafe { sys::EVP_MD_CTX_get0_md(self.ptr) };
318        if md_ptr.is_null() {
319            return None;
320        }
321        // Bump refcount so the returned DigestAlg is independently owned.
322        // SAFETY: md_ptr is non-null (checked above); the cast to *mut is safe
323        // because EVP_MD_up_ref only atomically increments a counter — it does
324        // not mutate algorithm data — and OpenSSL's API requires a mutable pointer
325        // for refcount operations even on logically immutable objects.
326        unsafe { sys::EVP_MD_up_ref(md_ptr.cast_mut()) };
327        Some(DigestAlg {
328            ptr: md_ptr.cast_mut(),
329            lib_ctx: None,
330        })
331    }
332
333    /// Return the raw `EVP_MD_CTX*` pointer.  Valid for the lifetime of `self`.
334    ///
335    /// Used by `Signer`/`Verifier` which call `EVP_DigestSign*`
336    /// on this context directly.  Returns a mutable pointer because most
337    /// OpenSSL EVP functions require `EVP_MD_CTX*` even for logically
338    /// read-only operations.
339    #[must_use]
340    pub fn as_ptr(&self) -> *mut sys::EVP_MD_CTX {
341        self.ptr
342    }
343}
344
345impl Drop for DigestCtx {
346    fn drop(&mut self) {
347        unsafe { sys::EVP_MD_CTX_free(self.ptr) };
348    }
349}
350
351// `EVP_MD_CTX` has no `up_ref` — it is `!Clone` and exclusively owned.
352// `Send` is safe because no thread-local state is stored in the context.
353// `Sync` is safe because Rust's &self/&mut self discipline prevents concurrent
354// mutation; read-only access from multiple threads is safe for EVP_MD_CTX.
355unsafe impl Send for DigestCtx {}
356unsafe impl Sync for DigestCtx {}
357
358impl DigestAlg {
359    /// Create a new digest context initialised with this algorithm.
360    ///
361    /// # Errors
362    ///
363    /// Returns `Err` if context allocation or init fails.
364    pub fn new_context(&self) -> Result<DigestCtx, ErrorStack> {
365        let ctx_ptr = unsafe { sys::EVP_MD_CTX_new() };
366        if ctx_ptr.is_null() {
367            return Err(ErrorStack::drain());
368        }
369        crate::ossl_call!(sys::EVP_DigestInit_ex2(ctx_ptr, self.ptr, std::ptr::null())).map_err(
370            |e| {
371                unsafe { sys::EVP_MD_CTX_free(ctx_ptr) };
372                e
373            },
374        )?;
375        Ok(DigestCtx { ptr: ctx_ptr })
376    }
377
378    /// Compute a digest in a single call (one-shot path).
379    ///
380    /// Zero-copy: reads from `data`, writes into `out`.
381    /// `out` must be at least `self.output_len()` bytes.
382    ///
383    /// # Errors
384    pub fn digest(&self, data: &[u8], out: &mut [u8]) -> Result<usize, ErrorStack> {
385        let mut len: u32 = 0;
386        crate::ossl_call!(sys::EVP_Digest(
387            data.as_ptr().cast(),
388            data.len(),
389            out.as_mut_ptr(),
390            std::ptr::addr_of_mut!(len),
391            self.ptr,
392            std::ptr::null_mut()
393        ))?;
394        Ok(usize::try_from(len).unwrap_or(0))
395    }
396
397    /// Compute a digest and return it in a freshly allocated `Vec<u8>`.
398    ///
399    /// # Errors
400    pub fn digest_to_vec(&self, data: &[u8]) -> Result<Vec<u8>, ErrorStack> {
401        let mut out = vec![0u8; self.output_len()];
402        let len = self.digest(data, &mut out)?;
403        out.truncate(len);
404        Ok(out)
405    }
406}
407
408// ── Tests ─────────────────────────────────────────────────────────────────────
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn fetch_sha256_properties() {
416        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
417        assert_eq!(alg.output_len(), 32);
418        assert_eq!(alg.block_size(), 64);
419    }
420
421    #[test]
422    fn fetch_nonexistent_fails() {
423        assert!(DigestAlg::fetch(c"NONEXISTENT_DIGEST_XYZ", None).is_err());
424    }
425
426    #[test]
427    fn clone_then_drop_both() {
428        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
429        let alg2 = alg.clone();
430        // Drop both — must not double-free.
431        drop(alg);
432        drop(alg2);
433    }
434
435    /// SHA-256("abc") known-answer test (verified against OpenSSL CLI + Python hashlib).
436    #[test]
437    fn sha256_known_answer() {
438        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
439        let mut ctx = alg.new_context().unwrap();
440        ctx.update(b"abc").unwrap();
441        let mut out = [0u8; 32];
442        let n = ctx.finish(&mut out).unwrap();
443        assert_eq!(n, 32);
444        assert_eq!(
445            hex::encode(out),
446            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
447        );
448    }
449
450    /// Same vector via oneshot path.
451    #[test]
452    fn sha256_oneshot() {
453        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
454        let got = alg.digest_to_vec(b"abc").unwrap();
455        let expected =
456            hex::decode("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
457                .unwrap();
458        assert_eq!(got, expected);
459    }
460
461    /// Fork mid-stream — two independent suffix completions.
462    #[test]
463    fn fork_mid_stream() {
464        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
465        let mut ctx = alg.new_context().unwrap();
466        ctx.update(b"common prefix").unwrap();
467
468        let mut fork = ctx.fork().unwrap();
469
470        ctx.update(b" A").unwrap();
471        fork.update(b" B").unwrap();
472
473        let mut out_a = [0u8; 32];
474        let mut out_b = [0u8; 32];
475        ctx.finish(&mut out_a).unwrap();
476        fork.finish(&mut out_b).unwrap();
477
478        // Different suffixes → different digests.
479        assert_ne!(out_a, out_b);
480    }
481
482    /// Verify that serialize/deserialize round-trip preserves mid-stream state.
483    ///
484    /// Strategy: hash "hello" in context A, serialize, restore into context B,
485    /// then append " world" in both and confirm identical final digests.
486    #[cfg(ossl_v400)]
487    #[test]
488    fn digest_ctx_serialize_roundtrip() {
489        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
490
491        // Init context A and hash "hello".
492        let mut ctx_a = alg.new_context().unwrap();
493        ctx_a.update(b"hello").unwrap();
494
495        // Serialize mid-stream state into an owned Vec<u8>.
496        let state = ctx_a.serialize().unwrap();
497        assert!(!state.is_empty(), "serialized state must be non-empty");
498
499        // Continue context A with " world" and finalize.
500        ctx_a.update(b" world").unwrap();
501        let mut out_a = [0u8; 32];
502        ctx_a.finish(&mut out_a).unwrap();
503
504        // Restore mid-stream state into a fresh context B, apply same suffix.
505        let mut ctx_b = alg.new_context().unwrap();
506        ctx_b.deserialize(&state).unwrap();
507        ctx_b.update(b" world").unwrap();
508        let mut out_b = [0u8; 32];
509        ctx_b.finish(&mut out_b).unwrap();
510
511        assert_eq!(out_a, out_b, "restored context produced different digest");
512    }
513
514    // ── DigestCtx::alg() tests ────────────────────────────────────────────────
515
516    /// Initialised context returns the algorithm it was created with.
517    #[test]
518    fn digest_ctx_alg_returns_some_after_init() {
519        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
520        let ctx = alg.new_context().unwrap();
521        let retrieved = ctx
522            .alg()
523            .expect("alg() must return Some on an initialised context");
524        // Properties must match the original algorithm.
525        assert_eq!(retrieved.output_len(), alg.output_len());
526        assert_eq!(retrieved.block_size(), alg.block_size());
527        assert_eq!(retrieved.nid(), alg.nid());
528    }
529
530    /// Empty (uninitialised) context returns None.
531    #[test]
532    fn digest_ctx_alg_returns_none_before_init() {
533        let ctx = DigestCtx::new_empty().unwrap();
534        assert!(
535            ctx.alg().is_none(),
536            "alg() must return None on an empty context"
537        );
538    }
539
540    /// The returned `DigestAlg` is independently owned and lives past the context.
541    #[test]
542    fn digest_ctx_alg_outlives_context() {
543        let alg_ref = DigestAlg::fetch(c"SHA2-512", None).unwrap();
544        let retrieved = {
545            let ctx = alg_ref.new_context().unwrap();
546            ctx.alg().unwrap()
547        }; // ctx dropped here; retrieved must still be valid
548        assert_eq!(retrieved.output_len(), 64);
549    }
550
551    /// `alg()` on a forked context returns the same algorithm.
552    #[test]
553    fn digest_ctx_alg_after_fork() {
554        let alg = DigestAlg::fetch(c"SHA2-256", None).unwrap();
555        let mut ctx = alg.new_context().unwrap();
556        ctx.update(b"data").unwrap();
557        let forked = ctx.fork().unwrap();
558        let retrieved = forked
559            .alg()
560            .expect("alg() must return Some on a forked context");
561        assert_eq!(retrieved.output_len(), alg.output_len());
562    }
563}