Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 when OPENSSL_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()
AvailabilityOpenSSL >= 3.5OpenSSL >= 4.0 (ossl_v400 cfg)
ScopeIn-process copyOwned byte buffer
CostDeep C context copySerialize to/from bytes
Use caseParallel suffix hashingCheckpointing, IPC, persistence
Bytes portable?N/ANo — build-specific

Algorithm Names

OpenSSL name stringOutput bytesNotes
c"SHA2-256"32SHA-256
c"SHA2-384"48SHA-384
c"SHA2-512"64SHA-512
c"SHA3-256"32SHA3-256
c"SHA3-512"64SHA3-512
c"SHA1"20Avoid in new protocols
c"MD5"16Avoid; legacy only
c"SHAKE128"variableXOF; use finish_xof
c"SHAKE256"variableXOF; use finish_xof
c"SM3"32Chinese 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() returns Option, not Result — a None result is not an error; it means the context was created with new_empty() and has not yet been initialised. Callers who always initialise via DigestAlg::new_context() can safely .unwrap().
  • alg() is independently ownedEVP_MD_up_ref is called before returning the DigestAlg handle, so it lives past the DigestCtx that produced it.
  • No reset() — after finish, create a new context via alg.new_context().
  • fork() not clone() — naming it fork signals that copying the C context state is explicit and potentially expensive, unlike a cheap reference clone.
  • XOF algorithms (SHAKE128, SHAKE256) must use finish_xof, not finish. Both are on the same DigestCtx type — there is no separate XOF context type.
  • Property queries (fips=yes) can be passed as the props argument to fetch or fetch_in to restrict which implementation is selected.
  • serialize/deserialize require OpenSSL >= 4.0 — these methods are compiled only when #[cfg(ossl_v400)] is active. The ossl_v400 cfg flag is set automatically by native-ossl/build.rs when the detected OPENSSL_VERSION_NUMBER is >= 0x4000_0000. Not all algorithms or providers may support serialization even on OpenSSL 4.0; serialize() returns Err in 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.