Support of hybrid MLKEM key exchange methods in FIPS mode

Resolves: RHEL-125929
This commit is contained in:
Dmitry Belyavskiy 2025-12-12 12:55:41 +01:00
parent 2c179221a3
commit 0eb85c5308
2 changed files with 426 additions and 1 deletions

View File

@ -0,0 +1,419 @@
diff --git a/kex-names.c b/kex-names.c
index a728e0b38..9b852e8ab 100644
--- a/kex-names.c
+++ b/kex-names.c
@@ -112,13 +112,30 @@ static const struct kexalg gss_kexalgs[] = {
{ NULL, 0, -1, -1},
};
+/*
+ * 0 - unavailable
+ * 1 - available in non-FIPS mode
+ * 2 - available in FIPS mode
+ */
static int is_mlkem768_available()
{
static int is_fetched = -1;
if (is_fetched == -1) {
- EVP_KEM *mlkem768 = EVP_KEM_fetch(NULL, "mlkem768", NULL);
- is_fetched = mlkem768 != NULL ? 1 : 0;
+ EVP_KEM *mlkem768 = NULL;
+
+ if (FIPS_mode() == 1) {
+ mlkem768 = EVP_KEM_fetch(NULL, "mlkem768", NULL);
+ is_fetched = mlkem768 != NULL ? 2 : 0;
+
+ if (is_fetched == 0) {
+ mlkem768 = EVP_KEM_fetch(NULL, "mlkem768", "provider=default,-fips");
+ is_fetched = mlkem768 != NULL ? 1 : 0;
+ }
+ } else {
+ mlkem768 = EVP_KEM_fetch(NULL, "mlkem768", NULL);
+ is_fetched = mlkem768 != NULL ? 1 : 0;
+ }
EVP_KEM_free(mlkem768);
}
@@ -131,13 +148,32 @@ kex_alg_list_internal(char sep, const struct kexalg *algs)
char *ret = NULL, *tmp;
size_t nlen, rlen = 0;
const struct kexalg *k;
+ int x25519mlkem_available = 0, nistmlkem_available = 0;
+
+ /*
+ * FIPS provider can provide ML-KEMs and then all hybrids are available
+ * Otherwise only NIST hybrids are available
+ * */
+ if (FIPS_mode()) {
+ if (is_mlkem768_available() == 2) {
+ x25519mlkem_available = 1;
+ nistmlkem_available = 1;
+ } else if (is_mlkem768_available() == 1) {
+ nistmlkem_available = 1;
+ }
+ } else {
+ if (is_mlkem768_available() > 0) {
+ x25519mlkem_available = 1;
+ nistmlkem_available = 1;
+ }
+ }
for (k = algs; k->name != NULL; k++) {
- if ( (strcmp(k->name, KEX_MLKEM768X25519_SHA256) == 0
- || strcmp(k->name, KEX_MLKEM768NISTP256_SHA256) == 0
- || strcmp(k->name, KEX_MLKEM1024NISTP384_SHA384) == 0)
- && !is_mlkem768_available())
+ if ( (strcmp(k->name, KEX_MLKEM768X25519_SHA256) == 0 && x25519mlkem_available == 0)
+ || (strcmp(k->name, KEX_MLKEM768NISTP256_SHA256) == 0 && nistmlkem_available == 0)
+ || (strcmp(k->name, KEX_MLKEM1024NISTP384_SHA384) == 0 && nistmlkem_available == 0))
continue;
+
if (ret != NULL)
ret[rlen++] = sep;
nlen = strlen(k->name);
@@ -168,12 +204,30 @@ static const struct kexalg *
kex_alg_by_name(const char *name)
{
const struct kexalg *k;
+ int x25519mlkem_available = 0, nistmlkem_available = 0;
- if ( (strcmp(name, KEX_MLKEM768X25519_SHA256) == 0
- || strcmp(name, KEX_MLKEM768NISTP256_SHA256) == 0
- || strcmp(name, KEX_MLKEM1024NISTP384_SHA384) == 0)
- && !is_mlkem768_available())
- return NULL;
+ /*
+ * FIPS provider can provide ML-KEMs and then all hybrids are available
+ * Otherwise only NIST hybrids are available
+ * */
+ if (FIPS_mode()) {
+ if (is_mlkem768_available() == 2) {
+ x25519mlkem_available = 1;
+ nistmlkem_available = 1;
+ } else if (is_mlkem768_available() == 1) {
+ nistmlkem_available = 1;
+ }
+ } else {
+ if (is_mlkem768_available() > 0) {
+ x25519mlkem_available = 1;
+ nistmlkem_available = 1;
+ }
+ }
+
+ if ( (strcmp(name, KEX_MLKEM768X25519_SHA256) == 0 && x25519mlkem_available == 0)
+ || (strcmp(name, KEX_MLKEM768NISTP256_SHA256) == 0 && nistmlkem_available == 0)
+ || (strcmp(name, KEX_MLKEM1024NISTP384_SHA384) == 0 && nistmlkem_available == 0))
+ return NULL;
for (k = kexalgs; k->name != NULL; k++) {
if (strcmp(k->name, name) == 0)
diff --git a/kexgen.c b/kexgen.c
index 6e910f84f..4bc3f9faf 100644
--- a/kexgen.c
+++ b/kexgen.c
@@ -133,27 +133,23 @@ kex_gen_client(struct ssh *ssh)
break;
case KEX_KEM_MLKEM768X25519_SHA256:
if (FIPS_mode()) {
- logit_f("Key exchange type mlkem768x25519 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
+ EVP_KEM *mlkem = EVP_KEM_fetch(NULL, "mlkem768", NULL);
+ if (mlkem == NULL) {
+ logit_f("Key exchange type mlkem768x25519 is not allowed in FIPS mode");
+ r = SSH_ERR_INVALID_ARGUMENT;
+ } else {
+ EVP_KEM_free(mlkem);
+ r = kex_kem_mlkem768x25519_keypair(kex);
+ }
} else {
r = kex_kem_mlkem768x25519_keypair(kex);
}
break;
case KEX_KEM_MLKEM768NISTP256_SHA256:
- if (FIPS_mode()) {
- logit_f("Key exchange type mlkem768nistp256 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
- } else {
r = kex_kem_mlkem768nistp256_keypair(kex);
- }
break;
case KEX_KEM_MLKEM1024NISTP384_SHA384:
- if (FIPS_mode()) {
- logit_f("Key exchange type mlkem1024nistp384 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
- } else {
r = kex_kem_mlkem1024nistp384_keypair(kex);
- }
break;
default:
r = SSH_ERR_INVALID_ARGUMENT;
@@ -239,30 +235,27 @@ input_kex_gen_reply(int type, u_int32_t seq, struct ssh *ssh)
break;
case KEX_KEM_MLKEM768X25519_SHA256:
if (FIPS_mode()) {
- logit_f("Key exchange type mlkem768x25519 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
+ EVP_KEM *mlkem = EVP_KEM_fetch(NULL, "mlkem768", NULL);
+ if (mlkem == NULL) {
+ logit_f("Key exchange type mlkem768x25519 is not allowed in FIPS mode");
+ r = SSH_ERR_INVALID_ARGUMENT;
+ } else {
+ EVP_KEM_free(mlkem);
+ r = kex_kem_mlkem768x25519_dec(kex, server_blob,
+ &shared_secret);
+ }
} else {
r = kex_kem_mlkem768x25519_dec(kex, server_blob,
&shared_secret);
}
break;
case KEX_KEM_MLKEM768NISTP256_SHA256:
- if (FIPS_mode()) {
- logit_f("Key exchange type mlkem768nistp256 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
- } else {
r = kex_kem_mlkem768nistp256_dec(kex, server_blob,
&shared_secret);
- }
break;
case KEX_KEM_MLKEM1024NISTP384_SHA384:
- if (FIPS_mode()) {
- logit_f("Key exchange type mlkem1024nistp384 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
- } else {
r = kex_kem_mlkem1024nistp384_dec(kex, server_blob,
&shared_secret);
- }
break;
default:
r = SSH_ERR_INVALID_ARGUMENT;
@@ -398,30 +391,27 @@ input_kex_gen_init(int type, u_int32_t seq, struct ssh *ssh)
break;
case KEX_KEM_MLKEM768X25519_SHA256:
if (FIPS_mode()) {
- logit_f("Key exchange type mlkem768x25519 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
+ EVP_KEM *mlkem = EVP_KEM_fetch(NULL, "mlkem768", NULL);
+ if (mlkem == NULL) {
+ logit_f("Key exchange type mlkem768x25519 is not allowed in FIPS mode");
+ r = SSH_ERR_INVALID_ARGUMENT;
+ } else {
+ EVP_KEM_free(mlkem);
+ r = kex_kem_mlkem768x25519_enc(kex, client_pubkey,
+ &server_pubkey, &shared_secret);
+ }
} else {
r = kex_kem_mlkem768x25519_enc(kex, client_pubkey,
&server_pubkey, &shared_secret);
}
break;
case KEX_KEM_MLKEM768NISTP256_SHA256:
- if (FIPS_mode()) {
- logit_f("Key exchange type mlkem768nistp256 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
- } else {
r = kex_kem_mlkem768nistp256_enc(kex, client_pubkey,
&server_pubkey, &shared_secret);
- }
break;
case KEX_KEM_MLKEM1024NISTP384_SHA384:
- if (FIPS_mode()) {
- logit_f("Key exchange type mlkem1024nistp384 is not allowed in FIPS mode");
- r = SSH_ERR_INVALID_ARGUMENT;
- } else {
r = kex_kem_mlkem1024nistp384_enc(kex, client_pubkey,
&server_pubkey, &shared_secret);
- }
break;
default:
r = SSH_ERR_INVALID_ARGUMENT;
diff --git a/kexmlkem768x25519.c b/kexmlkem768x25519.c
index 463d18771..e32edb077 100644
--- a/kexmlkem768x25519.c
+++ b/kexmlkem768x25519.c
@@ -48,10 +48,15 @@
#ifdef USE_MLKEM768X25519
#include "libcrux_mlkem768_sha3.h"
+#include <openssl/bn.h>
+#include <openssl/ec.h>
#include <openssl/err.h>
#include <openssl/evp.h>
+#include <openssl/fips.h>
#include <stdio.h>
+#define FIPS_FALLBACK_PROPQ "provider=default,-fips"
+
static int
mlkem_keypair_gen(const char *algname, unsigned char *pubkeybuf, size_t pubkey_size,
unsigned char *privkeybuf, size_t privkey_size)
@@ -62,6 +67,14 @@ mlkem_keypair_gen(const char *algname, unsigned char *pubkeybuf, size_t pubkey_s
size_t got_pub_size = pubkey_size, got_priv_size = privkey_size;
ctx = EVP_PKEY_CTX_new_from_name(NULL, algname, NULL);
+
+ if (ctx == NULL && FIPS_mode()) {
+ /* We have filtered x25519 + ML-KEM in FIPS mode earlier
+ * so if we are in FIPS mode and ML-KEM is not available with default propq,
+ * we can fetch it from the default provider */
+ ctx = EVP_PKEY_CTX_new_from_name(NULL, algname, FIPS_FALLBACK_PROPQ);
+ }
+
if (ctx == NULL) {
ret = SSH_ERR_LIBCRYPTO_ERROR;
goto err;
@@ -121,6 +134,7 @@ mlkem_encap_secret(const char *mlkem_alg, const u_char *pubkeybuf, u_char *secre
EVP_PKEY_CTX *ctx = NULL;
int r = SSH_ERR_INTERNAL_ERROR;
size_t outlen, expected_outlen, publen, secretlen = crypto_kem_mlkem768_BYTES;
+ int fips_fallback = 0;
if (strcmp(mlkem_alg, "mlkem768") == 0) {
outlen = crypto_kem_mlkem768_CIPHERTEXTBYTES;
@@ -135,12 +149,17 @@ mlkem_encap_secret(const char *mlkem_alg, const u_char *pubkeybuf, u_char *secre
pkey = EVP_PKEY_new_raw_public_key_ex(NULL, mlkem_alg, NULL,
pubkeybuf, publen);
+ if (pkey == NULL && FIPS_mode()) {
+ pkey = EVP_PKEY_new_raw_public_key_ex(NULL, mlkem_alg, FIPS_FALLBACK_PROPQ,
+ pubkeybuf, publen);
+ fips_fallback = 1;
+ }
if (pkey == NULL) {
r = SSH_ERR_LIBCRYPTO_ERROR;
goto err;
}
- ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
+ ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, fips_fallback ? FIPS_FALLBACK_PROPQ : NULL);
if (ctx == NULL
|| EVP_PKEY_encapsulate_init(ctx, NULL) <= 0
|| EVP_PKEY_encapsulate(ctx, out, &expected_outlen, secret, &secretlen) <= 0
@@ -182,15 +201,21 @@ mlkem_decap_secret(const char *algname,
EVP_PKEY_CTX *ctx = NULL;
int r = SSH_ERR_INTERNAL_ERROR;
size_t secretlen = crypto_kem_mlkem768_BYTES;
+ int fips_fallback = 0;
pkey = EVP_PKEY_new_raw_private_key_ex(NULL, algname,
NULL, privkeybuf, privkey_len);
+ if (pkey == NULL && FIPS_mode()) {
+ pkey = EVP_PKEY_new_raw_private_key_ex(NULL, algname,
+ FIPS_FALLBACK_PROPQ, privkeybuf, privkey_len);
+ fips_fallback = 1;
+ }
if (pkey == NULL) {
r = SSH_ERR_LIBCRYPTO_ERROR;
goto err;
}
- ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
+ ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, fips_fallback ? FIPS_FALLBACK_PROPQ : NULL);
if (ctx == NULL
|| EVP_PKEY_decapsulate_init(ctx, NULL) <= 0
|| EVP_PKEY_decapsulate(ctx, secret, &secretlen, wrapped, wrapped_len) <= 0
@@ -744,6 +769,48 @@ nist_pkey_keygen(size_t pub_key_len)
return pkey;
}
+static size_t decompress_pub_key(void *pub, size_t compressed_len, size_t decompressed_len)
+{
+ EC_GROUP *group = NULL;
+ EC_POINT *point = NULL;
+ BN_CTX *ctx = NULL;
+ size_t len = 0;
+ int group_nid = NID_undef;
+
+ switch (compressed_len) {
+ case NIST_P256_COMPRESSED_LEN:
+ group_nid = NID_X9_62_prime256v1;
+ break;
+ case NIST_P384_COMPRESSED_LEN:
+ group_nid = NID_secp384r1;
+ break;
+ default:
+ return 0;
+ break;
+ }
+
+ ctx = BN_CTX_new();
+ group = EC_GROUP_new_by_curve_name(group_nid);
+ if (ctx == NULL || group == NULL)
+ goto err;
+
+ point = EC_POINT_new(group);
+ if (point == NULL)
+ goto err;
+
+ if (!EC_POINT_oct2point(group, point, pub, compressed_len, ctx))
+ goto err;
+
+ len = EC_POINT_point2oct(group, point, POINT_CONVERSION_UNCOMPRESSED, pub, decompressed_len, ctx);
+
+err:
+ EC_POINT_free(point);
+ EC_GROUP_free(group);
+ BN_CTX_free(ctx);
+
+ return len;
+}
+
static int
get_uncompressed_ec_pubkey(EVP_PKEY *pkey, unsigned char *buf, size_t buf_len)
{
@@ -757,16 +824,25 @@ get_uncompressed_ec_pubkey(EVP_PKEY *pkey, unsigned char *buf, size_t buf_len)
if (EVP_PKEY_set_params(pkey, params) <= 0
|| EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY,
- NULL, 0, &required_len) <= 0) {
+ buf, buf_len, &required_len) <= 0) {
return SSH_ERR_LIBCRYPTO_ERROR;
}
- if (required_len != buf_len)
- return SSH_ERR_INTERNAL_ERROR;
-
- if (EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY,
- buf, required_len, &out_len) <= 0) {
- return SSH_ERR_LIBCRYPTO_ERROR;
+ if (required_len != buf_len) {
+ /* Red Hat certified FIPS provider ignores OSSL_PKEY_PARAM_EC_POINT_CONVERSION_FORMAT
+ * We may have to perform the conversion manually */
+ if (len2curve_name(required_len) == len2curve_name(buf_len)) {
+ out_len = decompress_pub_key(buf, required_len, buf_len);
+ if (out_len != buf_len) {
+ debug_f("Error decompressing the compressed public key");
+ return SSH_ERR_LIBCRYPTO_ERROR;
+ } else {
+ return 0;
+ }
+ } else {
+ debug_f("Unexpected length of uncompressed public key: expected %d, got %d", buf_len, required_len);
+ return SSH_ERR_LIBCRYPTO_ERROR;
+ }
}
return 0;
diff --git a/myproposal.h b/myproposal.h
index 3e0ec6826..007347fc3 100644
--- a/myproposal.h
+++ b/myproposal.h
@@ -26,6 +26,8 @@
"sntrup761x25519-sha512," \
"sntrup761x25519-sha512@openssh.com," \
"mlkem768x25519-sha256," \
+ "mlkem768nistp256-sha256," \
+ "mlkem1024nistp384-sha384," \
"curve25519-sha256," \
"curve25519-sha256@libssh.org," \
"ecdh-sha2-nistp256," \
@@ -97,6 +99,8 @@
"aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se," \
"aes128-gcm@openssh.com,aes256-gcm@openssh.com"
#define KEX_DEFAULT_KEX_FIPS \
+ "mlkem768nistp256-sha256," \
+ "mlkem1024nistp384-sha384," \
"ecdh-sha2-nistp256," \
"ecdh-sha2-nistp384," \
"ecdh-sha2-nistp521," \

View File

@ -43,7 +43,7 @@
Summary: An open source implementation of SSH protocol version 2
Name: openssh
Version: %{openssh_ver}
Release: 18%{?dist}
Release: 19%{?dist}
URL: http://www.openssh.com/portable.html
Source0: ftp://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-%{version}.tar.gz
Source1: ftp://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-%{version}.tar.gz.asc
@ -226,6 +226,7 @@ Patch1032: openssh-9.9p1-reject-cntrl-chars-in-username.patch
# upstream 43b3bff47bb029f2299bacb6a36057981b39fdb0
Patch1033: openssh-9.9p1-reject-null-char-in-url-string.patch
Patch1034: openssh-9.9p1-sshd-no-delegate-credentials.patch
Patch1035: openssh-10.0-mlkem-nist-fips.patch
License: BSD-3-Clause AND BSD-2-Clause AND ISC AND SSH-OpenSSH AND ssh-keyscan AND sprintf AND LicenseRef-Fedora-Public-Domain AND X11-distribute-modifications-variant
Requires: /sbin/nologin
@ -425,6 +426,7 @@ gpgv2 --quiet --keyring %{SOURCE3} %{SOURCE1} %{SOURCE0}
%patch -P 1032 -p1 -b .reject-cntrl-chars-in-username
%patch -P 1033 -p1 -b .reject-null-char-in-url-string
%patch -P 1034 -p1 -b .sshd-nogsscreds
%patch -P 1035 -p1 -b .mlkem-nist-fips
%patch -P 100 -p1 -b .coverity
@ -705,6 +707,10 @@ test -f %{sysconfig_anaconda} && \
%attr(0755,root,root) %{_libdir}/sshtest/sk-dummy.so
%changelog
* Thu Dec 11 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-19
- Support of hybrid MLKEM key exchange methods in FIPS mode
Resolves: RHEL-125929
* Fri Dec 05 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-18
- Adding a mechanism to disable GSSAPIDelegateCredentials in sshd_config
Resolves: RHEL-5281