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}