OpenSSL 3.x Providers: Why Your Crypto Calls Break and How to Fix Them
OpenSSL 3.x moved algorithms into loadable providers, so calls that worked under 1.1.1 can fail with 'unsupported' at runtime. Here's how the provider model works and how to fix the common breakage.
06 Aug 2025, 08:27 UTC

Your application linked fine against OpenSSL 3.x, but now a digest call that worked for years returns "unsupported." Nothing in your code changed. What changed is that OpenSSL 3.x moved cryptographic algorithms out of libcrypto and into loadable modules called providers — and some of the algorithms you were silently using no longer load by default.
The takeaway: in OpenSSL 3.x, algorithm availability is a runtime decision, not a build-time one. Once you understand providers, the breakage is predictable, and the fix is usually a few lines of config or code.
What providers actually are
Before 3.0, algorithms were compiled directly into libcrypto. If the library had MD4, every application that linked it had MD4. OpenSSL 3.0 replaced this with a provider architecture: libcrypto is a dispatcher, and algorithm implementations live in modules that are loaded into a library context at runtime.
The modules that matter in practice:
- default — the standard algorithm set (SHA-2, SHA-3, AES, RSA, and so on). Loaded automatically unless you configure otherwise.
- base — non-cryptographic encoders and decoders; paired with the FIPS provider in restricted setups.
- fips — the only module covered by a FIPS 140-3 validation certificate, for a specific provider version and platform set.
- legacy — algorithms removed from the default set: MD4, RIPEMD-160, Whirlpool, Blowfish, and friends.
- null — nothing, useful as a clean starting point.
Fetching an algorithm with a call like EVP_MD_fetch(NULL, "SHA-256", NULL) resolves against whichever providers are loaded in that context. Same binary, same library, different runtime config — different result.
The classic failure: legacy algorithms
The most common migration surprise is an application that reads an old file format or talks to a legacy system and needs, say, RIPEMD-160 or Blowfish. Under 1.1.1 it just worked. Under 3.x, the fetch fails because the legacy provider was never loaded.
Two fixes, depending on scope. For a single application, load the provider in code:
#include <openssl/provider.h>
#include <openssl/evp.h>
/* Run in your process; needs no special OS privileges. */
OSSL_PROVIDER *legacy = OSSL_PROVIDER_load(NULL, "legacy");
if (legacy == NULL) {
/* provider module not installed or not found */
}
OSSL_PROVIDER_load(NULL, "default"); /* loading anything explicitly disables auto-load */
EVP_MD *md = EVP_MD_fetch(NULL, "RIPEMD-160", NULL);
/* md != NULL only after legacy is loaded */
EVP_MD_free(md);Important detail: once you load any provider explicitly, the default provider is no longer auto-loaded, so load it too or your SHA-256 calls start failing instead.
For a whole system, edit openssl.cnf:
openssl_conf = openssl_init
[openssl_init]
providers = provider_sect
[provider_sect]
default = default_sect
legacy = legacy_sect
[default_sect]
activate = 1
[legacy_sect]
activate = 1Be careful here: that config file affects every application that reads it. A system-wide edit can quietly re-enable weak algorithms for unrelated tools. Prefer the programmatic route unless you genuinely want system-wide behavior.
Checking what you actually have
Don't guess — ask the library. From a shell, with the same config your application will use:
openssl list -providers
openssl list -digest-algorithmsRun the second command with and without the legacy provider activated and compare. You should see RIPEMD-160 and friends appear only when legacy is active. This is also the fastest way to confirm whether a config edit actually took effect — a common trap is editing an openssl.cnf that your build never reads. openssl version -d shows the config directory your installation uses.
FIPS: what loading the provider does and doesn't give you
Compliance-driven deployments often activate only the FIPS provider (plus base for encodings), which restricts the process to validated algorithms. That's a legitimate design, but keep the claim honest: FIPS 140-3 validation attaches to a specific provider version on specific platforms, under specific operating conditions. Loading the FIPS provider on an arbitrary build does not make your application "FIPS compliant." Check the certificate status for your exact OpenSSL release before writing that word in an audit document.
The ENGINE migration cost
If your application used the ENGINE API for hardware security modules or external crypto, that API is deprecated in 3.x. The replacement path is a provider implementation of the same functionality. Some HSM vendors ship providers now; some still only ship engines, which keep working for the time being but with deprecation warnings. For long-lived applications, budget real time for this migration — it's the least mechanical part of moving from 1.1.1, which reached end of life in September 2023.
Where to start
Run openssl version, then openssl list -providers in the environment your application actually runs in. If you're hitting "unsupported" errors after an upgrade, grep your code for fetches of MD4, RIPEMD-160, Whirlpool, or Blowfish and load the legacy provider narrowly, in code, with a comment explaining why. And if you rely on engines, start the provider conversation with your hardware vendor now rather than when the deprecation becomes a removal.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.