Обсуждение: Channel binding for post-quantum cryptography
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.
Вложения
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
Вложения
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.
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
Вложения
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
Вложения
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] (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/
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 --
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
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
Вложения
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 --
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/
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?
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/
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.
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
Вложения
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
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