OpenSSL 3.x Provider API: An Architecture Note
An architecture note for new applications on OpenSSL 3.x: explicit provider loading, per‑operation EVP contexts, trust boundaries around key material, startup verification checks, and failure modes that shape the design.
21 Jul 2025, 10:11 UTC

The decision this note addresses
If you are writing a new application that needs encryption, signing, or hashing, the OpenSSL 3.x Provider API changes how you should structure the code. The old model — implicit global initialization, legacy ENGINE hooks, and algorithms that are simply \"there\" — is gone. Providers are loadable modules that supply algorithm implementations, and your application chooses which ones exist. That choice is now part of your architecture, not an accident of the build.
The takeaway: treat provider selection as an explicit, one‑time startup decision; keep all cryptographic work on per‑operation EVP contexts; and verify at runtime that the providers and algorithms your policy requires are actually present. Everything below follows from those three rules.
Assumption: this note targets OpenSSL 3.0 or later. OpenSSL 1.1.1 has no Provider API, so none of this applies there — confirm your linked version before adopting the design.
Requirements the design must satisfy
- All symmetric, asymmetric, and digest operations go through the standardized EVP interfaces, never low‑level algorithm‑specific APIs.
- Algorithms can be enabled or disabled at runtime by loading or unloading providers, without recompiling (crypto agility).
- FIPS mode, where required, is enforceable and verifiable, not assumed.
- Failures produce drainable, loggable error information.
- No legacy ENGINE dependencies; engines are deprecated in 3.x.
The smallest suitable design
The minimal architecture has four pieces:
- One‑time startup. Load the providers your policy allows — typically \"default\", and \"fips\" plus \"base\" for FIPS deployments. Do this before any thread performs crypto.
- Explicit algorithm fetch. Fetch algorithms by name with a property query instead of relying on implicit defaults.
- Per‑operation contexts. Every operation creates its own
EVP_CIPHER_CTX,EVP_MD_CTX, orEVP_PKEY_CTXand frees it when done. Nothing cryptographic is global except the loaded providers. - Key hygiene. Plaintext key material lives in application memory only, for the shortest possible time, and is zeroized on release.
A minimal startup and fetch sequence in C looks like this:
/* Runs once in main(), before worker threads start.\n * Requires: OpenSSL >= 3.0, linked libcrypto. */\n#include <openssl/provider.h>\n#include <openssl/evp.h>\n#include <openssl/err.h>\n\nOSSL_PROVIDER *defprov = OSSL_PROVIDER_load(NULL, \"default\");\nif (defprov == NULL) {\n /* Startup abort: required provider missing. */\n ERR_print_errors_fp(stderr);\n exit(1);\n}\n\n/* Per operation, on the thread doing the work: */\nEVP_MD *sha256 = EVP_MD_fetch(NULL, \"SHA2-256\", NULL);\nif (sha256 == NULL) {\n /* Algorithm unavailable under current providers/properties. */\n unsigned long e;\n while ((e = ERR_get_error()) != 0) {\n /* log e with ERR_error_string_n */\n }\n}\nEVP_MD_CTX *ctx = EVP_MD_CTX_new(); /* fresh per operation */\n/* ... EVP_DigestInit_ex(ctx, sha256, NULL); etc. ... */\nEVP_MD_CTX_free(ctx);\nEVP_MD_free(sha256);
The property query argument (third parameter to EVP_MD_fetch, shown as NULL) is where crypto agility becomes concrete: passing \"provider=fips\" restricts the fetch to FIPS‑provider implementations, so the same code path serves both deployment modes by changing a string, not a branch.
Trust and data boundaries
Draw three boundaries explicitly:
- The library is the trusted computing base. You trust OpenSSL for primitive correctness. You do not re‑implement primitives, and you do not trust anything below the EVP layer that you have not configured.
- The provider is an implementation boundary. The FIPS provider, in particular, is a separately validated module with its own self‑tests. Your application treats it as opaque: it loads it, queries it, and checks its status indicator — it does not reach inside.
- Sensitive data stays in application memory. Plaintext keys, IVs, and unencrypted payloads exist only in buffers your code owns. Zeroize them with
OPENSSL_cleanse()(which the compiler cannot optimize away) before freeing. Do not let key material flow into logs, swap‑heavy structures, or shared buffers reused across tenants.
One boundary people miss: the OpenSSL error queue and provider list are process‑wide. In a multi‑tenant process, a shared EVP_PKEY_CTX or a lazily shared fetched algorithm with cached key material is a cross‑tenant leak path. Per‑operation contexts are not just cleanliness — they are the isolation mechanism.
Operational checks
Build these into startup and health endpoints:
- Version check. Call
OpenSSL_version(OPENSSL_VERSION)at startup and log it. Refuse to start if the major version is not 3.x — a build that silently linked 1.1.1 will fail later in confusing ways. - Provider check. Iterate loaded providers with
OSSL_PROVIDER_do_all()and log names. Assert the ones your policy requires are present. - FIPS indicator. In FIPS deployments, query the provider's status parameters (e.g., via
OSSL_PROVIDER_get_paramsor theEVP_default_properties_is_fips_enabledcheck) and compare against deployment policy. \"We configured FIPS\" and \"FIPS is active\" are different claims. - Algorithm availability. Fetch each algorithm your application depends on at startup and log success or failure. A missing algorithm discovered at startup is a deployment error; discovered mid‑request, it is an outage.
- Error queue discipline. After any EVP failure, drain the queue with
ERR_get_error()in a loop and log the entries. The queue is per‑thread and accumulates; leaving entries in it produces misleading errors attributed to later, unrelated calls.
Failure modes and how the design responds
| Failure | Symptom | Design response |
|---|---|---|
| Provider load failure | OSSL_PROVIDER_load returns NULL |
Abort startup; the app cannot meet its crypto policy |
| Algorithm not available | EVP_*_fetch returns NULL |
Fail the operation, drain and log the error queue, alert on startup check |
| FIPS self‑test failure | Provider enters error state; operations fail | Abort startup; do not fall back to the default provider silently — that breaks policy |
| Shared EVP context reuse | Intermittent corruption or cross‑tenant key exposure | Prevented by design: contexts are per‑operation and thread‑local |
| Key material left in freed memory | Recoverable secrets in core dumps or heap reuse | OPENSSL_cleanse before every free of sensitive buffers |
Conditions that would change this design
- Hard FIPS 140‑3 requirement. The approved algorithm set and module behavior are version‑ and policy‑sensitive. You would pin a specific OpenSSL version, manage
fipsmodule.cnfas controlled configuration, and re‑verify on every upgrade rather than treating FIPS as a runtime toggle. - Hardware keys (HSM, TPM). A third‑party provider replaces the legacy ENGINE path. The design holds, but key material may never enter application memory at all — operations become handle‑based, and the zeroization rule shifts to the provider.
- Process‑level multi‑tenancy with strict isolation. Per‑operation contexts may not be enough; separate provider instances per tenant (
OSSL_LIB_CTXchild library contexts) give each tenant its own provider and property configuration. - Long‑lived high‑throughput services. Fetching algorithms per operation has measurable cost; caching fetched
EVP_MD/EVP_CIPHERobjects (which are algorithm implementations, not key state) is safe, while contexts remain per‑operation.
Verifying the result
Write a small standalone program that prints OpenSSL_version(OPENSSL_VERSION), lists providers via OSSL_PROVIDER_do_all, fetches one required algorithm, and reports the FIPS property state. Run it in each target environment. Then run a negative test: configure a property query for an algorithm the loaded providers do not offer, and confirm your error path drains and logs the queue correctly. In a FIPS test build, attempt a non‑approved algorithm (for example MD5 under a strict FIPS property query) and confirm it is rejected. These three checks exercise every boundary this design depends on.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.