The attached patch makes the tls-server-end-point channel binding hash
(RFC 5929) select its digest with EVP_MD_fetch() when building against OpenSSL
3.0 or newer.

be_tls_get_certificate_hash() and pgtls_get_peer_certificate_hash() compute the
certificate hash using the implicit EVP_sha256() / EVP_get_digestbynid()
digests, which do not deterministically dispatch through a loaded provider.
The patch selects the digest by name and fetches it, so the certificate hash is
computed by the active provider.  The implicit path is retained 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.

The fetched EVP_MD is freed on every path, including the error paths: it is not
tracked by a resource owner, and the backend raises errors with elog(ERROR),
which does not return.  That is the part of this patch most worth a careful
look.

I checked what the digest path resolves to at run time using
EVP_MD_get0_provider().  On OpenSSL 3.0.13, EVP_sha256() and the MD a context
ends up with after EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) both report no
provider (the legacy built-in), while EVP_MD_fetch(NULL, "SHA256", NULL)
reports the default provider.  The probe program that prints this is attached
to the related cryptohash thread.

Details:

  * Against master, tested at 8b73ceb78f.  It touches
    src/backend/libpq/be-secure-openssl.c and
    src/interfaces/libpq/fe-secure-openssl.c, 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.
    src/test/ssl covers tls-server-end-point channel binding in 002_scram.pl,
    which is the path this patch changes on both the server and libpq side.

  * No new regression tests.  This changes how an existing hash is computed
    without changing the result or any API, and 002_scram.pl already exercises
    both sides.

  * No documentation change.

  * No performance impact expected.  The fetch is a provider lookup done at
    connection setup, not in any tight loop.

This was previously posted as a three-patch series in a single thread [1].
Reposting as separate threads with the patch attached, per review request.

Intended for the next commitfest.

[1] https://postgr.es/m/20260805004805.1174492-1-mark%40reviewcommit.com

--
Mark
>From 562354a501f4969ecd854ea4136b0f98c9b1f290 Mon Sep 17 00:00:00 2001
From: Mark Atwood <[email protected]>
Date: Fri, 24 Jul 2026 11:21:52 -0700
Subject: [PATCH v1 3/3] Fetch the channel binding digest explicitly with
 OpenSSL 3.0 and later

be_tls_get_certificate_hash() and pgtls_get_peer_certificate_hash()
computed the tls-server-end-point channel binding hash (RFC 5929) using
the implicit EVP_sha256()/EVP_get_digestbynid() digests.

On OpenSSL 3.0 and newer, select the digest by name and fetch it with
EVP_MD_fetch() so the certificate hash is computed by the active provider.
The fetched EVP_MD is freed on every path, including the error paths: it
is not tracked by a resource owner, and the backend raises errors with
elog(ERROR), which does not return.  The implicit path is retained for
older OpenSSL and for LibreSSL.
---
 src/backend/libpq/be-secure-openssl.c    | 73 +++++++++++++++-----
 src/interfaces/libpq/fe-secure-openssl.c | 86 ++++++++++++++++++------
 2 files changed, 125 insertions(+), 34 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 6a99a3d7f9..b50d733493 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -2270,7 +2270,6 @@ be_tls_get_certificate_hash(Port *port, size_t *len)
 {
 	X509	   *server_cert;
 	char	   *cert_hash;
-	const EVP_MD *algo_type = NULL;
 	unsigned char hash[EVP_MAX_MD_SIZE];	/* size for SHA-512 */
 	unsigned int hash_size;
 	int			algo_nid;
@@ -2299,23 +2298,67 @@ be_tls_get_certificate_hash(Port *port, size_t *len)
 	 * (https://tools.ietf.org/html/rfc5929#section-4.1).  If something else
 	 * is used, the same hash as the signature algorithm is used.
 	 */
-	switch (algo_nid)
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+
+	/*
+	 * On OpenSSL 3.0 and newer, explicitly fetch the digest by name so that it
+	 * is served by the loaded provider.  The fetched EVP_MD must be freed, so
+	 * do so on every path, including the error paths (elog(ERROR) does not
+	 * return and the fetched object is not tracked by a resource owner).
+	 */
 	{
-		case NID_md5:
-		case NID_sha1:
-			algo_type = EVP_sha256();
-			break;
-		default:
-			algo_type = EVP_get_digestbynid(algo_nid);
-			if (algo_type == NULL)
-				elog(ERROR, "could not find digest for NID %s",
-					 OBJ_nid2sn(algo_nid));
-			break;
+		EVP_MD	   *algo_type;
+		const char *algo_name;
+
+		switch (algo_nid)
+		{
+			case NID_md5:
+			case NID_sha1:
+				algo_name = "SHA256";
+				break;
+			default:
+				algo_name = OBJ_nid2sn(algo_nid);
+				if (algo_name == NULL)
+					elog(ERROR, "could not determine digest for server certificate signature algorithm");
+				break;
+		}
+
+		algo_type = EVP_MD_fetch(NULL, algo_name, NULL);
+		if (algo_type == NULL)
+			elog(ERROR, "could not load digest \"%s\"", algo_name);
+
+		/* generate and save the certificate hash */
+		if (!X509_digest(server_cert, algo_type, hash, &hash_size))
+		{
+			EVP_MD_free(algo_type);
+			elog(ERROR, "could not generate server certificate hash");
+		}
+
+		EVP_MD_free(algo_type);
 	}
+#else
+	{
+		const EVP_MD *algo_type = NULL;
 
-	/* generate and save the certificate hash */
-	if (!X509_digest(server_cert, algo_type, hash, &hash_size))
-		elog(ERROR, "could not generate server certificate hash");
+		switch (algo_nid)
+		{
+			case NID_md5:
+			case NID_sha1:
+				algo_type = EVP_sha256();
+				break;
+			default:
+				algo_type = EVP_get_digestbynid(algo_nid);
+				if (algo_type == NULL)
+					elog(ERROR, "could not find digest for NID %s",
+						 OBJ_nid2sn(algo_nid));
+				break;
+		}
+
+		/* generate and save the certificate hash */
+		if (!X509_digest(server_cert, algo_type, hash, &hash_size))
+			elog(ERROR, "could not generate server certificate hash");
+	}
+#endif
 
 	cert_hash = palloc(hash_size);
 	memcpy(cert_hash, hash, hash_size);
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 3e9b87940b..dc65a87ee7 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -371,7 +371,6 @@ char *
 pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
 {
 	X509	   *peer_cert;
-	const EVP_MD *algo_type;
 	unsigned char hash[EVP_MAX_MD_SIZE];	/* size for SHA-512 */
 	unsigned int hash_size;
 	int			algo_nid;
@@ -406,28 +405,77 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
 	 * (https://tools.ietf.org/html/rfc5929#section-4.1).  If something else
 	 * is used, the same hash as the signature algorithm is used.
 	 */
-	switch (algo_nid)
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+
+	/*
+	 * On OpenSSL 3.0 and newer, explicitly fetch the digest by name so that it
+	 * is served by the loaded provider.  The fetched EVP_MD must be freed on
+	 * every path, including the error paths.
+	 */
 	{
-		case NID_md5:
-		case NID_sha1:
-			algo_type = EVP_sha256();
-			break;
-		default:
-			algo_type = EVP_get_digestbynid(algo_nid);
-			if (algo_type == NULL)
-			{
-				libpq_append_conn_error(conn, "could not find digest for NID %s",
-										OBJ_nid2sn(algo_nid));
-				return NULL;
-			}
-			break;
-	}
+		EVP_MD	   *algo_type;
+		const char *algo_name;
+
+		switch (algo_nid)
+		{
+			case NID_md5:
+			case NID_sha1:
+				algo_name = "SHA256";
+				break;
+			default:
+				algo_name = OBJ_nid2sn(algo_nid);
+				if (algo_name == NULL)
+				{
+					libpq_append_conn_error(conn, "could not determine digest for server certificate signature algorithm");
+					return NULL;
+				}
+				break;
+		}
+
+		algo_type = EVP_MD_fetch(NULL, algo_name, NULL);
+		if (algo_type == NULL)
+		{
+			libpq_append_conn_error(conn, "could not load digest \"%s\"", algo_name);
+			return NULL;
+		}
 
-	if (!X509_digest(peer_cert, algo_type, hash, &hash_size))
+		if (!X509_digest(peer_cert, algo_type, hash, &hash_size))
+		{
+			EVP_MD_free(algo_type);
+			libpq_append_conn_error(conn, "could not generate peer certificate hash");
+			return NULL;
+		}
+
+		EVP_MD_free(algo_type);
+	}
+#else
 	{
-		libpq_append_conn_error(conn, "could not generate peer certificate hash");
-		return NULL;
+		const EVP_MD *algo_type;
+
+		switch (algo_nid)
+		{
+			case NID_md5:
+			case NID_sha1:
+				algo_type = EVP_sha256();
+				break;
+			default:
+				algo_type = EVP_get_digestbynid(algo_nid);
+				if (algo_type == NULL)
+				{
+					libpq_append_conn_error(conn, "could not find digest for NID %s",
+											OBJ_nid2sn(algo_nid));
+					return NULL;
+				}
+				break;
+		}
+
+		if (!X509_digest(peer_cert, algo_type, hash, &hash_size))
+		{
+			libpq_append_conn_error(conn, "could not generate peer certificate hash");
+			return NULL;
+		}
 	}
+#endif
 
 	/* save result */
 	cert_hash = malloc(hash_size);
-- 
2.43.0

Reply via email to