[PATCH v1 0/3] Route crypto through the OpenSSL 3 provider API

Started by Mark Atwood19 days ago5 messageshackers
Jump to latest
#1Mark Atwood
mark@reviewcommit.com

When built against OpenSSL 3.0 or newer, PostgreSQL's OpenSSL crypto backend
still uses pre-3.0 interfaces that do not go through the provider framework,
so a loaded provider's implementation is not used:

- HMAC uses HMAC_CTX / HMAC_Init_ex, deprecated since 3.0;
- cryptohash and channel binding initialize the digest with the implicit
static MDs (EVP_sha256() etc.).

I checked what the digest path actually resolves to at run time. After
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) -- what the backend does today --
EVP_MD_get0_provider() on the context's MD returns no provider (the legacy
built-in is used). After EVP_MD_fetch(NULL, "SHA256", NULL) the MD reports
the "default" provider. In other words the current code does not reach a
loaded provider for these digests at all; a third-party or FIPS provider is
not consulted. (A small standalone program that prints this via
EVP_MD_get0_provider()/EVP_MAC_get0_provider() is attached as
provider_probe.c.)

This series switches those paths to the provider-fetch APIs -- EVP_MAC_fetch
for HMAC, EVP_MD_fetch for digests -- on OpenSSL 3.0 and newer, keeping the
existing code for older OpenSSL and for LibreSSL (guarded by
OPENSSL_VERSION_NUMBER >= 0x30000000L). The fetches use the default library
context and a NULL property query, so no dependency is added and no
particular provider is required; the provider is selected by the usual
OpenSSL configuration (openssl.cnf).

On EVP_MAC (0001): when the HMAC abstraction was first built, EVP_MAC was
set aside deliberately -- "a bit too new to use though, as we need to support
OpenSSL down to 1.0.1 on HEAD ... So instead I have decided to rely on the
older interface based on HMAC_Init_ex()" [1]/messages/by-id/X9m0nkEJEzIPXjeZ@paquier.xyz. That constraint no longer
holds: the minimum supported OpenSSL is now 1.1.1, and this change is
confined to 3.0+ by the version guard, so the original reason for preferring
HMAC_CTX is gone. 0001 also removes use of an interface deprecated in 3.0,
which stands on its own regardless of provider routing.

Notes:

* Authentication does no crypto of its own -- SCRAM and md5 ride
pg_hmac/pg_cryptohash -- so the whole auth surface follows the active
provider once the backend does.

* md5 authentication then depends on MD5 being offered by the provider,
so it is unavailable under FIPS. That is the intended behavior and
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" [2]/messages/by-id/20201106073434.GA4961@paquier.xyz. Every md5 hashing call site already fails closed, so
a FIPS provider rejects md5 auth rather than bypassing it. 0002
documents this and points to scram-sha-256.

* EVP_MAC_fetch/EVP_MD_fetch do a provider lookup. The fetched object is
cached in the context (for cryptohash it is fetched once, since the
digest type is fixed for the context's lifetime). These paths are not
hot -- SCRAM runs at connection time and hash-seed setup at startup --
so the lookup is not on any tight loop.

* pg_strong_random() already calls RAND_bytes(), which draws from the
default provider's RNG under 3.0, so nothing there needs to change.

* 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.

Tested with OpenSSL 3.0.13: clean build, src/test/regress (245),
src/test/ssl (423, including tls-server-end-point channel binding in
002_scram.pl) and src/test/authentication (323) all pass; HMAC and the
digests were also checked against RFC 4231 and NIST known-answer vectors.

I'll add this to the open commitfest.

[1]: /messages/by-id/X9m0nkEJEzIPXjeZ@paquier.xyz
[2]: /messages/by-id/20201106073434.GA4961@paquier.xyz

Mark Atwood (3):
Use EVP_MAC for HMAC with OpenSSL 3.0 and later
Fetch digests explicitly for cryptohash with OpenSSL 3.0 and later
Fetch the channel binding digest explicitly with OpenSSL 3.0 and later

doc/src/sgml/client-auth.sgml | 11 +++
src/backend/libpq/be-secure-openssl.c | 73 +++++++++++++++----
src/common/cryptohash_openssl.c | 57 +++++++++++++++
src/common/hmac_openssl.c | 92 ++++++++++++++++++++++++
src/interfaces/libpq/fe-secure-openssl.c | 86 +++++++++++++++++-----
5 files changed, 285 insertions(+), 34 deletions(-)

--
2.43.0

#2Mark Atwood
mark@reviewcommit.com
In reply to: Mark Atwood (#1)
[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

#3Michael Paquier
michael@paquier.xyz
In reply to: Mark Atwood (#1)
Re: [PATCH v1 0/3] Route crypto through the OpenSSL 3 provider API

On Tue, Aug 04, 2026 at 05:48:02PM -0700, Mark Atwood wrote:

When built against OpenSSL 3.0 or newer, PostgreSQL's OpenSSL crypto backend
still uses pre-3.0 interfaces that do not go through the provider framework,
so a loaded provider's implementation is not used:

- HMAC uses HMAC_CTX / HMAC_Init_ex, deprecated since 3.0;
- cryptohash and channel binding initialize the digest with the implicit
static MDs (EVP_sha256() etc.).

Please note patches should be attached to the emails sent to the
community mailing lists. We do not use commands like `git send-mail`.

FYI, I am interested in what you are doing here for the HMAC and
channel binding parts, at least, having committed the code we have in
the tree and that you are updating here. Just make sure to begin one
thread for each patch proposed, with the patch attached, so as we are
able to discuss and review each item separately. Generating the
patches with format-patch is a common practice.

Please see also:
https://wiki.postgresql.org/wiki/Submitting_a_Patch#Patch_submission

Thanks,
--
Michael

#4Mark Atwood
mark@reviewcommit.com
In reply to: Michael Paquier (#3)
Re: [PATCH v1 0/3] Route crypto through the OpenSSL 3 provider API

Understood, and thanks for the pointer.

I am reposting the three patches as three separate threads, each with the patch
attached:

- Use EVP_MAC for HMAC with OpenSSL 3.0 and later
- Fetch digests explicitly for cryptohash with OpenSSL 3.0 and later
- Fetch the channel binding digest explicitly with OpenSSL 3.0 and later

They touch disjoint files and each applies to master on its own, so there is no
ordering dependency and they can be reviewed and committed independently.

Please disregard this thread. It also picked up a duplicate copy of 2/3.

--
Mark

#5Michael Paquier
michael@paquier.xyz
In reply to: Mark Atwood (#4)
Re: [PATCH v1 0/3] Route crypto through the OpenSSL 3 provider API

On Wed, Aug 05, 2026 at 01:09:04PM -0700, Mark Atwood wrote:

I am reposting the three patches as three separate threads, each with the patch
attached:

- Use EVP_MAC for HMAC with OpenSSL 3.0 and later
- Fetch digests explicitly for cryptohash with OpenSSL 3.0 and later
- Fetch the channel binding digest explicitly with OpenSSL 3.0 and later

They touch disjoint files and each applies to master on its own, so there is no
ordering dependency and they can be reviewed and committed independently.

Thanks for doing a clean split, Mark!
--
Michael