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

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