diff --git a/openssh-10.0-mlkem-nist-fips.patch b/openssh-10.0-mlkem-nist-fips.patch new file mode 100644 index 0000000..b610b6e --- /dev/null +++ b/openssh-10.0-mlkem-nist-fips.patch @@ -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 ++#include + #include + #include ++#include + #include + ++#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," \ diff --git a/openssh.spec b/openssh.spec index febecc4..057425b 100644 --- a/openssh.spec +++ b/openssh.spec @@ -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 - 9.9p1-19 +- Support of hybrid MLKEM key exchange methods in FIPS mode + Resolves: RHEL-125929 + * Fri Dec 05 2025 Dmitry Belyavskiy - 9.9p1-18 - Adding a mechanism to disable GSSAPIDelegateCredentials in sshd_config Resolves: RHEL-5281