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

native-ossl

native-ossl is an idiomatic Rust wrapper around the OpenSSL 3.x C library. It exposes the full EVP_* cryptographic API — hashing, symmetric encryption, MAC, asymmetric keys, key derivation, randomness, X.509, and TLS — through safe, zero-copy Rust types.

What it provides

  • Digests — streaming SHA-2, SHA-3, SHAKE (XOF), SM3 via DigestAlg + DigestCtx
  • Symmetric encryption — AES-GCM, AES-CBC, ChaCha20-Poly1305, and more via CipherAlg
  • MAC — HMAC, CMAC, KMAC via MacAlg + MacCtx
  • Asymmetric keys — RSA, ECDSA, Ed25519, X25519, ML-KEM, ML-DSA via Pkey<T>
  • Key derivation — HKDF, PBKDF2, scrypt via typed builders; raw KdfCtx for anything else
  • RandomnessRand::fill for simple use; RandCtx for seeded DRBG hierarchies
  • X.509 — certificate parsing, building, and self-signing via X509 and X509Builder
  • TLS — full TLS 1.2 / 1.3 client and server via SslCtx and Ssl
  • Library contexts — FIPS-provider isolation via LibCtx

What it does not do

  • Does not require OpenSSL headers at runtime; bindings are generated by bindgen at build time against the system’s installed headers.
  • Does not ship its own copy of OpenSSL; it links the system library dynamically.
  • Does not expose deprecated OpenSSL 1.x APIs (ENGINE, BN_* arithmetic, legacy cipher names, etc.).
  • Does not provide async I/O; Ssl is synchronous. Async wrappers are a separate concern and can be built on top of the BIO layer.

Minimum requirements

RequirementVersion
OpenSSL3.5.0 or later
Rust1.77 or later
bindgen (build dep)0.72 or later
pkg-configsystem

The build script enforces the OpenSSL version at compile time. If an older library is found, the build fails with a clear diagnostic message.

Quick example

#![allow(unused)]
fn main() {
use native_ossl::digest::DigestAlg;

// Fetch the algorithm descriptor once — it is Send + Sync and can be shared.
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
assert_eq!(sha256.output_len(), 32);

// Stream arbitrary data through a stateful context.
let mut ctx = sha256.new_context()?;
ctx.update(b"hello, ")?;
ctx.update(b"world")?;

let mut out = [0u8; 32];
ctx.finish(&mut out)?;
println!("{}", hex::encode(out));
}

Crate layout

native-ossl/              workspace root
├── native-ossl-sys/      raw FFI (bindgen output, unsafe only)
├── native-ossl/          safe Rust wrappers (public API)
└── examples/
    └── encrypt-demo/     hybrid RSA-OAEP + AES-256-GCM demo

All public API lives in native-ossl. The native-ossl-sys crate is an implementation detail; callers never import it directly.

API Reference for Rust Crates

Redirecting to the rustdoc API reference…

If you are not redirected automatically, click here.

Getting Started

Prerequisites

Install OpenSSL development headers (3.0.7 or later):

# Fedora / RHEL
sudo dnf install openssl-devel

# Ubuntu / Debian
sudo apt install libssl-dev pkg-config

# macOS (Homebrew)
brew install openssl@3
export PKG_CONFIG_PATH="$(brew --prefix openssl@3)/lib/pkgconfig"

Verify the version:

pkg-config --modversion openssl
# must print 3.0.7 or later

Adding the dependency

In your Cargo.toml:

[dependencies]
native-ossl = { path = "../native-ossl" }

The crate is not yet published to crates.io; use a path or git dependency for now.

First programme: SHA-256

use native_ossl::digest::DigestAlg;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;

    let mut ctx = sha256.new_context()?;
    ctx.update(b"The quick brown fox jumps over the lazy dog")?;

    let mut digest = [0u8; 32];
    ctx.finish(&mut digest)?;

    println!("{}", hex::encode(digest));
    // d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592
    Ok(())
}

First programme: AES-256-GCM

use native_ossl::{
    cipher::{AeadDecryptCtx, AeadEncryptCtx, CipherAlg},
    rand::Rand,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let alg = CipherAlg::fetch(c"AES-256-GCM", None)?;

    // Generate a random key and nonce.
    let mut key   = [0u8; 32];
    let mut nonce = [0u8; 12];
    Rand::fill(&mut key)?;
    Rand::fill(&mut nonce)?;

    let plaintext = b"secret message";

    // Encrypt.
    let mut enc = AeadEncryptCtx::new(&alg, &key, &nonce, None)?;
    let mut ciphertext = vec![0u8; plaintext.len()];
    let n = enc.update(plaintext, &mut ciphertext)?;
    enc.finalize(&mut ciphertext[n..])?;
    let mut tag = [0u8; 16];
    enc.tag(&mut tag)?;

    // Decrypt and verify.
    let mut dec = AeadDecryptCtx::new(&alg, &key, &nonce, None)?;
    dec.set_tag(&tag)?;
    let mut recovered = vec![0u8; ciphertext.len()];
    let n = dec.update(&ciphertext, &mut recovered)?;
    dec.finalize(&mut recovered[n..])?;

    assert_eq!(&recovered, plaintext);
    println!("round-trip OK");
    Ok(())
}

Error handling

Every fallible function returns Result<T, ErrorStack>. ErrorStack holds the full OpenSSL error queue captured at the point of failure. It implements std::error::Error, so it works with ? and Box<dyn Error> equally well.

#![allow(unused)]
fn main() {
match DigestAlg::fetch(c"NO-SUCH-ALGORITHM", None) {
    Ok(_) => unreachable!(),
    Err(e) => eprintln!("OpenSSL error: {e}"),
}
}

See Error Handling for a full treatment.

Error Handling

The OpenSSL error model

OpenSSL maintains a per-thread error queue. When a C function fails it pushes one or more error records onto the queue before returning an error indicator (0 or NULL). The queue must be drained before the next operation, or stale errors accumulate.

native-ossl drains the queue immediately after every failing C call, captures all records, and returns them as a single ErrorStack value inside Err(...).

Types

Error — one record

A single entry from the OpenSSL error queue.

#![allow(unused)]
fn main() {
pub struct Error {
    code: u64,                 // packed OpenSSL error code
    reason: Option<String>,    // human-readable reason (e.g. "unsupported algorithm")
    lib: Option<String>,       // library name (e.g. "EVP routines")
    file: Option<String>,      // C source file where the error was raised
    func: Option<String>,      // C function name
    data: Option<String>,      // caller-supplied context string (if any)
}
}

Accessors: code(), lib(), reason(), file(), func(), data() — all return Option<&str> (or u64 for code).

Error implements Display in the form:

reason (lib) in func: data

ErrorStack — the queue snapshot

#![allow(unused)]
fn main() {
pub struct ErrorStack(Vec<Error>);
}

ErrorStack implements:

  • std::error::Error — works with ? and Box<dyn Error>
  • Display — joins all records with "; "
  • Send — safe to transfer across thread boundaries

Key methods:

MethodDescription
errors()Iterator over the individual Error records
is_empty()true if the queue was empty when captured

Inspecting errors

#![allow(unused)]
fn main() {
let err: ErrorStack = some_failing_call().unwrap_err();

// Print the full stack.
eprintln!("{err}");

// Iterate and inspect individual records.
for e in err.errors() {
    eprintln!(
        "  lib={:?}  reason={:?}  func={:?}",
        e.lib(), e.reason(), e.func()
    );
}
}

Pattern at call sites

Every public function that may fail follows this pattern internally:

#![allow(unused)]
fn main() {
pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
    crate::ossl_call!(EVP_DigestUpdate(
        self.ptr,
        data.as_ptr() as *const _,
        data.len(),
    ));
    Ok(())
}
}

The ossl_call! macro calls the C function, checks the return value, and calls ErrorStack::drain() on failure. The ossl_ptr! variant does the same but for functions that signal failure by returning NULL.

Cross-thread error capture

The OpenSSL error queue is thread-local. If a blocking crypto operation runs on a worker thread and the future is polled on a different thread (common in tokio), the error queue can be lost. Use ErrState to move errors across a thread boundary:

#![allow(unused)]
fn main() {
use native_ossl::error::ErrState;

let handle = std::thread::spawn(|| {
    let result = some_crypto_op();
    // Capture the queue before this thread exits.
    let state = ErrState::capture();
    (result, state)
});

let (result, state) = handle.join().unwrap();
// Restore the queue on the calling thread before inspecting the error.
state.restore_and_drain();
if let Err(e) = result {
    eprintln!("crypto error: {e}");
}
}

Note: ErrState is intended for advanced use cases. Most applications do not need it; ErrorStack is Send and can be moved freely between threads as a value.

Never ignore the error queue

Even when a failure is expected or intentional, never leave the queue populated. Call ERR_clear_error() (via sys::ERR_clear_error()) before speculative operations, or let the next ossl_call! drain it automatically.

Library Contexts

What is a library context?

OpenSSL 3.x introduces OSSL_LIB_CTX — an isolated environment that controls which cryptographic providers are loaded and used for algorithm fetch. The global default context is created automatically on the first API call and loads the default provider.

You would need an explicit LibCtx when:

  • Running FIPS-validated operations in isolation from non-FIPS ones.
  • Building a library that consumes OpenSSL and is supposed to be loaded into other applications
  • Loading a non-standard configuration file for one component only.
  • Testing — spinning up a context with a known configuration and tearing it down.

LibCtx — library context wrapper

#![allow(unused)]
fn main() {
pub struct LibCtx { /* OSSL_LIB_CTX* */ }

impl LibCtx {
    /// Create a new, empty library context.
    pub fn new() -> Result<Self, ErrorStack>;

    /// Load a named provider into this context.
    /// The returned `Provider` handle keeps the provider alive; drop it to unload.
    pub fn load_provider(&self, name: &CStr) -> Result<Provider, ErrorStack>;
}
}

LibCtx is Send + Sync. Wrapping it in Arc<LibCtx> lets you share one context across many threads and algorithm descriptors.

Provider — provider handle

#![allow(unused)]
fn main() {
pub struct Provider { /* OSSL_PROVIDER* */ }
impl Drop for Provider { /* OSSL_PROVIDER_unload */ }
}

Drop the Provider to unload; keep it alive for the duration you need the provider.

FIPS isolation example

#![allow(unused)]
fn main() {
use std::sync::Arc;
use native_ossl::lib_ctx::LibCtx;
use native_ossl::cipher::CipherAlg;
use native_ossl::pkey::Pkey;

// Create an isolated context.
let fips_ctx = Arc::new(LibCtx::new()?);

// FIPS provider: validated algorithm implementations.
// Base provider: encoders/decoders (PEM, DER). Always load it alongside FIPS.
let _fips = fips_ctx.load_provider(c"fips")?;
let _base = fips_ctx.load_provider(c"base")?;

// Fetch algorithms from the FIPS context — only FIPS-approved implementations.
let alg = CipherAlg::fetch_in(&fips_ctx, c"AES-256-GCM", None)?;

// Load a private key bound to the FIPS context.
let key = Pkey::<native_ossl::pkey::Private>::from_pem_in(&fips_ctx, &pem_bytes)?;
}

Keep _fips and _base alive for as long as you need the context. In production code, store them in a long-lived struct field alongside the Arc<LibCtx>.

How fetch_in works

Every algorithm type (DigestAlg, CipherAlg, MacAlg, KdfAlg, RandAlg) has:

  • fetch(name, props) — uses the global default context (NULL libctx).
  • fetch_in(ctx, name, props) — uses the supplied explicit context.

The Arc<LibCtx> is cloned and stored inside the algorithm descriptor, so the context stays alive as long as the descriptor or any context derived from it is in use.

Property queries

The optional props argument is an OpenSSL property query string. Common values:

Query stringEffect
NoneNo restriction — any matching provider
c"fips=yes"FIPS-validated implementations only
c"fips=no"Non-FIPS implementations only
c"provider=default"Explicit provider name
#![allow(unused)]
fn main() {
// Fetch only FIPS-approved SHA-256 from the global default context.
let alg = DigestAlg::fetch(c"SHA2-256", Some(c"fips=yes"))?;
}

Thread safety

LibCtx is Send + Sync. All algorithm fetch operations are internally thread-safe; you may call fetch_in from multiple threads simultaneously on the same Arc<LibCtx>.

Parameters

Overview

OpenSSL 3.x passes configuration to algorithms and contexts through arrays of OSSL_PARAM structures. Each entry is a key-value pair. The ParamBuilder<'a> type provides a fluent, typed API for constructing these arrays, and Params<'a> provides typed getters for reading values back out.

Most callers never need ParamBuilder directly — it is used internally by algorithm constructors and KDF builders. You need it when:

  • Setting AEAD tag length or AAD before cipher operations
  • Configuring RSA-OAEP padding mode and digest
  • Passing scrypt/PBKDF2 parameters at derive time
  • Customising KMAC with a customisation string
  • Importing or querying key material via Pkey::from_params / Pkey::get_params

ParamBuilder<'a>

#![allow(unused)]
fn main() {
pub struct ParamBuilder<'a> { /* OSSL_PARAM_BLD* */ }

impl<'a> ParamBuilder<'a> {
    /// Allocate a new empty builder. Returns Err on OOM.
    pub fn new() -> Result<Self, ErrorStack>;

    // Integer parameters — values are copied into builder-internal storage
    pub fn push_int(self, key: &CStr, val: i32)    -> Result<Self, ErrorStack>;
    pub fn push_uint(self, key: &CStr, val: u32)   -> Result<Self, ErrorStack>;
    pub fn push_uint64(self, key: &CStr, val: u64) -> Result<Self, ErrorStack>;
    pub fn push_size(self, key: &CStr, val: usize) -> Result<Self, ErrorStack>;

    // Octet string — copies val into builder-internal storage
    pub fn push_octet_slice(self, key: &CStr, val: &[u8]) -> Result<Self, ErrorStack>;

    // Octet string — stores a pointer into val (zero-copy)
    // The lifetime of the resulting Params<'b> is narrowed to 'b ≤ 'a
    pub fn push_octet_ptr<'b>(self, key: &CStr, val: &'b [u8])
        -> Result<ParamBuilder<'b>, ErrorStack>
    where
        'a: 'b;

    // UTF-8 string — copies val into builder-internal storage
    pub fn push_utf8_string(self, key: &CStr, val: &CStr) -> Result<Self, ErrorStack>;

    // UTF-8 string — accepts a &'static CStr; copies in practice (see notes)
    pub fn push_utf8_ptr(self, key: &CStr, val: &'static CStr) -> Result<Self, ErrorStack>;

    // BIGNUM from big-endian bytes — copies into builder-internal storage
    pub fn push_bn(self, key: &CStr, bigendian_bytes: &[u8]) -> Result<Self, ErrorStack>;

    /// Finalise and return the OSSL_PARAM array. Consumes the builder.
    pub fn build(self) -> Result<Params<'a>, ErrorStack>;
}
}

Params<'a>

#![allow(unused)]
fn main() {
pub struct Params<'a> { /* OSSL_PARAM* */ }

impl<'a> Params<'a> {
    // Adopt an OpenSSL-allocated array (e.g. from EVP_PKEY_todata).
    // SAFETY: ptr must be a valid OSSL_PARAM_END-terminated array.
    pub unsafe fn from_owned_ptr(ptr: *mut OSSL_PARAM) -> Params<'static>;

    // Mutable pointer — pass to EVP_PKEY_get_params for in-place query fill.
    pub fn as_mut_ptr(&mut self) -> *mut OSSL_PARAM;

    // Membership test
    pub fn has_param(&self, key: &CStr) -> bool;

    // Integer getters
    pub fn get_int(&self, key: &CStr)    -> Result<i32,   ErrorStack>;
    pub fn get_uint(&self, key: &CStr)   -> Result<u32,   ErrorStack>;
    pub fn get_size_t(&self, key: &CStr) -> Result<usize, ErrorStack>;
    pub fn get_i64(&self, key: &CStr)    -> Result<i64,   ErrorStack>;
    pub fn get_u64(&self, key: &CStr)    -> Result<u64,   ErrorStack>;

    // BIGNUM — allocates a Vec<u8> of big-endian bytes
    pub fn get_bn(&self, key: &CStr) -> Result<Vec<u8>, ErrorStack>;

    // Zero-copy borrows — valid for the lifetime of &self
    pub fn get_octet_string(&self, key: &CStr) -> Result<&[u8], ErrorStack>;
    pub fn get_utf8_string(&self, key: &CStr)  -> Result<&CStr, ErrorStack>;

    // In-place setters — require &mut self; the element must already exist with
    // compatible type (built with the corresponding push_* method).
    pub fn set_int(&mut self, key: &CStr, val: i32)    -> Result<(), ErrorStack>;
    pub fn set_uint(&mut self, key: &CStr, val: u32)   -> Result<(), ErrorStack>;
    pub fn set_size(&mut self, key: &CStr, val: usize) -> Result<(), ErrorStack>;

    // Read a C `long` (64-bit on LP64 Linux, 32-bit on Windows); widened to i64.
    pub fn get_long(&self, key: &CStr) -> Result<i64, ErrorStack>;

    // True if this element was written by a get_params call (OSSL_PARAM_modified).
    // Returns false if the key is absent.
    pub fn was_modified(&self, key: &CStr) -> bool;
}
}

Params is Send + Sync. Pass it to any OpenSSL function that accepts const OSSL_PARAM[]. The lifetime 'a covers any borrowed slice stored via push_octet_ptr.

Copy vs. Pointer Reference (Building)

MethodCopies data?Use when
push_octet_sliceYesSlice may be dropped before Params is consumed
push_octet_ptrNoSlice is guaranteed to outlive Params
push_utf8_stringYesShort runtime strings
push_utf8_ptrNo (intent)'static string literals like c"oaep"
push_int/uint/…n/aInteger values always copied
push_bnYesBytes converted to BIGNUM; caller’s slice not retained

Note on push_utf8_ptr: The underlying C function OSSL_PARAM_BLD_push_utf8_ptr is not reliably available across OpenSSL builds. This method falls back to push_utf8_string, which copies. The 'static signature documents intent; the copy of a short literal is negligible.

Allocation vs. Zero-Copy (Reading)

GetterAllocates?Notes
get_int / get_uint / get_size_t / get_i64 / get_u64 / get_longNoScalar read
get_bnYesVec<u8>Must convert the internal BIGNUM to bytes
get_octet_stringNoReturns &[u8] borrowing from the Params array
get_utf8_stringNoReturns &CStr borrowing from the Params array

get_octet_string and get_utf8_string return references whose lifetime is tied to &self. They cannot outlive the Params value they were read from.

In-place Setters and was_modified

After filling a pre-prepared query array with EVP_PKEY_get_params (or any other get_params variant), you can update individual fields in place without rebuilding the entire array:

#![allow(unused)]
fn main() {
// Overwrite an int field that was previously pushed with push_int.
params.set_int(c"key_bits", 4096)?;
params.set_uint(c"flags", 3u32)?;
params.set_size(c"chunk", 4096usize)?;
}

set_int, set_uint, and set_size require &mut self and look up the element by key at runtime. They fail with Err if the key is absent or the stored type is incompatible with the requested setter.

get_long reads a C long value — 64 bits on LP64 Linux/macOS, 32 bits on Windows — and widens it to i64:

#![allow(unused)]
fn main() {
let n: i64 = params.get_long(c"iterations")?;
}

was_modified lets you check whether a particular element was populated by the last get_params call. OpenSSL marks each element as “modified” when it writes a value into it, so a false return means the provider left that field untouched:

#![allow(unused)]
fn main() {
let mut query = ParamBuilder::new()?
    .push_octet_slice(c"e", &[])?
    .build()?;

// Before get_params — not yet populated.
assert!(!query.was_modified(c"e"));

key.get_params(&mut query)?;

// OpenSSL filled the element.
assert!(query.was_modified(c"e"));
}

Returns false if the key does not exist in the array at all.

from_owned_ptr and as_mut_ptr

These two methods cover the ownership-transfer patterns needed when interacting directly with OpenSSL functions that allocate or fill OSSL_PARAM arrays.

from_owned_ptr — use when OpenSSL returns a freshly allocated array and you want Params to own and free it:

// EVP_PKEY_todata allocates a new OSSL_PARAM array.
// Params::from_owned_ptr wraps it and will call OSSL_PARAM_free on drop.
let exported: Params<'static> = unsafe { Params::from_owned_ptr(raw_ptr) };

as_mut_ptr — use when you pre-build a query array (with null data pointers) and pass it to EVP_PKEY_get_params for in-place fill:

// Build a query array asking only for the public exponent.
let mut query = ParamBuilder::new()?
    .push_octet_slice(c"e", &[])?  // placeholder — OpenSSL will fill this
    .build()?;
key.get_params(&mut query)?;
let e_bytes = query.get_bn(c"e")?;

In practice, prefer Pkey::export() and Pkey::get_params() over raw pointer manipulation. See Asymmetric Keys for examples.

RSA-OAEP Example

#![allow(unused)]
fn main() {
use native_ossl::params::ParamBuilder;

let oaep = ParamBuilder::new()?
    .push_utf8_ptr(c"pad-mode",    c"oaep")?
    .push_utf8_ptr(c"oaep-digest", c"SHA-256")?
    .build()?;

let mut enc = PkeyEncryptCtx::new(&pub_key, Some(&oaep))?;
}

AEAD Tag Length Example

#![allow(unused)]
fn main() {
// AES-128-CCM requires the tag length to be set before any data
let tag_params = ParamBuilder::new()?
    .push_size(c"tag-len", 16)?
    .build()?;

let mut enc = AeadEncryptCtx::new(&alg, &key, &nonce, Some(&tag_params))?;
}

Reading Exported Key Parameters

After calling Pkey::export(), inspect the returned Params<'static> with the getter methods. The example reads the public exponent and modulus from an RSA key pair:

use native_ossl::pkey::{KeygenCtx, Pkey, Private};
use native_ossl::params::ParamBuilder;

let mut ctx = KeygenCtx::new(c"RSA")?;
let bits = ParamBuilder::new()?.push_uint(c"bits", 2048)?.build()?;
ctx.set_params(&bits)?;
let key: Pkey<Private> = ctx.generate()?;

// Export all parameters (private + public material).
let exported = key.export()?;

// Check presence before reading.
assert!(exported.has_param(c"e"));
assert!(exported.has_param(c"n"));

// get_bn returns big-endian bytes; allocates a Vec<u8>.
let modulus  = exported.get_bn(c"n")?;
let exponent = exported.get_bn(c"e")?;

println!("RSA modulus  : {} bytes", modulus.len());
println!("RSA exponent : {:?}", exponent);

For a full round-trip (export then re-import), see Asymmetric Keys.

Design Notes

  • ParamBuilder::new() returns Result — unlike typical builders, OSSL_PARAM_BLD can fail to allocate immediately. Always propagate with ?.
  • Builder consumes self on each push — if a push fails, the builder is dropped and the error returned. There is no way to recover a partially-constructed builder.
  • build() uses mem::forget — to prevent double-free after transferring the raw pointer to OSSL_PARAM_BLD_to_param.
  • OSSL_PARAM_END is automaticOSSL_PARAM_BLD_to_param appends the terminator. Never add it manually.
  • push_bn copies the BIGNUMOSSL_PARAM_BLD_push_BN copies the value into the builder’s internal storage, so the caller’s byte slice is not retained after the call.
  • get_bn allocatesOSSL_PARAM_get_BN allocates a new BIGNUM internally; Params then converts it to a Vec<u8> and frees the BIGNUM. For all other numeric types, getters are copy-free scalar reads.

BIO — I/O Abstraction

Types

Bio — owned BIO

Bio is a reference-counted, owned wrapper around an OpenSSL BIO*. It supports synchronous I/O (read/write) and BIO chain management.

#![allow(unused)]
fn main() {
pub struct Bio { /* BIO* */ }

impl Bio {
    /// Create a linked in-memory BIO pair (for in-process TLS).
    pub fn new_pair() -> Result<(Bio, Bio), ErrorStack>;

    // ── I/O ──────────────────────────────────────────────────────────────────

    /// Read up to buf.len() bytes; returns bytes read.
    pub fn read(&mut self, buf: &mut [u8]) -> Result<usize, ErrorStack>;

    /// Read with exact byte count reported via BIO_read_ex.
    pub fn read_ex(&mut self, buf: &mut [u8]) -> Result<usize, ErrorStack>;

    /// Write buf; returns bytes written.
    pub fn write(&mut self, buf: &[u8]) -> Result<usize, ErrorStack>;

    // ── Read buffer inspection ───────────────────────────────────────────────

    /// Number of bytes available for reading (BIO_CTRL_PENDING).
    pub fn pending(&self) -> usize;

    /// Number of bytes pending in the write buffer (BIO_CTRL_WPENDING).
    pub fn wpending(&self) -> usize;

    // ── Chain management ─────────────────────────────────────────────────────

    /// Append `next` after `self`; takes ownership of `next`.
    pub fn push(self, next: Bio) -> Bio;

    /// Detach `self` from its chain; return the rest as `Some(Bio)`.
    pub fn pop(&mut self) -> Option<Bio>;

    /// Borrow the next BIO without consuming `self`.
    pub fn next(&self) -> Option<BorrowedBio<'_>>;

    /// Search the chain for the first BIO of the given OpenSSL type constant.
    pub fn find_type(&self, bio_type: c_int) -> Option<BorrowedBio<'_>>;
}
}

BorrowedBio<'a> — non-owning chain view

Returned by Bio::next and Bio::find_type. Does not free the underlying pointer on drop — ownership stays with the chain.

#![allow(unused)]
fn main() {
pub struct BorrowedBio<'a> { /* non-owning BIO* */ }

impl BorrowedBio<'_> {
    pub fn as_ptr(&self) -> *mut BIO;
}
}

MemBio — writable in-memory BIO

Wraps BIO_s_mem(). Data written with write() accumulates in an internal buffer; call data() for a zero-copy &[u8] view.

#![allow(unused)]
fn main() {
pub struct MemBio { /* BIO* */ }

impl MemBio {
    pub fn new() -> Result<Self, ErrorStack>;
    pub fn write(&mut self, data: &[u8]) -> Result<(), ErrorStack>;
    pub fn data(&self) -> &[u8];
    pub fn into_vec(self) -> Vec<u8>;
}
}

MemBioBuf<'a> — read-only view into a slice

Wraps BIO_new_mem_buf(). Zero-copy: OpenSSL reads directly from the caller’s slice. The lifetime 'a prevents the BIO from outliving the slice.

#![allow(unused)]
fn main() {
pub struct MemBioBuf<'a> { /* BIO* + PhantomData<&'a [u8]> */ }

impl<'a> MemBioBuf<'a> {
    pub fn new(data: &'a [u8]) -> Result<Self, ErrorStack>;
}
}

BIO chain ownership model

BIO_push in C transfers ownership of the appended BIO into the chain. In Rust, Bio::push consumes both self and next by value, preventing a double-free:

#![allow(unused)]
fn main() {
let bio1 = /* ... */;
let bio2 = /* ... */;
// bio2 is moved into the chain; do not free it separately.
let chain = bio1.push(bio2);
}

Bio::pop detaches the head from its successor and returns Option<Bio>; the detached tail is now an independent owner.

Bio::next and Bio::find_type return Option<BorrowedBio<'_>> — a borrowed, non-freeing view bounded by the lifetime of the chain head.

BIO_TYPE_* constants

find_type accepts the raw OpenSSL integer type constants, e.g.:

ConstantValueMeaning
BIO_TYPE_MEM8In-memory BIO (BIO_s_mem)
BIO_TYPE_BIO19BIO pair half
BIO_TYPE_SSL7SSL filter BIO

These are available as native_ossl_sys::BIO_TYPE_MEM etc. when using the sys crate directly.

Example

#![allow(unused)]
fn main() {
use native_ossl::bio::Bio;
use native_ossl_sys as sys;

// Create a mem BIO, write, then read back.
let raw = unsafe { sys::BIO_new(sys::BIO_s_mem()) };
let mut bio = unsafe { Bio::from_ptr_owned(raw) };

bio.write(b"hello").unwrap();
let mut buf = [0u8; 8];
let n = bio.read(&mut buf).unwrap();
assert_eq!(&buf[..n], b"hello");

// Chain two BIOs and inspect the link.
let ptr1 = unsafe { sys::BIO_new(sys::BIO_s_mem()) };
let ptr2 = unsafe { sys::BIO_new(sys::BIO_s_mem()) };
let bio1 = unsafe { Bio::from_ptr_owned(ptr1) };
let bio2 = unsafe { Bio::from_ptr_owned(ptr2) };
let raw2 = bio2.as_ptr();
let mut chain = bio1.push(bio2);
assert_eq!(chain.next().unwrap().as_ptr(), raw2);
let _tail = chain.pop(); // detach tail
}

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.

Symmetric Encryption

Two-Object Model

Symmetric encryption uses the same fetch-then-use pattern as digests:

  • CipherAlg — algorithm descriptor. Fetch once, reuse. Send + Sync.
  • CipherCtx<Dir> — stateful context. Either CipherCtx<Encrypt> or CipherCtx<Decrypt>.

For AEAD modes (GCM, CCM, ChaCha20-Poly1305), use the dedicated wrappers AeadEncryptCtx and AeadDecryptCtx instead of CipherCtx<Dir> directly.

CipherAlg — Algorithm Descriptor

#![allow(unused)]
fn main() {
pub struct CipherAlg { /* EVP_CIPHER* */ }

impl CipherAlg {
    /// 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>;

    /// Expected key length in bytes (e.g. 32 for AES-256-*).
    pub fn key_len(&self) -> usize;

    /// Expected IV/nonce length in bytes (0 for ECB mode).
    pub fn iv_len(&self) -> usize;

    /// Block size in bytes (1 for stream ciphers; 16 for AES).
    pub fn block_size(&self) -> usize;

    /// Raw OpenSSL cipher flags.
    pub fn flags(&self) -> u64;

    /// `true` if this is an AEAD cipher (GCM, CCM, ChaCha20-Poly1305).
    pub fn is_aead(&self) -> bool;

    /// Create an encryption context.
    /// Key and IV are always copied into OpenSSL's internal context.
    pub fn encrypt(&self, key: &[u8], iv: &[u8], params: Option<&Params<'_>>)
        -> Result<CipherCtx<Encrypt>, ErrorStack>;

    /// Create a decryption context.
    pub fn decrypt(&self, key: &[u8], iv: &[u8], params: Option<&Params<'_>>)
        -> Result<CipherCtx<Decrypt>, ErrorStack>;
}
}

CipherCtx<Dir> — Non-AEAD Context

#![allow(unused)]
fn main() {
pub struct CipherCtx<Dir> { /* EVP_CIPHER_CTX* */ }

impl<Dir: Direction> CipherCtx<Dir> {
    /// Feed `input` through the cipher; write output into `output`.
    /// `output` must be at least `input.len() + block_size - 1` bytes.
    /// Returns the number of bytes written.
    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack>;

    /// Like `update` but allocates the output buffer.
    pub fn update_to_vec(&mut self, input: &[u8]) -> Result<Vec<u8>, ErrorStack>;

    /// Finalise (flush padding / verify). Returns bytes written.
    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack>;

    /// Set dynamic parameters after construction.
    pub fn set_params(&mut self, params: &Params<'_>) -> Result<(), ErrorStack>;

    /// Query arbitrary parameters from the context via a pre-built getter array.
    /// Build a `Params` array with placeholder values, call this, then read results.
    pub fn get_params(&self, params: &mut Params<'_>) -> Result<(), ErrorStack>;

    /// Return the AEAD authentication tag length (e.g. 16 for AES-GCM).
    pub fn aead_tag_len(&self) -> Result<usize, ErrorStack>;

    /// Return the key length in bytes for this cipher context.
    pub fn key_len(&self) -> Result<usize, ErrorStack>;

    /// Return the IV length in bytes for this cipher context.
    pub fn iv_len(&self) -> Result<usize, ErrorStack>;
}
}

The marker types Encrypt and Decrypt prevent calling encrypt functions on a decrypt context and vice versa.

AEAD Contexts

AEAD modes add authentication. Use these wrappers rather than CipherCtx<Dir>:

#![allow(unused)]
fn main() {
pub struct AeadEncryptCtx(CipherCtx<Encrypt>);
pub struct AeadDecryptCtx(CipherCtx<Decrypt>);
}

AeadEncryptCtx

#![allow(unused)]
fn main() {
impl AeadEncryptCtx {
    /// Create an AEAD encryption context.
    ///
    /// # Panics
    ///
    /// Panics if `alg.is_aead()` is false. Check with `alg.is_aead()` if unsure.
    pub fn new(alg: &CipherAlg, key: &[u8], iv: &[u8], params: Option<&Params<'_>>)
        -> Result<Self, ErrorStack>;

    /// Set additional authenticated data (AAD). Call before first `update`.
    pub fn set_aad(&mut self, aad: &[u8]) -> Result<(), ErrorStack>;

    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack>;
    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack>;

    /// Retrieve the authentication tag after `finalize`. Typically 16 bytes for GCM.
    pub fn tag(&self, tag: &mut [u8]) -> Result<(), ErrorStack>;
}
}

AeadDecryptCtx

#![allow(unused)]
fn main() {
impl AeadDecryptCtx {
    pub fn new(alg: &CipherAlg, key: &[u8], iv: &[u8], params: Option<&Params<'_>>)
        -> Result<Self, ErrorStack>;

    pub fn set_aad(&mut self, aad: &[u8]) -> Result<(), ErrorStack>;
    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack>;

    /// Set the expected authentication tag before calling `finalize`.
    pub fn set_tag(&mut self, tag: &[u8]) -> Result<(), ErrorStack>;

    /// Finalise: returns `Err` if authentication fails.
    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack>;
}
}

Algorithm Names

OpenSSL name stringKey (bytes)IV (bytes)Notes
c"AES-128-CBC"1616PKCS#7 padding
c"AES-256-CBC"3216PKCS#7 padding
c"AES-128-GCM"1612AEAD
c"AES-256-GCM"3212AEAD
c"AES-256-CTR"3216Stream mode
c"ChaCha20-Poly1305"3212AEAD
c"AES-128-CCM"167–13AEAD; tag length required

Examples

AES-256-GCM (Encrypt)

#![allow(unused)]
fn main() {
use native_ossl::cipher::{AeadEncryptCtx, AeadDecryptCtx, CipherAlg};
use native_ossl::rand::Rand;

let alg = CipherAlg::fetch(c"AES-256-GCM", None)?;
assert_eq!(alg.key_len(), 32);
assert_eq!(alg.iv_len(), 12);

let mut key   = [0u8; 32];
let mut nonce = [0u8; 12];
Rand::fill(&mut key)?;
Rand::fill(&mut nonce)?;

let plaintext = b"secret message";

// Encrypt
let mut enc = AeadEncryptCtx::new(&alg, &key, &nonce, None)?;
enc.set_aad(b"additional data")?;
let mut ciphertext = vec![0u8; plaintext.len()];
let n = enc.update(plaintext, &mut ciphertext)?;
enc.finalize(&mut ciphertext[n..])?;
let mut tag = [0u8; 16];
enc.tag(&mut tag)?;

// Decrypt
let mut dec = AeadDecryptCtx::new(&alg, &key, &nonce, None)?;
dec.set_aad(b"additional data")?;
dec.set_tag(&tag)?;
let mut recovered = vec![0u8; ciphertext.len()];
let n = dec.update(&ciphertext, &mut recovered)?;
dec.finalize(&mut recovered[n..])?;

assert_eq!(&recovered, plaintext);
}

AES-256-CBC (Non-AEAD)

#![allow(unused)]
fn main() {
let alg = CipherAlg::fetch(c"AES-256-CBC", None)?;
assert!(!alg.is_aead());

let key = [0u8; 32];
let iv  = [0u8; 16];

let mut enc = alg.encrypt(&key, &iv, None)?;
let plaintext = b"sixteen bytes!!!";
let mut ciphertext = vec![0u8; plaintext.len() + alg.block_size()];
let n = enc.update(plaintext, &mut ciphertext)?;
let m = enc.finalize(&mut ciphertext[n..])?;
ciphertext.truncate(n + m);
}

Design Notes

  • Key and IV are always copiedEVP_EncryptInit_ex2 copies key material into the C context for key scheduling and eventual zeroization. The slices may be dropped immediately after construction.
  • Output buffer sizing — for update, the caller must provide at least input.len() + block_size - 1 bytes. When the exact output size is unknown, use update_to_vec.
  • finalize must be called — even for stream ciphers and GCM (which write 0 bytes), finalize advances the context into its final state and is required before calling tag() on the encrypt side.
  • Tag order — for AeadDecryptCtx, call set_tag before finalize, not after. finalize performs the authentication check.
  • push_size(key, 0) getter patternEVP_CIPHER_CTX_get_params fills a caller-provided OSSL_PARAM array rather than returning a value directly. To use it, build a Params array with a placeholder value (0) for each key you want to query (push_size(c"taglen", 0)), call get_params (or a typed helper such as aead_tag_len), then read the result with get_size_t. OpenSSL overwrites the placeholder in-place. This is the standard OSSL_PARAM getter protocol for all context-level get_params calls in OpenSSL 3.x.

Message Authentication

Overview

MAC operations use the same fetch-then-use pattern as digests:

  • MacAlg — algorithm descriptor. Fetch once, reuse. Send + Sync.
  • MacCtx — stateful context. Exclusive ownership; supports mid-stream forking.
  • HmacCtx / CmacCtx — higher-level wrappers that handle sub-algorithm parameters for HMAC and CMAC respectively.

MacAlg — Algorithm Descriptor

#![allow(unused)]
fn main() {
pub struct MacAlg { /* EVP_MAC* */ }

impl MacAlg {
    /// 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>;
}
}

MacCtx — Raw Stateful Context

MacCtx::new is public. Use it when you need a raw context for algorithms that HmacCtx and CmacCtx do not cover (KMAC, GMAC, Poly1305, SipHash), or when you want to pass parameters manually.

#![allow(unused)]
fn main() {
pub struct MacCtx { /* EVP_MAC_CTX* */ }

impl MacCtx {
    /// Create a new (uninitialised) MAC context from an algorithm descriptor.
    ///
    /// Call `init` with a key before feeding data.
    pub fn new(alg: &MacAlg) -> Result<Self, ErrorStack>;

    /// Initialise (or re-initialise) with a key and optional parameters.
    ///
    /// The key is always copied into OpenSSL's MAC context state.
    /// For HMAC, pass the digest name via params. For CMAC, pass the cipher name.
    pub fn init(&mut self, key: &[u8], params: Option<&Params<'_>>) -> Result<(), ErrorStack>;

    /// Feed data into the MAC computation. Zero-copy from caller's slice.
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack>;

    /// Finalise the MAC. `out` must be at least `self.mac_size()` bytes.
    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack>;

    /// Finalise with variable output length (for XOF MACs: KMAC-128, KMAC-256).
    pub fn finish_xof(&mut self, out: &mut [u8]) -> Result<(), ErrorStack>;

    /// Expected MAC output length. Available after `init`.
    pub fn mac_size(&self) -> usize;

    /// Block size of the underlying MAC algorithm in bytes. Returns `0` before `init`.
    pub fn block_size(&self) -> usize;

    /// Return the algorithm descriptor for this context (independent ownership).
    pub fn alg(&self) -> Option<MacAlg>;

    /// Fork the current mid-stream state into a new independent context.
    pub fn fork(&self) -> Result<MacCtx, ErrorStack>;
}
}

HmacCtx — HMAC Helper

HMAC always requires a digest algorithm. HmacCtx bundles this requirement:

#![allow(unused)]
fn main() {
pub struct HmacCtx(MacCtx);

impl HmacCtx {
    /// Create an HMAC context using the given digest algorithm and key.
    pub fn new(digest: &DigestAlg, key: &[u8]) -> Result<Self, ErrorStack>;

    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack>;
    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack>;
    pub fn finish_to_vec(&mut self) -> Result<Vec<u8>, ErrorStack>;
    pub fn mac_size(&self) -> usize;

    /// One-shot HMAC over a single input.
    pub fn oneshot(digest: &DigestAlg, key: &[u8], data: &[u8], out: &mut [u8])
        -> Result<usize, ErrorStack>;
}
}

CmacCtx — CMAC Helper

CMAC requires a block cipher (typically AES):

#![allow(unused)]
fn main() {
pub struct CmacCtx(MacCtx);

impl CmacCtx {
    /// Create a CMAC context using the given cipher algorithm and key.
    pub fn new(cipher: &CipherAlg, key: &[u8]) -> Result<Self, ErrorStack>;

    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack>;
    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack>;
}
}

Algorithm Names

MAC typeOpenSSL nameSub-algorithm parameter
HMACc"HMAC"digest = c"SHA2-256" (set via HmacCtx)
CMACc"CMAC"cipher = c"AES-256-CBC" (set via CmacCtx)
GMACc"GMAC"cipher = c"AES-256-GCM"
KMAC-128c"KMAC128"none
KMAC-256c"KMAC256"none
Poly1305c"POLY1305"none
SipHashc"SIPHASH"none

Examples

HMAC-SHA-256

#![allow(unused)]
fn main() {
use native_ossl::mac::HmacCtx;
use native_ossl::digest::DigestAlg;

let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let key = b"secret key";
let data = b"message";

// Streaming
let mut hmac = HmacCtx::new(&sha256, key)?;
hmac.update(data)?;
let mac = hmac.finish_to_vec()?;

// One-shot
let mut out = [0u8; 32];
HmacCtx::oneshot(&sha256, key, data, &mut out)?;
}

KMAC-256 (XOF, Variable Output)

#![allow(unused)]
fn main() {
use native_ossl::mac::{MacAlg, MacCtx};

let alg = MacAlg::fetch(c"KMAC256", None)?;
let mut ctx = MacCtx::new(&alg)?;
ctx.init(b"my key", None)?;
ctx.update(b"input data")?;

// Caller chooses the output length.
let mut out = vec![0u8; 64];
ctx.finish_xof(&mut out)?;
}

Raw MacCtx for Poly1305

MacCtx::new is the entry point for algorithms not covered by the typed helpers. Poly1305 takes no sub-algorithm parameter — pass None to init:

use native_ossl::mac::{MacAlg, MacCtx};

let alg = MacAlg::fetch(c"POLY1305", None)?;
let mut ctx = MacCtx::new(&alg)?;

// Poly1305 key is always 32 bytes.
let key = [0u8; 32];
ctx.init(&key, None)?;
ctx.update(b"authenticated message")?;

let mut tag = vec![0u8; ctx.mac_size()];
ctx.finish(&mut tag)?;

Fork Mid-Stream

#![allow(unused)]
fn main() {
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let mut ctx = HmacCtx::new(&sha256, b"key")?;
// ... (note: raw MacCtx supports fork; HmacCtx wraps MacCtx internally)

let alg = MacAlg::fetch(c"HMAC", None)?;
let mut mac_ctx = MacCtx::new(&alg)?;

// Build params manually for raw MacCtx
use native_ossl::params::ParamBuilder;
let params = ParamBuilder::new()?
    .push_utf8_string(c"digest", c"SHA2-256")?
    .build()?;
mac_ctx.init(b"key", Some(&params))?;
mac_ctx.update(b"common prefix")?;

// Fork at the current state.
let mut fork = mac_ctx.fork()?;
mac_ctx.update(b" path A")?;
fork.update(b" path B")?;

let mut mac_a = [0u8; 32];
let mut mac_b = [0u8; 32];
mac_ctx.finish(&mut mac_a)?;
fork.finish(&mut mac_b)?;
}

Design Notes

  • MacCtx::new is public — previously crate-internal, it is now part of the public API. Use it directly when you need a raw context for algorithms that the typed helpers (HmacCtx, CmacCtx) do not cover.
  • Key at init, not construction — OpenSSL passes the key to EVP_MAC_init. The key is always copied into the MAC context for correct zeroization on drop.
  • mac_size after init — for some algorithms, EVP_MAC_CTX_get_mac_size only returns a meaningful value after EVP_MAC_init. Always call init first.
  • fork vs cloneEVP_MAC_CTX_dup performs a deep copy of the MAC state. Naming it fork rather than clone signals that the operation is explicit and potentially expensive.
  • KMAC customisation string — pass it through Params at init time using push_utf8_string(c"custom", c"my_context").
  • block_size() before initEVP_MAC_CTX_get_block_size returns 0 when the sub-algorithm (e.g. the digest for HMAC) has not yet been set via init. Always call init before reading block_size() for HMAC/CMAC contexts.
  • alg() ownershipEVP_MAC_CTX_get0_mac returns a borrowed pointer; alg() immediately calls EVP_MAC_up_ref so the returned MacAlg owns its own reference and may outlive the MacCtx. This is the same get0 + up_ref pattern used by DigestCtx::alg().

Asymmetric Keys

Key Visibility Typestate

All asymmetric key material is held in Pkey<T>, where T is a zero-sized marker:

Private  →  HasPrivate (also HasPublic + HasParams)
Public   →  HasPublic  (also HasParams)
Params   →  HasParams  (DH/DSA parameters only; no key material)

A Pkey<Private> can be used anywhere a Pkey<Public> is accepted via the trait hierarchy — no explicit conversion is needed for operations that only require the public component. However, you cannot extract a Pkey<Private> from a Pkey<Public>.

Pkey<T> — Key Container

#![allow(unused)]
fn main() {
pub struct Pkey<T> { /* EVP_PKEY* */ }

impl<T: HasParams> Pkey<T> {
    /// Key size in bits (e.g. 2048 for RSA-2048, 256 for P-256).
    pub fn bits(&self) -> u32;

    /// Security level in bits.
    pub fn security_bits(&self) -> u32;

    /// Maximum output size in bytes (signature length for signing keys, ciphertext for encryption).
    /// Use this to size output buffers before signing. Returns 0 if not applicable.
    pub fn max_output_size(&self) -> usize;

    /// Check algorithm type by name (e.g. c"RSA", c"EC", c"ED25519").
    pub fn is_a(&self, name: &CStr) -> bool;

    /// Compare public components of two keys for equality.
    pub fn public_eq<U: HasPublic>(&self, other: &Pkey<U>) -> bool
    where T: HasPublic;

    /// Export public key as DER (SubjectPublicKeyInfo).
    pub fn public_key_to_der(&self) -> Result<Vec<u8>, ErrorStack>
    where T: HasPublic;

    /// Fill values into a pre-prepared mutable query array.
    /// Wraps EVP_PKEY_get_params. Works for all key roles.
    pub fn get_params(&self, params: &mut Params<'_>) -> Result<(), ErrorStack>;

    /// Returns the default digest algorithm name for this key, or None if the
    /// key type mandates no external digest (e.g. Ed25519, ML-DSA, SLH-DSA).
    ///
    /// Wraps EVP_PKEY_get_default_digest_name. Both return-value 1 (optional digest)
    /// and 2 (mandatory digest) are treated as success. "none" or empty string
    /// maps to None.
    ///
    /// Requires T: HasPublic (i.e. Pkey<Public> or Pkey<Private>).
    pub fn default_digest_name(&self) -> Result<Option<String>, ErrorStack>
    where T: HasPublic;
}
}

Pkey<T> is Clone (via EVP_PKEY_up_ref) and Send + Sync.

Key Loading

Private Key

#![allow(unused)]
fn main() {
impl Pkey<Private> {
    /// Load from PEM (PKCS#8 or traditional format).
    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Load from PEM using an explicit library context (FIPS isolation).
    pub fn from_pem_in(ctx: &Arc<LibCtx>, pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Load from passphrase-encrypted PEM.
    /// Passes `passphrase` to OpenSSL via a `pem_password_cb`.
    /// Use `from_pem` for unencrypted PEM.
    pub fn from_pem_passphrase(pem: &[u8], passphrase: &[u8]) -> Result<Self, ErrorStack>;

    /// Load from DER (PKCS#8 or traditional, auto-detected).
    /// Uses MemBioBuf — reads directly from caller's slice without copying.
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;

    /// Load from DER bytes using an explicit library context.
    /// Format is auto-detected (PKCS#8, traditional, etc.) via d2i_AutoPrivateKey_ex.
    /// Use when the key will be used with EVP operations in an isolated (e.g. FIPS) context.
    pub fn from_der_in(ctx: &Arc<LibCtx>, der: &[u8]) -> Result<Self, ErrorStack>;

    /// Encode this private key to legacy raw-key DER (not PKCS#8).
    /// Wraps i2d_PrivateKey. Output is the algorithm-native structure
    /// (e.g. RSAPrivateKey for RSA, ECPrivateKey for EC).
    /// For new code prefer to_pkcs8_der() which is algorithm-agnostic.
    pub fn to_der_legacy(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Serialize to PEM (PKCS#8 `BEGIN PRIVATE KEY` format).
    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Serialize to passphrase-encrypted PKCS#8 PEM (`BEGIN ENCRYPTED PRIVATE KEY`).
    /// `cipher` controls the wrapping algorithm (e.g. `CipherAlg::fetch(c"AES-256-CBC", None)`).
    pub fn to_pem_encrypted(&self, cipher: &CipherAlg, passphrase: &[u8]) -> Result<Vec<u8>, ErrorStack>;

    /// Serialize to unencrypted PKCS#8 DER (`PrivateKeyInfo` / RFC 5958).
    /// Avoids the base64 round-trip of `to_pem`. Use `to_pem_encrypted` for an encrypted form.
    pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, ErrorStack>;
}
}

Public Key

#![allow(unused)]
fn main() {
impl Pkey<Public> {
    /// Load from PEM (SubjectPublicKeyInfo or RSA public key format).
    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Load from PEM using an explicit library context (FIPS isolation).
    ///
    /// Uses PEM_read_bio_PUBKEY_ex so the key's internal algorithm fetch
    /// uses ctx's provider set. Necessary when the public key is later used
    /// for EVP operations inside an isolated (e.g. FIPS) context.
    pub fn from_pem_in(ctx: &Arc<LibCtx>, pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Load from DER (SubjectPublicKeyInfo).
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;

    /// Load SubjectPublicKeyInfo DER using an explicit library context.
    /// Wraps d2i_PUBKEY_ex so the key's algorithm fetch is bound to ctx's
    /// provider set. Use when the key will be used with EVP operations in
    /// an isolated (e.g. FIPS) context.
    pub fn from_der_in(ctx: &Arc<LibCtx>, der: &[u8]) -> Result<Self, ErrorStack>;

    /// Serialize to PEM.
    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack>;
}
}

Both from_der variants take &[u8] (not a cursor). The full slice is consumed.

Key Import and Export via OSSL_PARAM

These methods load or dump raw key material through OSSL_PARAM arrays. They are the basis for interoperability with external key stores, HSMs, and protocol implementations that work with raw key components (e.g. RSA modulus/exponent, EC coordinates, raw Ed25519 seed bytes).

Import

#![allow(unused)]
fn main() {
impl Pkey<Private> {
    /// Import a private key pair from an OSSL_PARAM array.
    ///
    /// Wraps EVP_PKEY_fromdata with KEYPAIR selection.
    /// Pass ctx = None to use the global default library context.
    pub fn from_params(
        ctx: Option<&Arc<LibCtx>>,
        pkey_type: &CStr,
        params: &Params<'_>,
    ) -> Result<Self, ErrorStack>;
}

impl Pkey<Public> {
    /// Import a public key from an OSSL_PARAM array.
    ///
    /// Wraps EVP_PKEY_fromdata with PUBLIC_KEY selection.
    pub fn from_params(
        ctx: Option<&Arc<LibCtx>>,
        pkey_type: &CStr,
        params: &Params<'_>,
    ) -> Result<Self, ErrorStack>;
}
}

Export

#![allow(unused)]
fn main() {
impl Pkey<Private> {
    /// Export all key parameters (private + public) as an owned OSSL_PARAM array.
    ///
    /// Wraps EVP_PKEY_todata with KEYPAIR selection.
    /// The returned Params<'static> is owned by the caller.
    pub fn export(&self) -> Result<Params<'static>, ErrorStack>;
}

impl Pkey<Public> {
    /// Export the public key parameters as an owned OSSL_PARAM array.
    ///
    /// Wraps EVP_PKEY_todata with PUBLIC_KEY selection.
    pub fn export(&self) -> Result<Params<'static>, ErrorStack>;
}
}

Targeted Parameter Query

#![allow(unused)]
fn main() {
impl<T: HasParams> Pkey<T> {
    /// Fill values into a pre-prepared mutable query array.
    ///
    /// Wraps EVP_PKEY_get_params. The array must already contain the parameter
    /// names of interest (built via ParamBuilder); OpenSSL writes the values
    /// in place.
    pub fn get_params(&self, params: &mut Params<'_>) -> Result<(), ErrorStack>;
}
}

get_params is more efficient than export when only a subset of parameters is needed, because OpenSSL only copies the requested fields.

Key Generation

#![allow(unused)]
fn main() {
pub struct KeygenCtx { /* EVP_PKEY_CTX in keygen mode */ }

impl KeygenCtx {
    /// Create a keygen context for the named algorithm.
    /// Calls EVP_PKEY_CTX_new_from_name + EVP_PKEY_keygen_init.
    ///
    /// Common names: c"RSA", c"EC", c"ED25519", c"X25519",
    ///               c"ML-KEM-768", c"ML-DSA-65".
    pub fn new(name: &CStr) -> Result<Self, ErrorStack>;

    /// Set keygen parameters (e.g. RSA bit length, EC curve name).
    /// Wraps EVP_PKEY_CTX_set_params.
    pub fn set_params(&mut self, params: &Params<'_>) -> Result<(), ErrorStack>;

    /// Retrieve parameter values from this keygen context.
    ///
    /// Build a Params with placeholder values for the keys you want,
    /// call this method, then read back with params.get_*.
    ///
    /// Useful for reading algorithm-negotiated parameters after keygen
    /// initialisation (e.g. security strength, EC group name, RSA modulus size).
    /// Wraps EVP_PKEY_CTX_get_params.
    pub fn get_params(&self, params: &mut Params<'_>) -> Result<(), ErrorStack>;

    /// Return the security strength of the key operation in bits.
    ///
    /// Available after keygen initialisation; reads OSSL_PKEY_PARAM_SECURITY_BITS
    /// ("security-bits"). Convenience wrapper around get_params.
    pub fn security_bits(&self) -> Result<u32, ErrorStack>;

    /// Generate the key pair.
    pub fn generate(&mut self) -> Result<Pkey<Private>, ErrorStack>;
}
}

There are no convenience methods like generate_rsa(). Use KeygenCtx::new directly.

Signing

#![allow(unused)]
fn main() {
/// Parameters for creating a Signer or Verifier.
#[derive(Default)]
pub struct SignInit<'a> {
    /// Digest algorithm, or None for EdDSA (Ed25519, Ed448).
    pub digest: Option<&'a DigestAlg>,
    /// Optional parameters (e.g. RSA PSS salt length).
    pub params: Option<&'a Params<'a>>,
}
}

Signer

#![allow(unused)]
fn main() {
pub struct Signer { ctx: DigestCtx, _key: Pkey<Private> }

impl Signer {
    /// Create a signing context. The key is cloned internally (EVP_PKEY_up_ref).
    pub fn new(key: &Pkey<Private>, init: &SignInit<'_>) -> Result<Self, ErrorStack>;

    /// Feed data. May be called multiple times.
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack>;

    /// Finalise: returns the signature as Vec<u8>.
    /// Queries size then writes — always allocates.
    pub fn finish(&mut self) -> Result<Vec<u8>, ErrorStack>;

    /// One-shot sign over `data`. Required for Ed25519 / Ed448.
    pub fn sign_oneshot(&mut self, data: &[u8]) -> Result<Vec<u8>, ErrorStack>;
}
}

Verifier

#![allow(unused)]
fn main() {
pub struct Verifier { ctx: DigestCtx, _key: Pkey<Public> }

impl Verifier {
    pub fn new(key: &Pkey<Public>, init: &SignInit<'_>) -> Result<Self, ErrorStack>;
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack>;

    /// Returns Ok(true) if valid, Ok(false) if signature is wrong,
    /// Err on a fatal OpenSSL error.
    pub fn verify(&mut self, signature: &[u8]) -> Result<bool, ErrorStack>;

    /// One-shot verify. Required for Ed25519 / Ed448.
    pub fn verify_oneshot(&mut self, data: &[u8], sig: &[u8]) -> Result<bool, ErrorStack>;
}
}

Raw (No-Digest) Signing — RawSigner / RawVerifier

RawSigner and RawVerifier wrap EVP_PKEY_CTX after EVP_PKEY_sign_init / EVP_PKEY_verify_init. They operate directly on pre-hashed data — the caller is responsible for hashing the message before calling sign or verify.

Use these types for:

  • ECDSA over a pre-computed hash (e.g. SHA-256 digest passed as the TBS input)
  • Raw RSA with explicit padding configuration

For algorithms that hash internally (Ed25519, RSA-PSS with automatic hashing), use Signer / Verifier or MessageSigner / MessageVerifier instead.

RawSigner

#![allow(unused)]
fn main() {
pub struct RawSigner { /* EVP_PKEY_CTX after sign_init */ }

impl RawSigner {
    /// Create and initialise a sign context.
    ///
    /// Pass libctx = Some(ctx) to restrict provider lookup to that library
    /// context; None uses the key's own library context.
    pub fn new(
        key: &Pkey<Private>,
        libctx: Option<&Arc<LibCtx>>,
    ) -> Result<Self, ErrorStack>;

    /// Apply parameters after init (e.g. RSA padding mode, PSS salt length).
    pub fn set_params(&mut self, params: &Params<'_>) -> Result<(), ErrorStack>;

    /// Query the signature output size for the given input length.
    /// Null-probe via EVP_PKEY_sign — does not consume the signing state.
    pub fn sign_len(&mut self, tbs_len: usize) -> Result<usize, ErrorStack>;

    /// Sign pre-hashed data into sig. Returns the number of bytes written.
    /// sig.len() must be >= sign_len(tbs.len()).
    pub fn sign(&mut self, tbs: &[u8], sig: &mut [u8]) -> Result<usize, ErrorStack>;

    /// Convenience: sign_len + sign with an allocated output Vec.
    pub fn sign_alloc(&mut self, tbs: &[u8]) -> Result<Vec<u8>, ErrorStack>;
}
}

The context is reusable across multiple sign calls — the padding parameters set via set_params remain in effect until the RawSigner is dropped.

RawVerifier

#![allow(unused)]
fn main() {
pub struct RawVerifier { /* EVP_PKEY_CTX after verify_init */ }

impl RawVerifier {
    /// Create and initialise a verify context.
    ///
    /// Accepts any key with public material (Pkey<Public> or Pkey<Private>).
    pub fn new<T: HasPublic>(
        key: &Pkey<T>,
        libctx: Option<&Arc<LibCtx>>,
    ) -> Result<Self, ErrorStack>;

    /// Apply parameters after init (e.g. RSA padding mode).
    pub fn set_params(&mut self, params: &Params<'_>) -> Result<(), ErrorStack>;

    /// Verify sig against pre-hashed tbs. Returns Ok(()) if valid.
    /// Returns Err if the signature is invalid or on any OpenSSL error.
    pub fn verify(&mut self, tbs: &[u8], sig: &[u8]) -> Result<(), ErrorStack>;
}
}

Note that RawVerifier::verify returns Result<()> — unlike Verifier::verify which returns Result<bool>, an invalid raw signature is an error rather than Ok(false). This reflects the EVP_PKEY_verify API convention.

SigAlg — Algorithm Descriptor

SigAlg is an EVP_SIGNATURE algorithm descriptor. It mirrors DigestAlg, CipherAlg, and MacAlg in naming and lifecycle. SigAlg is needed only when constructing MessageSigner or MessageVerifier; the older Signer/Verifier types use DigestAlg instead.

#![allow(unused)]
fn main() {
pub struct SigAlg { /* EVP_SIGNATURE* */ }

impl SigAlg {
    /// Fetch from the default library context.
    ///
    /// Example names: c"ML-DSA-44", c"ED25519", c"SLH-DSA-SHA2-128s".
    pub fn fetch(name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack>;

    /// Fetch within a specific library context.
    pub fn fetch_in(ctx: &Arc<LibCtx>, name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack>;
}
}

SigAlg is Clone (via EVP_SIGNATURE_up_ref) and Send + Sync. Drop calls EVP_SIGNATURE_free. Cloning is cheap — it increments a reference count inside OpenSSL.

Message Signing — MessageSigner / MessageVerifier

MessageSigner and MessageVerifier wrap EVP_PKEY_sign_message_* / EVP_PKEY_verify_message_* (OpenSSL 3.2+, present at the 3.5 minimum floor).

These types differ from Signer/Verifier in two ways:

  1. The algorithm is specified as a SigAlg rather than a DigestAlg. This enables post-quantum and other algorithms that do not separate hashing from signing at the API level.
  2. finish and sign_oneshot are consuming (self by value) — the context cannot be reused after finalisation.

MessageSigner

#![allow(unused)]
fn main() {
pub struct MessageSigner { /* EVP_PKEY_CTX after sign_message_init */ }

impl MessageSigner {
    /// Create and initialise a message-sign context.
    ///
    /// alg is consumed by EVP_PKEY_sign_message_init; pass alg.clone() if
    /// you need to keep the SigAlg alive.
    /// params sets algorithm-specific options (e.g. context string for Ed25519).
    pub fn new(
        key: &Pkey<Private>,
        alg: &mut SigAlg,
        params: Option<&Params<'_>>,
    ) -> Result<Self, ErrorStack>;

    /// Probe whether this algorithm backend supports incremental update calls.
    ///
    /// Uses ERR_set_mark / ERR_pop_to_mark so the error queue is clean
    /// regardless of the outcome.
    pub fn supports_streaming(&mut self) -> bool;

    /// Feed data into the signing operation.
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack>;

    /// Query the signature output length (null-probe — does not finalise).
    pub fn sig_len(&mut self) -> Result<usize, ErrorStack>;

    /// Finalise and produce the signature into sig. Consuming.
    /// Returns the number of bytes written.
    pub fn finish(self, sig: &mut [u8]) -> Result<usize, ErrorStack>;

    /// One-shot: update + finish in a single call. Consuming.
    pub fn sign_oneshot(self, data: &[u8], sig: &mut [u8]) -> Result<usize, ErrorStack>;
}
}

MessageVerifier

#![allow(unused)]
fn main() {
pub struct MessageVerifier { /* EVP_PKEY_CTX after verify_message_init */ }

impl MessageVerifier {
    /// Create and initialise a message-verify context.
    ///
    /// Accepts any key with public material.
    pub fn new<T: HasPublic>(
        key: &Pkey<T>,
        alg: &mut SigAlg,
        params: Option<&Params<'_>>,
    ) -> Result<Self, ErrorStack>;

    /// Apply parameters after init.
    pub fn set_params(&mut self, params: &Params<'_>) -> Result<(), ErrorStack>;

    /// Supply the signature to verify against. Required before streaming finish.
    /// Not needed for verify_oneshot.
    pub fn set_signature(&mut self, sig: &[u8]) -> Result<(), ErrorStack>;

    /// Probe whether this algorithm backend supports incremental update calls.
    pub fn supports_streaming(&mut self) -> bool;

    /// Feed data into the verification operation.
    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack>;

    /// Finalise and verify. Signature must be set via set_signature. Consuming.
    /// Returns Ok(()) if the signature is valid.
    pub fn finish(self) -> Result<(), ErrorStack>;

    /// One-shot: set signature, feed data, finalise. Consuming.
    pub fn verify_oneshot(self, data: &[u8], sig: &[u8]) -> Result<(), ErrorStack>;
}
}

ML-DSA Provider Caveat

OpenSSL 3.5’s built-in ML-DSA provider implements OSSL_FUNC_SIGNATURE_SIGN_MESSAGE (the one-shot EVP_DigestSign path) but not OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_FINAL. As a result:

  • MessageSigner::supports_streaming() returns false for ML-DSA with the default provider.
  • MessageSigner::finish() will fail for ML-DSA with the default provider.
  • ML-DSA signing with the default provider must use Signer with SignInit { digest: None, .. } (the EVP_DigestSign one-shot path).

MessageSigner / MessageVerifier work correctly for algorithms whose provider implements the full streaming path (e.g. Ed25519 with context strings in a provider that supports it).

Key Agreement (ECDH, X25519)

#![allow(unused)]
fn main() {
pub struct DeriveCtx { /* EVP_PKEY_CTX in derive mode */ }

impl DeriveCtx {
    /// Create from a private key. Calls EVP_PKEY_derive_init.
    pub fn new(key: &Pkey<Private>) -> Result<Self, ErrorStack>;

    /// Set the peer's public key.
    pub fn set_peer(&mut self, peer: &Pkey<Public>) -> Result<(), ErrorStack>;

    /// Query the shared secret length.
    pub fn derive_len(&mut self) -> Result<usize, ErrorStack>;

    /// Derive into caller-provided buffer.
    pub fn derive(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack>;
}
}

RSA Encryption

#![allow(unused)]
fn main() {
pub struct PkeyEncryptCtx { /* EVP_PKEY_CTX in encrypt mode */ }

impl PkeyEncryptCtx {
    /// Create from a public key. params sets padding mode (e.g. OAEP).
    pub fn new(key: &Pkey<Public>, params: Option<&Params<'_>>) -> Result<Self, ErrorStack>;

    /// Query ciphertext length for a given plaintext length.
    pub fn encrypt_len(&mut self, plaintext_len: usize) -> Result<usize, ErrorStack>;

    /// Encrypt plaintext into ciphertext. Returns bytes written.
    pub fn encrypt(&mut self, plaintext: &[u8], ciphertext: &mut [u8])
        -> Result<usize, ErrorStack>;
}

pub struct PkeyDecryptCtx { /* EVP_PKEY_CTX in decrypt mode */ }

impl PkeyDecryptCtx {
    pub fn new(key: &Pkey<Private>, params: Option<&Params<'_>>) -> Result<Self, ErrorStack>;
    pub fn decrypt(&mut self, ciphertext: &[u8], plaintext: &mut [u8])
        -> Result<usize, ErrorStack>;
}
}

ML-KEM Key Encapsulation

#![allow(unused)]
fn main() {
pub struct EncapResult {
    pub wrapped_key:   Vec<u8>,   // the encapsulated key (ciphertext)
    pub shared_secret: Vec<u8>,   // the shared secret
}

pub struct EncapCtx { /* EVP_PKEY_CTX in encapsulate mode */ }

impl EncapCtx {
    pub fn new(key: &Pkey<Public>) -> Result<Self, ErrorStack>;

    /// Encapsulate: generates a fresh shared secret and its encapsulation.
    /// Both outputs are returned as owned Vecs.
    pub fn encapsulate(&mut self) -> Result<EncapResult, ErrorStack>;
}

pub struct DecapCtx { /* EVP_PKEY_CTX in decapsulate mode */ }

impl DecapCtx {
    pub fn new(key: &Pkey<Private>) -> Result<Self, ErrorStack>;

    /// Recover the shared secret from a wrapped key.
    pub fn decapsulate(&mut self, wrapped_key: &[u8]) -> Result<Vec<u8>, ErrorStack>;
}
}

Examples

Generate RSA-4096 Key Pair

#![allow(unused)]
fn main() {
use native_ossl::pkey::{KeygenCtx, Pkey, Private};
use native_ossl::params::ParamBuilder;

let mut ctx = KeygenCtx::new(c"RSA")?;

let params = ParamBuilder::new()?
    .push_uint(c"bits", 4096)?
    .build()?;
ctx.set_params(&params)?;

let key: Pkey<Private> = ctx.generate()?;
assert!(key.is_a(c"RSA"));
assert_eq!(key.bits(), 4096);
}

Generate Ed25519 Key and Sign

#![allow(unused)]
fn main() {
use native_ossl::pkey::{KeygenCtx, Signer, Verifier, SignInit};

let key = KeygenCtx::new(c"ED25519")?.generate()?;
let pub_key = Pkey::<native_ossl::pkey::Public>::from_pem(&key.to_pem()?)?;

let init = SignInit::default();  // digest = None for Ed25519

let mut signer = Signer::new(&key, &init)?;
let sig = signer.sign_oneshot(b"message to sign")?;

let mut verifier = Verifier::new(&pub_key, &init)?;
assert!(verifier.verify_oneshot(b"message to sign", &sig)?);
}

ECDSA Sign with SHA-256

#![allow(unused)]
fn main() {
use native_ossl::pkey::{KeygenCtx, Signer, Verifier, SignInit};
use native_ossl::digest::DigestAlg;
use native_ossl::params::ParamBuilder;

// Generate P-256 key
let mut ctx = KeygenCtx::new(c"EC")?;
let curve_params = ParamBuilder::new()?
    .push_utf8_string(c"group", c"P-256")?
    .build()?;
ctx.set_params(&curve_params)?;
let key = ctx.generate()?;

let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let init = SignInit { digest: Some(&sha256), params: None };

let mut signer = Signer::new(&key, &init)?;
signer.update(b"hello ")?;
signer.update(b"world")?;
let sig = signer.finish()?;
}

Pre-Hashed ECDSA with RawSigner

When the message digest is computed separately (e.g. already SHA-256 hashed), use RawSigner to sign the hash directly without re-hashing:

#![allow(unused)]
fn main() {
use native_ossl::pkey::{KeygenCtx, RawSigner, RawVerifier};
use native_ossl::params::ParamBuilder;

// Generate P-256 key
let mut ctx = KeygenCtx::new(c"EC")?;
let curve_params = ParamBuilder::new()?
    .push_utf8_string(c"group", c"P-256")?
    .build()?;
ctx.set_params(&curve_params)?;
let priv_key = ctx.generate()?;
let pub_key = native_ossl::pkey::Pkey::<native_ossl::pkey::Public>::from(priv_key.clone());

// The caller provides the pre-computed SHA-256 hash (32 bytes).
let tbs: [u8; 32] = compute_sha256(b"my message");

// Sign the hash. None = use the key's own library context.
let mut signer = RawSigner::new(&priv_key, None)?;
let sig = signer.sign_alloc(&tbs)?;

// Verify the signature against the same hash.
let mut verifier = RawVerifier::new(&pub_key, None)?;
verifier.verify(&tbs, &sig)?;  // Ok(()) means valid
}

To configure RSA PSS padding after construction, use set_params:

#![allow(unused)]
fn main() {
use native_ossl::pkey::{KeygenCtx, RawSigner};
use native_ossl::params::ParamBuilder;

let key = KeygenCtx::new(c"RSA")?.generate()?;
let mut signer = RawSigner::new(&key, None)?;

let pss_params = ParamBuilder::new()?
    .push_utf8_ptr(c"pad-mode", c"pss")?
    .push_int(c"saltlen", 32)?
    .build()?;
signer.set_params(&pss_params)?;

let mut sig_buf = vec![0u8; signer.sign_len(32)?];
let n = signer.sign(&my_hash, &mut sig_buf)?;
sig_buf.truncate(n);
}

MessageSigner / MessageVerifier with Streaming Probe

#![allow(unused)]
fn main() {
use native_ossl::pkey::{KeygenCtx, SigAlg, MessageSigner, MessageVerifier};

let key = KeygenCtx::new(c"ED25519")?.generate()?;
let pub_key = native_ossl::pkey::Pkey::<native_ossl::pkey::Public>::from(key.clone());

let mut alg = SigAlg::fetch(c"ED25519", None)?;

// Construction takes alg by &mut — pass alg.clone() if you need to reuse it.
let mut signer = MessageSigner::new(&key, &mut alg, None)?;

if signer.supports_streaming() {
    // Algorithm supports incremental update calls.
    signer.update(b"chunk one")?;
    signer.update(b"chunk two")?;
    let siglen = signer.sig_len()?;  // query before allocating
    let mut sig = vec![0u8; siglen];
    let n = signer.finish(sig.as_mut_slice())?;   // consuming
    sig.truncate(n);

    // Verify using the streaming path.
    let mut alg2 = SigAlg::fetch(c"ED25519", None)?;
    let mut verifier = MessageVerifier::new(&pub_key, &mut alg2, None)?;
    verifier.set_signature(&sig)?;
    verifier.update(b"chunk one")?;
    verifier.update(b"chunk two")?;
    verifier.finish()?;  // Ok(()) means valid; consuming
} else {
    // Algorithm only supports one-shot operation (e.g. ML-DSA with default provider).
    let siglen = signer.sig_len()?;
    let mut sig = vec![0u8; siglen];
    let data = b"full message";
    let n = signer.sign_oneshot(data, &mut sig)?;  // consuming
    sig.truncate(n);

    let mut alg2 = SigAlg::fetch(c"ED25519", None)?;
    let verifier = MessageVerifier::new(&pub_key, &mut alg2, None)?;
    verifier.verify_oneshot(data, &sig)?;  // consuming
}
}

RSA-OAEP Encrypt / Decrypt

#![allow(unused)]
fn main() {
use native_ossl::pkey::{KeygenCtx, PkeyEncryptCtx, PkeyDecryptCtx};
use native_ossl::params::ParamBuilder;

let key = KeygenCtx::new(c"RSA")?.generate()?;

let oaep = ParamBuilder::new()?
    .push_utf8_ptr(c"pad-mode",    c"oaep")?
    .push_utf8_ptr(c"oaep-digest", c"SHA-256")?
    .build()?;

let pub_pem = key.to_pem()?;  // in practice load from file
let pub_key = native_ossl::pkey::Pkey::<native_ossl::pkey::Public>::from_pem(&pub_pem)?;

let mut enc = PkeyEncryptCtx::new(&pub_key, Some(&oaep))?;
let ct_len = enc.encrypt_len(b"secret".len())?;
let mut ciphertext = vec![0u8; ct_len];
let n = enc.encrypt(b"secret", &mut ciphertext)?;
ciphertext.truncate(n);

let mut dec = PkeyDecryptCtx::new(&key, Some(&oaep))?;
let mut plaintext = vec![0u8; ciphertext.len()];
let m = dec.decrypt(&ciphertext, &mut plaintext)?;
plaintext.truncate(m);
assert_eq!(&plaintext, b"secret");
}

OSSL_PARAM Round-Trip: Generate → Export → Import

Export a generated EC P-256 key pair to raw parameters and re-import it as a fresh Pkey<Private>. The public components of both keys must match.

use native_ossl::pkey::{KeygenCtx, Pkey, Private, Public};
use native_ossl::params::ParamBuilder;

// 1. Generate a P-256 key pair.
let mut ctx = KeygenCtx::new(c"EC")?;
let curve = ParamBuilder::new()?
    .push_utf8_string(c"group", c"P-256")?
    .build()?;
ctx.set_params(&curve)?;
let original: Pkey<Private> = ctx.generate()?;

// 2. Export all key material (private + public) to a Params array.
let exported = original.export()?;

// 3. Inspect individual parameters.
//    get_bn allocates a Vec<u8> of big-endian bytes.
if exported.has_param(c"priv") {
    let priv_bytes = exported.get_bn(c"priv")?;
    println!("private scalar: {} bytes", priv_bytes.len());
}

// 4. Re-import as a new private key.
let recovered = Pkey::<Private>::from_params(None, c"EC", &exported)?;
assert!(recovered.is_a(c"EC"));

// 5. Verify that the public components match.
let pub_original  = Pkey::<Public>::from(original.clone());
let pub_recovered = Pkey::<Public>::from(recovered);
assert!(pub_original.public_eq(&pub_recovered));

Targeted Parameter Query with get_params

Fetch only the RSA public exponent without exporting the full key:

use native_ossl::params::ParamBuilder;

// Build a query array with a placeholder for the "e" parameter.
// The zero-length octet slice signals to OpenSSL that this is a query slot.
let mut query = ParamBuilder::new()?
    .push_octet_slice(c"e", &[])?
    .build()?;

key.get_params(&mut query)?;

// get_bn allocates a Vec<u8> of the BIGNUM value.
let e_bytes = query.get_bn(c"e")?;
println!("public exponent: {:?}", e_bytes);

Load a Public Key in a FIPS Library Context

use native_ossl::lib_ctx::LibCtx;
use native_ossl::pkey::{Pkey, Public};
use std::sync::Arc;

// Load the FIPS provider into an isolated context.
let fips_ctx = Arc::new(LibCtx::new()?);
// fips_ctx.load_provider(...)?; — load your FIPS provider here

let pub_pem = std::fs::read("peer_pubkey.pem")?;
let pub_key = Pkey::<Public>::from_pem_in(&fips_ctx, &pub_pem)?;
// All subsequent operations on pub_key use the FIPS provider.

ML-KEM-768 (Post-Quantum KEM)

#![allow(unused)]
fn main() {
use native_ossl::pkey::{KeygenCtx, EncapCtx, DecapCtx};

// Generate ML-KEM-768 key pair (recipient)
let key = KeygenCtx::new(c"ML-KEM-768")?.generate()?;
let pub_pem = key.to_pem()?;
let pub_key = native_ossl::pkey::Pkey::<native_ossl::pkey::Public>::from_pem(&pub_pem)?;

// Sender encapsulates
let mut enc = EncapCtx::new(&pub_key)?;
let result = enc.encapsulate()?;

// Recipient decapsulates
let mut dec = DecapCtx::new(&key)?;
let shared = dec.decapsulate(&result.wrapped_key)?;

assert_eq!(shared, result.shared_secret);
}

Design Notes

  • max_output_size() returns usize, not Result<usize> — it mirrors the bits() / security_bits() pattern. It returns 0 rather than an error when the key does not support a size query (e.g., for key-agreement-only keys). For RSA the value equals bits() / 8; for Ed25519 it is always 64.
  • No convenience constructors — there are no generate_rsa(), generate_ed25519(), etc. shorthand methods. Use KeygenCtx::new(c"ED25519")?.generate() directly.
  • Signer/Verifier hold an owned clone of the key via _key: Pkey<T>. There is no lifetime parameter — the key is cloned at new() via EVP_PKEY_up_ref.
  • Verifier::verify returns Result<bool>Ok(false) means the signature was well-formed but incorrect; Err means a fatal operational error.
  • RawVerifier::verify returns Result<()>Err covers both invalid signatures and operational errors, matching the EVP_PKEY_verify API convention.
  • from_der uses MemBioBuf — reads directly from the caller’s slice without copying. The whole slice is consumed; there is no cursor-advancement API.
  • from_der_in uses a raw pointer pair, not a BIOd2i_AutoPrivateKey_ex / d2i_PUBKEY_ex take (const unsigned char**, long) directly. Passing the slice pointer lets OpenSSL bind the key’s internal algorithm fetch to the supplied LibCtx, which is necessary for FIPS-isolated operations. The format for Pkey<Private>::from_der_in is auto-detected (PKCS#8 or traditional); use it when you have DER bytes of unknown provenance and need FIPS isolation. Use the non-_in variants when the global provider context is sufficient.
  • to_der_legacy vs. to_pkcs8_derto_pkcs8_der (wraps i2d_PKCS8PrivateKeyInfo) produces a PKCS#8 PrivateKeyInfo / RFC 5958 structure that is algorithm-agnostic and the recommended format for all new code. to_der_legacy (wraps i2d_PrivateKey) produces the algorithm-specific raw structure (e.g. RSAPrivateKey per RFC 3447, ECPrivateKey per RFC 5915). Use to_der_legacy only for interoperability with legacy software that does not support PKCS#8; some algorithms (e.g. post-quantum) may not support the legacy format at all and will return an error.
  • Pkey<Public>::from_pem_in uses PEM_read_bio_PUBKEY_ex — unlike X509::from_pem_in (which ignores its ctx argument because no libctx-aware variant exists in OpenSSL 3.5), Pkey<Public>::from_pem_in genuinely binds the key’s algorithm fetch to the supplied LibCtx. Use it when the public key will be used for operations in an isolated provider context.
  • from_params selectionPkey<Private>::from_params uses KEYPAIR selection (both private and public material required); Pkey<Public>::from_params uses PUBLIC_KEY selection. Passing private-key parameters to Pkey<Public>::from_params is safe — OpenSSL ignores the private fields.
  • export returns Params<'static> — the array is allocated by OpenSSL and owned entirely by the returned Params value. There are no lifetime constraints from the source key; the Pkey may be dropped before the exported Params.
  • get_params vs. export — use get_params when you need only a subset of fields (more efficient); use export when you need a complete dump or plan to pass the Params array to from_params.
  • RawSigner is reusable — padding parameters configured via set_params persist across multiple sign calls on the same instance.
  • MessageSigner/MessageVerifier are consumingfinish and sign_oneshot take self by value; the context is freed at the end of the call.
  • SigAlg taken by &mutEVP_PKEY_sign_message_init modifies the algorithm object internally. Pass alg.clone() if you need the SigAlg to remain usable after constructing a MessageSigner or MessageVerifier.
  • ML-DSA streaming caveat — OpenSSL 3.5’s built-in ML-DSA provider does not implement OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_FINAL. Use Signer for ML-DSA signing with the default provider.
  • KeygenCtx::set_params vs constructor paramsset_params may be called any number of times before generate to layer parameter changes. Each call applies incrementally; earlier settings are not erased unless the same key is overwritten.
  • KeygenCtx::get_params getter pattern — build a Params array with placeholder values (e.g. push_uint(c"security-bits", 0)), call get_params, then read back the filled values with params.get_uint / params.get_utf8_string etc. The placeholder value is overwritten by OpenSSL; only the key name matters for the query. For string parameters, the placeholder must be long enough to receive the returned value.
  • KeygenCtx::get_params vs RawSigner::get_params — keygen contexts expose few gettable parameters (algorithm-specific; most algorithms expose none). Operation contexts (sign/verify) expose more: RSA exposes pad-mode, digest, mgf1-digest, saltlen, and algorithm-id. Use RawSigner::get_params to inspect or verify per-operation settings after EVP_PKEY_sign_init.
  • KeygenCtx::get_params vs Pkey::get_paramsKeygenCtx::get_params reads from the context (available immediately after keygen_init, before a key exists); Pkey::get_params reads from the generated key object (available only after generate). Use the key-object variant for security-bits and other key properties.
  • default_digest_name() returns None for pre-hash algorithms — For Ed25519, ML-DSA, and SLH-DSA the key type performs all hashing internally and there is no separate digest step. default_digest_name() returns None in that case, and callers should pass digest: None in SignInit (or omit the digest argument entirely). For RSA and ECDSA keys it returns a Some(name) (typically "SHA256") which can be fetched via DigestAlg::fetch and passed to Signer::new or DigestSignInit_ex. Check this value before deciding whether to supply a digest to DigestSign — supplying a digest to Ed25519 is an error.

Key Derivation

Overview

Key derivation uses the OpenSSL EVP_KDF API. Five typed builders cover the most common cases; raw KdfAlg + KdfCtx covers everything else.

  • HkdfBuilder — HKDF (RFC 5869)
  • Pbkdf2Builder — PBKDF2 (PKCS #5 / RFC 8018)
  • ScryptBuilder — scrypt (RFC 7914)
  • SshkdfBuilder — SSH key derivation (RFC 4253 §7.2)
  • KbkdfBuilder — KBKDF counter/feedback mode (NIST SP 800-108)
  • KdfAlg + KdfCtx — raw access to any EVP_KDF algorithm

Builders hold borrowed slices with lifetimes — no data is copied until the final derive() call. derive() writes directly into a caller-provided buffer.

Typed Builders

HkdfBuilder<'a>

#![allow(unused)]
fn main() {
pub struct HkdfBuilder<'a> { /* borrowed slices */ }

impl<'a> HkdfBuilder<'a> {
    pub fn new(digest: &'a DigestAlg) -> Self;   // no allocation

    pub fn key(self,  key:  &'a [u8]) -> Self;   // IKM (input key material)
    pub fn salt(self, salt: &'a [u8]) -> Self;   // optional
    pub fn info(self, info: &'a [u8]) -> Self;   // optional context info
    pub fn mode(self, mode: HkdfMode) -> Self;

    /// Derive into caller-provided buffer.
    pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;

    /// Derive `len` bytes into a freshly allocated Vec.
    pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}

HkdfMode

#![allow(unused)]
fn main() {
pub enum HkdfMode {
    ExtractAndExpand,  // RFC 5869 §2 default — extract then expand
    ExtractOnly,       // output is the PRK (pseudorandom key)
    ExpandOnly,        // input must already be a PRK
}
}

Pbkdf2Builder<'a>

#![allow(unused)]
fn main() {
pub struct Pbkdf2Builder<'a> { /* borrowed slices */ }

impl<'a> Pbkdf2Builder<'a> {
    /// Password and salt are required at construction; digest selects the PRF.
    /// Default iteration count: 600 000 (NIST SP 800-132 minimum for SHA-256).
    pub fn new(digest: &'a DigestAlg, password: &'a [u8], salt: &'a [u8]) -> Self;

    pub fn iterations(self, n: u32) -> Self;

    pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
    pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}

ScryptBuilder<'a>

#![allow(unused)]
fn main() {
pub struct ScryptBuilder<'a> { /* borrowed slices */ }

pub struct ScryptParams {
    pub n: u64,  // CPU/memory cost; must be a power of 2
    pub r: u32,  // block size factor
    pub p: u32,  // parallelization factor
}

impl Default for ScryptParams {
    fn default() -> Self {
        ScryptParams { n: 16384, r: 8, p: 1 }  // OWASP-recommended minimums
    }
}

impl<'a> ScryptBuilder<'a> {
    /// Password and salt are required; uses ScryptParams::default().
    pub fn new(password: &'a [u8], salt: &'a [u8]) -> Self;

    pub fn params(self, params: ScryptParams) -> Self;

    pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
    pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}

SshkdfBuilder<'a>

SSH key derivation as specified in RFC 4253 §7.2. Derives the six key components (initial IVs, encryption keys, and integrity keys for each direction) from the shared secret, exchange hash, and session identifier produced by the SSH handshake.

#![allow(unused)]
fn main() {
pub struct SshkdfBuilder<'a> { /* borrowed slices */ }

impl<'a> SshkdfBuilder<'a> {
    /// Create an SSH-KDF builder.
    ///
    /// - digest     — hash algorithm (e.g. SHA-256).
    /// - key        — shared secret K from the DH exchange.
    /// - xcghash    — exchange hash H.
    /// - session_id — session identifier (equals the first H for the session).
    /// - key_type   — which key/IV component to derive (A–F).
    pub fn new(
        digest:     &'a DigestAlg,
        key:        &'a [u8],
        xcghash:    &'a [u8],
        session_id: &'a [u8],
        key_type:   SshkdfKeyType,
    ) -> Self;

    /// Derive into caller-provided buffer.
    pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;

    /// Derive `len` bytes into a freshly allocated Vec.
    pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}

SshkdfKeyType

Each variant corresponds to one of the six letter codes from RFC 4253 §7.2:

#![allow(unused)]
fn main() {
pub enum SshkdfKeyType {
    InitialIvClientToServer,       // "A"
    InitialIvServerToClient,       // "B"
    EncryptionKeyClientToServer,   // "C"
    EncryptionKeyServerToClient,   // "D"
    IntegrityKeyClientToServer,    // "E"
    IntegrityKeyServerToClient,    // "F"
}
}

Derive all six components by calling SshkdfBuilder::new once per variant with the same key, xcghash, and session_id inputs.

KbkdfBuilder<'a>

KBKDF (key-based key derivation function) as specified in NIST SP 800-108. Supports both counter mode and feedback mode with HMAC or CMAC as the PRF.

#![allow(unused)]
fn main() {
pub struct KbkdfBuilder<'a> { /* borrowed slices */ }

impl<'a> KbkdfBuilder<'a> {
    /// Create a KBKDF builder.
    ///
    /// - mode — counter or feedback.
    /// - mac  — MAC algorithm (fetch c"HMAC" or c"CMAC" via MacAlg::fetch).
    /// - key  — the key derivation key (KDK).
    pub fn new(mode: KbkdfMode, mac: &'a MacAlg, key: &'a [u8]) -> Self;

    /// Hash digest for HMAC-based derivation (required when mac is HMAC).
    pub fn digest(self, digest: &'a DigestAlg) -> Self;

    /// Label: identifies the purpose of the derived key.
    pub fn label(self, label: &'a [u8]) -> Self;

    /// Context: caller-specific data bound into the derivation.
    pub fn context(self, context: &'a [u8]) -> Self;

    /// Salt / feedback IV (feedback mode only).
    pub fn salt(self, salt: &'a [u8]) -> Self;

    /// Counter field length (default: Bits32).
    pub fn counter_len(self, len: KbkdfCounterLen) -> Self;

    /// Include the length field L in the PRF input (default: true).
    pub fn use_l(self, enabled: bool) -> Self;

    /// Include the zero-byte separator in the PRF input (default: true).
    pub fn use_separator(self, enabled: bool) -> Self;

    /// Derive into caller-provided buffer.
    pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;

    /// Derive `len` bytes into a freshly allocated Vec.
    pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}

KbkdfMode

#![allow(unused)]
fn main() {
pub enum KbkdfMode {
    /// Counter mode — a monotonically incrementing counter is appended to each
    /// PRF input block. This is the most common SP 800-108 construction.
    Counter,

    /// Feedback mode — the output of each PRF invocation is fed as input to
    /// the next. An initial IV/salt is required for the first block.
    Feedback,
}
}

KbkdfCounterLen

Controls the bit-width of the counter field in counter mode. Defaults to Bits32.

#![allow(unused)]
fn main() {
pub enum KbkdfCounterLen {
    Bits8  = 8,
    Bits16 = 16,
    Bits24 = 24,
    Bits32 = 32,   // default
}
}

Pkcs12KdfBuilder<'a>

PKCS#12 (RFC 7292 Appendix B) legacy KDF. Needed only for interoperability with PKCS#12 files encrypted with deprecated algorithms such as PBEWithSHAAnd3-KeyTripleDES-CBC. New PKCS#12 files should use PBES2/PBKDF2 instead.

#![allow(unused)]
fn main() {
pub enum Pkcs12KdfId {
    Key = 1,  // cipher key bytes
    Iv  = 2,  // cipher IV bytes
    Mac = 3,  // MAC key bytes
}

pub struct Pkcs12KdfBuilder<'a> { /* borrowed slices */ }

impl<'a> Pkcs12KdfBuilder<'a> {
    /// Create a PKCS#12 KDF builder.
    ///
    /// - `md`       — hash algorithm (SHA-1 for legacy 3DES; SHA-256 for PBES2).
    /// - `password` — UTF-8 passphrase bytes.
    /// - `salt`     — random salt (RFC 7292 recommends 8 bytes).
    /// - `id`       — which component to derive: Key, Iv, or Mac.
    ///
    /// Default iteration count: 2048.
    pub fn new(md: &'a DigestAlg, password: &'a [u8], salt: &'a [u8], id: Pkcs12KdfId) -> Self;

    /// Override the iteration count.
    pub fn iterations(self, n: u32) -> Self;

    pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
    pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}

Raw KDF Access

For algorithms without a typed builder (TLS13-KDF, SSKDF, X963KDF, etc.):

#![allow(unused)]
fn main() {
pub struct KdfAlg { /* EVP_KDF* */ }

impl KdfAlg {
    pub fn fetch(name: &CStr) -> Result<Self, ErrorStack>;
}

pub struct KdfCtx { /* EVP_KDF_CTX* */ }

impl KdfCtx {
    pub fn new(alg: &KdfAlg) -> Result<Self, ErrorStack>;

    /// Derive key material. Parameters are supplied at derive time.
    pub fn derive(&mut self, out: &mut [u8], params: &Params<'_>) -> Result<(), ErrorStack>;

    /// Update parameters on this context without destroying and recreating it.
    ///
    /// Wraps `EVP_KDF_CTX_set_params`. Useful for changing HKDF mode (e.g.
    /// extract-only vs. expand-only) or updating salt/key between derivations.
    pub fn set_params(&mut self, params: &Params<'_>) -> Result<(), ErrorStack>;

    /// Retrieve current parameter values from this context.
    ///
    /// Wraps `EVP_KDF_CTX_get_params`. Build a `Params` with placeholder
    /// values for the keys you want, call this, then read back with `params.get_*`.
    pub fn get_params(&self, params: &mut Params<'_>) -> Result<(), ErrorStack>;

    /// Return the output length of this KDF (if fixed), or `usize::MAX` if variable.
    ///
    /// Wraps `EVP_KDF_CTX_get_kdf_size`. HKDF and most stream KDFs return
    /// `usize::MAX`; fixed-output KDFs such as SSKDF with a set output length
    /// return that fixed length.
    pub fn size(&self) -> usize;
}
}

Algorithm Reference

Builder / APIEVP_KDF nameStandard
HkdfBuilderc"HKDF"RFC 5869
Pbkdf2Builderc"PBKDF2"PKCS #5 / RFC 8018
ScryptBuilderc"SCRYPT"RFC 7914
SshkdfBuilderc"SSHKDF"RFC 4253 §7.2
KbkdfBuilderc"KBKDF"NIST SP 800-108
Pkcs12KdfBuilderc"PKCS12KDF"RFC 7292 Appendix B
KdfCtx directc"TLS13-KDF"RFC 8446
KdfCtx directc"SSKDF"NIST SP 800-56Cr2
KdfCtx directc"X963KDF"ANSI X9.63

Examples

HKDF (Expand from existing PRK)

#![allow(unused)]
fn main() {
use native_ossl::kdf::{HkdfBuilder, HkdfMode};
use native_ossl::digest::DigestAlg;

let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;

let ikm  = b"input key material";
let salt = b"random salt";
let info = b"context info";

let mut okm = [0u8; 32];
HkdfBuilder::new(&sha256)
    .key(ikm)
    .salt(salt)
    .info(info)
    .derive(&mut okm)?;
}

PBKDF2 (Password Hashing)

#![allow(unused)]
fn main() {
use native_ossl::kdf::Pbkdf2Builder;
use native_ossl::digest::DigestAlg;

let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;

let mut key = [0u8; 32];
Pbkdf2Builder::new(&sha256, b"my password", b"random salt")
    .iterations(100_000)
    .derive(&mut key)?;
}

scrypt (Memory-Hard Password Hashing)

#![allow(unused)]
fn main() {
use native_ossl::kdf::{ScryptBuilder, ScryptParams};

let mut key = [0u8; 32];
ScryptBuilder::new(b"my password", b"random salt")
    .params(ScryptParams { n: 32768, r: 8, p: 1 })
    .derive(&mut key)?;
}

SSH Key Derivation (RFC 4253)

Derive the client-to-server encryption key and IV after completing an SSH Diffie-Hellman key exchange:

use native_ossl::kdf::{SshkdfBuilder, SshkdfKeyType};
use native_ossl::digest::DigestAlg;

let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;

// shared_secret, exchange_hash, and session_id come from the DH handshake.
let shared_secret = /* K from ECDH / DH group exchange */;
let exchange_hash = /* H = hash of handshake transcript */;
let session_id    = /* first H for this session */;

// Derive the client-to-server IV (16 bytes for AES-128).
let mut iv = [0u8; 16];
SshkdfBuilder::new(
    &sha256,
    &shared_secret,
    &exchange_hash,
    &session_id,
    SshkdfKeyType::InitialIvClientToServer,
).derive(&mut iv)?;

// Derive the client-to-server encryption key (16 bytes for AES-128).
let mut enc_key = [0u8; 16];
SshkdfBuilder::new(
    &sha256,
    &shared_secret,
    &exchange_hash,
    &session_id,
    SshkdfKeyType::EncryptionKeyClientToServer,
).derive(&mut enc_key)?;

// Derive the client-to-server integrity key (32 bytes for HMAC-SHA-256).
let mut mac_key = [0u8; 32];
SshkdfBuilder::new(
    &sha256,
    &shared_secret,
    &exchange_hash,
    &session_id,
    SshkdfKeyType::IntegrityKeyClientToServer,
).derive(&mut mac_key)?;

KBKDF Counter Mode (HMAC-SHA-256 PRF)

use native_ossl::kdf::{KbkdfBuilder, KbkdfMode, KbkdfCounterLen};
use native_ossl::mac::MacAlg;
use native_ossl::digest::DigestAlg;

let hmac = MacAlg::fetch(c"HMAC", None)?;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;

let master_key = b"my 32-byte key derivation key!!!";

let derived = KbkdfBuilder::new(KbkdfMode::Counter, &hmac, master_key)
    .digest(&sha256)
    .label(b"session key")
    .context(b"connection id 42")
    .counter_len(KbkdfCounterLen::Bits32)
    .derive_to_vec(32)?;

KBKDF Feedback Mode (HMAC-SHA-256 PRF)

use native_ossl::kdf::{KbkdfBuilder, KbkdfMode};
use native_ossl::mac::MacAlg;
use native_ossl::digest::DigestAlg;

let hmac = MacAlg::fetch(c"HMAC", None)?;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;

let master_key = b"my 32-byte key derivation key!!!";
let iv = b"initial feedback value (16 bytes)";

let derived = KbkdfBuilder::new(KbkdfMode::Feedback, &hmac, master_key)
    .digest(&sha256)
    .label(b"session key")
    .salt(iv)    // feedback mode requires an IV/salt for the first block
    .derive_to_vec(32)?;

Raw KdfCtx (TLS 1.3 KDF)

#![allow(unused)]
fn main() {
use native_ossl::kdf::{KdfAlg, KdfCtx};
use native_ossl::params::ParamBuilder;

let alg = KdfAlg::fetch(c"TLS13-KDF")?;
let mut ctx = KdfCtx::new(&alg)?;

let params = ParamBuilder::new()?
    .push_utf8_string(c"digest", c"SHA2-256")?
    .push_utf8_string(c"mode",   c"EXTRACT_ONLY")?
    .push_octet_slice(c"key",    b"my ikm")?
    .build()?;

let mut out = [0u8; 32];
ctx.derive(&mut out, &params)?;
}

Design Notes

  • EVP_KDF, not EVP_PKEY_CTX — earlier versions of OpenSSL used EVP_PKEY_CTX in derive mode for KDFs. OpenSSL 3.x provides the dedicated EVP_KDF API, which has a cleaner parameter interface.
  • Re-parameterisation vs. rebuildKdfCtx::set_params wraps EVP_KDF_CTX_set_params, allowing algorithm-specific parameters (e.g. HKDF mode, salt, key) to be updated on an existing context without destroying and recreating it. This avoids the overhead of a KdfAlg::fetch + KdfCtx::new cycle and is the correct approach when performing multiple derivations with the same algorithm but different parameters. The typed builders (HkdfBuilder, etc.) always create a fresh context; use KdfCtx directly when re-parameterisation matters.
  • get_params query patternKdfCtx::get_params follows the OpenSSL query-array pattern: build a Params with placeholder values (e.g. push_uint(c"mode", 0)) for the keys you want, pass it by &mut, and read the filled-in values with params.get_uint(c"mode") etc. Only keys present in the query array are filled; unknown keys are silently skipped by OpenSSL.
  • Builders are consumed by derive — create a new builder for each derivation. This prevents accidental reuse of parameters from a previous call.
  • No context stored in builderKdfAlg and KdfCtx are created inside derive() and freed when it returns. There is no lifetime entanglement between the builder and the EVP context.
  • ScryptParams struct — scrypt has three interdependent cost parameters. A struct prevents passing them in the wrong order.
  • PBKDF2 iteration count — the default of 600 000 follows NIST SP 800-132 (2023 revision) for SHA-256. Override with .iterations() when compatibility with a specific value is required.
  • SshkdfBuilder is one-shot per component — each of the six RFC 4253 key components requires a separate builder instance with the same inputs but a different SshkdfKeyType. There is no loop API; construct each builder explicitly.
  • KbkdfBuilder::context maps to the "data" parameter — OpenSSL’s KBKDF provider uses "data" for the SP 800-108 context field. The method is named .context() to match the specification’s terminology.
  • KbkdfBuilder::digest is required for HMAC — when the MAC algorithm is HMAC, a digest must be set. For CMAC the digest is ignored (the cipher is bound to the MacAlg at fetch time, not at derive time).

Random Numbers

Two Tiers

Wraps OpenSSL’s RAND_bytes / RAND_priv_bytes. Uses the library’s built-in DRBG hierarchy automatically. No setup required.

#![allow(unused)]
fn main() {
pub struct Rand;

impl Rand {
    /// Fill `buf` with cryptographically random bytes (public randomness).
    /// Zero-copy: writes directly into caller's buffer.
    pub fn fill(buf: &mut [u8]) -> Result<(), ErrorStack>;

    /// Fill `buf` with private randomness (suitable for key material).
    /// In FIPS mode this uses a separate private DRBG.
    pub fn fill_private(buf: &mut [u8]) -> Result<(), ErrorStack>;

    /// Allocate and fill `n` random bytes.
    pub fn bytes(n: usize) -> Result<Vec<u8>, ErrorStack>;

    /// Allocate and fill `n` private random bytes.
    /// Uses RAND_priv_bytes — suitable for key material and other secret values.
    pub fn bytes_private(n: usize) -> Result<Vec<u8>, ErrorStack>;
}
}

Explicit DRBG — RandAlg + RandCtx

For FIPS compliance, DRBG hierarchy management, or prediction-resistant generation.

#![allow(unused)]
fn main() {
pub struct RandAlg { /* EVP_RAND* */ }

impl RandAlg {
    pub fn fetch(name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack>;

    /// Fetch within an explicit library context.
    ///
    /// Use this when the DRBG must be bound to a specific provider set
    /// (e.g. a FIPS-isolated LibCtx).
    pub fn fetch_in(ctx: &Arc<LibCtx>, name: &CStr, props: Option<&CStr>)
        -> Result<Self, ErrorStack>;

    /// Security strength of the algorithm descriptor in bits.
    ///
    /// Calls `EVP_RAND_get_params` with the `"strength"` key.
    /// Useful for verifying that a fetched algorithm meets a minimum strength
    /// requirement before creating a `RandCtx` from it.
    pub fn params_strength(&self) -> Result<u32, ErrorStack>;
}

pub struct RandCtx { /* EVP_RAND_CTX* */ }

impl RandCtx {
    /// Create with optional parent DRBG (for seeding hierarchy).
    pub fn new(alg: &RandAlg, parent: Option<&RandCtx>) -> Result<Self, ErrorStack>;

    /// Instantiate (seed) the DRBG.
    ///
    /// `strength` is the requested security strength in bits.
    /// Set `prediction_resistance` to true to reseed from entropy before instantiation.
    pub fn instantiate(
        &mut self,
        strength: u32,
        prediction_resistance: bool,
        params: Option<&Params<'_>>,
    ) -> Result<(), ErrorStack>;

    /// Generate with full control over strength and prediction resistance.
    pub fn generate(&mut self, out: &mut [u8], req: &GenerateRequest<'_>)
        -> Result<(), ErrorStack>;

    /// Simple fill with default parameters (strength=256, no prediction resistance).
    ///
    /// Equivalent to calling generate with GenerateRequest::default.
    pub fn fill(&mut self, out: &mut [u8]) -> Result<(), ErrorStack>;

    pub fn strength(&self) -> u32;

    /// Current lifecycle state of this DRBG instance.
    pub fn state(&self) -> RandState;

    /// Set DRBG parameters after instantiation (e.g. reseed interval,
    /// additional input length). Write counterpart to the read-side
    /// `RandAlg::params_strength` / `EVP_RAND_get_params` pattern.
    ///
    /// Common keys: `"reseed_requests"` (u32), `"reseed_time_interval"` (u32).
    pub fn set_params(&mut self, params: &Params<'_>) -> Result<(), ErrorStack>;

    // ── Global DRBG accessors (OpenSSL 3.2+) ──────────────────────────────────

    /// Borrow the process-wide public DRBG. Does not free the pointer on drop.
    #[cfg(ossl320)]
    pub fn public() -> Result<GlobalRandCtx, ErrorStack>;

    /// Borrow the process-wide private DRBG.
    #[cfg(ossl320)]
    pub fn private_global() -> Result<GlobalRandCtx, ErrorStack>;

    /// Borrow the process-wide primary (root) DRBG.
    /// The primary DRBG seeds both the public and private DRBGs.
    #[cfg(ossl320)]
    pub fn primary() -> Result<GlobalRandCtx, ErrorStack>;
}

    #[cfg(ossl310)]
    impl Clone for RandCtx { /* EVP_RAND_CTX_up_ref */ }
}

RandCtx is Clone (via EVP_RAND_CTX_up_ref, OpenSSL 3.1.0+) but generate takes &mut self because the DRBG state changes on each call. Use a Mutex<RandCtx> for shared access.

GenerateRequest

Wraps the EVP_RAND_generate parameters that go beyond the output buffer:

#![allow(unused)]
fn main() {
pub struct GenerateRequest<'a> {
    /// Minimum security strength in bits. Default: 256.
    pub strength: u32,
    /// If true, reseed from entropy source before generating.
    /// Has a performance cost; use only when required by policy.
    pub prediction_resistance: bool,
    /// Optional additional input to mix into the generation step.
    pub additional_input: Option<&'a [u8]>,
}

impl Default for GenerateRequest<'_> {
    fn default() -> Self {
        Self { strength: 256, prediction_resistance: false, additional_input: None }
    }
}
}

RandState — DRBG Lifecycle State

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RandState {
    /// Context created but not yet instantiated (seeded).
    Uninitialised,
    /// Context is seeded and ready to generate.
    Ready,
    /// Context entered an unrecoverable error state.
    Error,
    /// Unrecognised value returned by OpenSSL (forward-compat guard).
    Unknown(i32),
}
}

RandCtx::state returns RandState::Ready after a successful instantiate. Check the state before calling generate if the DRBG may have entered an error state (e.g. entropy source failure).

DRBG Algorithm Names

NameDescription
c"CTR-DRBG"NIST SP 800-90A CTR_DRBG (AES-256) — default
c"HASH-DRBG"NIST SP 800-90A Hash_DRBG (SHA-512)
c"HMAC-DRBG"NIST SP 800-90A HMAC_DRBG (SHA-256)
c"SEED-SRC"Entropy source (seeder for parent DRBG)

Examples

Simple Random Bytes

#![allow(unused)]
fn main() {
use native_ossl::rand::Rand;

// Fill a key buffer in place.
let mut key = [0u8; 32];
Rand::fill(&mut key)?;

// Allocate a random nonce.
let nonce = Rand::bytes(12)?;

// Private randomness for key material (uses RAND_priv_bytes).
let mut session_key = [0u8; 32];
Rand::fill_private(&mut session_key)?;

// Allocating variant for private randomness.
let key_bytes = Rand::bytes_private(32)?;
}

Explicit DRBG

use native_ossl::rand::{RandAlg, RandCtx, GenerateRequest};
use native_ossl::params::ParamBuilder;

let alg = RandAlg::fetch(c"CTR-DRBG", None)?;
let mut ctx = RandCtx::new(&alg, None)?;

// CTR-DRBG defaults to AES-256-CTR (256-bit strength). Many system entropy
// sources cap at 128 bits; use AES-128-CTR if instantiation fails.
let params = ParamBuilder::new()?
    .push_utf8_string(c"cipher", c"AES-128-CTR")?
    .build()?;
ctx.instantiate(128, false, Some(&params))?;

let mut buf = [0u8; 32];
ctx.fill(&mut buf)?;

Prediction-Resistant Generation

use native_ossl::rand::{RandAlg, RandCtx, GenerateRequest};

let alg = RandAlg::fetch(c"CTR-DRBG", None)?;
let mut ctx = RandCtx::new(&alg, None)?;
ctx.instantiate(128, false, None)?;

let mut buf = [0u8; 32];
let req = GenerateRequest {
    strength: 128,
    prediction_resistance: true,
    additional_input: Some(b"personalization string"),
};
ctx.generate(&mut buf, &req)?;

DRBG Hierarchy

Chain a private DRBG off the global public DRBG to avoid the SEED-SRC entropy strength limit:

use native_ossl::rand::{RandAlg, RandCtx};

// Use the global public DRBG as parent (does not free it on drop).
let parent = RandCtx::public()?;

let drbg_alg = RandAlg::fetch(c"CTR-DRBG", None)?;
let mut child = RandCtx::new(&drbg_alg, Some(&parent))?;
child.instantiate(256, false, None)?;

let mut buf = [0u8; 64];
child.fill(&mut buf)?;

Fetch within a FIPS Library Context

use native_ossl::rand::{RandAlg, RandCtx};
use std::sync::Arc;

let fips_ctx = Arc::new(native_ossl::lib_ctx::LibCtx::new()?);
// load FIPS provider into fips_ctx ...

let alg = RandAlg::fetch_in(&fips_ctx, c"CTR-DRBG", Some(c"fips=yes"))?;
let mut ctx = RandCtx::new(&alg, None)?;
ctx.instantiate(256, false, None)?;

let mut buf = [0u8; 32];
ctx.fill(&mut buf)?;

Query DRBG State

use native_ossl::rand::{RandAlg, RandCtx, RandState};

let alg = RandAlg::fetch(c"CTR-DRBG", None)?;
let mut ctx = RandCtx::new(&alg, None)?;
assert_eq!(ctx.state(), RandState::Uninitialised);

ctx.instantiate(128, false, None)?;
assert_eq!(ctx.state(), RandState::Ready);

Design Notes

  • RandCtx::primary() vs public() / private_global() — the primary DRBG is the root of the OpenSSL global DRBG hierarchy. Public and private DRBGs are re-seeded from it. Use primary() to inspect the root state or chain a custom DRBG off the hierarchy root. For generating random bytes, prefer public() or Rand::fill.
  • Rand::fill vs RandCtx — for most applications, Rand::fill and Rand::fill_private are sufficient. The explicit RandCtx API is for FIPS-validated use cases where the caller must manage the DRBG instance directly.
  • Rand::fill_private / bytes_private — in FIPS mode, public and private DRBGs are seeded independently. Use these for key material and other secret values.
  • generate takes &mut self — even though RandCtx is Clone (via up_ref), generate modifies DRBG internal state. Concurrent access without a mutex is a data race; &mut self prevents it.
  • instantiate strength parameter — the strength argument is the requested minimum security strength in bits. The DRBG may provide more than requested. For CTR-DRBG with AES-128-CTR, the maximum is 128 bits; with AES-256-CTR it is 256. When seeding from SEED-SRC directly (no parent DRBG), many OS entropy sources cap at 128 bits; in that case, configure AES-128-CTR via params and pass strength=128.
  • RandState — check ctx.state() if you suspect the DRBG may have failed (e.g. after an entropy source error). RandState::Error is unrecoverable; create a new RandCtx if this occurs.
  • Output size limitRAND_bytes accepts only int length (≤ ~2 GiB). The wrapper handles oversized requests by splitting into multiple calls internally.

X.509 Certificates

Overview

X509 wraps OpenSSL’s X509* structure, which is reference-counted. It can be cloned cheaply (via X509_up_ref) and shared across threads.

This module also provides:

  • X509Store — a trust store for chain verification
  • X509StoreCtx — the verification context that consumes store + certificate
  • X509Crl — a certificate revocation list (CRL)

For PKCS#12 bundles (combining key, certificate, and CA chain), see the PKCS#12 guide. For OCSP response handling, see the OCSP guide.

X509 — Certificate Wrapper

#![allow(unused)]
fn main() {
pub struct X509 { /* X509* */ }

impl X509 {
    // ── Construction ──────────────────────────────────────────────────────────

    /// Create a new, empty X509 bound to an explicit library context.
    /// Use when FIPS-awareness is required. propq is always NULL (default properties).
    pub fn new_in(ctx: &Arc<LibCtx>) -> Result<Self, ErrorStack>;

    // ── Loading ───────────────────────────────────────────────────────────────

    /// Parse from PEM bytes. Zero-copy: uses MemBioBuf (BIO_new_mem_buf).
    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Parse from PEM within a specific library context.
    /// Note: delegates to from_pem (PEM_read_bio_X509_ex does not exist in OpenSSL 3.5).
    pub fn from_pem_in(_ctx: &Arc<LibCtx>, pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Parse from a DER byte slice. The full slice is consumed.
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;

    // ── Serialization ─────────────────────────────────────────────────────────

    /// Encode as PEM into a freshly allocated Vec<u8>.
    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Encode as DER into a freshly allocated Vec<u8>.
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack>;

    // ── Field Accessors ───────────────────────────────────────────────────────

    /// Subject distinguished name. Borrowed, valid while self is alive.
    pub fn subject_name(&self) -> X509Name<'_>;

    /// Issuer distinguished name. Borrowed.
    pub fn issuer_name(&self) -> X509Name<'_>;

    /// Serial number as i64. Returns None if the serial number exceeds i64::MAX.
    pub fn serial_number(&self) -> Option<i64>;

    /// Serial number as raw big-endian bytes (content octets only, no DER tag/length).
    /// Handles serials that exceed i64::MAX (up to 20 bytes per RFC 5280).
    /// Returns None if the serial field is absent.
    pub fn serial_number_bytes(&self) -> Option<Vec<u8>>;

    /// notBefore validity as a human-readable string ("Mmm DD HH:MM:SS YYYY GMT").
    pub fn not_before_str(&self) -> Option<String>;

    /// notAfter validity as a human-readable string.
    pub fn not_after_str(&self) -> Option<String>;

    /// notBefore as a structured BrokenDownTime (year/month/day/hour/minute/second in UTC).
    pub fn not_before_tm(&self) -> Option<BrokenDownTime>;

    /// notAfter as a structured BrokenDownTime.
    pub fn not_after_tm(&self) -> Option<BrokenDownTime>;

    /// true if the current time is within [notBefore, notAfter].
    pub fn is_valid_now(&self) -> bool;

    // ── Public Key ────────────────────────────────────────────────────────────

    /// Extract the public key (owned Pkey<Public>).
    /// Calls X509_get_pubkey; the returned key is independently reference-counted.
    pub fn public_key(&self) -> Result<Pkey<Public>, ErrorStack>;

    /// Check whether the certificate's public key uses the named algorithm.
    /// Uses X509_get0_pubkey — no refcount increment. Returns false if absent.
    pub fn public_key_is_a(&self, alg: &CStr) -> bool;

    /// Bit size of the certificate's public key. Uses X509_get0_pubkey — no refcount
    /// increment. Returns None if the certificate has no public key.
    pub fn public_key_bits(&self) -> Option<u32>;

    // ── Extensions ────────────────────────────────────────────────────────────

    /// Number of extensions.
    pub fn extension_count(&self) -> usize;

    /// Extension at zero-based index. Returns None if out of range.
    pub fn extension(&self, idx: usize) -> Option<X509Extension<'_>>;

    /// First extension with the given NID. Returns None if absent.
    pub fn extension_by_nid(&self, nid: i32) -> Option<X509Extension<'_>>;

    /// DER-encoded value bytes of the first extension with the given NID.
    /// Zero-copy: borrows from the certificate's internal storage.
    /// Returns None if the extension is absent.
    pub fn extension_der(&self, nid: i32) -> Option<&[u8]>;

    // ── Verification ──────────────────────────────────────────────────────────

    /// Verify that this certificate was signed by key.
    /// Returns Ok(true) if valid, Ok(false) if not, Err on fatal error.
    pub fn verify(&self, key: &Pkey<Public>) -> Result<bool, ErrorStack>;

    /// true if subject name == issuer name (name match only; no signature check).
    pub fn is_self_signed(&self) -> bool;

    /// Inspect the signature algorithm fields of this certificate.
    /// Returns md_nid (digest), pk_nid (public-key algorithm), and security_bits.
    /// md_nid is 0 (NID_undef) for algorithms with no separate pre-hash step
    /// (e.g. Ed25519, ML-DSA).
    pub fn signature_info(&self) -> Result<SignatureInfo, ErrorStack>;
}

impl Clone for X509 { /* X509_up_ref */ }
impl Drop  for X509 { /* X509_free  */ }
unsafe impl Send for X509 {}
unsafe impl Sync for X509 {}
}

X509Name<'cert> — Distinguished Name

#![allow(unused)]
fn main() {
/// A borrowed distinguished name, lifetime-tied to the owning X509.
pub struct X509Name<'cert> { /* X509_NAME* */ }

impl X509Name<'_> {
    /// Number of RDN entries.
    pub fn entry_count(&self) -> usize;

    /// Entry at zero-based index. Returns None if out of range.
    pub fn entry(&self, idx: usize) -> Option<X509NameEntry<'_>>;

    /// Format as a string using X509_NAME_print_ex with XN_FLAG_COMPAT.
    /// Returns None if the BIO write fails.
    pub fn to_string(&self) -> Option<String>;

    /// Return the legacy one-line `/CN=.../O=.../C=...` representation
    /// produced by X509_NAME_oneline. Useful for logging and debugging.
    /// Prefer to_string() or entry() for structured access.
    /// Returns None if OpenSSL cannot allocate the string.
    pub fn to_oneline(&self) -> Option<String>;
}
}

X509NameEntry<'name> — One RDN Component

#![allow(unused)]
fn main() {
pub struct X509NameEntry<'name> { /* X509_NAME_ENTRY* */ }

impl X509NameEntry<'_> {
    /// NID of this field (e.g. NID_commonName = 13).
    pub fn nid(&self) -> i32;

    /// Raw DER-encoded value bytes.
    pub fn data(&self) -> &[u8];
}
}

X509Extension<'cert> — Certificate Extension

#![allow(unused)]
fn main() {
pub struct X509Extension<'cert> { /* X509_EXTENSION* */ }

impl X509Extension<'_> {
    /// NID of this extension.
    pub fn nid(&self) -> i32;

    /// true if this extension is marked critical.
    pub fn is_critical(&self) -> bool;

    /// Raw DER-encoded extension value bytes.
    pub fn data(&self) -> &[u8];
}
}

Building Certificates

X509NameOwned — Mutable Distinguished Name

#![allow(unused)]
fn main() {
/// An owned, mutable distinguished name for use with X509Builder.
pub struct X509NameOwned { /* X509_NAME* */ }

impl X509NameOwned {
    pub fn new() -> Result<Self, ErrorStack>;

    /// Append a field. `field` is a short name (c"CN", c"O", c"C").
    /// `value` is a UTF-8 byte slice.
    pub fn add_entry_by_txt(&mut self, field: &CStr, value: &[u8])
        -> Result<(), ErrorStack>;
}
}

X509Builder

#![allow(unused)]
fn main() {
pub struct X509Builder { /* X509* under construction */ }

impl X509Builder {
    pub fn new() -> Result<Self, ErrorStack>;

    /// Set version: 0=v1, 1=v2, 2=v3.
    pub fn set_version(self, version: i64) -> Result<Self, ErrorStack>;

    pub fn set_serial_number(self, n: i64) -> Result<Self, ErrorStack>;

    /// Set notBefore to now + offset_secs.
    pub fn set_not_before_offset(self, offset_secs: i64) -> Result<Self, ErrorStack>;

    /// Set notAfter to now + offset_secs.
    pub fn set_not_after_offset(self, offset_secs: i64) -> Result<Self, ErrorStack>;

    pub fn set_subject_name(self, name: &X509NameOwned) -> Result<Self, ErrorStack>;
    pub fn set_issuer_name(self, name: &X509NameOwned)  -> Result<Self, ErrorStack>;

    pub fn set_public_key<T: HasPublic>(self, key: &Pkey<T>) -> Result<Self, ErrorStack>;

    /// Sign the certificate.
    /// Pass digest = None for EdDSA (Ed25519, Ed448).
    pub fn sign(self, key: &Pkey<Private>, digest: Option<&DigestAlg>)
        -> Result<Self, ErrorStack>;

    /// Finalise and return the X509.
    pub fn build(self) -> X509;
}
}

X509Store — Trust Store

An X509Store holds trusted CA certificates and CRLs. Pass it to X509StoreCtx::init to verify a certificate chain, or to OcspResponse::basic_verify to verify an OCSP response signature.

#![allow(unused)]
fn main() {
pub struct X509Store { /* X509_STORE* */ }

impl X509Store {
    /// Create an empty trust store.
    pub fn new() -> Result<Self, ErrorStack>;

    /// Add a trusted certificate.
    /// The certificate's reference count is incremented internally.
    pub fn add_cert(&mut self, cert: &X509) -> Result<(), ErrorStack>;

    /// Add a CRL to the store.
    pub fn add_crl(&mut self, crl: &X509Crl) -> Result<(), ErrorStack>;

    /// Set verification flags (e.g. X509_V_FLAG_CRL_CHECK).
    pub fn set_flags(&mut self, flags: u64) -> Result<(), ErrorStack>;
}

impl Clone for X509Store { /* X509_STORE_up_ref */ }
}

X509Store is Clone (via X509_STORE_up_ref) and Send + Sync.

X509StoreCtx — Chain Verification Context

X509StoreCtx drives X509_verify_cert. It is single-use: create, initialise, verify, inspect, then drop.

#![allow(unused)]
fn main() {
pub struct X509StoreCtx { /* X509_STORE_CTX* */ }

impl X509StoreCtx {
    /// Allocate a new, uninitialised verification context.
    pub fn new() -> Result<Self, ErrorStack>;

    /// Initialise for verifying `cert` against `store`.
    /// Call before `verify`.
    pub fn init(&mut self, store: &X509Store, cert: &X509) -> Result<(), ErrorStack>;

    /// Verify the certificate chain.
    ///
    /// Returns Ok(true) if valid, Ok(false) if not (call `error()` for the
    /// error code), or Err on a fatal OpenSSL error.
    pub fn verify(&mut self) -> Result<bool, ErrorStack>;

    /// OpenSSL verification error code after a failed `verify`.
    /// Returns 0 (X509_V_OK) if no error occurred.
    /// See <openssl/x509_vfy.h> for X509_V_ERR_* constants.
    pub fn error(&self) -> i32;

    /// Collect the verified chain as owned X509 values.
    /// Only meaningful after a successful `verify`.
    pub fn chain(&self) -> Vec<X509>;
}
}

X509Crl — Certificate Revocation List

#![allow(unused)]
fn main() {
pub struct X509Crl { /* X509_CRL* */ }

impl X509Crl {
    /// Allocate a new, empty CRL structure (X509_CRL_new).
    pub fn new() -> Result<Self, ErrorStack>;

    /// Allocate a new, empty CRL bound to a library context (X509_CRL_new_ex).
    pub fn new_in(ctx: &Arc<LibCtx>) -> Result<Self, ErrorStack>;

    /// Parse from PEM bytes.
    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Parse from DER bytes.
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;

    /// Serialise to PEM.
    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Serialise to DER.
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Issuer distinguished name (borrowed).
    pub fn issuer_name(&self) -> X509Name<'_>;

    /// `thisUpdate` field as a human-readable string.
    pub fn last_update_str(&self) -> Option<String>;

    /// `nextUpdate` field as a human-readable string.
    pub fn next_update_str(&self) -> Option<String>;

    /// Verify this CRL was signed by `key`.
    /// Returns Ok(true) if valid, Ok(false) if not.
    pub fn verify(&self, key: &Pkey<Public>) -> Result<bool, ErrorStack>;
}

impl Clone for X509Crl { /* X509_CRL_up_ref */ }
}

X509Crl is Clone (via X509_CRL_up_ref) and Send + Sync.

NID Lookup Free Functions

OpenSSL uses numeric identifiers (NIDs) to represent OIDs internally. Two free functions in native_ossl::x509 convert human-readable names to NIDs:

#![allow(unused)]
fn main() {
/// Look up a NID by short name (e.g. c"sha256", c"CN", c"rsaEncryption").
/// Returns None if the name is not in OpenSSL's object table.
pub fn nid_from_short_name(sn: &CStr) -> Option<i32>;

/// Look up a NID by OID text or short name.
/// Accepts dotted decimal ("2.5.4.3") or short name ("CN").
/// Returns None if the string is not recognised.
pub fn nid_from_text(s: &CStr) -> Option<i32>;
}

These wrap OBJ_sn2nid and OBJ_txt2nid respectively. Use them when you have a NID-based API (e.g. X509::extension_by_nid) but only know the algorithm by name:

#![allow(unused)]
fn main() {
use native_ossl::x509::nid_from_short_name;

// Find Subject Key Identifier extension by NID.
let nid = nid_from_short_name(c"subjectKeyIdentifier").unwrap();
let ext = cert.extension_by_nid(nid);
}

Examples

Create an X509 Bound to a Library Context

#![allow(unused)]
fn main() {
use std::sync::Arc;
use native_ossl::lib_ctx::LibCtx;
use native_ossl::x509::X509;

let ctx = Arc::new(LibCtx::new()?);
let _prov = ctx.load_provider(c"default")?;
let cert = X509::new_in(&ctx)?;
// cert is now associated with ctx for any provider-dispatched operations.
}

Load and Inspect a Certificate

#![allow(unused)]
fn main() {
use native_ossl::x509::X509;

let pem = std::fs::read("cert.pem")?;
let cert = X509::from_pem(&pem)?;

// Print subject
if let Some(subject) = cert.subject_name().to_string() {
    println!("Subject: {subject}");
}

// Print validity
println!("Not before: {:?}", cert.not_before_str());
println!("Not after:  {:?}", cert.not_after_str());
println!("Valid now: {}", cert.is_valid_now());

// Serial number
if let Some(serial) = cert.serial_number() {
    println!("Serial: {serial}");
}
}

Inspect Name Entries Manually

#![allow(unused)]
fn main() {
let name = cert.subject_name();
for i in 0..name.entry_count() {
    if let Some(entry) = name.entry(i) {
        println!("  NID {}: {:?}", entry.nid(), entry.data());
    }
}
}

Build a Self-Signed Certificate (Ed25519)

#![allow(unused)]
fn main() {
use native_ossl::pkey::KeygenCtx;
use native_ossl::x509::{X509Builder, X509NameOwned};

let key = KeygenCtx::new(c"ED25519")?.generate()?;

let mut name = X509NameOwned::new()?;
name.add_entry_by_txt(c"CN", b"example.com")?;
name.add_entry_by_txt(c"O",  b"Example Corp")?;

let cert = X509Builder::new()?
    .set_version(2)?                    // X.509v3
    .set_serial_number(1)?
    .set_not_before_offset(0)?          // valid from now
    .set_not_after_offset(365 * 86400)? // valid for one year
    .set_subject_name(&name)?
    .set_issuer_name(&name)?            // self-signed
    .set_public_key(&key)?
    .sign(&key, None)?                  // None = no separate digest (EdDSA)
    .build();

let pem = cert.to_pem()?;
}

Build a Self-Signed Certificate (RSA)

#![allow(unused)]
fn main() {
use native_ossl::pkey::KeygenCtx;
use native_ossl::digest::DigestAlg;
use native_ossl::x509::{X509Builder, X509NameOwned};

let key = KeygenCtx::new(c"RSA")?.generate()?;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;

let mut name = X509NameOwned::new()?;
name.add_entry_by_txt(c"CN", b"example.com")?;

let cert = X509Builder::new()?
    .set_version(2)?
    .set_serial_number(1)?
    .set_not_before_offset(0)?
    .set_not_after_offset(365 * 86400)?
    .set_subject_name(&name)?
    .set_issuer_name(&name)?
    .set_public_key(&key)?
    .sign(&key, Some(&sha256))?         // RSA requires an explicit digest
    .build();
}

Verify a Certificate Signature

#![allow(unused)]
fn main() {
let cert = X509::from_pem(&pem)?;
let ca_cert = X509::from_pem(&ca_pem)?;
let ca_key = ca_cert.public_key()?;

match cert.verify(&ca_key)? {
    true  => println!("Signature valid"),
    false => println!("Signature INVALID"),
}
}

Chain Verification with X509Store

use native_ossl::x509::{X509, X509Store, X509StoreCtx};

// Build a trust store from PEM-encoded CA certificates.
let mut store = X509Store::new()?;
for ca_pem in &ca_pem_list {
    let ca = X509::from_pem(ca_pem)?;
    store.add_cert(&ca)?;
}

// Verify an end-entity certificate against the store.
let cert = X509::from_pem(&ee_pem)?;
let mut ctx = X509StoreCtx::new()?;
ctx.init(&store, &cert)?;

match ctx.verify()? {
    true => {
        println!("Chain valid");
        // Inspect the verified chain (leaf first).
        for c in ctx.chain() {
            println!("  {}", c.subject_name().to_string().unwrap_or_default());
        }
    }
    false => {
        println!("Chain INVALID: error code {}", ctx.error());
    }
}

Load and Verify a CRL

use native_ossl::x509::{X509, X509Crl};

let crl_pem = std::fs::read("crl.pem")?;
let crl = X509Crl::from_pem(&crl_pem)?;

println!("Issuer:      {}", crl.issuer_name().to_string().unwrap_or_default());
println!("This update: {:?}", crl.last_update_str());
println!("Next update: {:?}", crl.next_update_str());

// Verify the CRL signature with the issuer's public key.
let ca_cert = X509::from_pem(&ca_pem)?;
let ca_key = ca_cert.public_key()?;

match crl.verify(&ca_key)? {
    true  => println!("CRL signature valid"),
    false => println!("CRL signature INVALID"),
}

CRL-Checked Chain Verification

To enable CRL checking in X509StoreCtx, add the CRL to the store and set the appropriate flag:

use native_ossl::x509::{X509, X509Crl, X509Store, X509StoreCtx};

const X509_V_FLAG_CRL_CHECK: u64 = 0x4;

let mut store = X509Store::new()?;
store.add_cert(&ca_cert)?;
store.add_crl(&crl)?;
store.set_flags(X509_V_FLAG_CRL_CHECK)?;

let mut ctx = X509StoreCtx::new()?;
ctx.init(&store, &end_entity_cert)?;
let ok = ctx.verify()?;

NID Lookup Free Functions

OpenSSL uses numeric identifiers (NIDs) to identify algorithms and object types. These free functions convert between NIDs and human-readable names.

#![allow(unused)]
fn main() {
/// Short name for a NID (e.g. 13 → "CN", 672 → "SHA256"). Returns None if unknown.
/// Result points to OpenSSL's static table; lifetime is 'static, no allocation.
pub fn nid_to_short_name(nid: i32) -> Option<&'static CStr>;

/// Long name for a NID (e.g. 13 → "commonName"). Returns None if unknown.
pub fn nid_to_long_name(nid: i32) -> Option<&'static CStr>;
}

Provider-aware key-type comparison

When inspecting certificate signature algorithms with X509_get_signature_info, you get a pknid (signer’s key NID). Comparing it against a public key using EVP_PKEY_get_base_id is broken for provider-based keys (ML-DSA, ML-KEM, SLH-DSA) — that function returns NID_undef (0) for all of them. The correct pattern uses EVP_PKEY_is_a with the NID’s short name:

#![allow(unused)]
fn main() {
// WRONG for provider-based keys:
// pkey.get_base_id() == pknid   ← returns 0 for all provider keys

// Correct — works for both legacy and provider-based keys:
use native_ossl::x509::nid_to_short_name;

if let Some(sn) = nid_to_short_name(pknid) {
    if pkey.is_a(sn) {
        // subject key matches the signing algorithm
    }
}
}

Design Notes

  • from_der takes &[u8] — the full slice is consumed. A local pointer is passed to d2i_X509; the external slice reference is not advanced. Use for parsing a single, complete DER buffer.
  • Two serial number accessorsserial_number() returns Option<i64> (convenient for small serials); serial_number_bytes() returns the raw big-endian bytes via X509_get_serialNumber (handles serials up to 20 bytes per RFC 5280). Use serial_number_bytes() when interoperating with CAs that issue large random serials.
  • Two public key pathspublic_key() returns an owned Pkey<Public> with a refcount increment (X509_get_pubkey). public_key_is_a() / public_key_bits() use X509_get0_pubkey (no refcount increment) for cheap introspection. Prefer the get0 path when you only need to check the key type or size.
  • public_key() is always ownedX509_get_pubkey performs EVP_PKEY_up_ref internally. There is no borrowed &Pkey<Public> getter.
  • No Asn1Time type — validity dates are accessed as formatted strings via not_before_str() / not_after_str(), which return Option<String> using ASN1_TIME_print. No structured timestamp type is exposed.
  • X509NameOwned vs X509Name — the owned type is for the builder (mutable). The borrowed type (X509Name<'cert>) is for reading from a parsed certificate. They are not interchangeable.
  • is_self_signed checks name equality only — it does not verify the signature. To check the signature use cert.verify(&cert.public_key()?).
  • Extensions — raw DER bytes are accessible via X509Extension::data() or the convenience accessor X509::extension_der(nid). No structured extension parsing is provided; use the NID to identify the extension and interpret the bytes yourself or via native-ossl-sys directly.
  • X509Store is CloneX509_STORE_up_ref allows sharing the same trust store across threads or verification contexts.
  • X509StoreCtx is single-use — calling init again after verify is not supported. Allocate a fresh X509StoreCtx for each certificate to verify.
  • X509StoreCtx::chain — each X509 in the returned Vec is independently reference-counted via X509_up_ref. The vector may outlive the X509StoreCtx.
  • X509Crl is CloneX509_CRL_up_ref allows sharing CRL objects across multiple stores without copying.
  • not_before_tm / not_after_tm return BrokenDownTime — a simple struct with year (full Gregorian year), month (1–12), day, hour, minute, second. Backed by ASN1_TIME_to_tm; suitable for timestamp arithmetic without a datetime crate dependency. For comparisons against the current time, prefer is_valid_now() which uses OpenSSL’s own time comparison.
  • nid_to_short_name / nid_to_long_name return &'static CStr — the pointers come from OpenSSL’s static internal OBJ table (not heap-allocated per call). The 'static lifetime is correct and no deallocation is needed. These are pure table lookups backed by OBJ_nid2sn / OBJ_nid2ln.
  • X509Name::to_oneline — produces the legacy /CN=.../O=.../C=... format via X509_NAME_oneline. Allocates a C string that is immediately freed after conversion to a Rust String (using CRYPTO_free, the function underlying the OPENSSL_free macro). Prefer to_string() or entry() for structured access; use to_oneline() only for quick log output.
  • X509::new_in — creates an empty X509 bound to an explicit LibCtx, useful for FIPS-isolated certificate construction. Wraps X509_new_ex with a NULL property query string (default provider properties).
  • signature_info md_nid is 0 for pre-hash-free algorithms — Ed25519 and ML-DSA (FIPS 204) sign the message directly without a separate digest step. X509_get_signature_info returns NID_undef (0) for mdnid in these cases. Always check for md_nid == 0 before using it as a digest identifier. For traditional algorithms (RSA, ECDSA), md_nid is a valid non-zero NID such as the one for SHA-256 or SHA-384.

PKCS#12

Overview

A PKCS#12 file (also called a PFX bundle) packs a private key, its end-entity certificate, and an optional chain of CA certificates into a single password-protected DER blob. It is the interchange format used by browsers, Java keystores, and many TLS deployment tools.

Pkcs12 wraps OpenSSL’s PKCS12* structure. The two main operations are:

  • Pkcs12::from_der + parse — load an existing bundle and extract its contents.
  • Pkcs12::create + to_der — assemble a new bundle from Rust objects.

Pkcs12 — Bundle Wrapper

#![allow(unused)]
fn main() {
pub struct Pkcs12 { /* PKCS12* */ }

impl Pkcs12 {
    /// Allocate a new, empty PKCS#12 structure (PKCS12_new).
    pub fn new() -> Result<Self, ErrorStack>;

    /// Load a bundle from DER-encoded bytes.
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;

    /// Serialise the bundle to DER.
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Parse the bundle, returning the private key, end-entity certificate,
    /// and any additional CA certificates.
    ///
    /// Pass `""` for an unencrypted bundle.
    pub fn parse(
        &self,
        password: &str,
    ) -> Result<(Pkey<Private>, X509, Vec<X509>), ErrorStack>;

    /// Create a new bundle from a private key and certificate.
    ///
    /// - `password` — MAC / encryption passphrase.
    /// - `name`     — friendly name stored in the bundle (e.g. the subject CN).
    /// - `key`      — private key.
    /// - `cert`     — end-entity certificate.
    /// - `ca`       — slice of additional CA certificates (may be empty).
    ///
    /// Uses AES-256-CBC for key encryption and SHA-256 for the MAC.
    pub fn create(
        password: &str,
        name: &str,
        key: &Pkey<Private>,
        cert: &X509,
        ca: &[X509],
    ) -> Result<Self, ErrorStack>;
}

unsafe impl Send for Pkcs12 {}
unsafe impl Sync for Pkcs12 {}
}

Pkcs12 does not implement Clone — the underlying PKCS12* has no up_ref function.

Examples

Load a Bundle and Extract Key and Certificate

use native_ossl::pkcs12::Pkcs12;

let der = std::fs::read("bundle.p12")?;
let p12 = Pkcs12::from_der(&der)?;

let (key, cert, ca_chain) = p12.parse("my password")?;

println!("Key algorithm: {}", if key.is_a(c"RSA") { "RSA" } else { "other" });
if let Some(subject) = cert.subject_name().to_string() {
    println!("Certificate subject: {subject}");
}
println!("CA chain length: {}", ca_chain.len());

Create a Bundle and Write to Disk

use native_ossl::pkcs12::Pkcs12;
use native_ossl::pkey::KeygenCtx;
use native_ossl::x509::{X509Builder, X509NameOwned};

// Generate a key pair.
let key = KeygenCtx::new(c"ED25519")?.generate()?;

// Build a self-signed certificate.
let mut name = X509NameOwned::new()?;
name.add_entry_by_txt(c"CN", b"example.com")?;

let cert = X509Builder::new()?
    .set_version(2)?
    .set_serial_number(1)?
    .set_not_before_offset(0)?
    .set_not_after_offset(365 * 86400)?
    .set_subject_name(&name)?
    .set_issuer_name(&name)?
    .set_public_key(&key)?
    .sign(&key, None)?
    .build();

// Pack into a PKCS#12 bundle with no CA chain.
let p12 = Pkcs12::create("my password", "example.com", &key, &cert, &[])?;
let der = p12.to_der()?;
std::fs::write("bundle.p12", &der)?;

Round-Trip (Create, Serialise, Parse)

The DER encoding of a bundle is deterministic for a given set of inputs and OpenSSL defaults, so a DER round-trip can be used as a consistency check:

use native_ossl::pkcs12::Pkcs12;

let p12 = Pkcs12::create("pass", "friendly name", &key, &cert, &[])?;
let der1 = p12.to_der()?;

let p12b = Pkcs12::from_der(&der1)?;
let der2 = p12b.to_der()?;
assert_eq!(der1, der2);

let (key2, cert2, _ca) = p12b.parse("pass")?;
assert!(key2.is_a(c"ED25519"));
assert_eq!(cert.to_der()?, cert2.to_der()?);

Include a CA Chain

Pass CA certificates in chain order (intermediate first, root last):

use native_ossl::pkcs12::Pkcs12;

let p12 = Pkcs12::create(
    "pass",
    "my server",
    &server_key,
    &server_cert,
    &[intermediate_cert, root_cert],
)?;

Design Notes

  • Password encodingPkcs12::create and Pkcs12::parse accept a Rust &str. The string is converted to a null-terminated C string internally. Null bytes in the password are rejected.
  • Encryption defaultsPKCS12_create_ex is called with nid_key = 0 and nid_cert = 0, which tells OpenSSL to use its compiled-in defaults: AES-256-CBC for key bags and no additional certificate encryption. The MAC uses SHA-256.
  • CA certificate ownershipPkcs12::create calls X509_up_ref on each CA certificate before adding it to the internal stack. The original X509 values may be dropped before or after create returns.
  • parse output ownershipPkcs12::parse calls PKCS12_parse, which allocates a new EVP_PKEY* and X509* (both fully owned). The CA chain is returned as a Vec<X509> with each element independently reference-counted.
  • No ClonePKCS12 has no up_ref; serialise with to_der and re-parse with from_der if a second independent copy is needed.
  • new() returns an empty structurePkcs12::new wraps PKCS12_new and allocates an uninitialised bundle. It is useful for low-level construction via the OpenSSL API directly; for normal use, prefer Pkcs12::create.

OCSP

Overview

The Online Certificate Status Protocol (RFC 6960) allows a client to query a responder for the revocation status of a specific certificate. This module covers the full client-side stack: building requests, decoding responses, verifying signatures, and reading per-certificate status.

HTTP transport is out of scope — the caller is responsible for sending the DER-encoded request and receiving the DER-encoded response.

Types provided:

TypeDescription
[OcspCertId]Certificate identifier (OCSP_CERTID*) built from a subject + issuer pair
[OcspRequest]OCSP request (OCSP_REQUEST*)
[OcspResponse]Top-level OCSP response (OCSP_RESPONSE*)
[OcspBasicResp]Signed inner response (OCSP_BASICRESP*)
[OcspSingleResp]Owned single-response entry (OCSP_SINGLERESP*)
[BorrowedOcspSingleResp]Borrowed view of a single-response entry; provides status()
[SingleRespStatus]Status result from BorrowedOcspSingleResp::status()
[OcspResponseStatus]Top-level response status enum
[OcspCertStatus]Per-certificate revocation status (Good / Revoked / Unknown)
[OcspRevokeReason]CRL revocation reason code enum
[OcspSingleStatus]Full per-certificate result with timestamps

OcspCertId — Certificate Identifier

#![allow(unused)]
fn main() {
pub struct OcspCertId { /* OCSP_CERTID* */ }

impl OcspCertId {
    /// Allocate a new, empty OCSP_CERTID (all fields zeroed).
    pub fn new() -> Result<Self, ErrorStack>;

    /// Build a cert ID from a subject certificate and its direct issuer.
    ///
    /// Pass `None` for `digest` to use SHA-1 (the OCSP default, required by
    /// most deployed responders).
    pub fn from_cert(
        digest: Option<&DigestAlg>,
        subject: &X509,
        issuer: &X509,
    ) -> Result<Self, ErrorStack>;
}

impl Clone for OcspCertId { /* OCSP_CERTID_dup */ }
}

OcspRequest

#![allow(unused)]
fn main() {
pub struct OcspRequest { /* OCSP_REQUEST* */ }

impl OcspRequest {
    pub fn new() -> Result<Self, ErrorStack>;
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;

    /// Add a certificate ID (consumes `cert_id` — add0 semantics).
    pub fn add_cert_id(&mut self, cert_id: OcspCertId) -> Result<(), ErrorStack>;

    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack>;
}
}

OcspResponse

#![allow(unused)]
fn main() {
pub struct OcspResponse { /* OCSP_RESPONSE* */ }

impl OcspResponse {
    /// Allocate a new, empty OCSP_RESPONSE (responder side).
    pub fn new() -> Result<Self, ErrorStack>;

    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Top-level packet status (not per-certificate status).
    pub fn status(&self) -> OcspResponseStatus;

    /// Extract the signed inner response for signature verification and
    /// per-certificate status lookup.
    pub fn basic(&self) -> Result<OcspBasicResp, ErrorStack>;

    /// Convenience: verify signature + find_status in one call.
    pub fn verified_status(
        &self,
        store: &X509Store,
        cert_id: &OcspCertId,
    ) -> Result<Option<OcspSingleStatus>, ErrorStack>;
}
}

OcspBasicResp

#![allow(unused)]
fn main() {
pub struct OcspBasicResp { /* OCSP_BASICRESP* */ }

impl OcspBasicResp {
    /// Allocate a new, empty OCSP_BASICRESP (responder side).
    pub fn new() -> Result<Self, ErrorStack>;

    /// Verify the response signature against `store`.
    ///
    /// Pass `flags = 0` for default behaviour.
    pub fn verify(&self, store: &X509Store, flags: u64) -> Result<bool, ErrorStack>;

    /// Number of SingleResponse entries.
    pub fn count(&self) -> usize;

    /// Get the SingleResponse at position `idx`.
    ///
    /// Returns `None` if `idx` is out of range.
    pub fn get_response(&self, idx: usize) -> Option<BorrowedOcspSingleResp<'_>>;

    /// Find the index of the first SingleResponse matching `id`.
    ///
    /// Returns `Some(idx)` or `None` if not found.
    pub fn find_response(&self, id: &OcspCertId) -> Option<usize>;

    /// Look up status for a specific certificate.
    ///
    /// Returns `Ok(None)` if the certificate is not in this response.
    pub fn find_status(&self, cert_id: &OcspCertId)
        -> Result<Option<OcspSingleStatus>, ErrorStack>;

    /// Validate the thisUpdate / nextUpdate time window of a SingleResponse.
    ///
    /// `sec` — acceptable clock skew in seconds (typically 300).
    /// `maxsec` — maximum age of nextUpdate in seconds (-1 = no limit).
    pub fn check_validity(
        &self,
        cert_id: &OcspCertId,
        sec: i64,
        maxsec: i64,
    ) -> Result<bool, ErrorStack>;
}
}

BorrowedOcspSingleResp — Borrowed Single Response Entry

#![allow(unused)]
fn main() {
/// Borrowed OCSP_SINGLERESP* whose lifetime is tied to its parent OcspBasicResp.
pub struct BorrowedOcspSingleResp<'a> { /* ... */ }

impl BorrowedOcspSingleResp<'_> {
    /// Extract revocation status, reason, and timestamps from this entry.
    pub fn status(&self) -> Result<SingleRespStatus, ErrorStack>;
}

/// Status of a single certificate entry.
pub struct SingleRespStatus {
    pub cert_status:     OcspCertStatus,
    pub reason:          Option<OcspRevokeReason>,
    pub this_update:     Option<String>,
    pub next_update:     Option<String>,
    pub revocation_time: Option<String>,
}
}

Obtain a BorrowedOcspSingleResp via [OcspBasicResp::get_response]:

let basic = resp.basic()?;
if let Some(entry) = basic.get_response(0) {
    let s = entry.status()?;
    println!("cert status: {:?}", s.cert_status);
}

Status Enums and Structs

#![allow(unused)]
fn main() {
/// Top-level OCSP response packet status (RFC 6960 §4.2.1).
pub enum OcspResponseStatus {
    Successful,        // 0 — server produced a response
    MalformedRequest,  // 1
    InternalError,     // 2
    TryLater,          // 3
    SigRequired,       // 5
    Unauthorized,      // 6
    Unknown(i32),
}

/// Per-certificate revocation status from a SingleResponse.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcspCertStatus {
    Good,     // V_OCSP_CERTSTATUS_GOOD = 0
    Revoked,  // V_OCSP_CERTSTATUS_REVOKED = 1
    Unknown,  // V_OCSP_CERTSTATUS_UNKNOWN = 2
}

/// CRL revocation reason codes (RFC 5280 §5.3.1).
/// None = no reason given (OCSP_REVOKED_STATUS_NOSTATUS = -1).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcspRevokeReason {
    Unspecified,         // 0
    KeyCompromise,       // 1
    CaCompromise,        // 2
    AffiliationChanged,  // 3
    Superseded,          // 4
    CessationOfOperation,// 5
    CertificateHold,     // 6
    RemoveFromCrl,       // 8
    PrivilegeWithdrawn,  // 9
    AaCompromise,        // 10
    Other(i32),          // forward-compatibility guard
}

/// Full per-certificate result returned by OcspBasicResp::find_status.
pub struct OcspSingleStatus {
    pub cert_status:     OcspCertStatus,
    pub reason:          Option<OcspRevokeReason>,  // Some only when Revoked
    pub this_update:     Option<String>,  // "Apr 15 15:00:00 2026 GMT"
    pub next_update:     Option<String>,
    pub revocation_time: Option<String>,  // Some only when Revoked
}
}

Examples

Full Client Flow

use native_ossl::ocsp::{OcspCertId, OcspRequest, OcspResponse, OcspResponseStatus, OcspCertStatus};
use native_ossl::x509::{X509, X509Store};

// Load the end-entity cert and its issuer.
let ee_cert   = X509::from_pem(&ee_pem)?;
let issuer    = X509::from_pem(&issuer_pem)?;

// Build a certificate identifier (SHA-1 is the OCSP default).
let cert_id = OcspCertId::from_cert(None, &ee_cert, &issuer)?;

// Build the request.
let mut req = OcspRequest::new()?;
req.add_cert_id(cert_id.clone())?;   // clone because we reuse cert_id below
let req_der = req.to_der()?;

// ... send req_der via HTTP POST to the OCSP responder URL
// ... receive resp_der from the HTTP response body

// Parse the response.
let resp = OcspResponse::from_der(&resp_der)?;
if resp.status() != OcspResponseStatus::Successful {
    return Err(format!("OCSP error: {:?}", resp.status()).into());
}

// Verify the signature and look up the certificate status.
let mut store = X509Store::new()?;
store.add_cert(&issuer)?;

let status = resp.verified_status(&store, &cert_id)?;
match status {
    None => println!("certificate not in OCSP response"),
    Some(s) => match s.cert_status {
        OcspCertStatus::Good => println!("certificate is good (valid until {:?})", s.next_update),
        OcspCertStatus::Revoked => println!("REVOKED (reason {:?})", s.reason),
        OcspCertStatus::Unknown => println!("responder does not know this certificate"),
    },
}

Validate the Time Window

use native_ossl::ocsp::{OcspCertId, OcspResponse};

let cert_id = OcspCertId::from_cert(None, &ee_cert, &issuer)?;
let resp    = OcspResponse::from_der(&resp_der)?;
let basic   = resp.basic()?;

// Allow 5 minutes of clock skew; no limit on nextUpdate age.
if !basic.check_validity(&cert_id, 300, -1)? {
    return Err("OCSP response is outside its validity window".into());
}

Design Notes

  • HTTP transport is out of scope — the caller supplies the transport layer. The OCSP responder URL is found in the certificate’s Authority Information Access (AIA) extension.
  • add_cert_id uses add0 semantics — ownership of the OcspCertId is transferred to the request. Use clone() before calling add_cert_id if you need the cert ID afterwards (e.g. to pass it to find_status).
  • basic() allocatesOCSP_response_get1_basic allocates a new OCSP_BASICRESP; the OcspBasicResp owns it and frees on drop.
  • basic_verify flags — common values: OCSP_NOCERTS (skip bundled certs), OCSP_NOVERIFY (skip signature check), OCSP_NOCHAIN. Pass 0 for the default behaviour.
  • No Clone on Request/ResponseOCSP_REQUEST and OCSP_RESPONSE have no up_ref; use to_der + from_der for a second copy.
  • OcspCertId is Clone — implemented via OCSP_CERTID_dup.
  • Responder-side constructorsOcspBasicResp::new(), OcspCertId::new(), and OcspResponse::new() allocate empty objects for building responses from scratch on the responder side.
  • OcspCertStatus is now Copy — the revocation reason has been moved to OcspRevokeReason and is returned separately in OcspSingleStatus::reason.
  • BorrowedOcspSingleResp does not free — the pointer is owned by the parent OcspBasicResp; Drop on OcspSingleResp is a no-op. The borrow lifetime enforces that the parent is not dropped while the view is live.
  • get_response vs find_statusget_response(idx) + status() is the low-level index-based path; find_status(cert_id) is the higher-level cert-ID-based path and is preferred for most client-side code.

Utilities

Overview

The util module provides two utilities:

  • ct_eq — constant-time byte-slice equality backed by CRYPTO_memcmp
  • SecretBuf — a heap buffer that is securely zeroed via OPENSSL_cleanse when dropped

ct_eq — Constant-Time Equality

#![allow(unused)]
fn main() {
/// Compare two byte slices for equality in constant time.
///
/// Returns `true` if both slices have the same length and content.
/// Returns `false` without leaking which byte differed or at what index.
///
/// Backed by OpenSSL's `CRYPTO_memcmp`, which is resistant to
/// compiler dead-store elimination and branch-prediction side channels.
pub fn ct_eq(a: &[u8], b: &[u8]) -> bool;
}

Use ct_eq when comparing MAC tags, authentication tokens, or any value where a timing side channel could assist an attacker. Do not use == on secret byte slices in security-sensitive paths.

#![allow(unused)]
fn main() {
use native_ossl::util::ct_eq;

let expected_tag = /* HMAC output */;
let received_tag = /* from network */;

if !ct_eq(&expected_tag, &received_tag) {
    return Err("MAC verification failed");
}
}

SecretBuf — Secure Heap Buffer

#![allow(unused)]
fn main() {
pub struct SecretBuf { /* Vec<u8> */ }

impl SecretBuf {
    /// Wrap an existing allocation. The buffer will be securely zeroed on drop.
    pub fn new(data: Vec<u8>) -> Self;

    /// Allocate a zero-initialised buffer of `len` bytes.
    pub fn with_len(len: usize) -> Self;

    /// Copy `data` into a new secure buffer.
    pub fn from_slice(data: &[u8]) -> Self;

    /// Number of bytes in the buffer.
    pub fn len(&self) -> usize;

    /// `true` if the buffer holds no bytes.
    pub fn is_empty(&self) -> bool;

    /// Mutable byte slice. Write derived key material directly into this.
    pub fn as_mut_slice(&mut self) -> &mut [u8];
}

impl AsRef<[u8]> for SecretBuf { /* immutable byte slice */ }
impl Drop        for SecretBuf { /* OPENSSL_cleanse on the allocation */ }
unsafe impl Send for SecretBuf {}
unsafe impl Sync for SecretBuf {}
}

SecretBuf intentionally does not implement Copy or Clone. Both would defeat the zeroing guarantee: a clone creates a second copy of the secret material and the original’s Drop only zeroes the first allocation.

When to Use SecretBuf

Use SecretBuf when the buffer will hold key material, passwords, or any other secret that must not persist in memory after it is no longer needed.

For ordinary ciphertext or message data that is not secret in itself, a plain Vec<u8> is sufficient.

How OPENSSL_cleanse Differs from Rust’s Drop

Rust’s compiler may optimise away writes to memory that it can prove are dead before the allocation is freed. This “dead store elimination” can remove a zeroing loop added at the end of a function’s scope, leaving the secret in freed heap memory until it is overwritten by a future allocation.

OPENSSL_cleanse is specifically designed to resist this optimisation. OpenSSL implements it as a volatile write loop (or a platform memory-clearing intrinsic such as explicit_bzero) that the compiler cannot remove, regardless of optimisation level. This is the technique required by FIPS 140-3 for zeroization of sensitive data.

Examples

Allocate a Key Buffer and Fill with Random Data

use native_ossl::util::SecretBuf;
use native_ossl::rand::Rand;

let mut key = SecretBuf::with_len(32);
Rand::fill(key.as_mut_slice())?;
// Use key.as_ref() for read-only access.
// When `key` is dropped, OPENSSL_cleanse zeroes the 32 bytes.

Wrap KDF Output in a Secure Buffer

When deriving key material, write the output directly into a SecretBuf to ensure it is zeroed after use:

use native_ossl::util::SecretBuf;
use native_ossl::kdf::HkdfBuilder;
use native_ossl::digest::DigestAlg;

let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let ikm    = b"input key material";
let salt   = b"random salt";
let info   = b"context";

let mut derived = SecretBuf::with_len(32);
HkdfBuilder::new(&sha256)
    .key(ikm)
    .salt(salt)
    .info(info)
    .derive(derived.as_mut_slice())?;

// derived is now ready to use as a symmetric key.
// On drop it is cleared with OPENSSL_cleanse.

Wrap an Existing Allocation

If key material arrives as a Vec<u8> from a deserialization step, transfer ownership into a SecretBuf immediately:

use native_ossl::util::SecretBuf;

let raw_key: Vec<u8> = load_key_from_store()?;
let key = SecretBuf::new(raw_key);
// raw_key is consumed; only key owns the bytes now.

Design Notes

  • No Clone — cloning would create a second heap allocation containing the same secret. The original’s Drop would only clear the first copy. If a second reference is needed, use Arc<Mutex<SecretBuf>>.
  • No CopySecretBuf is not Copy for the same reason. Moving it transfers exclusive ownership; the destructor runs exactly once.
  • Zero-length buffersSecretBuf::new(vec![]) is valid. Drop skips the OPENSSL_cleanse call when the length is zero to avoid passing a null pointer to the C function.
  • from_slice copiesSecretBuf::from_slice allocates a new buffer and copies the input. If the input itself is sensitive, you are responsible for zeroing or dropping it promptly. Use from_slice when the caller holds a borrowed slice and needs an owned secure copy.

TLS

Overview

TLS involves three types:

  • SslCtx — shared configuration (certificates, ciphers, protocol version). Can be wrapped in Arc<SslCtx> and shared across threads.
  • Ssl — per-connection state. Exclusive ownership; not Clone.
  • SslSession — resumable session. Reference-counted; can be shared.

Protocol Version

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TlsVersion {
    Tls12 = 0x0303,
    Tls13 = 0x0304,
}
}

Verify Mode

#![allow(unused)]
fn main() {
pub struct SslVerifyMode(i32);

impl SslVerifyMode {
    pub const NONE: Self;                   // no peer certificate required
    pub const PEER: Self;                   // request peer certificate
    pub const FAIL_IF_NO_PEER_CERT: Self;   // require peer certificate

    pub fn or(self, other: Self) -> Self;
}
}

I/O Errors

#![allow(unused)]
fn main() {
pub enum SslIoError {
    WantRead,               // non-blocking: no data available yet
    WantWrite,              // non-blocking: write buffer full
    ZeroReturn,             // peer closed cleanly
    Syscall(ErrorStack),    // underlying I/O syscall error
    Ssl(ErrorStack),        // OpenSSL protocol error
    Other(i32),             // unexpected error code
}

impl std::error::Error for SslIoError {}
}

SslCtx — Shared Configuration

#![allow(unused)]
fn main() {
pub struct SslCtx { /* SSL_CTX* */ }

impl SslCtx {
    /// Generic context (TLS_method) — usable for both client and server.
    pub fn new() -> Result<Self, ErrorStack>;

    /// Client-only context (TLS_client_method).
    pub fn new_client() -> Result<Self, ErrorStack>;

    /// Server-only context (TLS_server_method).
    pub fn new_server() -> Result<Self, ErrorStack>;

    pub fn set_min_proto_version(&self, ver: TlsVersion) -> Result<(), ErrorStack>;
    pub fn set_max_proto_version(&self, ver: TlsVersion) -> Result<(), ErrorStack>;

    pub fn set_verify(&self, mode: SslVerifyMode);

    /// Set TLS 1.2 cipher list (colon-separated OpenSSL names).
    pub fn set_cipher_list(&self, list: &CStr) -> Result<(), ErrorStack>;

    /// Set TLS 1.3 ciphersuites.
    pub fn set_ciphersuites(&self, list: &CStr) -> Result<(), ErrorStack>;

    pub fn use_certificate(&self, cert: &X509) -> Result<(), ErrorStack>;
    pub fn use_private_key<T: HasPrivate>(&self, key: &Pkey<T>) -> Result<(), ErrorStack>;
    pub fn check_private_key(&self) -> Result<(), ErrorStack>;

    /// Load system default CA certificates.
    pub fn set_default_verify_paths(&self) -> Result<(), ErrorStack>;

    /// Disable session caching.
    pub fn disable_session_cache(&self);

    /// Create a new connection from this context.
    pub fn new_ssl(&self) -> Result<Ssl, ErrorStack>;
}

impl Clone for SslCtx { /* SSL_CTX_up_ref */ }
impl Drop  for SslCtx { /* SSL_CTX_free  */ }
unsafe impl Send for SslCtx {}
unsafe impl Sync for SslCtx {}
}

Ssl — Per-Connection State

#![allow(unused)]
fn main() {
pub struct Ssl { /* SSL* */ }

impl Ssl {
    /// Attach a single duplex BIO (rbio == wbio, e.g. a BIO pair end).
    pub fn set_bio_duplex(&mut self, bio: Bio);

    /// Attach separate read and write BIOs. Transfers ownership of both.
    pub fn set_bio(&mut self, rbio: Bio, wbio: Bio);

    /// Set the SNI hostname (calls SSL_ctrl directly; macro workaround).
    pub fn set_hostname(&mut self, hostname: &CStr) -> Result<(), ErrorStack>;

    pub fn set_connect_state(&mut self);
    pub fn set_accept_state(&mut self);

    pub fn connect(&mut self) -> Result<(), SslIoError>;
    pub fn accept(&mut self)  -> Result<(), SslIoError>;
    pub fn do_handshake(&mut self) -> Result<(), SslIoError>;

    pub fn read(&mut self, buf: &mut [u8]) -> Result<usize, SslIoError>;
    pub fn write(&mut self, buf: &[u8])    -> Result<usize, SslIoError>;

    pub fn shutdown(&mut self) -> Result<ShutdownResult, ErrorStack>;

    /// Peer's certificate (owned; X509_up_ref'd).
    pub fn peer_certificate(&self) -> Option<X509>;

    /// Peer's full certificate chain — leaf + intermediates (each X509_up_ref'd).
    /// Returns None before the handshake completes or when no peer cert was presented.
    pub fn peer_cert_chain(&self) -> Option<Vec<X509>>;

    /// Owned session (refcount bumped); suitable for storing beyond the connection.
    pub fn get1_session(&self) -> Option<SslSession>;

    /// Borrowed session (no refcount bump); valid only for the lifetime of `self`.
    pub fn session(&self) -> Option<BorrowedSslSession<'_>>;

    pub fn set_session(&mut self, session: &SslSession) -> Result<(), ErrorStack>;
}

impl Drop for Ssl { /* SSL_free */ }
unsafe impl Send for Ssl {}
// Ssl is !Clone and !Sync — exclusive ownership only
}

ShutdownResult

#![allow(unused)]
fn main() {
pub enum ShutdownResult {
    Sent,     // first close_notify sent; call shutdown() again to complete
    Complete, // bidirectional shutdown finished
}
}

SslSession

#![allow(unused)]
fn main() {
pub struct SslSession { /* SSL_SESSION* */ }

impl Clone for SslSession { /* SSL_SESSION_up_ref */ }
impl Drop  for SslSession { /* SSL_SESSION_free  */ }
unsafe impl Send for SslSession {}
unsafe impl Sync for SslSession {}
}

BorrowedSslSession<'ssl>

A borrowed view of the current TLS session, obtained from [Ssl::session].

#![allow(unused)]
fn main() {
pub struct BorrowedSslSession<'ssl> { /* ... */ }

// Derefs to SslSession — all read-only methods are available.
impl Deref for BorrowedSslSession<'_> { type Target = SslSession; }

unsafe impl Send for BorrowedSslSession<'_> {}
}

Unlike [Ssl::get1_session], which increments the reference count and returns an owned SslSession, Ssl::session calls SSL_get_session (the get0 variant) and wraps the result in a BorrowedSslSession that does not free the pointer when dropped. Use it when you only need to inspect the session within the scope of the connection.

#![allow(unused)]
fn main() {
// Borrow — no refcount bump, tied to the lifetime of `ssl`.
if let Some(sess) = ssl.session() {
    // use sess ...
} // sess is dropped here; the session pointer is NOT freed.

// Owned — refcount bumped; can be stored and passed to set_session later.
if let Some(sess) = ssl.get1_session() {
    new_ssl.set_session(&sess)?;
}
}

Examples

TLS Client

#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtx, TlsVersion, SslVerifyMode};
use native_ossl::bio::Bio;
use std::net::TcpStream;

let ctx = SslCtx::new_client()?;
ctx.set_min_proto_version(TlsVersion::Tls12)?;
ctx.set_verify(SslVerifyMode::PEER);
ctx.set_default_verify_paths()?;

let mut ssl = ctx.new_ssl()?;
ssl.set_hostname(c"example.com")?;

// Wrap a TCP stream with a memory BIO
// (attach the stream-backed BIO for a real connection)
ssl.set_connect_state();
ssl.connect()?;  // performs handshake

let request = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
ssl.write(request)?;

let mut buf = vec![0u8; 4096];
let n = ssl.read(&mut buf)?;
println!("{}", std::str::from_utf8(&buf[..n]).unwrap_or("(non-UTF-8)"));

ssl.shutdown()?;
}

TLS Server

#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtx, TlsVersion, SslVerifyMode};
use native_ossl::x509::X509;
use native_ossl::pkey::Pkey;

let cert = X509::from_pem(&std::fs::read("server.crt")?)?;
let key  = Pkey::<native_ossl::pkey::Private>::from_pem(&std::fs::read("server.key")?)?;

let ctx = SslCtx::new_server()?;
ctx.set_min_proto_version(TlsVersion::Tls12)?;
ctx.use_certificate(&cert)?;
ctx.use_private_key(&key)?;
ctx.check_private_key()?;

// For each accepted connection:
let mut ssl = ctx.new_ssl()?;
// ssl.set_bio(...);  // attach the accepted socket's BIO
ssl.set_accept_state();
ssl.accept()?;  // performs handshake
}

In-Memory TLS Handshake (Testing)

Use BIO_new_bio_pair to create two linked in-memory BIOs for testing without a real socket:

#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtx, TlsVersion};
use native_ossl::bio::Bio;

// Set up client and server contexts
let client_ctx = SslCtx::new_client()?;
client_ctx.set_min_proto_version(TlsVersion::Tls12)?;

let server_ctx = SslCtx::new_server()?;
server_ctx.use_certificate(&cert)?;
server_ctx.use_private_key(&key)?;
server_ctx.check_private_key()?;

let mut client = client_ctx.new_ssl()?;
let mut server = server_ctx.new_ssl()?;

// Create paired in-memory BIOs.
// Data written to bio1 is readable from bio2, and vice versa.
let (bio1, bio2) = Bio::new_bio_pair()?;
client.set_bio_duplex(bio1);
server.set_bio_duplex(bio2);

// Drive the handshake until both sides report success.
loop {
    let c = client.do_handshake();
    let s = server.do_handshake();
    match (c, s) {
        (Ok(()), Ok(())) => break,
        (Err(native_ossl::ssl::SslIoError::WantRead), _) => continue,
        (_, Err(native_ossl::ssl::SslIoError::WantRead)) => continue,
        (Err(e), _) | (_, Err(e)) => return Err(e.into()),
    }
}

// Exchange application data.
client.write(b"ping")?;
let mut buf = [0u8; 4];
server.read(&mut buf)?;
assert_eq!(&buf, b"ping");
}

Fluent Builders

SslCtxBuilder<R> is a typestate fluent builder for constructing a [SslCtx]. The type parameter R (either Server or Client) restricts which methods are available, ensuring that server-only methods (e.g. certificate, private_key) cannot be called on a client builder and vice versa.

Server Builder

#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtxBuilder, Server, TlsVersion};
use native_ossl::x509::X509;
use native_ossl::pkey::Pkey;

let cert = X509::from_pem(&std::fs::read("server.crt")?)?;
let key  = Pkey::<native_ossl::pkey::Private>::from_pem(&std::fs::read("server.key")?)?;

let ctx = SslCtxBuilder::<Server>::new()?
    .min_proto_version(TlsVersion::Tls12)?
    .max_proto_version(TlsVersion::Tls13)?
    .certificate(&cert)?
    .private_key(&key)?
    .check_private_key()?
    // Optionally request a client certificate:
    // .verify_client(false)   // request but not mandatory
    // .verify_client(true)    // require
    .build()?;
}

Client Builder

#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtxBuilder, Client, TlsVersion, HostnameFlags};

let ctx = SslCtxBuilder::<Client>::new()?
    .min_proto_version(TlsVersion::Tls12)?
    .default_ca_paths()?
    .verify_peer()
    .verify_hostname("example.com")?
    // Restrict wildcard matching — no partial wildcards like *.example.*:
    .verify_hostname_flags(HostnameFlags::NO_PARTIAL_WILDCARDS)?
    // Advertise HTTP/2 and HTTP/1.1 via ALPN:
    .alpn_protos_list(&["h2", "http/1.1"])?
    .build()?;
}

ALPN: alpn_protos_list vs alpn_protocols

alpn_protos_list takes a &[&str] and encodes the protocol names into the OpenSSL wire format (each name prefixed by its length as a single byte) before calling SSL_CTX_set_alpn_protos. Protocol names longer than 255 bytes are rejected.

alpn_protocols is the lower-level variant that accepts the pre-encoded wire format directly (e.g. b"\x02h2\x08http/1.1"). Use it when you need to share a pre-built buffer across multiple contexts.

Hostname verification: builder vs per-connection

SslCtxBuilder::<Client>::verify_hostname("host") sets the hostname on the context-level X509_VERIFY_PARAM. Every Ssl connection derived from that context inherits the check automatically.

When one SslCtx is reused across connections to different servers, call Ssl::set_verify_hostname on each per-connection object instead:

#![allow(unused)]
fn main() {
let ctx = SslCtxBuilder::<Client>::new()?
    .default_ca_paths()?
    .verify_peer()
    .build()?;

// Connection to server A
let mut ssl_a = ctx.new_ssl()?;
ssl_a.set_verify_hostname("a.example.com")?;

// Connection to server B
let mut ssl_b = ctx.new_ssl()?;
ssl_b.set_verify_hostname("b.example.com")?;
}

Wildcard control with verify_hostname_flags

verify_hostname_flags fine-tunes how wildcard certificates are matched. It is typically called after verify_hostname and applies to all connections derived from the context.

#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtxBuilder, Client, HostnameFlags};

let ctx = SslCtxBuilder::<Client>::new()?
    .default_ca_paths()?
    .verify_peer()
    .verify_hostname("example.com")?
    // Disallow *.example.* style multi-label wildcards.
    .verify_hostname_flags(HostnameFlags::NO_PARTIAL_WILDCARDS)?
    .build()?;
}

Available flags:

ConstantValueEffect
HostnameFlags::NONE0x0Default OpenSSL behaviour
HostnameFlags::NO_PARTIAL_WILDCARDS0x4Disallow *.example.* style wildcards
HostnameFlags::MULTI_LABEL_WILDCARDS0x8Allow wildcards matching multiple labels

Flags can be combined with .or():

#![allow(unused)]
fn main() {
let flags = HostnameFlags::NO_PARTIAL_WILDCARDS.or(HostnameFlags::MULTI_LABEL_WILDCARDS);
builder.verify_hostname_flags(flags)?;
}

Builder API reference

MethodAvailable onDescription
min_proto_version(ver)BothMinimum TLS version
max_proto_version(ver)BothMaximum TLS version
cipher_list(list)BothTLS 1.2 cipher list
ciphersuites(list)BothTLS 1.3 ciphersuites
session_cache_mode(mode)BothSession cache mode (0 to disable)
alpn_protocols(bytes)BothALPN list in wire format
alpn_protos_list(names)BothALPN list from string slice
build()BothConsume builder, produce SslCtx
certificate(cert)ServerLeaf certificate to present
add_chain_cert(cert)ServerAppend intermediate CA to chain
private_key(key)ServerPrivate key for the certificate
check_private_key()ServerVerify cert/key consistency
verify_client(require)ServerRequest/require client certificate
default_ca_paths()ClientLoad system CA bundle
ca_bundle_file(path)ClientLoad CA certificates from PEM file
ca_cert(cert)ClientAdd one CA certificate to trust store
verify_peer()ClientEnable server certificate verification
verify_hostname(host)ClientContext-level hostname check
verify_hostname_flags(flags)ClientWildcard matching flags (combine with verify_hostname)

Design Notes

  • SslCtx methods take &self — the underlying C functions take SSL_CTX* but do not invalidate shared state. &self allows multiple threads to configure from the same Arc<SslCtx>.
  • Protocol version macrosSSL_CTX_set_min_proto_version and SSL_CTX_set_max_proto_version are C macros and cannot be exposed by bindgen. The implementation calls SSL_CTX_ctrl directly with the numeric ctrl codes.
  • SNI is also a macroSSL_set_tlsext_host_name is likewise a C macro. The implementation calls SSL_ctrl with ctrl code 55.
  • set_bio transfers ownership — after set_bio or set_bio_duplex, the BIO is owned by the SSL object. The Rust wrapper uses mem::forget to avoid a double-free.
  • Ssl is Send but !Sync — exclusive ownership makes moving it to another thread safe, but sharing a reference without a mutex is not.
  • Session resumption — save the session with ssl.get1_session() after a successful handshake, then pass it to ssl.set_session(...) for the next connection to the same server.
  • Borrowed vs owned sessionssl.session() calls SSL_get_session (the get0 variant), returning a BorrowedSslSession<'_> that is tied to the connection’s lifetime and does not free the pointer on drop. ssl.get1_session() calls SSL_get1_session (the get1 variant), bumps the reference count, and returns an owned SslSession that can outlive the connection. Prefer the borrow when you only need to inspect the session in place.
  • SslCtxBuilder is Send but !Sync — the builder holds an exclusive SSL_CTX*; moving it between threads is safe, but concurrent access via & reference is not (no Arc is involved).
  • SSL_CTX_set_alpn_protos inverted return — this OpenSSL function returns 0 on success and non-zero on failure, opposite to most OpenSSL functions. The builder checks for this correctly.
  • peer_cert_chain vs peer_certificatepeer_certificate wraps SSL_get0_peer_certificate and always returns the peer’s leaf certificate. peer_cert_chain wraps SSL_get_peer_cert_chain and returns the entire chain (leaf + intermediates) as a Vec<X509> with each element independently ref-counted. Note: on the server side OpenSSL’s SSL_get_peer_cert_chain omits the client leaf from the returned stack — use peer_certificate to obtain the leaf in all cases.

FIPS

Overview

OpenSSL’s FIPS support is delivered by the FIPS provider — a separate shared library that implements a validated cryptographic module. Native-ossl exposes two distinct levels of FIPS integration:

  1. Running code in FIPS mode — ordinary application code that uses the FIPS provider for its cryptography. No special feature flag is required.
  2. Implementing a FIPS provider — writing a new FIPS provider in Rust that hooks into OpenSSL’s internal provider API. This requires the fips-provider Cargo feature and non-public OpenSSL headers.

This guide covers level 1. Level 2 is a future capability gated behind the fips-provider feature.

Checking Whether the FIPS Provider Is Active

#![allow(unused)]
fn main() {
use native_ossl::fips;

if fips::is_running(None) {
    println!("FIPS provider is loaded in the default library context");
}
}

fips::is_running wraps OSSL_PROVIDER_available. It returns true if the FIPS provider is currently loaded and available in the specified library context.

Signature

#![allow(unused)]
fn main() {
pub fn is_running(libctx: Option<&Arc<LibCtx>>) -> bool
}

Pass None to query the default (process-wide) library context. Pass Some(ctx) to query a specific isolated LibCtx:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use native_ossl::lib_ctx::LibCtx;
use native_ossl::fips;

let ctx = Arc::new(LibCtx::new()?);
// Load FIPS and base providers into the isolated context.
// ...

if fips::is_running(Some(&ctx)) {
    // The FIPS provider is active in this isolated context.
}
}

is_running is always compiled — it requires no feature flag. It is safe to call even when the FIPS provider is not installed; it simply returns false.

Loading the FIPS Provider

is_running only reports whether the provider is already loaded. To load it, use LibCtx and Provider:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use native_ossl::lib_ctx::{LibCtx, Provider};
use native_ossl::fips;

let ctx = Arc::new(LibCtx::new()?);
let _fips = Provider::load(&ctx, c"fips")?;
let _base = Provider::load(&ctx, c"base")?;

assert!(fips::is_running(Some(&ctx)));
}

Loading the base provider alongside fips is required — base supplies encoding, decoding, and other non-cryptographic primitives that the FIPS provider does not implement.

Using FIPS-Approved Algorithms

Once the FIPS provider is loaded into a LibCtx, fetch algorithms from that context to ensure FIPS-approved implementations are used:

#![allow(unused)]
fn main() {
use native_ossl::digest::DigestAlg;
use native_ossl::pkey::Pkey;

// Fetch SHA-256 from the FIPS context — guaranteed to use the FIPS provider.
let sha256 = DigestAlg::fetch_in(&ctx, c"SHA2-256", None)?;

// Load a key within the FIPS context.
let key = Pkey::<native_ossl::pkey::Private>::from_pem_in(&ctx, pem_bytes)?;
}

Property strings can further restrict algorithm selection:

#![allow(unused)]
fn main() {
// Require fips=yes explicitly on the algorithm fetch.
let sha256 = DigestAlg::fetch_in(&ctx, c"SHA2-256", Some(c"fips=yes"))?;
}

FIPS Mode vs. Implementing a FIPS Provider

These two use cases are often confused:

ScenarioDescriptionFeature required
Running in FIPS modeApplication uses the FIPS provider for cryptographyNone
Implementing a FIPS providerWriting a new provider that exposes a FIPS-validated modulefips-provider

Most applications fall into the first category. They load the FIPS provider, verify it is active with fips::is_running, and then use normal DigestAlg, Pkey, and other types — the FIPS provider enforces algorithm restrictions transparently.

The fips-provider Feature

The fips-provider Cargo feature is declared in both native-ossl and native-ossl-sys. It is intended for code that implements a FIPS provider from Rust — accessing OpenSSL’s internal provider vtable, PROV_CTX helpers, and ossl_prov_is_running.

[dependencies]
native-ossl = { version = "0.1", features = ["fips-provider"] }

This feature is not needed for ordinary application code that runs in FIPS mode. Enable it only if you are writing a FIPS provider implementation.

What fips-provider provides

Enabling this feature performs a second bindgen pass against the non-public OpenSSL headers (pointed to by OPENSSL_SOURCE_DIR) and exposes:

  • ProviderSignatureCtx — vtable-based signature context that calls EVP_SIGNATURE function pointers directly, bypassing EVP_DigestSign* (which would cause a circular provider dependency inside a FIPS module).
  • prov_ctx_new / prov_ctx_free / prov_ctx_set_handle / prov_ctx_set_libctx / prov_ctx_get_libctx — wrappers around ossl_prov_ctx_*.
  • check_state_ok / set_error_stateossl_prov_is_running and ossl_set_error_state for provider self-test reporting.
  • Pkey<Private>::keydata() — reads the void *keydata pointer from an EVP_PKEY, needed to pass provider-side key material to vtable functions.
  • ProvCtx / OSSL_CORE_HANDLE — type aliases re-exported from native_ossl_sys::fips_internal.

These APIs require the OpenSSL source tree because they are declared in non-public headers (crypto/evp.h, prov/provider_ctx.h, internal/provider.h). Set OPENSSL_SOURCE_DIR to the root of the OpenSSL source tree before building:

OPENSSL_SOURCE_DIR=~/src/openssl cargo build --features fips-provider

Platform note: The fips-provider feature is currently verified for x86_64 Linux only. The hand-written EVP_SIGNATURE vtable layout and EVP_PKEY::keydata offset are confirmed against OpenSSL 3.5.x on x86_64; compile-time offsetof assertions for other architectures are planned.

Testing with the FIPS Provider

Install the FIPS provider for your system:

# Fedora / RHEL
sudo dnf install openssl-fips-provider

# Verify it is loadable
openssl list -providers -provider fips

Run FIPS-related tests:

cargo test

No feature flag is required for FIPS tests. Tests that require the FIPS provider call fips::is_running at runtime and skip themselves if the provider is not installed. The fips-provider Cargo feature is unrelated — it is for writing a FIPS provider implementation, not for using one.

Architecture

Crate Layout

native-ossl/                   ← workspace root
├── Cargo.toml                 ← [workspace] members, shared lint config
│
├── native-ossl-sys/           ← raw FFI layer (unsafe only)
│   ├── Cargo.toml             ← links = "ssl"
│   ├── build.rs               ← pkg-config + bindgen + version gate
│   └── src/lib.rs             ← #![allow(...)] + include!(bindings.rs)
│
├── native-ossl/               ← safe Rust wrappers (public API)
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs             ← pub use; pedantic lints active
│       ├── error.rs           ← Error, ErrorStack, ossl_call!, ossl_ptr!
│       ├── lib_ctx.rs         ← LibCtx (OSSL_LIB_CTX), Provider
│       ├── params.rs          ← ParamBuilder<'_>, Params<'_>
│       ├── bio.rs             ← MemBio, MemBioBuf<'a>, Bio
│       ├── digest.rs          ← DigestAlg, DigestCtx
│       ├── cipher.rs          ← CipherAlg, CipherCtx<Dir>, AeadEncryptCtx, AeadDecryptCtx
│       ├── mac.rs             ← MacAlg, MacCtx, HmacCtx, CmacCtx
│       ├── fips.rs            ← fips::is_running
│       ├── pkey.rs            ← Pkey<T>, SignInit, Signer, Verifier,
│       │                           RawSigner, RawVerifier,
│       │                           SigAlg, MessageSigner, MessageVerifier,
│       │                           DeriveCtx, KeygenCtx,
│       │                           PkeyEncryptCtx, PkeyDecryptCtx,
│       │                           EncapCtx, DecapCtx
│       ├── kdf.rs             ← KdfAlg, KdfCtx, HkdfBuilder, Pbkdf2Builder, ScryptBuilder,
│       │                           SshkdfBuilder, KbkdfBuilder
│       ├── rand.rs            ← Rand, RandAlg, RandCtx, RandState
│       ├── util.rs            ← SecretBuf
│       ├── x509.rs            ← X509, X509Name, X509NameOwned, X509Builder,
│       │                           X509Store, X509StoreCtx, X509Crl
│       ├── pkcs12.rs          ← Pkcs12
│       ├── ocsp.rs            ← OcspCertId, OcspRequest, OcspResponse, OcspBasicResp,
│       │                           OcspResponseStatus, OcspCertStatus, OcspSingleStatus
│       └── ssl.rs             ← SslCtx, Ssl, SslSession, SslIoError
│
└── examples/
    └── encrypt-demo/          ← hybrid RSA-OAEP + AES-256-GCM demo

Module Dependency Order

native-ossl-sys   (FFI; no safe abstractions)
       ↑
native-ossl
  ├── error       (no deps within crate)
  ├── lib_ctx     → error
  ├── params      → error
  ├── bio         → error
  ├── util        → error (OPENSSL_cleanse)
  ├── digest      → error, params, lib_ctx
  ├── cipher      → error, params, lib_ctx
  ├── mac         → error, params, lib_ctx, digest (for HmacCtx)
  ├── rand        → error, params, lib_ctx
  ├── fips        → lib_ctx
  ├── pkey        → error, params, bio, lib_ctx, digest (for Signer/Verifier)
  ├── kdf         → error, params, digest
  ├── x509        → error, params, bio, pkey, lib_ctx
  ├── pkcs12      → error, bio, pkey, x509
  ├── ocsp        → error, x509
  └── ssl         → error, params, bio, pkey, x509, lib_ctx

Build and review modules in this order when extending the crate.

Guiding Principles

P1 — Own or Borrow, Never Copy Unnecessarily

The C API takes (const void *data, size_t len). Rust &[u8] maps directly with no intermediate allocation. Input data is always borrowed (&[u8]). Output is either:

  • caller-provided &mut [u8] — zero-copy into caller’s memory, or
  • freshly allocated Vec<u8> — only when the output size is not known at the call site.

P2 — Ownership Dictated by Whether _up_ref Exists

C typeHas _up_ref?Rust ownership
EVP_MD_CTXNoExclusive; !Clone
EVP_CIPHER_CTXNoExclusive; !Clone
EVP_PKEY_CTXNoExclusive; !Clone
EVP_MAC_CTXNoExclusive; dup() ok
EVP_RAND_CTXYesClone via up_ref
BIOYesClone via up_ref
X509YesClone via up_ref
X509_STOREYesClone via up_ref
X509_CRLYesClone via up_ref
EVP_PKEYYesClone via up_ref
SSL_CTXYesClone via up_ref
SSL_SESSIONYesClone via up_ref
EVP_MD (alg)Yes (fetch-owned)Clone via up_ref
EVP_CIPHER (alg)Yes (fetch-owned)Clone via up_ref
EVP_SIGNATURE (alg)Yes (fetch-owned)Clone via up_ref
OSSL_LIB_CTXNo public APIArc<LibCtx>

Stateful contexts without _up_ref must never be shared. All mutating operations take &mut self.

P3 — Typestate for Compile-Time Correctness

Where the C API has distinct init functions that lock in a direction or key type, Rust uses zero-sized marker types:

#![allow(unused)]
fn main() {
pub struct Encrypt;
pub struct Decrypt;
pub struct CipherCtx<Dir> { /* ... */ }
}

Calling EVP_EncryptUpdate on a decrypt context is undefined behaviour in C. CipherCtx<Encrypt> vs CipherCtx<Decrypt> prevents this at compile time.

Key visibility uses a sealed trait hierarchy:

HasPrivate: HasPublic: HasParams
    │           │           │
  Private     Public      Params

A Pkey<Private> is accepted wherever HasPublic or HasParams is required. You cannot extract private material from a Pkey<Public>.

P4 — Query Before Create

Algorithm descriptors expose metadata before any stateful context is allocated:

#![allow(unused)]
fn main() {
let alg = DigestAlg::fetch(c"SHA2-256", None)?;
let size = alg.output_len();     // query before allocating
let mut buf = vec![0u8; size];
let mut ctx = alg.new_context()?;
ctx.update(data)?;
ctx.finish(&mut buf)?;
}

P5 — Named Structs for Multi-Output and Multi-Parameter APIs

When a function would take more than ~4 parameters (triggering clippy::too_many_arguments) or return a tuple of two or more values, introduce a named struct. These lints are never suppressed; the fix is always a struct:

#![allow(unused)]
fn main() {
// Instead of -> Result<(Vec<u8>, Vec<u8>), ErrorStack>:
pub struct EncapResult {
    pub wrapped_key:   Vec<u8>,
    pub shared_secret: Vec<u8>,
}
pub fn encapsulate(&mut self) -> Result<EncapResult, ErrorStack>;

// Instead of fn generate(out, strength, pred_res, adin):
pub struct GenerateRequest<'a> {
    pub strength: u32,
    pub prediction_resistance: bool,
    pub additional_input: Option<&'a [u8]>,
}
}

P6 — Algorithm Names as &CStr

OpenSSL 3.x identifies algorithms by name string, not numeric IDs. Rust uses:

  • c"SHA2-256" literal syntax (Rust 1.77+) at call sites
  • None for the default property query
  • Explicit property strings (c"fips=yes") when isolation is required

P7 — Error Propagation via ErrorStack

Every public function that calls into OpenSSL returns Result<T, ErrorStack>. On failure, the full per-thread OpenSSL error queue is drained into ErrorStack. No synchronization is needed because the queue is thread-local.

Build System

native-ossl-sys/build.rs

  1. pkg_config::Config::new().atleast_version("3.0.7").probe("openssl") — locate the system OpenSSL and verify the minimum version.
  2. bindgen::Builder — generate bindings from <openssl/evp.h>, <openssl/err.h>, <openssl/x509.h>, <openssl/ssl.h>, etc.
  3. OsslCallbacks::int_macro — reads OPENSSL_VERSION_NUMBER from the C macro and panics if the version is below 3.0.7. Emits version-gating cfg flags for the version thresholds above the floor (see Version Policy table below).
  4. OsslCallbacks::str_macro — inspects string constant names to detect algorithm-presence flags: OSSL_PKEY_PARAM_SLH_DSA_SEEDossl_slhdsa, OSSL_PKEY_PARAM_ML_DSA_SEEDossl_mldsa, OSSL_PKEY_PARAM_ML_KEM_SEEDossl_mlkem.
  5. OsslCallbacks::func_macro — detects OSSL_PARAM_clear_freeparam_clear_free.
  6. allowlist_function / allowlist_type — generate only what native-ossl uses, keeping the output small and the compilation fast.
  7. Each emitted cfg flag is also emitted as cargo::metadata=FLAG=1. These propagate to dependent crates as DEP_SSL_FLAG environment variables via the links = "ssl" manifest key.

The allowlists include:

  • EVP_SIGNATURE_fetch, EVP_SIGNATURE_free, EVP_SIGNATURE_up_ref, and the EVP_SIGNATURE type — required for SigAlg.
  • Raw sign/verify init + sign/verify (EVP_PKEY_sign_init, EVP_PKEY_sign, EVP_PKEY_verify_init, EVP_PKEY_verify) — required for RawSigner / RawVerifier.
  • Full sign_message_* / verify_message_* family and EVP_PKEY_CTX_set_signature — required for MessageSigner / MessageVerifier.
  • ERR_set_mark and ERR_pop_to_mark — used by the supports_streaming probe to keep the error queue clean.
  • PKCS12_*, d2i_PKCS12_bio, i2d_PKCS12_bio — required for Pkcs12.
  • OCSP_REQUEST_*, OCSP_RESPONSE_*, OCSP_BASICRESP_*, OCSP_basic_verify, d2i_OCSP_REQUEST, i2d_OCSP_REQUEST, d2i_OCSP_RESPONSE, i2d_OCSP_RESPONSE — required for OcspRequest / OcspResponse.
  • X509_STORE_*, X509_STORE_CTX_*, X509_CRL_* — required for X509Store, X509StoreCtx, and X509Crl.
  • OPENSSL_cleanse — required for SecretBuf.

native-ossl/build.rs

Re-emits all version and feature cfg flags for the safe wrapper crate:

  1. Declares all eight flag names with cargo::rustc-check-cfg so the unexpected_cfgs lint is satisfied regardless of installed OpenSSL version.
  2. Reads each DEP_SSL_* env var (forwarded by Cargo from native-ossl-sys via cargo::metadata) and re-emits the corresponding cargo::rustc-cfg flag.

Version Policy

Minimum: OpenSSL 3.0.7. Eight cfg flags are emitted:

Version flags (from OPENSSL_VERSION_NUMBER in int_macro):

cfg flagConditionAPIs gated
ossl307always (≥ 3.0.7 floor)baseline; signals minimum is met
ossl310>= 0x3010_0000Clone for RandCtx
ossl320>= 0x3020_0000MessageSigner, MessageVerifier, SigAlg, EncapCtx, DecapCtx, GlobalRandCtx, RandCtx::public, RandCtx::private_global, ErrState
ossl350>= 0x3050_0000SshkdfBuilder, SshkdfKeyType, KbkdfBuilder, KbkdfMode, KbkdfCounterLen
ossl_v400>= 0x4000_0000DigestCtx::serialize_size, DigestCtx::serialize, DigestCtx::deserialize

Algorithm-presence flags (from str_macro / func_macro; present only when the corresponding algorithm family is compiled into the installed OpenSSL):

cfg flagDetected viaMeaning
ossl_slhdsaOSSL_PKEY_PARAM_SLH_DSA_SEED presentSLH-DSA available
ossl_mldsaOSSL_PKEY_PARAM_ML_DSA_SEED presentML-DSA available
ossl_mlkemOSSL_PKEY_PARAM_ML_KEM_SEED presentML-KEM available
param_clear_freeOSSL_PARAM_clear_free macro presentsecure param clearing available

Algorithm flags are inferred from the header macros present at bindgen time rather than from version numbers, so they remain accurate when a distribution backports or forward-ports individual algorithm families.

Cargo Features

FeatureDefaultEffect
dynamicyesLink against system OpenSSL
vendorednoBuild from KRYOPTIC_OPENSSL_SOURCES or NATIVE_OSSL_OPENSSL_SOURCES env
fipsnoLink libfips.a instead of libcrypto.a
fips-providernoEnable bindings for non-public provider-internal headers (x86_64 Linux)

fips-provider is declared in both native-ossl and native-ossl-sys. It is intended for code that implements a FIPS provider, not for ordinary code that runs in FIPS mode. fips::is_running is always available without this feature.

Lint Configuration

Pedantic Clippy lints are active across the entire workspace. The only exception is native-ossl-sys/src/lib.rs, which suppresses all lints on bindgen-generated code.

Two lints are never suppressed: too_many_arguments and type_complexity. The correct fix is always a named struct with Default. See principle P5.

#[must_use] is applied to:

  • Terminal builder methods (build(), derive(), finish())
  • Property query methods that return primitives
  • Constructors and allocating functions

Known Scope Limitations (v0.1)

The following are out of scope for the initial release:

  • QUIC — separate connection model
  • Engines — deprecated in OpenSSL 3.x; use providers
  • Async I/O — Ssl is synchronous; async wrappers are a separate crate concern
  • Custom provider development in Rust (fips-provider feature is a future stub)
  • Structured extension parsing for X509Extension — raw DER bytes only
  • HTTP transport for OCSP — the caller is responsible for fetching the DER-encoded response; OcspResponse handles parsing and verification only

Safety Model

Contract

Every unsafe block in native-ossl must be accompanied by a // SAFETY: comment explaining:

  1. Why the raw pointer is non-null at that point
  2. Why the lifetime of any borrowed pointers is correctly bounded
  3. Why the operation is free of data races

Template:

#![allow(unused)]
fn main() {
// SAFETY:
// - self.ptr is non-null (invariant upheld by constructor)
// - data slice outlives this call (borrow checker enforces via &self lifetime)
// - &mut self ensures exclusive access to the context
let rc = unsafe { sys::EVP_DigestUpdate(self.ptr, data.as_ptr().cast(), data.len()) };
}

Inline safety comments that are vague (“safe because we checked”) or missing are treated as defects and are rejected in review.

Pointer Invariants

Every wrapper struct maintains the invariant that its raw pointer is non-null and valid for the object’s lifetime. This is established by constructors and never violated by wrapper methods.

#![allow(unused)]
fn main() {
pub fn new() -> Result<Self, ErrorStack> {
    let ptr = unsafe { sys::EVP_MD_CTX_new() };
    if ptr.is_null() {
        return Err(ErrorStack::drain());
    }
    // From this point on, ptr is guaranteed non-null.
    Ok(DigestCtx { ptr })
}
}

No public method takes an Option<*mut T> or nullable pointer. All NULL-return C functions are checked immediately and converted to Err(ErrorStack::drain()).

Ownership Rules

Objects with _up_ref

When a C type has an _up_ref function, Rust wrappers use Clone to share ownership:

#![allow(unused)]
fn main() {
impl Clone for DigestAlg {
    fn clone(&self) -> Self {
        // SAFETY: self.ptr is non-null (constructor invariant).
        // EVP_MD_up_ref increments the refcount; both clones free in Drop.
        unsafe { sys::EVP_MD_up_ref(self.ptr) };
        DigestAlg { ptr: self.ptr }
    }
}

impl Drop for DigestAlg {
    fn drop(&mut self) {
        // SAFETY: self.ptr is non-null (constructor invariant).
        unsafe { sys::EVP_MD_free(self.ptr) };
    }
}
}

Both sides call free in Drop; OpenSSL releases the object when the count reaches zero. The Rust borrow checker ensures no use-after-free within safe code.

Objects without _up_ref

Stateful contexts (EVP_MD_CTX, EVP_CIPHER_CTX, etc.) have no up_ref. They are !Clone and all mutating methods take &mut self:

#![allow(unused)]
fn main() {
// EVP_MD_CTX is !Clone — the type system enforces exclusive ownership.
pub struct DigestCtx {
    ptr: *mut sys::EVP_MD_CTX,
}
}

This mirrors the OpenSSL threading contract: do not call EVP_DigestUpdate from two threads simultaneously on the same EVP_MD_CTX.

BIO Ownership and Transfer

SSL_set_bio transfers ownership of the BIO to the SSL object. The Rust wrapper uses std::mem::forget to relinquish its own Drop obligation:

#![allow(unused)]
fn main() {
pub fn set_bio_duplex(&mut self, bio: Bio) {
    let ptr = bio.as_ptr();
    std::mem::forget(bio);  // transfer ownership to OpenSSL
    // SAFETY:
    // - self.ptr is non-null (constructor invariant)
    // - ptr is non-null (Bio::as_ptr invariant)
    // - mem::forget prevents the Rust Drop from running, avoiding double-free
    unsafe { sys::SSL_set_bio(self.ptr, ptr, ptr) };
}
}

This pattern is also used by X509Builder::build() to transfer the X509* from builder to the returned X509 without running the builder’s Drop.

Borrowed Pointers: Lifetime Binding

OpenSSL’s get0_* functions return internal borrowed pointers. These are exposed as Rust references with a lifetime bound to the owning object:

#![allow(unused)]
fn main() {
/// Subject distinguished name. Borrowed — valid while `self` is alive.
pub fn subject_name(&self) -> X509Name<'_> {
    // SAFETY:
    // - self.ptr is non-null (constructor invariant)
    // - X509_get_subject_name returns a pointer valid for the lifetime of self
    // - X509Name carries PhantomData<&'cert X509> to enforce the lifetime
    let ptr = unsafe { sys::X509_get_subject_name(self.ptr) };
    X509Name { ptr, _owner: PhantomData }
}
}

The PhantomData field ties the borrowed value’s lifetime to the parent, so the compiler rejects any attempt to hold the X509Name past the owning X509.

Error Macros

Two internal macros centralise the pattern of calling a C function and draining the error queue on failure:

#![allow(unused)]
fn main() {
/// For functions that return 1 on success and 0 on failure.
macro_rules! ossl_call {
    ($expr:expr) => {{
        let rc = unsafe { $expr };
        if rc != 1 {
            return Err($crate::error::ErrorStack::drain());
        }
    }};
}

/// For functions that return NULL on failure.
macro_rules! ossl_ptr {
    ($expr:expr) => {{
        let ptr = unsafe { $expr };
        if ptr.is_null() {
            return Err($crate::error::ErrorStack::drain());
        }
        ptr
    }};
}
}

Using these macros prevents accidental omission of the error-queue drain. If the queue is not drained, stale errors accumulate and corrupt the output of future calls.

Thread Safety

Send and Sync annotations

All wrapper types implement Send where the C object may safely be moved between threads. Sync is implemented where the C object can be read concurrently.

Algorithm descriptors (DigestAlg, CipherAlg, MacAlg) are Send + Sync: OpenSSL algorithm objects are read-only after fetching.

Stateful contexts (DigestCtx, CipherCtx<Dir>, MacCtx) are Send but not Sync: they can be moved between threads, but concurrent mutation is not allowed.

SslCtx is Send + Sync: SSL_CTX_* setup functions are thread-safe when called before any SSL* objects are created from the context.

Ssl is Send but not Sync: a connection is moved to a thread, but sharing across threads without a mutex is not permitted.

Error queue is thread-local

OpenSSL’s error queue is per-thread. ErrorStack::drain() is always called on the same thread that made the failing C call. If you need to transport an error across a thread boundary, use ErrState:

#![allow(unused)]
fn main() {
let handle = std::thread::spawn(|| {
    let result = some_crypto_op();
    let state = native_ossl::error::ErrState::capture();
    (result, state)
});
let (result, state) = handle.join().unwrap();
state.restore_and_drain();
if let Err(e) = result { eprintln!("{e}"); }
}

Integer Casts

Rust as casts between integer types can silently truncate. The codebase uses explicit conversions:

  • usize to i32 (for RAND_bytes length): use i32::try_from(len).expect(...), or split into multiple calls if len > i32::MAX.
  • usize to c_int (for various length parameters): use libc::c_int::try_from.
  • C int return values that represent lengths: check for < 0 before converting to usize.

Never use as usize on a value that could be negative (e.g. EVP_MD_get_size returns int; check for > 0 before the cast).

Testing Safety Properties

  • Miri: running cargo miri test detects use-after-free, invalid pointer dereferences, and incorrect unsafe patterns in pure-Rust tests. C calls are not executed under Miri (they are replaced with stubs), but the Rust-side wrapper logic is validated.
  • Valgrind: valgrind --leak-check=full cargo test detects OpenSSL-side memory leaks and use-after-free that Miri cannot reach.
  • ASAN/UBSAN: compile with RUSTFLAGS="-Z sanitizer=address" under nightly to catch buffer overflows and undefined behaviour at the C boundary.

Zero-Copy Patterns

What “Zero-Copy” Means Here

In this crate, zero-copy means no intermediate byte buffer is allocated between the caller’s memory and the OpenSSL C API boundary. It does not mean OpenSSL never copies — OpenSSL always builds its own internal representation of keys, certificates, and algorithm state. The goal is to eliminate unnecessary Rust-side copies before or after the C call.

Tier 1 — Fully Zero-Copy

These APIs pass the caller’s buffer pointer directly to OpenSSL and write back into caller-provided memory with no intermediate allocation.

APIC functionDirection
DigestCtx::update(data)EVP_DigestUpdateread from caller
DigestCtx::finish(out)EVP_DigestFinal_exwrite into caller
DigestCtx::serialize(out) 1EVP_MD_CTX_serializewrite into caller
DigestCtx::deserialize(data) 1EVP_MD_CTX_deserializeread from caller
DigestAlg::digest(data, out)EVP_Digestboth
CipherCtx::update(input, output)EVP_Encrypt/DecryptUpdateboth
CipherCtx::finalize(output)EVP_Encrypt/DecryptFinal_exwrite into caller
AeadEncryptCtx::set_aad(aad)EVP_EncryptUpdate(out=NULL)read only
AeadEncryptCtx::tag(tag)EVP_CIPHER_CTX_ctrl GCM_GET_TAGwrite into caller
AeadDecryptCtx::set_tag(tag)EVP_CIPHER_CTX_ctrl GCM_SET_TAGread from caller
MacCtx::update(data)EVP_MAC_updateread from caller
MacCtx::finish(out)EVP_MAC_finalwrite into caller
Signer::update(data)EVP_DigestSignUpdateread from caller
Verifier::update(data)EVP_DigestVerifyUpdateread from caller
Verifier::verify(sig)EVP_DigestVerifyFinalread from caller
RawSigner::sign(tbs, sig)EVP_PKEY_signboth
RawVerifier::verify(tbs, sig)EVP_PKEY_verifyread from caller
DeriveCtx::derive(out)EVP_PKEY_derivewrite into caller
PkeyEncryptCtx::encrypt(pt, ct)EVP_PKEY_encryptboth
PkeyDecryptCtx::decrypt(ct, pt)EVP_PKEY_decryptboth
Rand::fill(buf)RAND_byteswrite into caller
RandCtx::generate(out, req)EVP_RAND_generatewrite into caller
X509::from_der(der)d2i_X509 via local ptrread from caller
Pkey::from_der(der)d2i_* via MemBioBufread from caller
KdfBuilder::derive(out)EVP_KDF_derivewrite into caller
Params::get_octet_string(key)OSSL_PARAM_get_octet_string_ptrborrow from Params
Params::get_utf8_string(key)OSSL_PARAM_get_utf8_string_ptrborrow from Params

Tier 2 — Unavoidable Copies (in OpenSSL, not Rust)

OpenSSL must own copies of this data for security reasons — key scheduling, zeroization on free, constant-time access. These copies happen inside C and cannot be eliminated without bypassing OpenSSL’s security model.

APIWhat is copiedWhy unavoidable
CipherAlg::encrypt(key, iv)Key + IVOpenSSL schedules and zeroizes
AeadEncryptCtx::new(key, iv)Key + IVSame
MacCtx::init(key)Key → IPAD/OPAD for HMACMAC context owns key schedule
HkdfBuilder::derivekey/salt/info via push_octet_sliceKDF expands material internally
Pbkdf2Builder::derivepassword + saltIteration state
ScryptBuilder::derivepassword + salt + working memoryLarge scratch space
SshkdfBuilder::derivekey/xcghash/session-id via push_octet_sliceKDF builds internal state
KbkdfBuilder::derivekey/label/context via push_octet_sliceKDF builds internal state
X509::from_der / from_pemd2i_X509 builds an ASN.1 treeOpenSSL’s internal representation
Pkey::from_der / from_pemBIGNUM, EC point, etc.Constant-time key management
Pkey::from_paramsall key fieldsEVP_PKEY_fromdata copies into EVP_PKEY
Params::get_bnBIGNUM → Vec<u8>OSSL_PARAM_get_BN allocates a BIGNUM; wrapper converts to bytes

Tier 3 — Allocating Convenience Variants

These variants allocate a Vec<u8> and are explicitly opt-in. A zero-copy primary path always exists alongside them.

Allocating variantZero-copy primaryWhen to prefer allocating
DigestAlg::digest_to_vec(data)digest(data, out)Output size unknown
MacCtx::finish_to_vec()finish(out)MAC size varies
HmacCtx::finish_to_vec()finish(out)Same
Signer::finish()(always allocates)Signature size algorithm-dependent
RawSigner::sign_alloc(tbs)sign(tbs, sig)Signature buffer size not known
DeriveCtx::derive_to_vec()derive(out)Secret length not known
KdfBuilder::derive_to_vec(len)derive(out)No preallocated buffer
Rand::bytes(n)Rand::fill(buf)Dynamic size
X509::to_der()(always allocates)Length query required
Pkey::to_pem()(always allocates)Length query required
Pkey::export()Pkey::get_params(&mut query)Full dump vs. targeted query
Params::get_bn(key)(always allocates)BIGNUM-to-bytes conversion unavoidable

Signer::finish always allocates — the signature length varies by algorithm (RSA-2048 produces 256 bytes; Ed25519 produces 64 bytes; ECDSA produces a variable-length DER blob). The method queries the size, allocates, then writes.

RawSigner::sign is zero-copy — the caller provides a pre-allocated buffer. Use sign_len(tbs_len) to query the required size, then call sign(tbs, &mut buf). sign_alloc is the convenience variant that handles the allocation for you.

Params::get_bn always allocatesOSSL_PARAM_get_BN allocates a new BIGNUM internally. The wrapper converts it to a Vec<u8> and frees the BIGNUM. Use get_octet_string for raw byte fields that do not need BIGNUM semantics; that call is zero-copy.

PEM I/O — MemBio and MemBioBuf

Input (zero-copy read)

MemBioBuf wraps BIO_new_mem_buf, which creates a read-only BIO backed by a pointer into the caller’s slice. No bytes are copied:

#![allow(unused)]
fn main() {
// Internal implementation pattern:
let bio = MemBioBuf::from_slice(pem)?;  // BIO_new_mem_buf — no copy
let cert = unsafe { sys::PEM_read_bio_X509(bio.as_ptr(), ...) };
}

The MemBioBuf borrows the slice; the BIO is freed before the function returns. OpenSSL reads directly from the caller’s memory.

Output (always allocates)

MemBio wraps BIO_s_mem, which maintains an internal growable buffer. After writing PEM/DER, BIO_get_mem_data returns a slice into that buffer. The wrapper copies it into a Vec<u8> before dropping the BIO:

#![allow(unused)]
fn main() {
// Internal implementation pattern:
let bio = MemBio::new()?;
unsafe { sys::PEM_write_bio_X509(bio.as_ptr(), self.ptr) };
let pem = bio.into_vec()?;  // BIO_get_mem_data → Vec::from_slice
}

The bio.data() method exposes the buffer as &[u8] without copying, for callers who only need to write the bytes directly (e.g. to a socket) before dropping the BIO.

ParamBuilder — Copy vs. Pointer (Building)

OSSL_PARAM_BLD has two families of push functions:

MethodC functionCopies?Use when
push_octet_slice(key, val)push_octet_stringYesval may not outlive Params
push_octet_ptr(key, val: &'a [u8])push_octet_ptrNoval lifetime 'a is guaranteed
push_utf8_string(key, val)push_utf8_stringYesShort runtime strings
push_utf8_ptr(key, val: &'static CStr)(copies in practice)No intent'static literals
push_bn(key, bigendian_bytes)push_BN via BN_bin2bnYesBIGNUM fields; caller’s bytes not retained

KDF builders hold &'a [u8] slices and pass them via push_octet_slice at derive() time — the copy is inside OpenSSL, not an extra Rust-side allocation. Cipher and MAC key material is also passed via push_octet_slice since OpenSSL immediately schedules the key.

Params — Zero-Copy vs. Allocating (Reading)

When reading values out of a Params array, allocation behaviour depends on the type:

GetterC functionAllocates?Notes
get_int / get_uint / get_size_t / get_i64 / get_u64OSSL_PARAM_get_*NoScalar read into a local variable
get_octet_stringOSSL_PARAM_get_octet_string_ptrNoReturns &[u8] borrowing into the Params array
get_utf8_stringOSSL_PARAM_get_utf8_string_ptrNoReturns &CStr borrowing into the Params array
get_bnOSSL_PARAM_get_BN + BN_bn2binYesVec<u8>BIGNUM must be converted to bytes

get_octet_string and get_utf8_string use the _ptr variants of the OpenSSL getters, which return a pointer into the existing OSSL_PARAM storage rather than copying the data. The returned reference is tied to the lifetime of &self, so it cannot outlive the Params value.

get_bn cannot be zero-copy because OpenSSL’s OSSL_PARAM_get_BN allocates a new BIGNUM object internally. The Bn helper in params.rs converts this to a Vec<u8> and frees the BIGNUM before returning.

DER Parsing — No Cursor Advancement

The C d2i_* functions advance a const unsigned char ** pointer past the parsed bytes, enabling sequential parsing from one buffer. The Rust wrappers do not expose this cursor API. All from_der methods take &[u8] and consume the full slice. A local pointer is created inside the wrapper and discarded after the call:

#![allow(unused)]
fn main() {
// Internal pattern for X509::from_der:
pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
    let mut ptr = der.as_ptr();
    // SAFETY: ptr is valid for der.len() bytes; X509** is NULL (allocate new)
    let x = unsafe { sys::d2i_X509(std::ptr::null_mut(), &mut ptr, der.len() as i64) };
    if x.is_null() { return Err(ErrorStack::drain()); }
    Ok(X509 { ptr: x })
}
}

The external der reference is not mutated. Use separate calls to from_der with sub-slices if you need to parse multiple sequential DER objects.

cfg Propagation — ossl_v400

Some zero-copy APIs (currently DigestCtx::serialize and DigestCtx::deserialize) are only available when the crate is built against OpenSSL >= 4.0. They are compiled conditionally behind #[cfg(ossl_v400)].

The ossl_v400 flag is propagated through two build scripts:

  1. native-ossl-sys/build.rs — the OsslCallbacks::int_macro hook reads OPENSSL_VERSION_NUMBER. When the value is >= 0x4000_0000, it emits:

    cargo::rustc-cfg=ossl_v400
    cargo::metadata=OSSL400=1
    

    The cargo::metadata line, combined with links = "ssl" in native-ossl-sys/Cargo.toml, causes Cargo to expose the value to dependent crates as the DEP_SSL_OSSL400 environment variable.

  2. native-ossl/build.rs — reads DEP_SSL_OSSL400. If present, emits cargo::rustc-cfg=ossl_v400 for the native-ossl crate. Also unconditionally emits cargo::rustc-check-cfg=cfg(ossl_v400) so the unexpected_cfgs lint is satisfied whether or not OpenSSL 4.0 is installed.

Call sites guarded by #[cfg(ossl_v400)] compile to nothing on OpenSSL 3.x with no stub or runtime check needed.

What Zero-Copy Cannot Cover

Semantic-level copies are unavoidable. When OpenSSL parses a Pkey or X509, it builds its own internal representation (BIGNUMs for RSA, EC points, ASN.1 trees). These are not zero-copy at the semantic level, and they should not be — OpenSSL manages key memory independently for zeroization and constant-time guarantees.

The zero-copy guarantee is at the byte-transfer level: no intermediate byte buffer is allocated on the Rust side before handing data to OpenSSL. What OpenSSL does with that data internally is its own concern.


  1. Only compiled when #[cfg(ossl_v400)] is active (OpenSSL >= 4.0). See cfg propagation below. ↩2

Building and Testing

Prerequisites

Install OpenSSL development headers and pkg-config:

# Fedora / RHEL
sudo dnf install openssl-devel

# Ubuntu / Debian
sudo apt install libssl-dev pkg-config

# macOS (Homebrew)
brew install openssl@3
export PKG_CONFIG_PATH="$(brew --prefix openssl@3)/lib/pkgconfig"

Verify the version (3.0.7 or later required):

pkg-config --modversion openssl

Minimum Rust version: 1.77 (required for c"..." C-string literal syntax).

Building

# Check all crates without producing binaries
cargo check --all-targets

# Build in release mode
cargo build --release

# Build and run the encrypt-demo example
cargo run -p encrypt-demo

Testing

# Run the full test suite
cargo test --all

# Run tests for a specific crate
cargo test -p native-ossl

# Run a specific test
cargo test -p native-ossl digest::tests::sha256_known_vector

The test suite requires a live OpenSSL library. Tests connect to no external services; all cryptographic operations are in-process.

Linting

# Run Clippy with pedantic lints as errors
cargo clippy --all-targets -- -D warnings

# Format check (does not modify files)
cargo fmt --all --check

# Apply formatting
cargo fmt --all

All pedantic Clippy lints are active workspace-wide. The only exception is native-ossl-sys/src/lib.rs, which suppresses lints on bindgen-generated code locally. Do not add blanket #![allow(...)] to any hand-written file.

bindgen Regeneration

Bindings are regenerated automatically by build.rs whenever OpenSSL headers change. No manual step is required. To force regeneration:

cargo clean -p native-ossl-sys && cargo build -p native-ossl-sys

The generated file is written to $OUT_DIR/ossl_bindings.rs and is not committed to the repository.

FIPS Mode

To test with the OpenSSL FIPS provider, install it:

# Fedora
sudo dnf install openssl-fips-provider

# Verify FIPS provider is loadable
openssl list -providers -provider fips

Run FIPS-related tests:

cargo test

No feature flag is required. FIPS tests load a LibCtx, load the fips and base providers, and fetch algorithms from that isolated context. They call fips::is_running at runtime and skip themselves if the FIPS provider is not installed.

The fips-provider Feature

The fips-provider Cargo feature is only for code that implements a FIPS provider from Rust — accessing OpenSSL’s internal provider vtable, PROV_CTX helpers, and related non-public APIs. It is unrelated to running in FIPS mode. Ordinary application code does not need this feature; fips::is_running() is always available without it.

Building with fips-provider requires the OpenSSL source tree (not just the development headers) because the feature accesses non-public headers such as crypto/evp.h, prov/provider_ctx.h, and internal/provider.h:

export OPENSSL_SOURCE_DIR=/path/to/openssl-3.5.0
cargo build --features fips-provider

The build system runs a second bindgen pass against fips_wrapper.h (written to $OUT_DIR) that includes these private headers with appropriate #define guards. It also compiles a small C probe to determine the byte offset of keydata inside evp_pkey_st for the current target ABI, writing the result to $OUT_DIR/keydata_offset.rs and exposing it as native_ossl_sys::fips_internal::EVP_PKEY_KEYDATA_OFFSET.

This feature is validated on 64-bit LP64 Linux targets (x86_64, aarch64, riscv64, s390x). 32-bit targets are not yet supported.

Version-Gated Features

Several APIs are conditionally compiled based on the installed OpenSSL version. The build system detects capabilities automatically — no manual flags are needed.

Version flags (from OPENSSL_VERSION_NUMBER):

cfg flagMinimum versionAPIs enabled
ossl3073.0.7 (floor)always active; baseline signal
ossl3103.1.0Clone for RandCtx
ossl3203.2.0MessageSigner, MessageVerifier, SigAlg, EncapCtx, DecapCtx, GlobalRandCtx, RandCtx::public/private_global, ErrState
ossl3503.5.0SshkdfBuilder, KbkdfBuilder and related types
ossl_v4004.0.0DigestCtx::serialize_size, DigestCtx::serialize, DigestCtx::deserialize

Algorithm-presence flags (from header macros; absent when the algorithm family was not compiled into this OpenSSL installation):

cfg flagMeaning
ossl_slhdsaSLH-DSA algorithm family available
ossl_mldsaML-DSA algorithm family available
ossl_mlkemML-KEM algorithm family available
param_clear_freeOSSL_PARAM_clear_free available (secure param cleanup)

To inspect which flags are active for the current build:

cargo rustc -p native-ossl -- --print=cfg 2>&1 | grep ossl

Tests for version-gated features carry matching #[cfg(...)] attributes and are automatically skipped when the flag is not set.

Integration Tests

Integration tests live in native-ossl/tests/:

tests/
├── test_digest.rs      — SHA-2, SHA-3 NIST test vectors
├── test_cipher.rs      — AES-GCM, ChaCha20-Poly1305 NIST vectors
├── test_mac.rs         — HMAC, CMAC NIST vectors
├── test_pkey.rs        — RSA, ECDSA, Ed25519 sign/verify round-trips
├── test_kdf.rs         — HKDF, PBKDF2, scrypt test vectors
├── test_x509.rs        — parse real certificates, verify self-signed
└── test_ssl.rs         — in-process TLS with paired memory BIOs

Run them separately to reduce noise:

cargo test -p native-ossl --test test_digest
cargo test -p native-ossl --test test_ssl

The encrypt-demo Example

examples/encrypt-demo demonstrates hybrid encryption using RSA-OAEP + AES-256-GCM:

cargo run -p encrypt-demo

It exercises: key generation, public key serialization, RSA-OAEP encryption of a symmetric key, AES-256-GCM encryption of a payload, decryption and verification of the authentication tag, and round-trip assertion.

Documentation

Build the mdbook documentation locally:

# Install mdBook if not present
cargo install mdbook

# Build and open in browser
cd docs
mdbook build --open

# Serve with live reload during editing
mdbook serve

The built book is written to docs/book/ and is not committed to the repository.

Vendored OpenSSL Build

To build against a specific OpenSSL source tree rather than the system library:

# Point to the OpenSSL source tree
export KRYOPTIC_OPENSSL_SOURCES=/path/to/openssl-3.5.0   # or NATIVE_OSSL_OPENSSL_SOURCES

cargo build --features vendored

The vendored feature invokes the build_vendored() path in build.rs, which runs ./Configure and make against the source tree and links the result statically. This is useful for reproducible builds and CI environments without a system OpenSSL installation.

Continuous Integration Checklist

A release-ready state requires:

cargo fmt --all --check                    # formatting
cargo clippy --all-targets -- -D warnings  # lints
cargo test --all                           # tests (FIPS skipped if provider absent)
cargo doc --no-deps                        # documentation compiles

All of these must pass with zero warnings.