Channel binding for post-quantum cryptography

Started by Filip Janus3 months ago15 messages
#1Filip Janus
fjanus@redhat.com
1 attachment(s)

Hi hackers,

While working on testing post-quantum cryptography integration in my
system, I discovered that PostgreSQL has an issue with channel binding when
using ML-DSA cryptographic algorithms.

The problem is caused by a difference between the currently used algorithms
and post-quantum ones. For example, commonly used algorithms like RSA have
a defined digest algorithm, but ML-DSA does not.

PostgreSQL's channel binding implementation expects all signature
algorithms to have a traditional digest mapping, but post-quantum
algorithms such as ML-DSA use their hash function internally as part of the
signature process.

As a result, the connection fails with the following error:

could not find digest for NID UNDEF

The issue can be worked around by disabling channel binding.

Although the RFC is not entirely clear on how to handle this situation, in
my patch I propose using SHA-256 as the default digest in such cases.

-Filip-

Attachments:

0001-Support-post-quantum-signature-algorithms-in-SCRAM-c.patchapplication/octet-stream; name=0001-Support-post-quantum-signature-algorithms-in-SCRAM-c.patchDownload
From f59b9d564395f8854de2a5166d2acb921231f1bf Mon Sep 17 00:00:00 2001
From: Filip Janus <fjanus@redhat.com>
Date: Tue, 14 Oct 2025 10:52:47 +0200
Subject: [PATCH] Support post-quantum signature algorithms in SCRAM channel
 binding

When using post-quantum signature algorithms (e.g., ML-DSA/Dilithium)
in server certificates, SCRAM channel binding with tls-server-end-point
would fail with "could not find digest for NID UNDEF" error.

The issue occurs because post-quantum algorithms like ML-DSA don't have
a traditional separate digest algorithm that can be queried via
EVP_get_digestbynid(). Unlike traditional algorithms (e.g., RSA-SHA256),
where the hash function is a separate step, ML-DSA uses SHAKE256
internally as an integral part of the signature algorithm.

This commit adds a fallback mechanism:
- When EVP_get_digestbynid() returns NULL, use X509_get_signature_nid()
  to verify the certificate has a valid signature algorithm
- If valid, use SHA-256 for certificate hashing as recommended for
  unknown algorithms

While RFC 5929 doesn't clearly define the behavior for signature
algorithms without separate digest mappings, SHA-256 is a reasonable
choice as it is widely used, secure for modern standards, and matches
the 256-bit security level of algorithms like ML-DSA.

This change allows PostgreSQL to work with post-quantum cryptography
without requiring channel_binding=disable.
---
 src/backend/libpq/be-secure-openssl.c    | 32 ++++++++++++++++++++++--
 src/interfaces/libpq/fe-secure-openssl.c | 32 +++++++++++++++++++++---
 2 files changed, 59 insertions(+), 5 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index c8b63ef8249..21ba0c09353 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1621,8 +1621,36 @@ be_tls_get_certificate_hash(Port *port, size_t *len)
 		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));
+			{
+				/*
+				 * EVP_get_digestbynid() returned NULL. This can happen for:
+				 * 1. Post-quantum algorithms (ML-DSA, Falcon) that don't have
+				 *    a traditional digest mapping
+				 * 2. Invalid/corrupted certificates
+				 *
+				 * Use X509_get_signature_nid() to check if this is a valid
+				 * signature algorithm. If valid, use SHA-256 for hashing.
+				 */
+				int sig_nid = X509_get_signature_nid(server_cert);
+				
+				if (sig_nid != NID_undef && sig_nid > 0)
+				{
+					/*
+					 * Valid signature algorithm without digest mapping (likely
+					 * post-quantum). RFC 5929 doesn't clearly define this case.
+					 * We use SHA-256 as it's widely used, reasonably secure for
+					 * modern standards, and matches the security level of ML-DSA
+					 * which internally uses a 256-bit hash algorithm (SHAKE256).
+					 */
+					algo_type = EVP_sha256();
+				}
+				else
+				{
+					/* Invalid or corrupted certificate */
+					elog(ERROR, "could not find digest for NID %s",
+						 OBJ_nid2sn(algo_nid));
+				}
+			}
 			break;
 	}
 
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 51dd7b9fec0..a3a35e171ff 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -385,9 +385,35 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
 			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;
+				/*
+				 * EVP_get_digestbynid() returned NULL. This can happen for:
+				 * 1. Post-quantum algorithms (ML-DSA, Falcon) that don't have
+				 *    a traditional digest mapping
+				 * 2. Invalid/corrupted certificates
+				 *
+				 * Use X509_get_signature_nid() to check if this is a valid
+				 * signature algorithm. If valid, use SHA-256 for hashing.
+				 */
+				int sig_nid = X509_get_signature_nid(peer_cert);
+				
+				if (sig_nid != NID_undef && sig_nid > 0)
+				{
+					/*
+					 * Valid signature algorithm without digest mapping (likely
+					 * post-quantum). RFC 5929 doesn't clearly define this case.
+					 * We use SHA-256 as it's widely used, reasonably secure for
+					 * modern standards, and matches the security level of ML-DSA
+					 * which internally uses a 256-bit hash algorithm (SHAKE256).
+					 */
+					algo_type = EVP_sha256();
+				}
+				else
+				{
+					/* Invalid or corrupted certificate */
+					libpq_append_conn_error(conn, "could not find digest for NID %s",
+											OBJ_nid2sn(algo_nid));
+					return NULL;
+				}
 			}
 			break;
 	}
-- 
2.39.5 (Apple Git-154)

#2Michael Paquier
michael@paquier.xyz
In reply to: Filip Janus (#1)
Re: Channel binding for post-quantum cryptography

On Mon, Oct 20, 2025 at 09:12:52AM +0200, Filip Janus wrote:

The problem is caused by a difference between the currently used algorithms
and post-quantum ones. For example, commonly used algorithms like RSA have
a defined digest algorithm, but ML-DSA does not.

PostgreSQL's channel binding implementation expects all signature
algorithms to have a traditional digest mapping, but post-quantum
algorithms such as ML-DSA use their hash function internally as part of the
signature process.

Noted.

As a result, the connection fails with the following error:

could not find digest for NID UNDEF

The issue can be worked around by disabling channel binding.

Although the RFC is not entirely clear on how to handle this situation, in
my patch I propose using SHA-256 as the default digest in such cases.

Based on the RFC at [1]https://datatracker.ietf.org/doc/html/rfc5929#section-4.1 -- Michael, we have indeed:
if the certificate's signatureAlgorithm uses a single hash function
and that hash function neither MD5 nor SHA-1, then use the hash
function associated with the certificate's signatureAlgorithm;

So it would be essential to reuse the hash function used by this
algorithm. Except that you are saying that we have no way to retrieve
that, even if it's a different routine than EVP_get_digestbynid()?
Enforcing blindly SHA-256 does not seem to be the right move because
it could enforce an incorrect behavior, even if it would be more
user-friendly in some cases like this one.

So, X509_get_signature_info() is able to return an algo NID that we
can then use to decide which algo type we should take. I can see
three NIDs associated to ML-DSA: NID_ML_DSA_44, NID_ML_DSA_65 and
NID_ML_DSA_87. Could this list grow?

Based on ml-dsa.md, I am wondering if we could do something based on
ML_DSA_PARAM. I am not sure, but OpenSSL, while being a spaghetti
code base, usually has some internal way to extract some of its
contents. Saying that, they tend to hide more internals behind
pointers, 3.0 has added some of that stuff.

A good first step would be to design a reproducible test case to
investigate the issue. Could it be possible to craft a test case that
could then be added into the tree? We have automated tests in
src/test/ssl/. See for example the level of craft done for a similar
past issue, as of commit 9244c11afe23. That would be the minimum
required for a potential fix.

Note that the use of X509_get_signature_info() is conditional in our
code, so your patch would likely fail to compile depending on the
version of OpenSSL linked to. The minimum version of OpenSSL
supported by PG on HEAD is 1.1.1. On the oldest stable branches (v13
or v14), this requirement is.. Cough.. 1.0.1.

[1]: https://datatracker.ietf.org/doc/html/rfc5929#section-4.1 -- Michael
--
Michael

#3Filip Janus
fjanus@redhat.com
In reply to: Michael Paquier (#2)
2 attachment(s)
Re: Channel binding for post-quantum cryptography

Hi,

Thank you for the detailed feedback. Let me address your points:

Test Case
=========

I have prepared a test case following the pattern from commit 9244c11afe23
(RSA-PSS fix).

Regarding the Hash Algorithm
=============================

You are correct that, according to RFC 5929, we should ideally use the hash
function associated with the certificate's signatureAlgorithm. However, if
I understand it correctly, there are distinctions with ML-DSA:
I investigated OpenSSL's API to retrieve the hash algorithm used by ML-DSA,
and I haven't found a suitable solution.

ML-DSA seems to have an internal structure but no public API to extract
SHAKE128/256 configuration

The ML-DSA Specifies

ML-DSA (FIPS 204) uses SHAKE internally:
- ML-DSA-44: SHAKE128 (128-bit security)
- ML-DSA-65: SHAKE256 (192-bit security)
- ML-DSA-87: SHAKE256 (256-bit security)

However, this is a different approach from traditional signature algorithms:
- Traditional (e.g., RSA-SHA256): Hash is a separate, associated algorithm
that can be queried
- ML-DSA: SHAKE is an internal component of the signature algorithm, not an
associated digest for external use

ML-DSA doesn't have an "associated" hash function in the sense that
RSA-SHA256 does. The SHAKE function is internal to the signing process, not
a separate digest step. For the purpose of channel binding (hashing the
entire certificate), we need a traditional hash function. So that's why
I've chosen SHA-256

SHA-256 is appropriate because:
1. It matches the security level of ML-DSA-65 (both ~256-bit security)
2. RFC 5929 recommends SHA-256 for unknown/unsupported algorithms
3. It's the standard fallback used throughout the codebase
4. SHAKE256 (via EVP_shake256()) is an XOF (eXtendable Output Function),
not a traditional fixed-size hash suitable for this use case

Regarding NIDs and Future Extensions, I would expect growth, but I am not a
security specialist.

Current Patch Status
====================

I'm keeping the current patch as-is for now, and I am adding the requested
test case in a separate commit.

-Filip-

po 20. 10. 2025 v 10:06 odesílatel Michael Paquier <michael@paquier.xyz>
napsal:

Show quoted text

On Mon, Oct 20, 2025 at 09:12:52AM +0200, Filip Janus wrote:

The problem is caused by a difference between the currently used

algorithms

and post-quantum ones. For example, commonly used algorithms like RSA

have

a defined digest algorithm, but ML-DSA does not.

PostgreSQL's channel binding implementation expects all signature
algorithms to have a traditional digest mapping, but post-quantum
algorithms such as ML-DSA use their hash function internally as part of

the

signature process.

Noted.

As a result, the connection fails with the following error:

could not find digest for NID UNDEF

The issue can be worked around by disabling channel binding.

Although the RFC is not entirely clear on how to handle this situation,

in

my patch I propose using SHA-256 as the default digest in such cases.

Based on the RFC at [1], we have indeed:
if the certificate's signatureAlgorithm uses a single hash function
and that hash function neither MD5 nor SHA-1, then use the hash
function associated with the certificate's signatureAlgorithm;

So it would be essential to reuse the hash function used by this
algorithm. Except that you are saying that we have no way to retrieve
that, even if it's a different routine than EVP_get_digestbynid()?
Enforcing blindly SHA-256 does not seem to be the right move because
it could enforce an incorrect behavior, even if it would be more
user-friendly in some cases like this one.

So, X509_get_signature_info() is able to return an algo NID that we
can then use to decide which algo type we should take. I can see
three NIDs associated to ML-DSA: NID_ML_DSA_44, NID_ML_DSA_65 and
NID_ML_DSA_87. Could this list grow?

Based on ml-dsa.md, I am wondering if we could do something based on
ML_DSA_PARAM. I am not sure, but OpenSSL, while being a spaghetti
code base, usually has some internal way to extract some of its
contents. Saying that, they tend to hide more internals behind
pointers, 3.0 has added some of that stuff.

A good first step would be to design a reproducible test case to
investigate the issue. Could it be possible to craft a test case that
could then be added into the tree? We have automated tests in
src/test/ssl/. See for example the level of craft done for a similar
past issue, as of commit 9244c11afe23. That would be the minimum
required for a potential fix.

Note that the use of X509_get_signature_info() is conditional in our
code, so your patch would likely fail to compile depending on the
version of OpenSSL linked to. The minimum version of OpenSSL
supported by PG on HEAD is 1.1.1. On the oldest stable branches (v13
or v14), this requirement is.. Cough.. 1.0.1.

[1]: https://datatracker.ietf.org/doc/html/rfc5929#section-4.1
--
Michael

Attachments:

0002-Support-post-quantum-signature-algorithms-in-SCRAM-c.patchapplication/octet-stream; name=0002-Support-post-quantum-signature-algorithms-in-SCRAM-c.patchDownload
From 469d7b478aa8c1f05003411d023b96486221f8c4 Mon Sep 17 00:00:00 2001
From: Filip Janus <fjanus@redhat.com>
Date: Tue, 14 Oct 2025 10:52:47 +0200
Subject: [PATCH 2/2] Support post-quantum signature algorithms in SCRAM
 channel binding

When using post-quantum signature algorithms (e.g., ML-DSA/Dilithium)
in server certificates, SCRAM channel binding with tls-server-end-point
would fail with "could not find digest for NID UNDEF" error.

The issue occurs because post-quantum algorithms like ML-DSA don't have
a traditional separate digest algorithm that can be queried via
EVP_get_digestbynid(). Unlike traditional algorithms (e.g., RSA-SHA256),
where the hash function is a separate step, ML-DSA uses SHAKE256
internally as an integral part of the signature algorithm.

This commit adds a fallback mechanism:
- When EVP_get_digestbynid() returns NULL, use X509_get_signature_nid()
  to verify the certificate has a valid signature algorithm
- If valid, use SHA-256 for certificate hashing as recommended for
  unknown algorithms

While RFC 5929 doesn't clearly define the behavior for signature
algorithms without separate digest mappings, SHA-256 is a reasonable
choice as it is widely used, secure for modern standards, and matches
the 256-bit security level of algorithms like ML-DSA.

This change allows PostgreSQL to work with post-quantum cryptography
without requiring channel_binding=disable.
---
 src/backend/libpq/be-secure-openssl.c    | 32 ++++++++++++++++++++++--
 src/interfaces/libpq/fe-secure-openssl.c | 32 +++++++++++++++++++++---
 2 files changed, 59 insertions(+), 5 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index c8b63ef8249..21ba0c09353 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1621,8 +1621,36 @@ be_tls_get_certificate_hash(Port *port, size_t *len)
 		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));
+			{
+				/*
+				 * EVP_get_digestbynid() returned NULL. This can happen for:
+				 * 1. Post-quantum algorithms (ML-DSA, Falcon) that don't have
+				 *    a traditional digest mapping
+				 * 2. Invalid/corrupted certificates
+				 *
+				 * Use X509_get_signature_nid() to check if this is a valid
+				 * signature algorithm. If valid, use SHA-256 for hashing.
+				 */
+				int sig_nid = X509_get_signature_nid(server_cert);
+				
+				if (sig_nid != NID_undef && sig_nid > 0)
+				{
+					/*
+					 * Valid signature algorithm without digest mapping (likely
+					 * post-quantum). RFC 5929 doesn't clearly define this case.
+					 * We use SHA-256 as it's widely used, reasonably secure for
+					 * modern standards, and matches the security level of ML-DSA
+					 * which internally uses a 256-bit hash algorithm (SHAKE256).
+					 */
+					algo_type = EVP_sha256();
+				}
+				else
+				{
+					/* Invalid or corrupted certificate */
+					elog(ERROR, "could not find digest for NID %s",
+						 OBJ_nid2sn(algo_nid));
+				}
+			}
 			break;
 	}
 
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 51dd7b9fec0..a3a35e171ff 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -385,9 +385,35 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
 			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;
+				/*
+				 * EVP_get_digestbynid() returned NULL. This can happen for:
+				 * 1. Post-quantum algorithms (ML-DSA, Falcon) that don't have
+				 *    a traditional digest mapping
+				 * 2. Invalid/corrupted certificates
+				 *
+				 * Use X509_get_signature_nid() to check if this is a valid
+				 * signature algorithm. If valid, use SHA-256 for hashing.
+				 */
+				int sig_nid = X509_get_signature_nid(peer_cert);
+				
+				if (sig_nid != NID_undef && sig_nid > 0)
+				{
+					/*
+					 * Valid signature algorithm without digest mapping (likely
+					 * post-quantum). RFC 5929 doesn't clearly define this case.
+					 * We use SHA-256 as it's widely used, reasonably secure for
+					 * modern standards, and matches the security level of ML-DSA
+					 * which internally uses a 256-bit hash algorithm (SHAKE256).
+					 */
+					algo_type = EVP_sha256();
+				}
+				else
+				{
+					/* Invalid or corrupted certificate */
+					libpq_append_conn_error(conn, "could not find digest for NID %s",
+											OBJ_nid2sn(algo_nid));
+					return NULL;
+				}
 			}
 			break;
 	}
-- 
2.39.5 (Apple Git-154)

0001-Add-regression-test-for-ML-DSA-channel-binding-suppo.patchapplication/octet-stream; name=0001-Add-regression-test-for-ML-DSA-channel-binding-suppo.patchDownload
From 3f1f372feab12f1373973c4287a0cd5fcd4c2a6d Mon Sep 17 00:00:00 2001
From: Filip Janus <fjanus@redhat.com>
Date: Wed, 22 Oct 2025 22:31:25 +0200
Subject: [PATCH 1/2] Add regression test for ML-DSA channel binding support

Add a test case to verify that SCRAM channel binding works correctly
with ML-DSA-65 (post-quantum) server certificates.

This test is similar to the existing RSA-PSS test and verifies:
- ML-DSA-65 certificates can be loaded
- Channel binding works with post-quantum signature algorithms
- SCRAM-SHA-256 authentication succeeds
- No 'could not find digest for NID UNDEF' error occurs

The test uses a self-signed ML-DSA-65 certificate generated via
sslfiles.mk
---
 src/test/ssl/conf/server-mldsa65.config | 15 +++++++++++++++
 src/test/ssl/sslfiles.mk                | 14 ++++++++++++--
 src/test/ssl/t/002_scram.pl             | 19 +++++++++++++++++++
 3 files changed, 46 insertions(+), 2 deletions(-)
 create mode 100644 src/test/ssl/conf/server-mldsa65.config

diff --git a/src/test/ssl/conf/server-mldsa65.config b/src/test/ssl/conf/server-mldsa65.config
new file mode 100644
index 00000000000..acda640e58c
--- /dev/null
+++ b/src/test/ssl/conf/server-mldsa65.config
@@ -0,0 +1,15 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+# This is identical to server-cn-only certificate, but we specify
+# ML-DSA-65 as the algorithm on the command line.
+
+[ req ]
+distinguished_name     = req_distinguished_name
+prompt                 = no
+
+[ req_distinguished_name ]
+CN = common-name.pg-ssltest.test
+OU = PostgreSQL test suite
+
+# No Subject Alternative Names
+
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index 23aaad0c766..b7e07ba5e44 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -40,14 +40,16 @@ CLIENTS := client client-dn client-revoked client_ext client-long \
 # To add a new non-standard certificate, add it to SPECIAL_CERTS and then add
 # a recipe for creating it to the "Special-case certificates" section below.
 #
-SPECIAL_CERTS := ssl/server-rsapss.crt
+SPECIAL_CERTS := ssl/server-rsapss.crt \
+	ssl/server-mldsa65.crt
 
 # Likewise for non-standard keys
 SPECIAL_KEYS := ssl/server-password.key \
 	ssl/client-der.key \
 	ssl/client-encrypted-pem.key \
 	ssl/client-encrypted-der.key \
-	ssl/server-rsapss.key
+	ssl/server-rsapss.key \
+	ssl/server-mldsa65.key
 
 #
 # These files are just concatenations of other files. You can add new ones to
@@ -101,6 +103,10 @@ ssl/root_ca.crt: ssl/root_ca.key conf/root_ca.config
 ssl/server-rsapss.crt: ssl/server-rsapss.key conf/server-rsapss.config
 	$(OPENSSL) req -new -x509 -config conf/server-rsapss.config -key $< -out $@
 
+# Certificate using ML-DSA-65 algorithm. Also self-signed.
+ssl/server-mldsa65.crt: ssl/server-mldsa65.key conf/server-mldsa65.config
+	$(OPENSSL) req -new -x509 -config conf/server-mldsa65.config -key $< -out $@
+
 #
 # Special-case keys
 #
@@ -115,6 +121,10 @@ ssl/server-password.key: ssl/server-cn-only.key
 ssl/server-rsapss.key:
 	$(OPENSSL) genpkey -algorithm rsa-pss -out $@
 
+# Key that uses the ML-DSA-65 algorithm
+ssl/server-mldsa65.key:
+	$(OPENSSL) genpkey -algorithm ML-DSA-65 -out $@
+
 # DER-encoded version of client.key
 ssl/client-der.key: ssl/client.key
 	$(OPENSSL) rsa -in $< -outform DER -out $@
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 60b60b28657..3132b6ba758 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -171,4 +171,23 @@ if ($supports_rsapss_certs)
 			qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 		]);
 }
+
+# Now test with a server certificate that uses the ML-DSA-65 algorithm.
+# This tests post-quantum cryptography support for channel binding.
+SKIP:
+{
+	# Check if ML-DSA-65 certificate exists (requires OpenSSL 3.5+)
+	my $mldsa_cert = "ssl/server-mldsa65.crt";
+	skip "ML-DSA-65 requires OpenSSL 3.5+ for certificate generation", 1
+	  unless -f $mldsa_cert;
+
+	switch_server_cert($node, certfile => 'server-mldsa65');
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require",
+		"SCRAM with SSL and channel_binding=require, server certificate uses 'ML-DSA-65'",
+		log_like => [
+			qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
+		]);
+}
+
 done_testing();
-- 
2.39.5 (Apple Git-154)

#4Michael Paquier
michael@paquier.xyz
In reply to: Filip Janus (#3)
Re: Channel binding for post-quantum cryptography

On Sun, Oct 26, 2025 at 11:20:53AM +0100, Filip Janus wrote:

I have prepared a test case following the pattern from commit 9244c11afe23
(RSA-PSS fix).

Thanks, I'm able to reproduce your problem with the error you have,
after generating the certs.

+   my $mldsa_cert = "ssl/server-mldsa65.crt";
+   skip "ML-DSA-65 requires OpenSSL 3.5+ for certificate generation",1
+     unless -f $mldsa_cert;

The certs are stored in the tree, regenerated by a `make sslfiles` run
in src/test/ssl/. We cannot rely on such a check to decide if this
scenario should be skipped or not. In past branches, like
REL_13_STABLE, one example of a "correct" way is done in 002_scram.pl
with HAVE_X509_GET_SIGNATURE_NID, where we rely on a compile check
when running the test.

You are correct that, according to RFC 5929, we should ideally use the hash
function associated with the certificate's signatureAlgorithm. However, if
I understand it correctly, there are distinctions with ML-DSA:
I investigated OpenSSL's API to retrieve the hash algorithm used by ML-DSA,
and I haven't found a suitable solution.

ML-DSA seems to have an internal structure but no public API to extract
SHAKE128/256 configuration.

Hmm. Has this question been asked to upstream OpenSSL? Perhaps their
reply would be "you-are-doing-it-wrong", but it may be something where
their input may drive the implementation.

The ML-DSA Specifies

ML-DSA (FIPS 204) uses SHAKE internally:
- ML-DSA-44: SHAKE128 (128-bit security)
- ML-DSA-65: SHAKE256 (192-bit security)
- ML-DSA-87: SHAKE256 (256-bit security)

Yeah, I've been reading around this area as well, while browsing the
code:
https://github.com/openssl/openssl/blob/master/doc/designs/ml-dsa.md
https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf

There are traces in the OpenSSL code of the following things, not sure
if these could point at something:
NID_HASH_ML_DSA_44_WITH_SHA512
NID_HASH_ML_DSA_65_WITH_SHA512
NID_HASH_ML_DSA_87_WITH_SHA512

ML-DSA doesn't have an "associated" hash function in the sense that
RSA-SHA256 does. The SHAKE function is internal to the signing process, not
a separate digest step. For the purpose of channel binding (hashing the
entire certificate), we need a traditional hash function. So that's why
I've chosen SHA-256

Another thing that bugs me is that this patch would force sha-256 for
everything, without at least checks based on NID_ML_DSA_44,
NID_ML_DSA_65 or NID_ML_DSA_87. That may be more flexible, but I'm
wondering if it could become a problem long-term to enforce blindly
such a policy every time algo_nid is undefined.

Regarding NIDs and Future Extensions, I would expect growth, but I am not a
security specialist.

This needs more study on my part, at least. Adding a couple more
folks in CC for now. Perhaps they have an opinion on the matter, I am
not the most familiar with these new toys in OpenSSL 3.5.

Anyway, even with these points, I am not much a fan of adding again
a dependency to X509_get_signature_nid() while we have switched to
X509_get_signature_info() to be able to support RSA-PSS signatures.
It is annoying to have to rely again on X509_get_signature_nid() for
what's a new special case, NID_undef being the synonym of an error
usually, and that's what EVP_get_digestbynid() is complaining about in
this case.
--
Michael

#5Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#4)
Re: Channel binding for post-quantum cryptography

On Mon, Oct 27, 2025 at 10:55 PM Michael Paquier <michael@paquier.xyz> wrote:

Another thing that bugs me is that this patch would force sha-256 for
everything, without at least checks based on NID_ML_DSA_44,
NID_ML_DSA_65 or NID_ML_DSA_87. That may be more flexible, but I'm
wondering if it could become a problem long-term to enforce blindly
such a policy every time algo_nid is undefined.

I think it would be a problem, at least if the previous conversations
around X509_get_signature_nid() are any indication.

Filip, you said

RFC 5929 recommends SHA-256 for unknown/unsupported algorithms

but I don't see any language like that; can you provide a quote? That
doesn't seem like a recommendation that would allow for
interoperability in the long term.

The IETF draft at [1]https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates/ (which was updated just last month) seems to
provide new signatureAlgorithm IDs for ML-DSA. Is this just a matter
of waiting until the specs are released and OpenSSL implements them?

Thanks,
--Jacob

[1]: https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates/

#6Nico Williams
nico@cryptonector.com
In reply to: Michael Paquier (#2)
Re: Channel binding for post-quantum cryptography

On Mon, Oct 20, 2025 at 05:06:12PM +0900, Michael Paquier wrote:

On Mon, Oct 20, 2025 at 09:12:52AM +0200, Filip Janus wrote:

The problem is caused by a difference between the currently used algorithms
and post-quantum ones. For example, commonly used algorithms like RSA have
a defined digest algorithm, but ML-DSA does not.

PostgreSQL's channel binding implementation expects all signature
algorithms to have a traditional digest mapping, but post-quantum
algorithms such as ML-DSA use their hash function internally as part of the
signature process.

Noted.

As a result, the connection fails with the following error:

could not find digest for NID UNDEF

The issue can be worked around by disabling channel binding.

Although the RFC is not entirely clear on how to handle this situation, in
my patch I propose using SHA-256 as the default digest in such cases.

Based on the RFC at [1], we have indeed:

RFC 5929 co-author here. We should take this to the IETF TLS WG mailing
list and update RFC 5929 and the tls-server-end-point registraion to fix
this.

Options in the case that the certificate's signature algorithm does not
have a digest associated with it include:

- use the whole certificate un-digested (but smallish CB data is
somewhat useful)

or

- else specify the use of a digest negotiated by TLS (except that this
is rather inconvenient since it means extracting that metadata from
the connection)

or

- we could specify `tls-server-end-point-<digest>` channel bindings
(but the the PG client and server would have to negotiate _that_)

or

- we could specify a disgest for this purpose for each signature
algorithm that does not have an associated digest

The app could pick a digest by itself, but if the app were using a TLS
library API to get at the `tls-server-end-point` CB as such then that
wouldn't help unless there was also an API to get at the raw server
certificate.

Maybe there are more options still. But we're not likely to solve this
problem here. This really belongs on the IETF TLS WG mailing list.

Nico
--

#7Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Nico Williams (#6)
Re: Channel binding for post-quantum cryptography

On Tue, Oct 28, 2025 at 9:46 AM Nico Williams <nico@cryptonector.com> wrote:

RFC 5929 co-author here. We should take this to the IETF TLS WG mailing
list and update RFC 5929 and the tls-server-end-point registraion to fix
this.

Options in the case that the certificate's signature algorithm does not
have a digest associated with it include:

Ah. (Filip, disregard my earlier question about the draft RFC and
sigalgs; I think I understand now. I didn't look closely enough at the
patch before sending.)

Maybe there are more options still. But we're not likely to solve this
problem here. This really belongs on the IETF TLS WG mailing list.

+1. (Any immediate takers on the committer side?)

--Jacob

#8Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#7)
Re: Channel binding for post-quantum cryptography

On Tue, Oct 28, 2025 at 10:34:27AM -0700, Jacob Champion wrote:

On Tue, Oct 28, 2025 at 9:46 AM Nico Williams <nico@cryptonector.com> wrote:

RFC 5929 co-author here. We should take this to the IETF TLS WG mailing
list and update RFC 5929 and the tls-server-end-point registraion to fix
this.

Wow. Thanks Nico for the update!

Options in the case that the certificate's signature algorithm does not
have a digest associated with it include:

Ah. (Filip, disregard my earlier question about the draft RFC and
sigalgs; I think I understand now. I didn't look closely enough at the
patch before sending.)

Maybe there are more options still. But we're not likely to solve this
problem here. This really belongs on the IETF TLS WG mailing list.

+1. (Any immediate takers on the committer side?)

+1. Yes, I don't see how we can decide anything immediately without
making sure that our choice is compliant with the existing .
Assuming that OpenSSL provides an API that could help us here, do you
think that it could be possible to have access to it in 3.5 where
ML-DSA has been added? That's a matter to bring to OpenSSL upstream,
of course.

Btw, I don't think I would be that useful if we enter in these level
of details within the RFC, so perhaps it had better not be me who
follows up. :)

Among the options presented, I won't hide that being able to extract
an algorithm when we don't have an associated digest would be the most
interesting option when it comes to PG: that's a no-brainer in terms
of backpatching and would require no protocol changes while still
using the same name "tls-server-end-point" when exchanging the
SASL messages.
--
Michael

#9Nico Williams
nico@cryptonector.com
In reply to: Filip Janus (#1)
Re: Channel binding for post-quantum cryptography

On Mon, Oct 20, 2025 at 09:12:52AM +0200, Filip Janus wrote:

The problem is caused by a difference between the currently used algorithms
and post-quantum ones. For example, commonly used algorithms like RSA have
a defined digest algorithm, but ML-DSA does not.

Looking more carefully, ML-DSA uses two hash functions internally,
though not to digest the to-be-signed data: SHAKE128 and SHAK256, so
this falls in to the sub-case of the certificate's signatureAlgorithm
using multiple hash functions in RFC 5929, section 4.1, third item, so
indeed we can't define tls-server-end-point.

Perhaps the fix for this is for signing algorithms to specify what hash
or "hash" function to use for tls-server-end-point channel bindings
(possibly the identity function).

I will post as much to the TLS mailing list, but since ML-DSA is
specified by NIST, any change to ML-DSA to say this will have to go
through them, and so on for others, so we might just be best off instead
altering RFC 5929 and maybe setting up an IANA registry.

Fun stuff.

Nico
--

#10Nico Williams
nico@cryptonector.com
In reply to: Filip Janus (#1)
Re: Channel binding for post-quantum cryptography

I posted (including your attachment, by accident, since at first I was
going to forward your post) about this to the IETF TLS WG mailing list.

https://mailarchive.ietf.org/arch/msg/tls/CEaZg1l-4iVg0_wdEr5_rXfGYWc/

#11Filip Janus
fjanus@redhat.com
In reply to: Nico Williams (#10)
Re: Channel binding for post-quantum cryptography

Thank you for posting it there. If I understand correctly, the resolution
should be to use internal hash algorithms — in this case, SHAKE.
Now, the question is whether to wait for the implementation of a public API
to make the change as general as possible, or to try implementing it on the
PG side?

-Filip-

st 29. 10. 2025 v 6:17 odesílatel Nico Williams <nico@cryptonector.com>
napsal:

Show quoted text

I posted (including your attachment, by accident, since at first I was
going to forward your post) about this to the IETF TLS WG mailing list.

https://mailarchive.ietf.org/arch/msg/tls/CEaZg1l-4iVg0_wdEr5_rXfGYWc/

#12Nico Williams
nico@cryptonector.com
In reply to: Filip Janus (#11)
Re: Channel binding for post-quantum cryptography

On Thu, Oct 30, 2025 at 11:39:38AM +0100, Filip Janus wrote:

Thank you for posting it there. If I understand correctly, the resolution
should be to use internal hash algorithms — in this case, SHAKE.

In this case, yes, it seem the consensus (though it's early to call it)
is SHAKE256.

Now, the question is whether to wait for the implementation of a public API
to make the change as general as possible, or to try implementing it on the
PG side?

If you can wait, wait. Otherwise if the consensus changes then you'll
be stuck with flag day eventually.

#13Filip Janus
fjanus@redhat.com
In reply to: Michael Paquier (#4)
1 attachment(s)
Re: Channel binding for post-quantum cryptography

út 28. 10. 2025 v 6:55 odesílatel Michael Paquier <michael@paquier.xyz>
napsal:

On Sun, Oct 26, 2025 at 11:20:53AM +0100, Filip Janus wrote:

I have prepared a test case following the pattern from commit

9244c11afe23

(RSA-PSS fix).

Thanks, I'm able to reproduce your problem with the error you have,
after generating the certs.

+   my $mldsa_cert = "ssl/server-mldsa65.crt";
+   skip "ML-DSA-65 requires OpenSSL 3.5+ for certificate generation",1
+     unless -f $mldsa_cert;

The certs are stored in the tree, regenerated by a `make sslfiles` run
in src/test/ssl/. We cannot rely on such a check to decide if this
scenario should be skipped or not. In past branches, like
REL_13_STABLE, one example of a "correct" way is done in 002_scram.pl
with HAVE_X509_GET_SIGNATURE_NID, where we rely on a compile check
when running the test.

While fixing the actual issue will take some time, I’ve fixed the requested
test.
Since I’m still quite new to the PG community, would it make sense to
propose a patch that only adds the test?

Show quoted text

You are correct that, according to RFC 5929, we should ideally use the

hash

function associated with the certificate's signatureAlgorithm. However,

if

I understand it correctly, there are distinctions with ML-DSA:
I investigated OpenSSL's API to retrieve the hash algorithm used by

ML-DSA,

and I haven't found a suitable solution.

ML-DSA seems to have an internal structure but no public API to extract
SHAKE128/256 configuration.

Hmm. Has this question been asked to upstream OpenSSL? Perhaps their
reply would be "you-are-doing-it-wrong", but it may be something where
their input may drive the implementation.

The ML-DSA Specifies

ML-DSA (FIPS 204) uses SHAKE internally:
- ML-DSA-44: SHAKE128 (128-bit security)
- ML-DSA-65: SHAKE256 (192-bit security)
- ML-DSA-87: SHAKE256 (256-bit security)

Yeah, I've been reading around this area as well, while browsing the
code:
https://github.com/openssl/openssl/blob/master/doc/designs/ml-dsa.md
https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf

There are traces in the OpenSSL code of the following things, not sure
if these could point at something:
NID_HASH_ML_DSA_44_WITH_SHA512
NID_HASH_ML_DSA_65_WITH_SHA512
NID_HASH_ML_DSA_87_WITH_SHA512

ML-DSA doesn't have an "associated" hash function in the sense that
RSA-SHA256 does. The SHAKE function is internal to the signing process,

not

a separate digest step. For the purpose of channel binding (hashing the
entire certificate), we need a traditional hash function. So that's why
I've chosen SHA-256

Another thing that bugs me is that this patch would force sha-256 for
everything, without at least checks based on NID_ML_DSA_44,
NID_ML_DSA_65 or NID_ML_DSA_87. That may be more flexible, but I'm
wondering if it could become a problem long-term to enforce blindly
such a policy every time algo_nid is undefined.

Regarding NIDs and Future Extensions, I would expect growth, but I am

not a

security specialist.

This needs more study on my part, at least. Adding a couple more
folks in CC for now. Perhaps they have an opinion on the matter, I am
not the most familiar with these new toys in OpenSSL 3.5.

Anyway, even with these points, I am not much a fan of adding again
a dependency to X509_get_signature_nid() while we have switched to
X509_get_signature_info() to be able to support RSA-PSS signatures.
It is annoying to have to rely again on X509_get_signature_nid() for
what's a new special case, NID_undef being the synonym of an error
usually, and that's what EVP_get_digestbynid() is complaining about in
this case.
--
Michael

Attachments:

0001-Add-regression-test-for-ML-DSA-channel-binding-suppo.patchapplication/octet-stream; name=0001-Add-regression-test-for-ML-DSA-channel-binding-suppo.patchDownload
From 5e4c2eb07e3be5bbabe71c315b36702e0004ca25 Mon Sep 17 00:00:00 2001
From: Filip Janus <fjanus@redhat.com>
Date: Wed, 22 Oct 2025 22:31:25 +0200
Subject: [PATCH 1/2] Add regression test for ML-DSA channel binding support

Add a test case to verify that SCRAM channel binding works correctly
with ML-DSA-65 (post-quantum) server certificates.

This test is similar to the existing RSA-PSS test and verifies:
- ML-DSA-65 certificates can be loaded
- Channel binding works with post-quantum signature algorithms
- SCRAM-SHA-256 authentication succeeds
- No 'could not find digest for NID UNDEF' error occurs

The test uses a self-signed ML-DSA-65 certificate generated via
sslfiles.mk
---
 configure                               | 285 ++++++++++++------------
 configure.ac                            |   5 +
 meson.build                             |   9 +
 src/include/pg_config.h.in              |   3 +
 src/test/ssl/README                     |   3 +-
 src/test/ssl/conf/server-mldsa65.config |  15 ++
 src/test/ssl/ssl/server-mldsa65.crt     | 118 ++++++++++
 src/test/ssl/ssl/server-mldsa65.key     |  88 ++++++++
 src/test/ssl/sslfiles.mk                |  14 +-
 src/test/ssl/t/002_scram.pl             |  18 ++
 10 files changed, 417 insertions(+), 141 deletions(-)
 create mode 100644 src/test/ssl/conf/server-mldsa65.config
 create mode 100644 src/test/ssl/ssl/server-mldsa65.crt
 create mode 100644 src/test/ssl/ssl/server-mldsa65.key

diff --git a/configure b/configure
index 22cd866147b..690cbd04ac5 100755
--- a/configure
+++ b/configure
@@ -2128,6 +2128,56 @@ $as_echo "$ac_res" >&6; }
 
 } # ac_fn_c_check_func
 
+# ac_fn_c_check_decl LINENO SYMBOL VAR INCLUDES
+# ---------------------------------------------
+# Tests whether SYMBOL is declared in INCLUDES, setting cache variable VAR
+# accordingly.
+ac_fn_c_check_decl ()
+{
+  as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+  # Initialize each $ac_[]_AC_LANG_ABBREV[]_decl_warn_flag once.
+      as_decl_name=`echo $2|sed 's/ *(.*//'`
+  as_decl_use=`echo $2|sed -e 's/(/((/' -e 's/)/) 0&/' -e 's/,/) 0& (/g'`
+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $as_decl_name is declared" >&5
+$as_echo_n "checking whether $as_decl_name is declared... " >&6; }
+if eval \${$3+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  ac_save_werror_flag=$ac_c_werror_flag
+  ac_c_werror_flag="$ac_c_decl_warn_flag$ac_c_werror_flag"
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+$4
+int
+main ()
+{
+#ifndef $as_decl_name
+#ifdef __cplusplus
+  (void) $as_decl_use;
+#else
+  (void) $as_decl_name;
+#endif
+#endif
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"; then :
+  eval "$3=yes"
+else
+  eval "$3=no"
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+  ac_c_werror_flag=$ac_save_werror_flag
+fi
+eval ac_res=\$$3
+	       { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
+$as_echo "$ac_res" >&6; }
+  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+
+} # ac_fn_c_check_decl
+
 # ac_fn_c_check_member LINENO AGGR MEMBER VAR INCLUDES
 # ----------------------------------------------------
 # Tries to find if the field MEMBER exists in type AGGR, after including
@@ -2421,56 +2471,6 @@ rm -f conftest.val
   as_fn_set_status $ac_retval
 
 } # ac_fn_c_compute_int
-
-# ac_fn_c_check_decl LINENO SYMBOL VAR INCLUDES
-# ---------------------------------------------
-# Tests whether SYMBOL is declared in INCLUDES, setting cache variable VAR
-# accordingly.
-ac_fn_c_check_decl ()
-{
-  as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
-  # Initialize each $ac_[]_AC_LANG_ABBREV[]_decl_warn_flag once.
-      as_decl_name=`echo $2|sed 's/ *(.*//'`
-  as_decl_use=`echo $2|sed -e 's/(/((/' -e 's/)/) 0&/' -e 's/,/) 0& (/g'`
-  { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $as_decl_name is declared" >&5
-$as_echo_n "checking whether $as_decl_name is declared... " >&6; }
-if eval \${$3+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  ac_save_werror_flag=$ac_c_werror_flag
-  ac_c_werror_flag="$ac_c_decl_warn_flag$ac_c_werror_flag"
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-$4
-int
-main ()
-{
-#ifndef $as_decl_name
-#ifdef __cplusplus
-  (void) $as_decl_use;
-#else
-  (void) $as_decl_name;
-#endif
-#endif
-
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  eval "$3=yes"
-else
-  eval "$3=no"
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-  ac_c_werror_flag=$ac_save_werror_flag
-fi
-eval ac_res=\$$3
-	       { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
-$as_echo "$ac_res" >&6; }
-  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
-
-} # ac_fn_c_check_decl
 cat >config.log <<_ACEOF
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
@@ -12969,6 +12969,103 @@ _ACEOF
 fi
 done
 
+  # Check for ML-DSA support (OpenSSL 3.5+)
+  # The Clang compiler raises a warning for an undeclared identifier that matches
+# a compiler builtin function.  All extant Clang versions are affected, as of
+# Clang 3.6.0.  Test a builtin known to every version.  This problem affects the
+# C and Objective C languages, but Clang does report an error under C++ and
+# Objective C++.
+#
+# Passing -fno-builtin to the compiler would suppress this problem.  That
+# strategy would have the advantage of being insensitive to stray warnings, but
+# it would make tests less realistic.
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how $CC reports undeclared, standard C functions" >&5
+$as_echo_n "checking how $CC reports undeclared, standard C functions... " >&6; }
+if ${ac_cv_c_decl_report+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main ()
+{
+(void) strchr;
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"; then :
+  if test -s conftest.err; then :
+      # For AC_CHECK_DECL to react to warnings, the compiler must be silent on
+    # valid AC_CHECK_DECL input.  No library function is consistently available
+    # on freestanding implementations, so test against a dummy declaration.
+    # Include always-available headers on the off chance that they somehow
+    # elicit warnings.
+    cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <float.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stddef.h>
+extern void ac_decl (int, char *);
+int
+main ()
+{
+#ifdef __cplusplus
+  (void) ac_decl ((int) 0, (char *) 0);
+  (void) ac_decl;
+#else
+  (void) ac_decl;
+#endif
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"; then :
+  if test -s conftest.err; then :
+  { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "cannot detect from compiler exit status or warnings
+See \`config.log' for more details" "$LINENO" 5; }
+else
+  ac_cv_c_decl_report=warning
+fi
+else
+  { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "cannot compile a simple declaration test
+See \`config.log' for more details" "$LINENO" 5; }
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+else
+  { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "compiler does not report undeclared identifiers
+See \`config.log' for more details" "$LINENO" 5; }
+fi
+else
+  ac_cv_c_decl_report=error
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_decl_report" >&5
+$as_echo "$ac_cv_c_decl_report" >&6; }
+
+case $ac_cv_c_decl_report in
+  warning) ac_c_decl_warn_flag=yes ;;
+  *) ac_c_decl_warn_flag= ;;
+esac
+
+ac_fn_c_check_decl "$LINENO" "NID_ML_DSA_65" "ac_cv_have_decl_NID_ML_DSA_65" "#include <openssl/obj_mac.h>
+"
+if test "x$ac_cv_have_decl_NID_ML_DSA_65" = xyes; then :
+
+$as_echo "#define HAVE_ML_DSA_SUPPORT 1" >>confdefs.h
+
+fi
+
 
 $as_echo "#define USE_OPENSSL 1" >>confdefs.h
 
@@ -15809,94 +15906,6 @@ fi
 # posix_fadvise() is a no-op on Solaris, so don't incur function overhead
 # by calling it, 2009-04-02
 # http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/lib/libc/port/gen/posix_fadvise.c
-# The Clang compiler raises a warning for an undeclared identifier that matches
-# a compiler builtin function.  All extant Clang versions are affected, as of
-# Clang 3.6.0.  Test a builtin known to every version.  This problem affects the
-# C and Objective C languages, but Clang does report an error under C++ and
-# Objective C++.
-#
-# Passing -fno-builtin to the compiler would suppress this problem.  That
-# strategy would have the advantage of being insensitive to stray warnings, but
-# it would make tests less realistic.
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking how $CC reports undeclared, standard C functions" >&5
-$as_echo_n "checking how $CC reports undeclared, standard C functions... " >&6; }
-if ${ac_cv_c_decl_report+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-
-int
-main ()
-{
-(void) strchr;
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  if test -s conftest.err; then :
-      # For AC_CHECK_DECL to react to warnings, the compiler must be silent on
-    # valid AC_CHECK_DECL input.  No library function is consistently available
-    # on freestanding implementations, so test against a dummy declaration.
-    # Include always-available headers on the off chance that they somehow
-    # elicit warnings.
-    cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <float.h>
-#include <limits.h>
-#include <stdarg.h>
-#include <stddef.h>
-extern void ac_decl (int, char *);
-int
-main ()
-{
-#ifdef __cplusplus
-  (void) ac_decl ((int) 0, (char *) 0);
-  (void) ac_decl;
-#else
-  (void) ac_decl;
-#endif
-
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  if test -s conftest.err; then :
-  { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
-$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
-as_fn_error $? "cannot detect from compiler exit status or warnings
-See \`config.log' for more details" "$LINENO" 5; }
-else
-  ac_cv_c_decl_report=warning
-fi
-else
-  { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
-$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
-as_fn_error $? "cannot compile a simple declaration test
-See \`config.log' for more details" "$LINENO" 5; }
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-else
-  { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
-$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
-as_fn_error $? "compiler does not report undeclared identifiers
-See \`config.log' for more details" "$LINENO" 5; }
-fi
-else
-  ac_cv_c_decl_report=error
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_decl_report" >&5
-$as_echo "$ac_cv_c_decl_report" >&6; }
-
-case $ac_cv_c_decl_report in
-  warning) ac_c_decl_warn_flag=yes ;;
-  *) ac_c_decl_warn_flag= ;;
-esac
-
 if test "$PORTNAME" != "solaris"; then :
 
 for ac_func in posix_fadvise
diff --git a/configure.ac b/configure.ac
index e44943aa6fe..2ae50ffdccc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1394,6 +1394,11 @@ if test "$with_ssl" = openssl ; then
   AC_CHECK_FUNCS([SSL_CTX_set_cert_cb])
   # Function introduced in OpenSSL 1.1.1, not in LibreSSL.
   AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback])
+  # Check for ML-DSA support (OpenSSL 3.5+)
+  AC_CHECK_DECL([NID_ML_DSA_65],
+    [AC_DEFINE([HAVE_ML_DSA_SUPPORT], 1, [Define if OpenSSL supports ML-DSA])],
+    [],
+    [#include <openssl/obj_mac.h>])
   AC_DEFINE([USE_OPENSSL], 1, [Define to 1 to build with OpenSSL support. (--with-ssl=openssl)])
 elif test "$with_ssl" != no ; then
   AC_MSG_ERROR([--with-ssl must specify openssl])
diff --git a/meson.build b/meson.build
index 395416a6060..d2730917b43 100644
--- a/meson.build
+++ b/meson.build
@@ -1575,6 +1575,15 @@ if sslopt in ['auto', 'openssl']
                 description: 'Define to 1 to build with OpenSSL support. (-Dssl=openssl)')
       cdata.set('OPENSSL_API_COMPAT', '0x10101000L',
                 description: 'Define to the OpenSSL API version in use. This avoids deprecation warnings from newer OpenSSL versions.')
+      
+      # Check for ML-DSA support (OpenSSL 3.5+)
+      if cc.has_header_symbol('openssl/obj_mac.h', 'NID_ML_DSA_65',
+                              args: test_c_args,
+                              dependencies: ssl_int)
+        cdata.set('HAVE_ML_DSA_SUPPORT', 1,
+                  description: 'Define if OpenSSL supports ML-DSA')
+      endif
+      
       ssl_library = 'openssl'
     else
       ssl = not_found_dep
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index c4dc5d72bdb..a2e3412bf41 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -301,6 +301,9 @@
 /* Define to 1 if you have the `mkdtemp' function. */
 #undef HAVE_MKDTEMP
 
+/* Define if OpenSSL supports ML-DSA */
+#undef HAVE_ML_DSA_SUPPORT
+
 /* Define to 1 if you have the <ossp/uuid.h> header file. */
 #undef HAVE_OSSP_UUID_H
 
diff --git a/src/test/ssl/README b/src/test/ssl/README
index 2101a466d22..668db5cda2e 100644
--- a/src/test/ssl/README
+++ b/src/test/ssl/README
@@ -92,7 +92,8 @@ ssl/ subdirectory. The Makefile also contains a rule, "make sslfiles", to
 recreate them if you need to make changes. "make sslfiles-clean" is required
 in order to recreate the full set of keypairs and certificates. To rebuild
 separate files, touch (or remove) the files in question and run "make sslfiles".
-This step requires at least OpenSSL 1.1.1.
+This step requires at least OpenSSL 1.1.1. For generating the ML-DSA-65
+certificate, OpenSSL 3.5 or later is required.
 
 Note
 ====
diff --git a/src/test/ssl/conf/server-mldsa65.config b/src/test/ssl/conf/server-mldsa65.config
new file mode 100644
index 00000000000..acda640e58c
--- /dev/null
+++ b/src/test/ssl/conf/server-mldsa65.config
@@ -0,0 +1,15 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+# This is identical to server-cn-only certificate, but we specify
+# ML-DSA-65 as the algorithm on the command line.
+
+[ req ]
+distinguished_name     = req_distinguished_name
+prompt                 = no
+
+[ req_distinguished_name ]
+CN = common-name.pg-ssltest.test
+OU = PostgreSQL test suite
+
+# No Subject Alternative Names
+
diff --git a/src/test/ssl/ssl/server-mldsa65.crt b/src/test/ssl/ssl/server-mldsa65.crt
new file mode 100644
index 00000000000..929fa5d5251
--- /dev/null
+++ b/src/test/ssl/ssl/server-mldsa65.crt
@@ -0,0 +1,118 @@
+-----BEGIN CERTIFICATE-----
+MIIVtDCCCLGgAwIBAgIUIH4CjpeOAhzglphXw/lR8sSkzvswCwYJYIZIAWUDBAMS
+MEYxJDAiBgNVBAMMG2NvbW1vbi1uYW1lLnBnLXNzbHRlc3QudGVzdDEeMBwGA1UE
+CwwVUG9zdGdyZVNRTCB0ZXN0IHN1aXRlMB4XDTI1MTAyMjIwMDczMFoXDTI1MTEy
+MTIwMDczMFowRjEkMCIGA1UEAwwbY29tbW9uLW5hbWUucGctc3NsdGVzdC50ZXN0
+MR4wHAYDVQQLDBVQb3N0Z3JlU1FMIHRlc3Qgc3VpdGUwggeyMAsGCWCGSAFlAwQD
+EgOCB6EA/6siV1bdS9QVks5BoKdWSaoc5gs5Q59EHKsU7CpG10Y5sUuOAN4FOScv
+EBueVG85/GIThoK3QG2IBmtbpyKXso+fwypGDw559q35HgQKk6Uj0PaQdxQ7rUJc
+z5L1eBY5FbOf545RDaXa/04WnT5f9N5gLXvxdxrrqV757c25+UFc+pdHFAZB6KrL
+aAUmarQd+AwTlwHbJunsST6M5aPMx2l4xB1bucCW9jbDNH2WYjW5FsNYx358kk1f
+ikTVfJOhDTomqxOCyRAiVFkp+9cJfRmdWQgdYKGGwEwrB5oiRsDiLX3fEFszx0Jn
+Wq55AwFMBSb5ukL3t8SSDzLlytG8J05Enrju2eVT4fLqfJXLpJQ76Dlkw3BBMOZ3
+a0afk/BAieG23bZpYSYEZzcE7rksmCIccD4bcJZYwavjXHD3QmGp7mVnPNh/8fAb
+h9mL8hpNMmASCM/ig/t5FW47P/vDibZB4sgbYlCe/1Q2c6NBD63w2+RBo6JAGcOf
+Q8ZAlE1BqjbM7QIFoN8y9icSI3MaxJdiwpMnxrqjId0n2oLvBSxavYhIuWD2Khrn
+/gQjxG2t0rwMeiMWpeeK89w80BQPn03oUw7inMHbWJrkxYKKy0FPdYh+b5eeq+Zq
+ek6N27Qo07vpMuuiAnGqh1yjR6bQNHA/rvKBy6XRNRdUWtXmAKHlylL7iMpA9s5i
+29/9gN6UTpBjSVum8vgOckJp9I9viI7XhzeSE3MIlByzclIHQEzoVPX2mieJgMks
+ANtoN9N633XRSyS87an6TTLkFPkQZGkiRvn07SOjsTg7v4lx3OBBHIoMCF5GlQ8P
+jlHss5JAiaIyb8DQN2Ragft32UBpFo4cq1fU0lX6dZsToB85e5ymyKL/iYosKnRu
+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
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-mldsa65.key b/src/test/ssl/ssl/server-mldsa65.key
new file mode 100644
index 00000000000..abb7b8023c6
--- /dev/null
+++ b/src/test/ssl/ssl/server-mldsa65.key
@@ -0,0 +1,88 @@
+-----BEGIN PRIVATE KEY-----
+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
+EXQoCki1MArL4VWcyBax3Nhw
+-----END PRIVATE KEY-----
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index 23aaad0c766..b7e07ba5e44 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -40,14 +40,16 @@ CLIENTS := client client-dn client-revoked client_ext client-long \
 # To add a new non-standard certificate, add it to SPECIAL_CERTS and then add
 # a recipe for creating it to the "Special-case certificates" section below.
 #
-SPECIAL_CERTS := ssl/server-rsapss.crt
+SPECIAL_CERTS := ssl/server-rsapss.crt \
+	ssl/server-mldsa65.crt
 
 # Likewise for non-standard keys
 SPECIAL_KEYS := ssl/server-password.key \
 	ssl/client-der.key \
 	ssl/client-encrypted-pem.key \
 	ssl/client-encrypted-der.key \
-	ssl/server-rsapss.key
+	ssl/server-rsapss.key \
+	ssl/server-mldsa65.key
 
 #
 # These files are just concatenations of other files. You can add new ones to
@@ -101,6 +103,10 @@ ssl/root_ca.crt: ssl/root_ca.key conf/root_ca.config
 ssl/server-rsapss.crt: ssl/server-rsapss.key conf/server-rsapss.config
 	$(OPENSSL) req -new -x509 -config conf/server-rsapss.config -key $< -out $@
 
+# Certificate using ML-DSA-65 algorithm. Also self-signed.
+ssl/server-mldsa65.crt: ssl/server-mldsa65.key conf/server-mldsa65.config
+	$(OPENSSL) req -new -x509 -config conf/server-mldsa65.config -key $< -out $@
+
 #
 # Special-case keys
 #
@@ -115,6 +121,10 @@ ssl/server-password.key: ssl/server-cn-only.key
 ssl/server-rsapss.key:
 	$(OPENSSL) genpkey -algorithm rsa-pss -out $@
 
+# Key that uses the ML-DSA-65 algorithm
+ssl/server-mldsa65.key:
+	$(OPENSSL) genpkey -algorithm ML-DSA-65 -out $@
+
 # DER-encoded version of client.key
 ssl/client-der.key: ssl/client.key
 	$(OPENSSL) rsa -in $< -outform DER -out $@
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 60b60b28657..a543f407bb7 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -171,4 +171,22 @@ if ($supports_rsapss_certs)
 			qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
 		]);
 }
+
+# Now test with a server certificate that uses the ML-DSA-65 algorithm.
+# This tests post-quantum cryptography support for channel binding.
+# Requires OpenSSL 3.5+.
+my $supports_mldsa_certs =
+  check_pg_config("#define HAVE_ML_DSA_SUPPORT 1");
+
+if ($supports_mldsa_certs)
+{
+	switch_server_cert($node, certfile => 'server-mldsa65');
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser channel_binding=require",
+		"SCRAM with SSL and channel_binding=require, server certificate uses 'ML-DSA-65'",
+		log_like => [
+			qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
+		]);
+}
+
 done_testing();
-- 
2.39.5 (Apple Git-154)

#14Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Filip Janus (#13)
Re: Channel binding for post-quantum cryptography

On Fri, Oct 31, 2025 at 2:26 AM Filip Janus <fjanus@redhat.com> wrote:

While fixing the actual issue will take some time, I’ve fixed the requested test.
Since I’m still quite new to the PG community, would it make sense to propose a patch that only adds the test?

You mean like in a TODO: block in the test? Maybe, but in my opinion
the damage to configure alone is not worth the benefit for this case,
until the test is passing. (And if OpenSSL were to change to provide a
digest through its API, as briefly mentioned in the IETF discussion,
the new test might not actually test any new code.)

--Jacob

#15Michael Paquier
michael@paquier.xyz
In reply to: Filip Janus (#13)
Re: Channel binding for post-quantum cryptography

On Fri, Oct 31, 2025 at 10:26:01AM +0100, Filip Janus wrote:

While fixing the actual issue will take some time, I’ve fixed the requested
test.
Since I’m still quite new to the PG community, would it make sense to
propose a patch that only adds the test?

Yes, we could add a test that tracks the current behavior first. That
would be half of the job already done until we figure out the details
of the implementation, changing the test once we know which approach
makes the most sense.

Not sure how the others feel about that, but I'm OK with that.
--
Michael