Digests
Two-Object Model
Hash operations use two distinct types:
DigestAlg— the algorithm descriptor. Fetch once, reuse many times.Send + Sync.DigestCtx— the stateful computation context. One per hash computation.
This separation lets you query algorithm metadata (output size, block size) before allocating any context, and lets multiple contexts share the same algorithm descriptor concurrently without copies.
DigestAlg — Algorithm Descriptor
#![allow(unused)]
fn main() {
pub struct DigestAlg { /* EVP_MD* */ }
impl DigestAlg {
/// Fetch from the global default library context.
pub fn fetch(name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack>;
/// Fetch from an explicit library context (for FIPS isolation).
pub fn fetch_in(ctx: &Arc<LibCtx>, name: &CStr, props: Option<&CStr>)
-> Result<Self, ErrorStack>;
/// Output length in bytes (e.g. 32 for SHA-256).
pub fn output_len(&self) -> usize;
/// Block size in bytes (e.g. 64 for SHA-256).
pub fn block_size(&self) -> usize;
/// OpenSSL numeric ID; prefer name-based dispatch over this.
pub fn nid(&self) -> i32;
/// Create a new stateful hashing context.
pub fn new_context(&self) -> Result<DigestCtx, ErrorStack>;
/// One-shot hash: hash `data` and write the result into `out`.
/// `out` must be at least `self.output_len()` bytes.
pub fn digest(&self, data: &[u8], out: &mut [u8]) -> Result<usize, ErrorStack>;
/// One-shot hash returning an owned `Vec<u8>`.
pub fn digest_to_vec(&self, data: &[u8]) -> Result<Vec<u8>, ErrorStack>;
}
}
DigestAlg is Clone (via EVP_MD_up_ref) and Drop (via EVP_MD_free). Both
clones point to the same C object; the last drop frees it.
DigestCtx — Stateful Context
#![allow(unused)]
fn main() {
pub struct DigestCtx { /* EVP_MD_CTX* */ }
impl DigestCtx {
/// Feed data into the hash. May be called multiple times.
/// Zero-copy: reads directly from caller's slice.
pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack>;
/// Finalise and write the hash into `out`.
/// `out` must be at least `alg.output_len()` bytes.
/// Returns the number of bytes written.
pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack>;
/// Finalise with variable output length (XOF: SHAKE-128, SHAKE-256).
/// `out.len()` determines the output length.
pub fn finish_xof(&mut self, out: &mut [u8]) -> Result<(), ErrorStack>;
/// Fork the current mid-stream state into a new independent context.
/// Equivalent to `EVP_MD_CTX_copy_ex` — a deep copy of the hashing state.
pub fn fork(&self) -> Result<DigestCtx, ErrorStack>;
/// Return the algorithm associated with this context.
/// Returns `None` if the context was created with `new_empty()` and has not
/// yet been initialised. The returned `DigestAlg` is independently owned
/// (refcount bumped) and lives past the context's lifetime.
pub fn alg(&self) -> Option<DigestAlg>;
/// Create an uninitialized context not yet associated with any algorithm.
/// Useful when a raw `EVP_MD_CTX*` handle is needed before a higher-level
/// init call (e.g. `EVP_DigestSignInit_ex`).
pub fn new_empty() -> Result<DigestCtx, ErrorStack>;
/// Reinitialise this context for reuse, associating it with a new algorithm.
/// Pass `None` for `params` when no extra initialisation parameters are needed.
/// Cheaper than dropping and creating a fresh context.
pub fn reinit(&mut self, alg: &DigestAlg, params: Option<&Params<'_>>) -> Result<(), ErrorStack>;
// ── State serialization — requires OpenSSL >= 4.0 (#[cfg(ossl_v400)]) ────────
/// Serialize the current mid-stream hash state to an owned byte buffer.
///
/// Calls `EVP_MD_CTX_serialize`, which allocates the buffer internally via
/// `OPENSSL_malloc`. The buffer is copied into a `Vec<u8>` and the
/// OpenSSL-owned memory is freed before returning.
///
/// The returned bytes are opaque and **version-specific** to this OpenSSL
/// build — they cannot be exchanged between different OpenSSL versions or
/// builds. Pass the bytes to `deserialize()` to restore the state.
///
/// Only compiled when built against OpenSSL >= 4.0.
#[cfg(ossl_v400)]
pub fn serialize(&self) -> Result<Vec<u8>, ErrorStack>;
/// Restore mid-stream hash state from bytes produced by [`DigestCtx::serialize`].
///
/// The context must already be initialized with the same algorithm before
/// calling `deserialize()`. After a successful call, the context is in
/// exactly the same state as when `serialize()` was called.
///
/// Only compiled when built against OpenSSL >= 4.0.
#[cfg(ossl_v400)]
pub fn deserialize(&mut self, data: &[u8]) -> Result<(), ErrorStack>;
}
}
DigestCtx has no lifetime parameter. EVP_DigestInit_ex2 internalises the algorithm
pointer inside the C context; the DigestAlg wrapper can be dropped independently.
DigestCtx is !Clone — use fork() to copy mid-stream state.
State Serialization (OpenSSL >= 4.0 only)
These methods are only compiled when the crate is built against OpenSSL >= 4.0. They are gated by
#[cfg(ossl_v400)], which is set automatically by the build system whenOPENSSL_VERSION_NUMBER >= 0x4000_0000. Code using them must guard call sites with#[cfg(ossl_v400)]as well.
OpenSSL 4.0 introduced EVP_MD_CTX_serialize and EVP_MD_CTX_deserialize,
which let you snapshot the internal state of a mid-stream hash context to an
opaque byte buffer and restore it later. This is distinct from fork():
fork()duplicates state within the same process in memory (cheap, in-process only).serialize()/deserialize()produce a portable byte representation that can be stored to disk, sent over a network, or passed to another process, as long as both sides use the same OpenSSL provider, version, and algorithm.
Important: The returned bytes are build-specific — they must not be exchanged between different OpenSSL versions or builds.
Example: Checkpoint and Resume a SHA-256 Hash
use native_ossl::digest::DigestAlg;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
// Hash the first chunk.
let mut ctx = sha256.new_context()?;
ctx.update(b"first chunk")?;
// Checkpoint: serialize returns an owned Vec<u8>.
let checkpoint = ctx.serialize()?;
// Continue hashing in the original context.
ctx.update(b" second chunk")?;
let mut out_original = [0u8; 32];
ctx.finish(&mut out_original)?;
// Restore the checkpoint into a fresh context and hash the same second chunk.
let mut ctx2 = sha256.new_context()?;
ctx2.deserialize(&checkpoint)?;
ctx2.update(b" second chunk")?;
let mut out_restored = [0u8; 32];
ctx2.finish(&mut out_restored)?;
// Both contexts produce the same digest.
assert_eq!(out_original, out_restored);
Serialization vs. Fork
fork() | serialize() / deserialize() | |
|---|---|---|
| Availability | OpenSSL >= 3.5 | OpenSSL >= 4.0 (ossl_v400 cfg) |
| Scope | In-process copy | Owned byte buffer |
| Cost | Deep C context copy | Serialize to/from bytes |
| Use case | Parallel suffix hashing | Checkpointing, IPC, persistence |
| Bytes portable? | N/A | No — build-specific |
Algorithm Names
| OpenSSL name string | Output bytes | Notes |
|---|---|---|
c"SHA2-256" | 32 | SHA-256 |
c"SHA2-384" | 48 | SHA-384 |
c"SHA2-512" | 64 | SHA-512 |
c"SHA3-256" | 32 | SHA3-256 |
c"SHA3-512" | 64 | SHA3-512 |
c"SHA1" | 20 | Avoid in new protocols |
c"MD5" | 16 | Avoid; legacy only |
c"SHAKE128" | variable | XOF; use finish_xof |
c"SHAKE256" | variable | XOF; use finish_xof |
c"SM3" | 32 | Chinese national standard |
Examples
Streaming SHA-256
#![allow(unused)]
fn main() {
use native_ossl::digest::DigestAlg;
// Fetch once — descriptor is Send + Sync and can be shared across threads.
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
assert_eq!(sha256.output_len(), 32);
let mut buf = [0u8; 32];
let mut ctx = sha256.new_context()?;
ctx.update(b"hello, ")?;
ctx.update(b"world")?;
ctx.finish(&mut buf)?;
}
One-Shot Hash
#![allow(unused)]
fn main() {
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let mut buf = [0u8; 32];
sha256.digest(b"hello, world", &mut buf)?;
}
XOF (SHAKE-256)
#![allow(unused)]
fn main() {
let shake = DigestAlg::fetch(c"SHAKE256", None)?;
let mut ctx = shake.new_context()?;
ctx.update(b"input data")?;
// Caller chooses the output length.
let mut out = vec![0u8; 64];
ctx.finish_xof(&mut out)?;
}
Fork Mid-Stream
#![allow(unused)]
fn main() {
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let mut ctx = sha256.new_context()?;
ctx.update(b"common prefix")?;
// Deep copy of the hashing state at this point.
let mut fork = ctx.fork()?;
ctx.update(b" path A")?;
fork.update(b" path B")?;
let mut hash_a = [0u8; 32];
let mut hash_b = [0u8; 32];
ctx.finish(&mut hash_a)?;
fork.finish(&mut hash_b)?;
// hash_a and hash_b differ only in suffix
}
Design Notes
alg()returnsOption, notResult— aNoneresult is not an error; it means the context was created withnew_empty()and has not yet been initialised. Callers who always initialise viaDigestAlg::new_context()can safely.unwrap().alg()is independently owned —EVP_MD_up_refis called before returning theDigestAlghandle, so it lives past theDigestCtxthat produced it.- No
reset()— afterfinish, create a new context viaalg.new_context(). fork()notclone()— naming itforksignals that copying the C context state is explicit and potentially expensive, unlike a cheap reference clone.- XOF algorithms (
SHAKE128,SHAKE256) must usefinish_xof, notfinish. Both are on the sameDigestCtxtype — there is no separate XOF context type. - Property queries (
fips=yes) can be passed as thepropsargument tofetchorfetch_into restrict which implementation is selected. serialize/deserializerequire OpenSSL >= 4.0 — these methods are compiled only when#[cfg(ossl_v400)]is active. Theossl_v400cfg flag is set automatically bynative-ossl/build.rswhen the detectedOPENSSL_VERSION_NUMBERis>= 0x4000_0000. Not all algorithms or providers may support serialization even on OpenSSL 4.0;serialize()returnsErrin that case.serialize()bytes are build-specific — the opaque byte representation is tied to the exact OpenSSL version and build. Do not persist or transmit them across different OpenSSL installations.