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 type | Has _up_ref? | Rust ownership |
|---|---|---|
EVP_MD_CTX | No | Exclusive; !Clone |
EVP_CIPHER_CTX | No | Exclusive; !Clone |
EVP_PKEY_CTX | No | Exclusive; !Clone |
EVP_MAC_CTX | No | Exclusive; dup() ok |
EVP_RAND_CTX | Yes | Clone via up_ref |
BIO | Yes | Clone via up_ref |
X509 | Yes | Clone via up_ref |
X509_STORE | Yes | Clone via up_ref |
X509_CRL | Yes | Clone via up_ref |
EVP_PKEY | Yes | Clone via up_ref |
SSL_CTX | Yes | Clone via up_ref |
SSL_SESSION | Yes | Clone 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_CTX | No public API | Arc<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 sitesNonefor 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
pkg_config::Config::new().atleast_version("3.0.7").probe("openssl")— locate the system OpenSSL and verify the minimum version.bindgen::Builder— generate bindings from<openssl/evp.h>,<openssl/err.h>,<openssl/x509.h>,<openssl/ssl.h>, etc.OsslCallbacks::int_macro— readsOPENSSL_VERSION_NUMBERfrom 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).OsslCallbacks::str_macro— inspects string constant names to detect algorithm-presence flags:OSSL_PKEY_PARAM_SLH_DSA_SEED→ossl_slhdsa,OSSL_PKEY_PARAM_ML_DSA_SEED→ossl_mldsa,OSSL_PKEY_PARAM_ML_KEM_SEED→ossl_mlkem.OsslCallbacks::func_macro— detectsOSSL_PARAM_clear_free→param_clear_free.allowlist_function/allowlist_type— generate only whatnative-ossluses, keeping the output small and the compilation fast.- Each emitted cfg flag is also emitted as
cargo::metadata=FLAG=1. These propagate to dependent crates asDEP_SSL_FLAGenvironment variables via thelinks = "ssl"manifest key.
The allowlists include:
EVP_SIGNATURE_fetch,EVP_SIGNATURE_free,EVP_SIGNATURE_up_ref, and theEVP_SIGNATUREtype — required forSigAlg.- Raw sign/verify init + sign/verify (
EVP_PKEY_sign_init,EVP_PKEY_sign,EVP_PKEY_verify_init,EVP_PKEY_verify) — required forRawSigner/RawVerifier. - Full
sign_message_*/verify_message_*family andEVP_PKEY_CTX_set_signature— required forMessageSigner/MessageVerifier. ERR_set_markandERR_pop_to_mark— used by thesupports_streamingprobe to keep the error queue clean.PKCS12_*,d2i_PKCS12_bio,i2d_PKCS12_bio— required forPkcs12.OCSP_REQUEST_*,OCSP_RESPONSE_*,OCSP_BASICRESP_*,OCSP_basic_verify,d2i_OCSP_REQUEST,i2d_OCSP_REQUEST,d2i_OCSP_RESPONSE,i2d_OCSP_RESPONSE— required forOcspRequest/OcspResponse.X509_STORE_*,X509_STORE_CTX_*,X509_CRL_*— required forX509Store,X509StoreCtx, andX509Crl.OPENSSL_cleanse— required forSecretBuf.
native-ossl/build.rs
Re-emits all version and feature cfg flags for the safe wrapper crate:
- Declares all eight flag names with
cargo::rustc-check-cfgso theunexpected_cfgslint is satisfied regardless of installed OpenSSL version. - Reads each
DEP_SSL_*env var (forwarded by Cargo fromnative-ossl-sysviacargo::metadata) and re-emits the correspondingcargo::rustc-cfgflag.
Version Policy
Minimum: OpenSSL 3.0.7. Eight cfg flags are emitted:
Version flags (from OPENSSL_VERSION_NUMBER in int_macro):
| cfg flag | Condition | APIs gated |
|---|---|---|
ossl307 | always (≥ 3.0.7 floor) | baseline; signals minimum is met |
ossl310 | >= 0x3010_0000 | Clone for RandCtx |
ossl320 | >= 0x3020_0000 | MessageSigner, MessageVerifier, SigAlg, EncapCtx, DecapCtx, GlobalRandCtx, RandCtx::public, RandCtx::private_global, ErrState |
ossl350 | >= 0x3050_0000 | SshkdfBuilder, SshkdfKeyType, KbkdfBuilder, KbkdfMode, KbkdfCounterLen |
ossl_v400 | >= 0x4000_0000 | DigestCtx::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 flag | Detected via | Meaning |
|---|---|---|
ossl_slhdsa | OSSL_PKEY_PARAM_SLH_DSA_SEED present | SLH-DSA available |
ossl_mldsa | OSSL_PKEY_PARAM_ML_DSA_SEED present | ML-DSA available |
ossl_mlkem | OSSL_PKEY_PARAM_ML_KEM_SEED present | ML-KEM available |
param_clear_free | OSSL_PARAM_clear_free macro present | secure 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
| Feature | Default | Effect |
|---|---|---|
dynamic | yes | Link against system OpenSSL |
vendored | no | Build from KRYOPTIC_OPENSSL_SOURCES or NATIVE_OSSL_OPENSSL_SOURCES env |
fips | no | Link libfips.a instead of libcrypto.a |
fips-provider | no | Enable 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 —
Sslis synchronous; async wrappers are a separate crate concern - Custom provider development in Rust (
fips-providerfeature 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;
OcspResponsehandles parsing and verification only