The attached patch makes src/common/cryptohash_openssl.c select its digest with
EVP_MD_fetch() when building against OpenSSL 3.0 or newer.
cryptohash_openssl.c initializes the EVP_MD_CTX with the implicit static digest
objects (EVP_sha256() and friends), which do not deterministically dispatch
through a loaded provider. The patch fetches the digest by name, caches it in
the context, and frees it on teardown, so hashing is served by the active
provider. The digest type is fixed for the lifetime of the context, so the
fetch is done once. The implicit path is kept for older OpenSSL and for
LibreSSL, guarded by OPENSSL_VERSION_NUMBER >= 0x30000000L.
The fetch uses the default library context and a NULL property query, so no
dependency is added and no particular provider is required.
All of PostgreSQL's authentication hashing (SCRAM, md5) rides pg_cryptohash, so
it follows the active provider automatically. As a consequence md5
authentication depends on MD5 being offered by the provider, and is therefore
unavailable under FIPS. That is the intended behavior, and it matches the
reasoning already given for moving MD5 to EVP: otherwise PostgreSQL would
"cheat if FIPS is enabled because MD5 should not be authorized", whereas EVP
"allows us to rely on OpenSSL to control such restrictions" [1]. Every md5
hashing call site already fails closed, so a FIPS provider rejects md5 auth
rather than silently bypassing it. The patch documents this in
client-auth.sgml and points to scram-sha-256.
I checked what these paths resolve to at run time rather than assuming. The
probe program is attached as provider_probe.c; it makes no assertions, it
prints what OpenSSL reports via EVP_MD_get0_provider(). Build it with
"cc -o provider_probe provider_probe.c -lcrypto". On OpenSSL 3.0.13:
Q1 EVP_sha256() provider : LEGACY BUILT-IN (no provider)
Q2 ctx MD after DigestInit_ex(EVP_sha256()) : LEGACY BUILT-IN (no provider)
Q3 EVP_MD_fetch(NULL,"SHA256",NULL) : default
Q4a EVP_MD_fetch(legacy-only ctx,"SHA256") : FETCH FAILED
Q4b EVP_MD_fetch(legacy-only ctx,"MD5") : FETCH FAILED
Q1/Q2 are what the backend does today; Q3 is what the patch switches to. Q4 is
the control: with a library context holding only the legacy provider, the fetch
fails rather than silently falling back to a built-in. That is the behavior
that makes a FIPS provider's restrictions actually take effect.
Details:
* Against master, tested at 8b73ceb78f. It touches
src/common/cryptohash_openssl.c and doc/src/sgml/client-auth.sgml, and
applies on its own; there is no dependency on the two related patches I am
posting in separate threads.
* Built and tested with OpenSSL 3.0.13 on Ubuntu 24.04 (x86-64): clean build,
src/test/regress, src/test/ssl and src/test/authentication all pass. The
digests were also checked against the NIST known-answer vectors.
* No new regression tests. This replaces the implementation behind
pg_cryptohash without changing its behavior or API, and the existing SCRAM
and md5 coverage in src/test/authentication exercises it.
* Documentation: client-auth.sgml gains a note that md5 authentication is
unavailable when the provider does not offer MD5, as under FIPS, with a
pointer to scram-sha-256.
* No performance impact expected. The fetch is a provider lookup done once
per context, and these paths run at connection time and at hash-seed setup,
not in any tight loop.
* pgcrypto is deliberately out of scope. It exposes legacy ciphers (DES,
Blowfish, CAST5) that depend on OpenSSL's legacy provider and warrant a
separate discussion.
This was previously posted as a three-patch series in a single thread [2].
Reposting as separate threads with the patch attached, per review request.
Intended for the next commitfest.
[1] https://postgr.es/m/[email protected]
[2] https://postgr.es/m/20260805004805.1174492-1-mark%40reviewcommit.com
--
Mark
>From b2b37f9ae0415bff017a928623e8a09f1c69779e Mon Sep 17 00:00:00 2001
From: Mark Atwood <[email protected]>
Date: Fri, 24 Jul 2026 11:21:52 -0700
Subject: [PATCH v1 2/3] Fetch digests explicitly for cryptohash with OpenSSL
3.0 and later
cryptohash_openssl.c initialized the EVP_MD_CTX with the implicit static
digest objects (EVP_sha256() and friends), which do not deterministically
dispatch through a loaded provider.
On OpenSSL 3.0 and newer, fetch the digest by name with EVP_MD_fetch(),
cache it in the context, and free it on teardown, so hashing is served by
the active provider. The digest type is fixed for the lifetime of the
context, so the fetch is done once. The implicit path is kept for older
OpenSSL and for LibreSSL.
All of PostgreSQL's authentication hashing (SCRAM, md5) rides pg_cryptohash,
so it follows the active provider automatically. As a consequence, md5
authentication requires MD5 to be offered by the provider and is therefore
unavailable under FIPS; document that and point to scram-sha-256.
---
doc/src/sgml/client-auth.sgml | 11 +++++++
src/common/cryptohash_openssl.c | 57 +++++++++++++++++++++++++++++++++
2 files changed, 68 insertions(+)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index e4e65f8feb..a65894a7a5 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1262,6 +1262,17 @@ omicron bryanh guest1
attacks.
</para>
+ <para>
+ When <productname>PostgreSQL</productname> is built with
+ <productname>OpenSSL</productname>, <literal>md5</literal> authentication
+ relies on the MD5 implementation supplied by the active
+ <productname>OpenSSL</productname> provider. MD5 is unavailable when
+ <productname>OpenSSL</productname> is operating in FIPS mode, or with any
+ provider that disables MD5, and <literal>md5</literal> authentication
+ will fail in that configuration; use <literal>scram-sha-256</literal>
+ instead.
+ </para>
+
<para>
To ease transition from the <literal>md5</literal> method to the newer
SCRAM method, if <literal>md5</literal> is specified as a method
diff --git a/src/common/cryptohash_openssl.c b/src/common/cryptohash_openssl.c
index 51b7e04093..6772bcdbda 100644
--- a/src/common/cryptohash_openssl.c
+++ b/src/common/cryptohash_openssl.c
@@ -67,6 +67,9 @@ struct pg_cryptohash_ctx
const char *errreason;
EVP_MD_CTX *evpctx;
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+ EVP_MD *algo;
+#endif
#ifndef FRONTEND
ResourceOwner resowner;
@@ -182,6 +185,56 @@ pg_cryptohash_init(pg_cryptohash_ctx *ctx)
if (ctx == NULL)
return -1;
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+
+ /*
+ * On OpenSSL 3.0 and newer, explicitly fetch the digest implementation so
+ * that it is served by the loaded provider. This lets a third-party or
+ * FIPS provider service PostgreSQL's hashing, rather than relying on the
+ * implicit lookup done by EVP_md5()/EVP_sha*(). The fetched EVP_MD is
+ * cached in the context and released in pg_cryptohash_free().
+ */
+ {
+ const char *name = NULL;
+
+ switch (ctx->type)
+ {
+ case PG_MD5:
+ name = "MD5";
+ break;
+ case PG_SHA1:
+ name = "SHA1";
+ break;
+ case PG_SHA224:
+ name = "SHA224";
+ break;
+ case PG_SHA256:
+ name = "SHA256";
+ break;
+ case PG_SHA384:
+ name = "SHA384";
+ break;
+ case PG_SHA512:
+ name = "SHA512";
+ break;
+ }
+
+ /*
+ * ctx->type is fixed for the lifetime of the context, so the digest
+ * only needs to be fetched once; a second pg_cryptohash_init() on the
+ * same context reuses it. Freeing and re-fetching here would drop the
+ * EVP_MD while the previous EVP_MD_CTX still references it, and would
+ * also repeat the relatively expensive provider lookup needlessly.
+ */
+ if (ctx->algo == NULL && name != NULL)
+ ctx->algo = EVP_MD_fetch(NULL, name, NULL);
+
+ if (ctx->algo != NULL)
+ status = EVP_DigestInit_ex(ctx->evpctx, ctx->algo, NULL);
+ else
+ status = 0;
+ }
+#else
switch (ctx->type)
{
case PG_MD5:
@@ -203,6 +256,7 @@ pg_cryptohash_init(pg_cryptohash_ctx *ctx)
status = EVP_DigestInit_ex(ctx->evpctx, EVP_sha512(), NULL);
break;
}
+#endif
/* OpenSSL internals return 1 on success, 0 on failure */
if (status <= 0)
@@ -329,6 +383,9 @@ pg_cryptohash_free(pg_cryptohash_ctx *ctx)
return;
EVP_MD_CTX_destroy(ctx->evpctx);
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+ EVP_MD_free(ctx->algo);
+#endif
#ifndef FRONTEND
if (ctx->resowner)
--
2.43.0
/*
* Empirically determine whether PostgreSQL's EXISTING crypto code paths
* dispatch through an OpenSSL 3 provider, versus the fetch-based paths the
* patch switches to. Uses OpenSSL provider introspection only; no assertions
* about the outcome are baked in -- it prints whatever OpenSSL reports.
*/
#include <stdio.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/provider.h>
#include <openssl/core_names.h>
#include <openssl/params.h>
static const char *
pn(const OSSL_PROVIDER *p)
{
return p ? OSSL_PROVIDER_get0_name(p) : "LEGACY BUILT-IN (no provider)";
}
int
main(void)
{
printf("Runtime: %s\n\n", OpenSSL_version(OPENSSL_VERSION));
/* ---------------- DIGEST ---------------- */
/* Q1: is EVP_sha256() itself provider-backed? */
printf("Q1 EVP_sha256() provider : %s\n",
pn(EVP_MD_get0_provider(EVP_sha256())));
/* Q2: what does PG do today -- EVP_DigestInit_ex(ctx, EVP_sha256(), NULL).
* Inspect the MD the context actually ends up using. */
{
EVP_MD_CTX *c = EVP_MD_CTX_new();
EVP_DigestInit_ex(c, EVP_sha256(), NULL);
printf("Q2 ctx MD after DigestInit_ex(EVP_sha256()) : %s\n",
pn(EVP_MD_get0_provider(EVP_MD_CTX_get0_md(c))));
EVP_MD_CTX_free(c);
}
/* Q3: what the patch does -- explicit fetch, default libctx, NULL propq. */
{
EVP_MD *f = EVP_MD_fetch(NULL, "SHA256", NULL);
printf("Q3 EVP_MD_fetch(NULL,\"SHA256\",NULL) : %s\n",
pn(EVP_MD_get0_provider(f)));
EVP_MD_free(f);
}
/* Q4: does fetch honor the library context / provider configuration?
* Build a libctx with ONLY the legacy provider loaded (no default). */
{
OSSL_LIB_CTX *lc = OSSL_LIB_CTX_new();
OSSL_PROVIDER *leg = OSSL_PROVIDER_load(lc, "legacy");
EVP_MD *sha = EVP_MD_fetch(lc, "SHA256", NULL);
EVP_MD *md5 = EVP_MD_fetch(lc, "MD5", NULL);
printf("Q4a EVP_MD_fetch(legacy-only ctx,\"SHA256\") : %s\n",
sha ? pn(EVP_MD_get0_provider(sha)) : "FETCH FAILED (SHA256 absent from legacy)");
printf("Q4b EVP_MD_fetch(legacy-only ctx,\"MD5\") : %s\n",
md5 ? pn(EVP_MD_get0_provider(md5)) : "FETCH FAILED");
if (sha)
EVP_MD_free(sha);
if (md5)
EVP_MD_free(md5);
OSSL_PROVIDER_unload(leg);
OSSL_LIB_CTX_free(lc);
}
/* ---------------- HMAC ---------------- */
/* Q5: PG today uses HMAC_CTX/HMAC_Init_ex -- the low-level legacy HMAC,
* deprecated since 3.0. It has no provider introspection at all. */
printf("Q5 legacy HMAC_CTX/HMAC_Init_ex : legacy low-level API (deprecated 3.0; not provider-dispatched)\n");
/* Q6: the patch uses EVP_MAC_fetch("HMAC") -- provider-backed. */
{
EVP_MAC *mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
printf("Q6 EVP_MAC_fetch(NULL,\"HMAC\",NULL) : %s\n",
pn(EVP_MAC_get0_provider(mac)));
EVP_MAC_free(mac);
}
return 0;
}