From 4fcff4b9133563733c903789b0f87b1e9e292f12 Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Tue, 19 May 2026 19:16:12 -0400 Subject: [PATCH] import UBI openssh-9.9p1-23.el10_2 --- openssh-10.0-mlkem-nist-fips.patch | 503 +++++++++ openssh-10.0-mlkem-nist.patch | 987 ++++++++++++++++++ openssh-7.7p1-fips.patch | 131 +-- openssh-9.9p1-canonical-match-user.patch | 120 +++ openssh-9.9p1-fips-gss.patch | 42 + openssh-9.9p1-scp-traversing.patch | 437 ++++++++ ...h-9.9p1-sshd-no-delegate-credentials.patch | 122 +++ openssh.spec | 105 +- 8 files changed, 2324 insertions(+), 123 deletions(-) create mode 100644 openssh-10.0-mlkem-nist-fips.patch create mode 100644 openssh-10.0-mlkem-nist.patch create mode 100644 openssh-9.9p1-canonical-match-user.patch create mode 100644 openssh-9.9p1-fips-gss.patch create mode 100644 openssh-9.9p1-scp-traversing.patch create mode 100644 openssh-9.9p1-sshd-no-delegate-credentials.patch diff --git a/openssh-10.0-mlkem-nist-fips.patch b/openssh-10.0-mlkem-nist-fips.patch new file mode 100644 index 0000000..951e367 --- /dev/null +++ b/openssh-10.0-mlkem-nist-fips.patch @@ -0,0 +1,503 @@ +diff --git a/compat.c b/compat.c +index 4e611dc39..b5f4cc685 100644 +--- a/compat.c ++++ b/compat.c +@@ -36,6 +36,7 @@ + #include "compat.h" + #include "log.h" + #include "match.h" ++#include + + /* determine bug flags from SSH protocol banner */ + void +@@ -148,8 +149,9 @@ char * + compat_kex_proposal(struct ssh *ssh, const char *p) + { + char *cp = NULL, *cp2 = NULL; ++ int mlkem_available = is_mlkem768_available(); + +- if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0) ++ if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0 && mlkem_available == 2) + return xstrdup(p); + debug2_f("original KEX proposal: %s", p); + if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0) +@@ -164,6 +166,25 @@ compat_kex_proposal(struct ssh *ssh, const char *p) + free(cp); + cp = cp2; + } ++ if (mlkem_available == 2) ++ return cp ? cp : xstrdup(p); ++ if (mlkem_available == 1 && FIPS_mode()) { ++ if ((cp2 = match_filter_denylist(cp ? cp : p, ++ "mlkem768x25519-sha256")) == NULL) ++ fatal("match_filter_denylist failed"); ++ free(cp); ++ cp = cp2; ++ } ++ if (mlkem_available == 0) { ++ if ((cp2 = match_filter_denylist(cp ? cp : p, ++ "mlkem768x25519-sha256," ++ "mlkem768nistp256-sha256," ++ "mlkem1024nistp384-sha384")) == NULL) ++ fatal("match_filter_denylist failed"); ++ free(cp); ++ cp = cp2; ++ } ++ + if (cp == NULL || *cp == '\0') + fatal("No supported key exchange algorithms found"); + debug2_f("compat KEX proposal: %s", cp); +diff --git a/compat.h b/compat.h +index 2e6db5bf9..b78e55b69 100644 +--- a/compat.h ++++ b/compat.h +@@ -62,4 +62,5 @@ struct ssh; + + void compat_banner(struct ssh *, const char *); + char *compat_kex_proposal(struct ssh *, const char *); ++int is_mlkem768_available(void); + #endif +diff --git a/kex-names.c b/kex-names.c +index a728e0b38..9b852e8ab 100644 +--- a/kex-names.c ++++ b/kex-names.c +@@ -35,6 +35,7 @@ + #include + #include + #include ++#include + #endif + + #include "kex.h" +@@ -113,14 +113,27 @@ static const struct kexalg gss_kexalgs[] + { NULL, 0, -1, -1}, + }; + ++/* ++ * 0 - unavailable ++ * 1 - available in non-FIPS mode ++ * 2 - available always ++ */ +-static int is_mlkem768_available() ++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; ++ ++ ERR_set_mark(); ++ mlkem768 = EVP_KEM_fetch(NULL, "mlkem768", NULL); ++ is_fetched = (mlkem768 == NULL) ? 0 : 2; ++ if (is_fetched == 0 && FIPS_mode() == 1) { ++ mlkem768 = EVP_KEM_fetch(NULL, "mlkem768", "provider=default,-fips"); ++ is_fetched = (mlkem768 == NULL) ? 0 : 1; ++ } + EVP_KEM_free(mlkem768); ++ ERR_pop_to_mark(); + } + + return is_fetched; +@@ -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) +@@ -292,8 +295,16 @@ kex_names_valid(const char *names) + for ((p = strsep(&cp, ",")); p && *p != '\0'; + (p = strsep(&cp, ","))) { + if (kex_alg_by_name(p) == NULL) { +- if (FIPS_mode()) +- error("\"%.100s\" is not allowed in FIPS mode", p); ++ if (FIPS_mode()) { ++ if ((strcmp(p, KEX_MLKEM768X25519_SHA256) == 0) ++ || (strcmp(p, KEX_MLKEM768NISTP256_SHA256) == 0) ++ || (strcmp(p, KEX_MLKEM1024NISTP384_SHA384) == 0)) { ++ debug("\"%.100s\" is not allowed in FIPS mode", p); ++ continue; ++ } ++ else ++ error("\"%.100s\" is not allowed in FIPS mode", p); ++ } + else + error("Unsupported KEX algorithm \"%.100s\"", p); + free(s); +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-10.0-mlkem-nist.patch b/openssh-10.0-mlkem-nist.patch new file mode 100644 index 0000000..22b8ec6 --- /dev/null +++ b/openssh-10.0-mlkem-nist.patch @@ -0,0 +1,987 @@ +diff --git a/crypto_api.h b/crypto_api.h +index 8bbc3a08a..6940bbf0e 100644 +--- a/crypto_api.h ++++ b/crypto_api.h +@@ -58,4 +58,8 @@ int crypto_kem_sntrup761_keypair(unsigned char *pk, unsigned char *sk); + #define crypto_kem_mlkem768_CIPHERTEXTBYTES 1088 + #define crypto_kem_mlkem768_BYTES 32 + ++#define crypto_kem_mlkem1024_PUBLICKEYBYTES 1568 ++#define crypto_kem_mlkem1024_SECRETKEYBYTES 3168 ++#define crypto_kem_mlkem1024_CIPHERTEXTBYTES 1568 ++ + #endif /* crypto_api_h */ +diff --git a/kex-names.c b/kex-names.c +index 36a953ab2..a728e0b38 100644 +--- a/kex-names.c ++++ b/kex-names.c +@@ -90,6 +90,10 @@ static const struct kexalg kexalgs[] = { + #ifdef USE_MLKEM768X25519 + { KEX_MLKEM768X25519_SHA256, KEX_KEM_MLKEM768X25519_SHA256, 0, + SSH_DIGEST_SHA256 }, ++ { KEX_MLKEM768NISTP256_SHA256, KEX_KEM_MLKEM768NISTP256_SHA256, 0, ++ SSH_DIGEST_SHA256 }, ++ { KEX_MLKEM1024NISTP384_SHA384, KEX_KEM_MLKEM1024NISTP384_SHA384, 0, ++ SSH_DIGEST_SHA384 }, + #endif + #endif /* HAVE_EVP_SHA256 || !WITH_OPENSSL */ + { NULL, 0, -1, -1}, +@@ -129,7 +133,9 @@ kex_alg_list_internal(char sep, const struct kexalg *algs) + const struct kexalg *k; + + for (k = algs; k->name != NULL; k++) { +- if (strcmp(k->name, KEX_MLKEM768X25519_SHA256) == 0 ++ 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()) + continue; + if (ret != NULL) +@@ -163,7 +169,9 @@ kex_alg_by_name(const char *name) + { + const struct kexalg *k; + +- if (strcmp(name, KEX_MLKEM768X25519_SHA256) == 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; + +diff --git a/kex.c b/kex.c +index ce6a7b818..efaf5749c 100644 +--- a/kex.c ++++ b/kex.c +@@ -753,6 +753,7 @@ kex_free(struct kex *kex) + #ifdef OPENSSL_HAS_ECC + EC_KEY_free(kex->ec_client_key); + #endif /* OPENSSL_HAS_ECC */ ++ EVP_PKEY_free(kex->ec_hybrid_client_key); + #endif /* WITH_OPENSSL */ + for (mode = 0; mode < MODE_MAX; mode++) { + kex_free_newkeys(kex->newkeys[mode]); +diff --git a/kex.h b/kex.h +index 48f3bb875..50a461ddb 100644 +--- a/kex.h ++++ b/kex.h +@@ -72,6 +72,8 @@ + #define KEX_SNTRUP761X25519_SHA512 "sntrup761x25519-sha512" + #define KEX_SNTRUP761X25519_SHA512_OLD "sntrup761x25519-sha512@openssh.com" + #define KEX_MLKEM768X25519_SHA256 "mlkem768x25519-sha256" ++#define KEX_MLKEM768NISTP256_SHA256 "mlkem768nistp256-sha256" ++#define KEX_MLKEM1024NISTP384_SHA384 "mlkem1024nistp384-sha384" + + #define COMP_NONE 0 + #define COMP_DELAYED 2 +@@ -110,6 +112,8 @@ enum kex_exchange { + KEX_C25519_SHA256, + KEX_KEM_SNTRUP761X25519_SHA512, + KEX_KEM_MLKEM768X25519_SHA256, ++ KEX_KEM_MLKEM768NISTP256_SHA256, ++ KEX_KEM_MLKEM1024NISTP384_SHA384, + #ifdef GSSAPI + KEX_GSS_GRP1_SHA1, + KEX_GSS_GRP14_SHA1, +@@ -206,6 +210,9 @@ struct kex { + u_char sntrup761_client_key[crypto_kem_sntrup761_SECRETKEYBYTES]; /* KEM */ + u_char mlkem768_client_key[crypto_kem_mlkem768_SECRETKEYBYTES]; /* KEM */ + struct sshbuf *client_pub; ++ /* FIXME */ ++ EVP_PKEY *ec_hybrid_client_key; /* NIST hybrids */ ++ u_char mlkem1024_client_key[crypto_kem_mlkem1024_SECRETKEYBYTES]; /* ML-KEM 1024 + NIST */ + }; + + int kex_name_valid(const char *); +@@ -287,6 +294,18 @@ int kex_kem_mlkem768x25519_enc(struct kex *, const struct sshbuf *, + int kex_kem_mlkem768x25519_dec(struct kex *, const struct sshbuf *, + struct sshbuf **); + ++int kex_kem_mlkem768nistp256_keypair(struct kex *); ++int kex_kem_mlkem768nistp256_enc(struct kex *, const struct sshbuf *, ++ struct sshbuf **, struct sshbuf **); ++int kex_kem_mlkem768nistp256_dec(struct kex *, const struct sshbuf *, ++ struct sshbuf **); ++ ++int kex_kem_mlkem1024nistp384_keypair(struct kex *); ++int kex_kem_mlkem1024nistp384_enc(struct kex *, const struct sshbuf *, ++ struct sshbuf **, struct sshbuf **); ++int kex_kem_mlkem1024nistp384_dec(struct kex *, const struct sshbuf *, ++ struct sshbuf **); ++ + int kex_dh_keygen(struct kex *); + int kex_dh_compute_key(struct kex *, BIGNUM *, struct sshbuf *); + +diff --git a/kexgen.c b/kexgen.c +index eecdceba2..6e910f84f 100644 +--- a/kexgen.c ++++ b/kexgen.c +@@ -139,6 +139,22 @@ kex_gen_client(struct ssh *ssh) + 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; + break; +@@ -230,6 +246,24 @@ input_kex_gen_reply(int type, u_int32_t seq, struct ssh *ssh) + &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; + break; +@@ -283,6 +317,8 @@ out: + sizeof(kex->sntrup761_client_key)); + explicit_bzero(kex->mlkem768_client_key, + sizeof(kex->mlkem768_client_key)); ++ explicit_bzero(kex->mlkem1024_client_key, ++ sizeof(kex->mlkem1024_client_key)); + sshbuf_free(server_host_key_blob); + free(signature); + sshbuf_free(tmp); +@@ -369,6 +405,24 @@ input_kex_gen_init(int type, u_int32_t seq, struct ssh *ssh) + &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; + break; +diff --git a/kexmlkem768x25519.c b/kexmlkem768x25519.c +index 670049dcd..463d18771 100644 +--- a/kexmlkem768x25519.c ++++ b/kexmlkem768x25519.c +@@ -53,14 +53,15 @@ + #include + + static int +-mlkem768_keypair_gen(unsigned char *pubkeybuf, unsigned char *privkeybuf) ++mlkem_keypair_gen(const char *algname, unsigned char *pubkeybuf, size_t pubkey_size, ++ unsigned char *privkeybuf, size_t privkey_size) + { + EVP_PKEY_CTX *ctx = NULL; + EVP_PKEY *pkey = NULL; + int ret = SSH_ERR_INTERNAL_ERROR; +- size_t pubkey_size = crypto_kem_mlkem768_PUBLICKEYBYTES, privkey_size = crypto_kem_mlkem768_SECRETKEYBYTES; ++ size_t got_pub_size = pubkey_size, got_priv_size = privkey_size; + +- ctx = EVP_PKEY_CTX_new_from_name(NULL, "mlkem768", NULL); ++ ctx = EVP_PKEY_CTX_new_from_name(NULL, algname, NULL); + if (ctx == NULL) { + ret = SSH_ERR_LIBCRYPTO_ERROR; + goto err; +@@ -72,17 +73,23 @@ mlkem768_keypair_gen(unsigned char *pubkeybuf, unsigned char *privkeybuf) + goto err; + } + +- if (EVP_PKEY_get_raw_public_key(pkey, pubkeybuf, &pubkey_size) <= 0 +- || EVP_PKEY_get_raw_private_key(pkey, privkeybuf, &privkey_size) <= 0) { ++ if (EVP_PKEY_get_raw_public_key(pkey, NULL, &got_pub_size) <= 0 ++ || EVP_PKEY_get_raw_private_key(pkey, NULL, &got_priv_size) <= 0) { ++ ret = SSH_ERR_LIBCRYPTO_ERROR; ++ goto err; ++ } ++ ++ if (privkey_size != got_priv_size || pubkey_size != got_pub_size) { + ret = SSH_ERR_LIBCRYPTO_ERROR; + goto err; + } + +- if (privkey_size != crypto_kem_mlkem768_SECRETKEYBYTES +- || pubkey_size != crypto_kem_mlkem768_PUBLICKEYBYTES) { ++ if (EVP_PKEY_get_raw_public_key(pkey, pubkeybuf, &got_pub_size) <= 0 ++ || EVP_PKEY_get_raw_private_key(pkey, privkeybuf, &got_priv_size) <= 0) { + ret = SSH_ERR_LIBCRYPTO_ERROR; + goto err; + } ++ + ret = 0; + + err: +@@ -94,16 +101,40 @@ mlkem768_keypair_gen(unsigned char *pubkeybuf, unsigned char *privkeybuf) + } + + static int +-mlkem768_encap_secret(const u_char *pubkeybuf, u_char *secret, u_char *out) ++mlkem768_keypair_gen(unsigned char *pubkeybuf, unsigned char *privkeybuf) ++{ ++ return mlkem_keypair_gen("mlkem768", pubkeybuf, crypto_kem_mlkem768_PUBLICKEYBYTES, ++ privkeybuf, crypto_kem_mlkem768_SECRETKEYBYTES); ++} ++ ++static int ++mlkem1024_keypair_gen(unsigned char *pubkeybuf, unsigned char *privkeybuf) ++{ ++ return mlkem_keypair_gen("mlkem1024", pubkeybuf, crypto_kem_mlkem1024_PUBLICKEYBYTES, ++ privkeybuf, crypto_kem_mlkem1024_SECRETKEYBYTES); ++} ++ ++static int ++mlkem_encap_secret(const char *mlkem_alg, const u_char *pubkeybuf, u_char *secret, u_char *out) + { + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *ctx = NULL; + int r = SSH_ERR_INTERNAL_ERROR; +- size_t outlen = crypto_kem_mlkem768_CIPHERTEXTBYTES, +- secretlen = crypto_kem_mlkem768_BYTES; ++ size_t outlen, expected_outlen, publen, secretlen = crypto_kem_mlkem768_BYTES; ++ ++ if (strcmp(mlkem_alg, "mlkem768") == 0) { ++ outlen = crypto_kem_mlkem768_CIPHERTEXTBYTES; ++ publen = crypto_kem_mlkem768_PUBLICKEYBYTES; ++ } else if (strcmp(mlkem_alg, "mlkem1024") == 0) { ++ outlen = crypto_kem_mlkem1024_CIPHERTEXTBYTES; ++ publen = crypto_kem_mlkem1024_PUBLICKEYBYTES; ++ } else ++ return r; + +- pkey = EVP_PKEY_new_raw_public_key_ex(NULL, "mlkem768", NULL, +- pubkeybuf, crypto_kem_mlkem768_PUBLICKEYBYTES); ++ expected_outlen = outlen; ++ ++ pkey = EVP_PKEY_new_raw_public_key_ex(NULL, mlkem_alg, NULL, ++ pubkeybuf, publen); + if (pkey == NULL) { + r = SSH_ERR_LIBCRYPTO_ERROR; + goto err; +@@ -112,9 +143,9 @@ mlkem768_encap_secret(const u_char *pubkeybuf, u_char *secret, u_char *out) + ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL); + if (ctx == NULL + || EVP_PKEY_encapsulate_init(ctx, NULL) <= 0 +- || EVP_PKEY_encapsulate(ctx, out, &outlen, secret, &secretlen) <= 0 ++ || EVP_PKEY_encapsulate(ctx, out, &expected_outlen, secret, &secretlen) <= 0 + || secretlen != crypto_kem_mlkem768_BYTES +- || outlen != crypto_kem_mlkem768_CIPHERTEXTBYTES) { ++ || outlen != expected_outlen) { + r = SSH_ERR_LIBCRYPTO_ERROR; + goto err; + } +@@ -130,16 +161,30 @@ mlkem768_encap_secret(const u_char *pubkeybuf, u_char *secret, u_char *out) + } + + static int +-mlkem768_decap_secret(const u_char *privkeybuf, const u_char *wrapped, u_char *secret) ++mlkem768_encap_secret(const u_char *pubkeybuf, u_char *secret, u_char *out) ++{ ++ return mlkem_encap_secret("mlkem768", pubkeybuf, secret, out); ++} ++ ++static int ++mlkem1024_encap_secret(const u_char *pubkeybuf, u_char *secret, u_char *out) ++{ ++ return mlkem_encap_secret("mlkem1024", pubkeybuf, secret, out); ++} ++ ++static int ++mlkem_decap_secret(const char *algname, ++ const u_char *privkeybuf, size_t privkey_len, ++ const u_char *wrapped, size_t wrapped_len, ++ u_char *secret) + { + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *ctx = NULL; + int r = SSH_ERR_INTERNAL_ERROR; +- size_t wrappedlen = crypto_kem_mlkem768_CIPHERTEXTBYTES, +- secretlen = crypto_kem_mlkem768_BYTES; ++ size_t secretlen = crypto_kem_mlkem768_BYTES; + +- pkey = EVP_PKEY_new_raw_private_key_ex(NULL, "mlkem768", NULL, +- privkeybuf, crypto_kem_mlkem768_SECRETKEYBYTES); ++ pkey = EVP_PKEY_new_raw_private_key_ex(NULL, algname, ++ NULL, privkeybuf, privkey_len); + if (pkey == NULL) { + r = SSH_ERR_LIBCRYPTO_ERROR; + goto err; +@@ -148,7 +193,7 @@ mlkem768_decap_secret(const u_char *privkeybuf, const u_char *wrapped, u_char *s + ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL); + if (ctx == NULL + || EVP_PKEY_decapsulate_init(ctx, NULL) <= 0 +- || EVP_PKEY_decapsulate(ctx, secret, &secretlen, wrapped, wrappedlen) <= 0 ++ || EVP_PKEY_decapsulate(ctx, secret, &secretlen, wrapped, wrapped_len) <= 0 + || secretlen != crypto_kem_mlkem768_BYTES) { + r = SSH_ERR_LIBCRYPTO_ERROR; + goto err; +@@ -165,6 +210,20 @@ mlkem768_decap_secret(const u_char *privkeybuf, const u_char *wrapped, u_char *s + return r; + } + ++static int ++mlkem768_decap_secret(const u_char *privkeybuf, const u_char *wrapped, u_char *secret) ++{ ++ return mlkem_decap_secret("mlkem768", privkeybuf, crypto_kem_mlkem768_SECRETKEYBYTES, ++ wrapped, crypto_kem_mlkem768_CIPHERTEXTBYTES, secret); ++} ++ ++static int ++mlkem1024_decap_secret(const u_char *privkeybuf, const u_char *wrapped, u_char *secret) ++{ ++ return mlkem_decap_secret("mlkem1024", privkeybuf, crypto_kem_mlkem1024_SECRETKEYBYTES, ++ wrapped, crypto_kem_mlkem1024_CIPHERTEXTBYTES, secret); ++} ++ + int + kex_kem_mlkem768x25519_keypair(struct kex *kex) + { +@@ -341,7 +400,7 @@ kex_kem_mlkem768x25519_enc(struct kex *kex, + u_char hash[SSH_DIGEST_MAX_LENGTH]; + size_t need; + int r = SSH_ERR_INTERNAL_ERROR; +- struct libcrux_mlkem768_enc_result enc; /* FIXME */ ++ struct libcrux_mlkem768_enc_result enc; + + *server_blobp = NULL; + *shared_secretp = NULL; +@@ -550,6 +609,468 @@ kex_kem_mlkem768x25519_dec(struct kex *kex, + return r; + #endif + } ++ ++#define NIST_P256_COMPRESSED_LEN 33 ++#define NIST_P256_UNCOMPRESSED_LEN 65 ++#define NIST_P384_COMPRESSED_LEN 49 ++#define NIST_P384_UNCOMPRESSED_LEN 97 ++#define NIST_BUF_MAX_SIZE NIST_P384_UNCOMPRESSED_LEN ++ ++static const char ec256[] = "P-256"; ++static const char ec384[] = "P-384"; ++static const char *len2curve_name(size_t len) ++{ ++ switch (len) { ++ case NIST_P256_COMPRESSED_LEN: ++ case NIST_P256_UNCOMPRESSED_LEN: ++ return ec256; ++ break; ++ case NIST_P384_COMPRESSED_LEN: ++ case NIST_P384_UNCOMPRESSED_LEN: ++ return ec384; ++ break; ++ } ++ return NULL; ++} ++ ++static EVP_PKEY * ++buf2nist_key(const unsigned char *pub_key_buf, size_t pub_key_len) ++{ ++ EVP_PKEY *pkey = NULL; ++ EVP_PKEY_CTX *ctx = NULL; ++ OSSL_PARAM params[3]; ++ const char *curve_name = len2curve_name(pub_key_len); ++ ++ if (curve_name == NULL) ++ return NULL; ++ ++ ctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL); ++ if (!ctx) ++ goto err; ++ ++ if (EVP_PKEY_fromdata_init(ctx) <= 0) ++ goto err; ++ ++ params[0] = OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, curve_name, 0); ++ params[1] = OSSL_PARAM_construct_octet_string( ++ OSSL_PKEY_PARAM_PUB_KEY, (void *)pub_key_buf, pub_key_len); ++ params[2] = OSSL_PARAM_construct_end(); ++ ++ if (EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) ++ goto err; ++ ++ EVP_PKEY_CTX_free(ctx); ++ return pkey; ++ ++err: ++ EVP_PKEY_CTX_free(ctx); ++ EVP_PKEY_free(pkey); ++ return NULL; ++} ++ ++static int ++kex_nist_shared_key_ext(EVP_PKEY *priv_key, ++ const u_char *pub_key_buf, size_t pub_key_len, struct sshbuf *out) ++{ ++ EVP_PKEY_CTX *ctx = NULL; ++ unsigned char *shared_secret = NULL; ++ size_t shared_secret_len = 0; ++ EVP_PKEY *peer_key = buf2nist_key(pub_key_buf, pub_key_len); ++ int r = SSH_ERR_INTERNAL_ERROR; ++ ++ if (peer_key == NULL) ++ return SSH_ERR_KEY_LENGTH; ++ ++ ctx = EVP_PKEY_CTX_new_from_pkey(NULL, priv_key, NULL); ++ if (!ctx) ++ goto end; ++ ++ if ((EVP_PKEY_derive_init(ctx) <= 0) ++ || EVP_PKEY_derive_set_peer(ctx, peer_key) <= 0 ++ || EVP_PKEY_derive(ctx, NULL, &shared_secret_len) <= 0) ++ goto end; ++ ++ shared_secret = OPENSSL_malloc(shared_secret_len); ++ if (shared_secret == NULL) ++ goto end; ++ ++ if (EVP_PKEY_derive(ctx, shared_secret, &shared_secret_len) <= 0) ++ goto end; ++ ++ if ((r = sshbuf_put(out, shared_secret, shared_secret_len)) != 0) ++ goto end; ++ ++ r = 0; ++ ++end: ++ EVP_PKEY_free(peer_key); ++ if (shared_secret) ++ OPENSSL_clear_free(shared_secret, shared_secret_len); ++ EVP_PKEY_CTX_free(ctx); ++ ++ return r; ++} ++ ++static EVP_PKEY * ++nist_pkey_keygen(size_t pub_key_len) ++{ ++ const char *curve_name = len2curve_name(pub_key_len); ++ EVP_PKEY_CTX *pctx = NULL; ++ EVP_PKEY *pkey = NULL; ++ OSSL_PARAM params[2]; ++ ++ if (curve_name == NULL) ++ return NULL; ++ ++ pctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL); ++ if (!pctx) ++ return NULL; ++ ++ if (EVP_PKEY_keygen_init(pctx) <= 0) { ++ EVP_PKEY_CTX_free(pctx); ++ return NULL; ++ } ++ ++ params[0] = OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, curve_name, 0); ++ params[1] = OSSL_PARAM_construct_end(); ++ ++ if (EVP_PKEY_CTX_set_params(pctx, params) <= 0 ++ || EVP_PKEY_keygen(pctx, &pkey) <= 0) { ++ EVP_PKEY_CTX_free(pctx); ++ return NULL; ++ } ++ ++ EVP_PKEY_CTX_free(pctx); ++ return pkey; ++} ++ ++static int ++get_uncompressed_ec_pubkey(EVP_PKEY *pkey, unsigned char *buf, size_t buf_len) ++{ ++ OSSL_PARAM params[2]; ++ size_t required_len = 0, out_len = 0; ++ ++ params[0] = OSSL_PARAM_construct_utf8_string( ++ OSSL_PKEY_PARAM_EC_POINT_CONVERSION_FORMAT, ++ "uncompressed", 0); ++ params[1] = OSSL_PARAM_construct_end(); ++ ++ 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) { ++ 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; ++ } ++ ++ return 0; ++} ++/* nist_bytes_len should always be uncompressed */ ++static int ++kex_kem_mlkem_nist_keypair(struct kex *kex, size_t mlkem_bytes_len, size_t nist_bytes_len) ++{ ++ struct sshbuf *buf = NULL; ++ u_char *cp = NULL; ++ size_t need; ++ int r = SSH_ERR_INTERNAL_ERROR; ++ u_char *client_key = NULL; ++ ++ if ((buf = sshbuf_new()) == NULL) ++ return SSH_ERR_ALLOC_FAIL; ++ need = mlkem_bytes_len + nist_bytes_len; ++ if ((r = sshbuf_reserve(buf, need, &cp)) != 0) ++ goto out; ++ ++ if (mlkem_bytes_len == crypto_kem_mlkem768_PUBLICKEYBYTES) { ++ client_key = kex->mlkem768_client_key; ++ r = mlkem768_keypair_gen(cp, client_key); ++ } ++ ++ if (mlkem_bytes_len == crypto_kem_mlkem1024_PUBLICKEYBYTES) { ++ client_key = kex->mlkem1024_client_key; ++ r = mlkem1024_keypair_gen(cp, client_key); ++ } ++ ++ if (client_key == NULL) ++ goto out; ++ ++ if (r != 0) ++ goto out; ++#ifdef DEBUG_KEXECDH ++ dump_digest("client public key mlkemXXX:", cp, mlkem_bytes_len); ++#endif ++ cp += mlkem_bytes_len; ++ if ((kex->ec_hybrid_client_key = nist_pkey_keygen(nist_bytes_len)) == NULL) ++ goto out; ++ ++ if ((r = get_uncompressed_ec_pubkey(kex->ec_hybrid_client_key, cp, nist_bytes_len)) != 0) ++ goto out; ++ ++#ifdef DEBUG_KEXECDH ++ dump_digest("client public key NIST:", cp, nist_bytes_len); ++#endif ++ /* success */ ++ r = 0; ++ kex->client_pub = buf; ++ buf = NULL; ++ out: ++ sshbuf_free(buf); ++ if (r == SSH_ERR_LIBCRYPTO_ERROR) ++ ERR_print_errors_fp(stderr); ++ ++ return r; ++} ++ ++static int ++kex_kem_mlkem_nist_enc(struct kex *kex, const char *nist_curve, ++ const struct sshbuf *client_blob, struct sshbuf **server_blobp, ++ struct sshbuf **shared_secretp) ++{ ++ struct sshbuf *server_blob = NULL; ++ struct sshbuf *buf = NULL; ++ const u_char *client_pub; ++ u_char server_pub[NIST_BUF_MAX_SIZE]; ++ u_char enc_out[crypto_kem_mlkem1024_CIPHERTEXTBYTES]; ++ u_char secret[crypto_kem_mlkem768_BYTES]; ++ EVP_PKEY *server_key = NULL; ++ u_char hash[SSH_DIGEST_MAX_LENGTH]; ++ size_t client_buf_len, mlkem_buf_len, ecdh_buf_len, server_key_len, enc_out_len; ++ int r = SSH_ERR_INTERNAL_ERROR; ++ ++ *server_blobp = NULL; ++ *shared_secretp = NULL; ++ ++ client_buf_len = sshbuf_len(client_blob); ++ /* client_blob contains both KEM and ECDH client pubkeys */ ++ if (strcmp(nist_curve, "P-256") == 0) { ++ if (crypto_kem_mlkem768_PUBLICKEYBYTES > client_buf_len) ++ return r; ++ ++ ecdh_buf_len = client_buf_len - crypto_kem_mlkem768_PUBLICKEYBYTES; ++ if (ecdh_buf_len != NIST_P256_COMPRESSED_LEN && ++ ecdh_buf_len != NIST_P256_UNCOMPRESSED_LEN) ++ return r; ++ mlkem_buf_len = crypto_kem_mlkem768_PUBLICKEYBYTES; ++ enc_out_len = crypto_kem_mlkem768_CIPHERTEXTBYTES; ++ server_key_len = NIST_P256_UNCOMPRESSED_LEN; ++ } else if (strcmp(nist_curve, "P-384") == 0) { ++ if (crypto_kem_mlkem1024_PUBLICKEYBYTES > client_buf_len) ++ return r; ++ ++ ecdh_buf_len = client_buf_len - crypto_kem_mlkem1024_PUBLICKEYBYTES; ++ if (ecdh_buf_len != NIST_P384_COMPRESSED_LEN && ++ ecdh_buf_len != NIST_P384_UNCOMPRESSED_LEN) ++ return r; ++ mlkem_buf_len = crypto_kem_mlkem1024_PUBLICKEYBYTES; ++ enc_out_len = crypto_kem_mlkem1024_CIPHERTEXTBYTES; ++ server_key_len = NIST_P384_UNCOMPRESSED_LEN; ++ } else ++ return r; ++ ++ client_pub = sshbuf_ptr(client_blob); ++#ifdef DEBUG_KEXECDH ++ dump_digest("client public key mlkem:", client_pub, mlkem_buf_len); ++ dump_digest("client public key NIST:", client_pub + mlkem_buf_len, ecdh_buf_len); ++#endif ++ ++ /* allocate buffer for concatenation of KEM key and ECDH shared key */ ++ /* the buffer will be hashed and the result is the shared secret */ ++ if ((buf = sshbuf_new()) == NULL) { ++ r = SSH_ERR_ALLOC_FAIL; ++ goto out; ++ } ++ /* allocate space for encrypted KEM key and ECDH pub key */ ++ if ((server_blob = sshbuf_new()) == NULL) { ++ r = SSH_ERR_ALLOC_FAIL; ++ goto out; ++ } ++ r = (mlkem_buf_len == crypto_kem_mlkem768_PUBLICKEYBYTES) ? ++ mlkem768_encap_secret(client_pub, secret, enc_out) : ++ mlkem1024_encap_secret(client_pub, secret, enc_out); ++ ++ if (r != 0) ++ goto out; ++ ++ /* generate ECDH key pair, store server pubkey after ciphertext */ ++ server_key = nist_pkey_keygen(server_key_len); ++ ++ if ((r = get_uncompressed_ec_pubkey(server_key, server_pub, server_key_len) != 0) || ++ (r = sshbuf_put(buf, secret, sizeof(secret))) != 0 || ++ (r = sshbuf_put(server_blob, enc_out, enc_out_len) != 0)|| ++ (r = sshbuf_put(server_blob, server_pub, server_key_len)) != 0) ++ goto out; ++ ++ /* append ECDH shared key */ ++ client_pub += mlkem_buf_len; ++ if ((r = kex_nist_shared_key_ext(server_key, client_pub, ecdh_buf_len, buf)) < 0) ++ goto out; ++ if ((r = ssh_digest_buffer(kex->hash_alg, buf, hash, sizeof(hash))) != 0) ++ goto out; ++#ifdef DEBUG_KEXECDH ++ dump_digest("server public NIST:", server_pub, server_key_len); ++ dump_digest("server cipher text:", enc_out, enc_out_len); ++ dump_digest("server kem key:", secret, sizeof(secret)); ++ dump_digest("concatenation of KEM key and ECDH shared key:", ++ sshbuf_ptr(buf), sshbuf_len(buf)); ++#endif ++ /* string-encoded hash is resulting shared secret */ ++ sshbuf_reset(buf); ++ if ((r = sshbuf_put_string(buf, hash, ++ ssh_digest_bytes(kex->hash_alg))) != 0) ++ goto out; ++#ifdef DEBUG_KEXECDH ++ dump_digest("encoded shared secret:", sshbuf_ptr(buf), sshbuf_len(buf)); ++#endif ++ /* success */ ++ r = 0; ++ *server_blobp = server_blob; ++ *shared_secretp = buf; ++ server_blob = NULL; ++ buf = NULL; ++ out: ++ explicit_bzero(hash, sizeof(hash)); ++ EVP_PKEY_free(server_key); ++ explicit_bzero(enc_out, sizeof(enc_out)); ++ explicit_bzero(secret, sizeof(secret)); ++ sshbuf_free(server_blob); ++ sshbuf_free(buf); ++ return r; ++} ++ ++static int ++kex_kem_mlkem_nist_dec(struct kex *kex, ++ const struct sshbuf *server_blob, struct sshbuf **shared_secretp, ++ size_t mlkem_len) ++{ ++ struct sshbuf *buf = NULL; ++ const u_char *ciphertext, *server_pub; ++ u_char hash[SSH_DIGEST_MAX_LENGTH]; ++ u_char decap[crypto_kem_mlkem768_BYTES]; ++ int r; ++ size_t nist_len; ++ ++ *shared_secretp = NULL; ++ ++ if (sshbuf_len(server_blob) < mlkem_len) { ++ r = SSH_ERR_SIGNATURE_INVALID; ++ goto out; ++ } ++ ++ nist_len = sshbuf_len(server_blob) - mlkem_len; ++ ++ switch (mlkem_len) { ++ case crypto_kem_mlkem768_CIPHERTEXTBYTES: ++ if (nist_len != NIST_P256_COMPRESSED_LEN ++ && nist_len != NIST_P256_UNCOMPRESSED_LEN) { ++ r = SSH_ERR_SIGNATURE_INVALID; ++ goto out; ++ } ++ break; ++ case crypto_kem_mlkem1024_CIPHERTEXTBYTES: ++ if (nist_len != NIST_P384_COMPRESSED_LEN ++ && nist_len != NIST_P384_UNCOMPRESSED_LEN) { ++ r = SSH_ERR_SIGNATURE_INVALID; ++ goto out; ++ } ++ break; ++ } ++ ++ ciphertext = sshbuf_ptr(server_blob); ++ server_pub = ciphertext + mlkem_len; ++ /* hash concatenation of KEM key and ECDH shared key */ ++ if ((buf = sshbuf_new()) == NULL) { ++ r = SSH_ERR_ALLOC_FAIL; ++ goto out; ++ } ++#ifdef DEBUG_KEXECDH ++ dump_digest("server cipher text:", ciphertext, mlkem_len); ++ dump_digest("server public key NIST:", server_pub, nist_len); ++#endif ++ r = (mlkem_len == crypto_kem_mlkem768_CIPHERTEXTBYTES) ? ++ mlkem768_decap_secret(kex->mlkem768_client_key, ciphertext, decap) : ++ mlkem1024_decap_secret(kex->mlkem1024_client_key, ciphertext, decap); ++ ++ if (r != 0) ++ goto out; ++ if ((r = sshbuf_put(buf, decap, sizeof(decap))) != 0) ++ goto out; ++ if ((r = kex_nist_shared_key_ext(kex->ec_hybrid_client_key, server_pub, ++ nist_len, buf)) < 0) ++ goto out; ++ if ((r = ssh_digest_buffer(kex->hash_alg, buf, ++ hash, sizeof(hash))) != 0) ++ goto out; ++#ifdef DEBUG_KEXECDH ++ dump_digest("client kem key:", decap, sizeof(decap)); ++ dump_digest("concatenation of KEM key and ECDH shared key:", ++ sshbuf_ptr(buf), sshbuf_len(buf)); ++#endif ++ sshbuf_reset(buf); ++ if ((r = sshbuf_put_string(buf, hash, ++ ssh_digest_bytes(kex->hash_alg))) != 0) ++ goto out; ++#ifdef DEBUG_KEXECDH ++ dump_digest("encoded shared secret:", sshbuf_ptr(buf), sshbuf_len(buf)); ++#endif ++ /* success */ ++ r = 0; ++ *shared_secretp = buf; ++ buf = NULL; ++ out: ++ explicit_bzero(hash, sizeof(hash)); ++ explicit_bzero(decap, sizeof(decap)); ++ sshbuf_free(buf); ++ return r; ++} ++ ++int ++kex_kem_mlkem768nistp256_keypair(struct kex *kex) ++{ ++ return kex_kem_mlkem_nist_keypair(kex, crypto_kem_mlkem768_PUBLICKEYBYTES, NIST_P256_UNCOMPRESSED_LEN); ++} ++ ++int ++kex_kem_mlkem768nistp256_enc(struct kex *kex, const struct sshbuf *client_blob, ++ struct sshbuf **server_blobp, struct sshbuf **shared_secretp) ++{ ++ return kex_kem_mlkem_nist_enc(kex, "P-256", client_blob, server_blobp, shared_secretp); ++} ++ ++int ++kex_kem_mlkem768nistp256_dec(struct kex *kex, const struct sshbuf *server_blob, ++ struct sshbuf **shared_secretp) ++{ ++ return kex_kem_mlkem_nist_dec(kex, server_blob, shared_secretp, ++ crypto_kem_mlkem768_CIPHERTEXTBYTES); ++} ++ ++int ++kex_kem_mlkem1024nistp384_keypair(struct kex *kex) ++{ ++ return kex_kem_mlkem_nist_keypair(kex, crypto_kem_mlkem1024_PUBLICKEYBYTES, NIST_P384_UNCOMPRESSED_LEN); ++} ++ ++int ++kex_kem_mlkem1024nistp384_enc(struct kex *kex, const struct sshbuf *client_blob, ++ struct sshbuf **server_blobp, struct sshbuf **shared_secretp) ++{ ++ return kex_kem_mlkem_nist_enc(kex, "P-384", client_blob, server_blobp, shared_secretp); ++} ++ ++int ++kex_kem_mlkem1024nistp384_dec(struct kex *kex, const struct sshbuf *server_blob, ++ struct sshbuf **shared_secretp) ++{ ++ return kex_kem_mlkem_nist_dec(kex, server_blob, shared_secretp, ++ crypto_kem_mlkem1024_CIPHERTEXTBYTES); ++} ++ + #else /* USE_MLKEM768X25519 */ + int + kex_kem_mlkem768x25519_keypair(struct kex *kex) +@@ -571,4 +1092,39 @@ kex_kem_mlkem768x25519_dec(struct kex *kex, + { + return SSH_ERR_SIGN_ALG_UNSUPPORTED; + } ++ ++int kex_kem_mlkem768nistp256_keypair(struct kex *) ++{ ++ return SSH_ERR_SIGN_ALG_UNSUPPORTED; ++} ++ ++int kex_kem_mlkem768nistp256_enc(struct kex *, const struct sshbuf *, ++ struct sshbuf **, struct sshbuf **) ++{ ++ return SSH_ERR_SIGN_ALG_UNSUPPORTED; ++} ++ ++int kex_kem_mlkem768nistp256_dec(struct kex *, const struct sshbuf *, ++ struct sshbuf **) ++{ ++ return SSH_ERR_SIGN_ALG_UNSUPPORTED; ++} ++ ++int kex_kem_mlkem1024nistp384_keypair(struct kex *) ++{ ++ return SSH_ERR_SIGN_ALG_UNSUPPORTED; ++} ++ ++int kex_kem_mlkem1024nistp384_enc(struct kex *, const struct sshbuf *, ++ struct sshbuf **, struct sshbuf **) ++{ ++ return SSH_ERR_SIGN_ALG_UNSUPPORTED; ++} ++ ++int kex_kem_mlkem1024nistp384_dec(struct kex *, const struct sshbuf *, ++ struct sshbuf **) ++{ ++ return SSH_ERR_SIGN_ALG_UNSUPPORTED; ++} ++ + #endif /* USE_MLKEM768X25519 */ +diff --git a/monitor.c b/monitor.c +index 58fbac9df..75413ce71 100644 +--- a/monitor.c ++++ b/monitor.c +@@ -2009,6 +2009,8 @@ monitor_apply_keystate(struct ssh *ssh, struct monitor *pmonitor) + kex->kex[KEX_C25519_SHA256] = kex_gen_server; + kex->kex[KEX_KEM_SNTRUP761X25519_SHA512] = kex_gen_server; + kex->kex[KEX_KEM_MLKEM768X25519_SHA256] = kex_gen_server; ++ kex->kex[KEX_KEM_MLKEM768NISTP256_SHA256] = kex_gen_server; ++ kex->kex[KEX_KEM_MLKEM1024NISTP384_SHA384] = kex_gen_server; + kex->load_host_public_key=&get_hostkey_public_by_type; + kex->load_host_private_key=&get_hostkey_private_by_type; + kex->host_key_index=&get_hostkey_index; +diff --git a/regress/unittests/kex/test_kex.c b/regress/unittests/kex/test_kex.c +index 09016aead..2baf9a7a0 100644 +--- a/regress/unittests/kex/test_kex.c ++++ b/regress/unittests/kex/test_kex.c +@@ -155,6 +155,8 @@ do_kex_with_key(char *kex, int keytype, int bits) + server2->kex->kex[KEX_C25519_SHA256] = kex_gen_server; + server2->kex->kex[KEX_KEM_SNTRUP761X25519_SHA512] = kex_gen_server; + server2->kex->kex[KEX_KEM_MLKEM768X25519_SHA256] = kex_gen_server; ++ server2->kex->kex[KEX_KEM_MLKEM768NISTP256_SHA256] = kex_gen_server; ++ server2->kex->kex[KEX_KEM_MLKEM1024NISTP384_SHA384] = kex_gen_server; + server2->kex->load_host_public_key = server->kex->load_host_public_key; + server2->kex->load_host_private_key = server->kex->load_host_private_key; + server2->kex->sign = server->kex->sign; +@@ -211,6 +213,8 @@ kex_tests(void) + do_kex("diffie-hellman-group1-sha1"); + # ifdef USE_MLKEM768X25519 + do_kex("mlkem768x25519-sha256"); ++ do_kex("mlkem768nistp256-sha256"); ++ do_kex("mlkem1024nistp384-sha384"); + # endif /* USE_MLKEM768X25519 */ + # ifdef USE_SNTRUP761X25519 + do_kex("sntrup761x25519-sha512@openssh.com"); +diff --git a/ssh-keyscan.c b/ssh-keyscan.c +index 9f76ad225..8660088b7 100644 +--- a/ssh-keyscan.c ++++ b/ssh-keyscan.c +@@ -304,6 +304,8 @@ keygrab_ssh2(con *c) + c->c_ssh->kex->kex[KEX_C25519_SHA256] = kex_gen_client; + c->c_ssh->kex->kex[KEX_KEM_SNTRUP761X25519_SHA512] = kex_gen_client; + c->c_ssh->kex->kex[KEX_KEM_MLKEM768X25519_SHA256] = kex_gen_client; ++ c->c_ssh->kex->kex[KEX_KEM_MLKEM768NISTP256_SHA256] = kex_gen_client; ++ c->c_ssh->kex->kex[KEX_KEM_MLKEM1024NISTP384_SHA384] = kex_gen_client; + ssh_set_verify_host_key_callback(c->c_ssh, key_print_wrapper); + /* + * do the key-exchange until an error occurs or until +diff --git a/ssh_api.c b/ssh_api.c +index 7bdcee148..d20b03a00 100644 +--- a/ssh_api.c ++++ b/ssh_api.c +@@ -135,6 +135,8 @@ ssh_init(struct ssh **sshp, int is_server, struct kex_params *kex_params) + ssh->kex->kex[KEX_C25519_SHA256] = kex_gen_server; + ssh->kex->kex[KEX_KEM_SNTRUP761X25519_SHA512] = kex_gen_server; + ssh->kex->kex[KEX_KEM_MLKEM768X25519_SHA256] = kex_gen_server; ++ ssh->kex->kex[KEX_KEM_MLKEM768NISTP256_SHA256] = kex_gen_server; ++ ssh->kex->kex[KEX_KEM_MLKEM1024NISTP384_SHA384] = kex_gen_server; + ssh->kex->load_host_public_key=&_ssh_host_public_key; + ssh->kex->load_host_private_key=&_ssh_host_private_key; + ssh->kex->sign=&_ssh_host_key_sign; +@@ -154,6 +156,8 @@ ssh_init(struct ssh **sshp, int is_server, struct kex_params *kex_params) + ssh->kex->kex[KEX_C25519_SHA256] = kex_gen_client; + ssh->kex->kex[KEX_KEM_SNTRUP761X25519_SHA512] = kex_gen_client; + ssh->kex->kex[KEX_KEM_MLKEM768X25519_SHA256] = kex_gen_client; ++ ssh->kex->kex[KEX_KEM_MLKEM768NISTP256_SHA256] = kex_gen_client; ++ ssh->kex->kex[KEX_KEM_MLKEM1024NISTP384_SHA384] = kex_gen_client; + ssh->kex->verify_host_key =&_ssh_verify_host_key; + } + *sshp = ssh; +diff --git a/sshconnect2.c b/sshconnect2.c +index 5beae1c10..6d9f1238b 100644 +--- a/sshconnect2.c ++++ b/sshconnect2.c +@@ -347,6 +347,8 @@ ssh_kex2(struct ssh *ssh, char *host, struct sockaddr *hostaddr, u_short port, + ssh->kex->kex[KEX_C25519_SHA256] = kex_gen_client; + ssh->kex->kex[KEX_KEM_SNTRUP761X25519_SHA512] = kex_gen_client; + ssh->kex->kex[KEX_KEM_MLKEM768X25519_SHA256] = kex_gen_client; ++ ssh->kex->kex[KEX_KEM_MLKEM768NISTP256_SHA256] = kex_gen_client; ++ ssh->kex->kex[KEX_KEM_MLKEM1024NISTP384_SHA384] = kex_gen_client; + ssh->kex->verify_host_key=&verify_host_key_callback; + + #if defined(GSSAPI) && defined(WITH_OPENSSL) +diff -up openssh-9.9p1-build/openssh-9.9p1/sshd-session.c.xxx openssh-9.9p1-build/openssh-9.9p1/sshd-session.c +--- a/sshd-session.c.xxx 2025-11-03 12:07:44.196167561 +0100 ++++ b/sshd-session.c 2025-11-03 12:08:26.809214111 +0100 +@@ -1659,6 +1659,8 @@ do_ssh2_kex(struct ssh *ssh) + kex->kex[KEX_C25519_SHA256] = kex_gen_server; + kex->kex[KEX_KEM_SNTRUP761X25519_SHA512] = kex_gen_server; + kex->kex[KEX_KEM_MLKEM768X25519_SHA256] = kex_gen_server; ++ kex->kex[KEX_KEM_MLKEM768NISTP256_SHA256] = kex_gen_server; ++ kex->kex[KEX_KEM_MLKEM1024NISTP384_SHA384] = kex_gen_server; + kex->load_host_public_key=&get_hostkey_public_by_type; + kex->load_host_private_key=&get_hostkey_private_by_type; + kex->host_key_index=&get_hostkey_index; diff --git a/openssh-7.7p1-fips.patch b/openssh-7.7p1-fips.patch index 5d8da08..fcd0245 100644 --- a/openssh-7.7p1-fips.patch +++ b/openssh-7.7p1-fips.patch @@ -268,9 +268,24 @@ diff -up openssh-8.6p1/ssh.c.fips openssh-8.6p1/ssh.c /* Expand SecurityKeyProvider if it refers to an environment variable */ if (options.sk_provider != NULL && *options.sk_provider == '$' && strlen(options.sk_provider) > 1) { -diff -up openssh-8.6p1/sshconnect2.c.fips openssh-8.6p1/sshconnect2.c ---- openssh-8.6p1/sshconnect2.c.fips 2021-05-06 12:08:36.485926777 +0200 -+++ openssh-8.6p1/sshconnect2.c 2021-05-06 12:08:36.501926900 +0200 +diff -up openssh-9.9p1/ssh-gss.h.xxx openssh-9.9p1/ssh-gss.h +--- openssh-9.9p1/ssh-gss.h.xxx 2025-08-04 15:02:15.895341244 +0200 ++++ openssh-9.9p1/ssh-gss.h 2025-08-04 15:02:54.826304548 +0200 +@@ -94,6 +94,11 @@ extern char **k5users_allowed_cmds; + KEX_GSS_GRP14_SHA1_ID "," \ + KEX_GSS_GEX_SHA1_ID + ++#define GSS_KEX_DEFAULT_KEX_FIPS \ ++ KEX_GSS_GRP14_SHA256_ID "," \ ++ KEX_GSS_GRP16_SHA512_ID "," \ ++ KEX_GSS_NISTP256_SHA256_ID ++ + #include "digest.h" /* SSH_DIGEST_MAX_LENGTH */ + + typedef struct { +diff -up openssh-9.9p1/sshconnect2.c.xxx openssh-9.9p1/sshconnect2.c +--- openssh-9.9p1/sshconnect2.c.xxx 2025-08-04 14:58:08.908229826 +0200 ++++ openssh-9.9p1/sshconnect2.c 2025-08-04 15:07:59.942477440 +0200 @@ -45,6 +45,8 @@ #include #endif @@ -280,75 +295,27 @@ diff -up openssh-8.6p1/sshconnect2.c.fips openssh-8.6p1/sshconnect2.c #include "openbsd-compat/sys-queue.h" #include "xmalloc.h" -@@ -269,36 +271,41 @@ ssh_kex2(struct ssh *ssh, char *host, st +@@ -274,6 +277,9 @@ ssh_kex2(struct ssh *ssh, char *host, st #if defined(GSSAPI) && defined(WITH_OPENSSL) if (options.gss_keyex) { -- /* Add the GSSAPI mechanisms currently supported on this -- * client to the key exchange algorithm proposal */ -- orig = myproposal[PROPOSAL_KEX_ALGS]; -- -- if (options.gss_server_identity) { -- gss_host = xstrdup(options.gss_server_identity); -- } else if (options.gss_trust_dns) { -- gss_host = remote_hostname(ssh); -- /* Fall back to specified host if we are using proxy command -- * and can not use DNS on that socket */ -- if (strcmp(gss_host, "UNKNOWN") == 0) { -- free(gss_host); -+ if (FIPS_mode()) { -+ logit("Disabling GSSAPIKeyExchange. Not usable in FIPS mode"); -+ options.gss_keyex = 0; -+ } else { -+ /* Add the GSSAPI mechanisms currently supported on this -+ * client to the key exchange algorithm proposal */ -+ orig = myproposal[PROPOSAL_KEX_ALGS]; ++ char * gss_kex_filtered = FIPS_mode() ? ++ match_filter_allowlist(options.gss_kex_algorithms, GSS_KEX_DEFAULT_KEX_FIPS) : xstrdup(options.gss_kex_algorithms); + -+ if (options.gss_server_identity) { -+ gss_host = xstrdup(options.gss_server_identity); -+ } else if (options.gss_trust_dns) { -+ gss_host = remote_hostname(ssh); -+ /* Fall back to specified host if we are using proxy command -+ * and can not use DNS on that socket */ -+ if (strcmp(gss_host, "UNKNOWN") == 0) { -+ free(gss_host); -+ gss_host = xstrdup(host); -+ } -+ } else { - gss_host = xstrdup(host); - } -- } else { -- gss_host = xstrdup(host); -- } - -- gss = ssh_gssapi_client_mechanisms(gss_host, -- options.gss_client_identity, options.gss_kex_algorithms); -- if (gss) { -- debug("Offering GSSAPI proposal: %s", gss); -- xasprintf(&myproposal[PROPOSAL_KEX_ALGS], -- "%s,%s", gss, orig); -- -- /* If we've got GSSAPI algorithms, then we also support the -- * 'null' hostkey, as a last resort */ -- orig = myproposal[PROPOSAL_SERVER_HOST_KEY_ALGS]; -- xasprintf(&myproposal[PROPOSAL_SERVER_HOST_KEY_ALGS], -- "%s,null", orig); -+ gss = ssh_gssapi_client_mechanisms(gss_host, -+ options.gss_client_identity, options.gss_kex_algorithms); -+ if (gss) { -+ debug("Offering GSSAPI proposal: %s", gss); -+ xasprintf(&myproposal[PROPOSAL_KEX_ALGS], -+ "%s,%s", gss, orig); -+ -+ /* If we've got GSSAPI algorithms, then we also support the -+ * 'null' hostkey, as a last resort */ -+ orig = myproposal[PROPOSAL_SERVER_HOST_KEY_ALGS]; -+ xasprintf(&myproposal[PROPOSAL_SERVER_HOST_KEY_ALGS], -+ "%s,null", orig); -+ } + /* Add the GSSAPI mechanisms currently supported on this + * client to the key exchange algorithm proposal */ + orig = myproposal[PROPOSAL_KEX_ALGS]; +@@ -293,7 +299,9 @@ ssh_kex2(struct ssh *ssh, char *host, st } - } - #endif + + gss = ssh_gssapi_client_mechanisms(gss_host, +- options.gss_client_identity, options.gss_kex_algorithms); ++ options.gss_client_identity, gss_kex_filtered); ++ free(gss_kex_filtered); ++ + if (gss) { + debug("Offering GSSAPI proposal: %s", gss); + xasprintf(&myproposal[PROPOSAL_KEX_ALGS], diff -up openssh-8.6p1/sshd.c.fips openssh-8.6p1/sshd.c --- openssh-8.6p1/sshd.c.fips 2021-05-06 12:08:36.493926838 +0200 +++ openssh-8.6p1/sshd.c 2021-05-06 12:13:56.501492639 +0200 @@ -393,36 +360,6 @@ diff -up openssh-8.6p1/sshd.c.fips openssh-8.6p1/sshd.c /* * Chdir to the root directory so that the current disk can be * unmounted if desired. -diff -up openssh-8.6p1/sshd-session.c.fips openssh-8.6p1/sshd-session.c ---- a/sshd-session.c.fips 2021-05-06 12:08:36.493926838 +0200 -+++ b/sshd-session.c 2021-05-06 12:13:56.501492639 +0200 -@@ -78,6 +79,7 @@ - #include - #include - #include -+#include - #include "openbsd-compat/openssl-compat.h" - #endif - -@@ -2506,10 +2513,14 @@ do_ssh2_kex(struct ssh *ssh) - if (strlen(myproposal[PROPOSAL_SERVER_HOST_KEY_ALGS]) == 0) - orig = NULL; - -- if (options.gss_keyex) -- gss = ssh_gssapi_server_mechanisms(); -- else -- gss = NULL; -+ if (options.gss_keyex) { -+ if (FIPS_mode()) { -+ logit("Disabling GSSAPIKeyExchange. Not usable in FIPS mode"); -+ options.gss_keyex = 0; -+ } else { -+ gss = ssh_gssapi_server_mechanisms(); -+ } -+ } - - if (gss && orig) - xasprintf(&newstr, "%s,%s", gss, orig); diff -up openssh-8.6p1/sshkey.c.fips openssh-8.6p1/sshkey.c --- openssh-8.6p1/sshkey.c.fips 2021-05-06 12:08:36.493926838 +0200 +++ openssh-8.6p1/sshkey.c 2021-05-06 12:08:36.502926908 +0200 diff --git a/openssh-9.9p1-canonical-match-user.patch b/openssh-9.9p1-canonical-match-user.patch new file mode 100644 index 0000000..1adf688 --- /dev/null +++ b/openssh-9.9p1-canonical-match-user.patch @@ -0,0 +1,120 @@ +diff --color -ruNp a/auth.c b/auth.c +--- a/auth.c 2025-10-29 09:57:20.440640281 +0100 ++++ b/auth.c 2025-10-29 09:57:54.747956792 +0100 +@@ -476,8 +476,12 @@ getpwnamallow(struct ssh *ssh, const cha + u_int i; + + ci = server_get_connection_info(ssh, 1, options.use_dns); +- ci->user = user; +- ci->user_invalid = getpwnam(user) == NULL; ++ pw = getpwnam(user); ++ if (pw != NULL && options.canonical_match_user) ++ ci->user = pw->pw_name; ++ else ++ ci->user = user; ++ ci->user_invalid = pw == NULL; + parse_server_match_config(&options, &includes, ci); + log_change_level(options.log_level); + log_verbose_reset(); +diff --color -ruNp a/servconf.c b/servconf.c +--- a/servconf.c 2025-10-29 09:57:20.502465586 +0100 ++++ b/servconf.c 2025-10-29 09:57:54.748205464 +0100 +@@ -225,6 +225,7 @@ initialize_server_options(ServerOptions + options->unused_connection_timeout = -1; + options->sshd_session_path = NULL; + options->refuse_connection = -1; ++ options->canonical_match_user = -1; + } + + /* Returns 1 if a string option is unset or set to "none" or 0 otherwise. */ +@@ -526,6 +527,8 @@ fill_default_server_options(ServerOption + options->sshd_session_path = xstrdup(_PATH_SSHD_SESSION); + if (options->refuse_connection == -1) + options->refuse_connection = 0; ++ if (options->canonical_match_user == -1) ++ options->canonical_match_user = 0; + + assemble_algorithms(options); + +@@ -609,7 +612,7 @@ typedef enum { + sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding, + sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider, + sRequiredRSASize, sChannelTimeout, sUnusedConnectionTimeout, +- sSshdSessionPath, sRefuseConnection, ++ sSshdSessionPath, sRefuseConnection, sCanonicalMatchUser, + sDeprecated, sIgnore, sUnsupported + } ServerOpCodes; + +@@ -798,6 +801,7 @@ static struct { + { "unusedconnectiontimeout", sUnusedConnectionTimeout, SSHCFG_ALL }, + { "sshdsessionpath", sSshdSessionPath, SSHCFG_GLOBAL }, + { "refuseconnection", sRefuseConnection, SSHCFG_ALL }, ++ { "canonicalmatchuser", sCanonicalMatchUser, SSHCFG_GLOBAL }, + { NULL, sBadOption, 0 } + }; + +@@ -2807,6 +2811,11 @@ process_server_config_line_depth(ServerO + multistate_ptr = multistate_flag; + goto parse_multistate; + ++ case sCanonicalMatchUser: ++ intptr = &options->canonical_match_user; ++ multistate_ptr = multistate_flag; ++ goto parse_multistate; ++ + case sDeprecated: + case sIgnore: + case sUnsupported: +@@ -3028,6 +3037,7 @@ copy_set_server_options(ServerOptions *d + M_CP_INTOPT(required_rsa_size); + M_CP_INTOPT(unused_connection_timeout); + M_CP_INTOPT(refuse_connection); ++ M_CP_INTOPT(canonical_match_user); + + /* + * The bind_mask is a mode_t that may be unsigned, so we can't use +@@ -3368,6 +3378,7 @@ dump_config(ServerOptions *o) + dump_cfg_fmtint(sFingerprintHash, o->fingerprint_hash); + dump_cfg_fmtint(sExposeAuthInfo, o->expose_userauth_info); + dump_cfg_fmtint(sRefuseConnection, o->refuse_connection); ++ dump_cfg_fmtint(sCanonicalMatchUser, o->canonical_match_user); + + /* string arguments */ + dump_cfg_string(sPidFile, o->pid_file); +diff --color -ruNp a/servconf.h b/servconf.h +--- a/servconf.h 2025-10-29 09:57:20.503199202 +0100 ++++ b/servconf.h 2025-10-29 09:57:54.749236422 +0100 +@@ -261,6 +261,8 @@ typedef struct { + char *sshd_session_path; + + int refuse_connection; ++ ++ int canonical_match_user; + } ServerOptions; + + /* Information about the incoming connection as used by Match */ +diff --color -ruNp a/sshd_config.5 b/sshd_config.5 +--- a/sshd_config.5 2025-10-29 09:57:20.503465608 +0100 ++++ b/sshd_config.5 2025-10-29 09:57:54.749544972 +0100 +@@ -1384,6 +1384,7 @@ Available keywords are + .Cm PubkeyAuthentication , + .Cm PubkeyAuthOptions , + .Cm RefuseConnection , ++.Cm CanonicalMatchUser , + .Cm RekeyLimit , + .Cm RevokedKeys , + .Cm RDomain , +@@ -1828,6 +1829,13 @@ are enabled. + This option is only really useful in a + .Cm Match + block. ++.It Cm CanonicalMatchUser ++Some password databases allow users to define aliases for their username. ++This directive indicates that ++.Xr sshd 8 ++should attempt to first obtain a canonical username from a password database before evaluating a ++.Cm Match User ++conditional block. + .It Cm RekeyLimit + Specifies the maximum amount of data that may be transmitted or received + before the session key is renegotiated, optionally followed by a maximum diff --git a/openssh-9.9p1-fips-gss.patch b/openssh-9.9p1-fips-gss.patch new file mode 100644 index 0000000..c94e34c --- /dev/null +++ b/openssh-9.9p1-fips-gss.patch @@ -0,0 +1,42 @@ +diff -up openssh-9.9p1-build/openssh-9.9p1/gss-genr.c.xxx openssh-9.9p1-build/openssh-9.9p1/gss-genr.c +--- a/gss-genr.c.xxx 2025-09-01 17:15:41.070677784 +0200 ++++ b/gss-genr.c 2025-09-01 17:31:20.376362078 +0200 +@@ -149,19 +149,29 @@ ssh_gssapi_kex_mechs(gss_OID_set gss_sup + for (i = 0; i < gss_supported->count; i++) { + if (gss_supported->elements[i].length < 128 && + (*check)(NULL, &(gss_supported->elements[i]), host, client)) { ++ EVP_MD_CTX * ctx = NULL; ++ EVP_MD *md5 = NULL; /* Here we don't use MD5 for crypto purposes */ ++ unsigned int md_size = sizeof(digest); + + deroid[0] = SSH_GSS_OIDTYPE; + deroid[1] = gss_supported->elements[i].length; +- +- if ((md = ssh_digest_start(SSH_DIGEST_MD5)) == NULL || +- (r = ssh_digest_update(md, deroid, 2)) != 0 || +- (r = ssh_digest_update(md, +- gss_supported->elements[i].elements, +- gss_supported->elements[i].length)) != 0 || +- (r = ssh_digest_final(md, digest, sizeof(digest))) != 0) ++ if ((md5 = EVP_MD_fetch(NULL, "MD5", "provider=default,-fips")) == NULL) ++ fatal_fr(r, "MD5 fetch failed"); ++ if ((ctx = EVP_MD_CTX_new()) == NULL) { ++ EVP_MD_free(md5); ++ fatal_fr(r, "digest ctx failed"); ++ } ++ if (EVP_DigestInit(ctx, md5) <= 0 ++ || EVP_DigestUpdate(ctx, deroid, 2) <= 0 ++ || EVP_DigestUpdate(ctx, gss_supported->elements[i].elements, ++ gss_supported->elements[i].length) <= 0 ++ || EVP_DigestFinal(ctx, digest, &md_size) <= 0) { ++ EVP_MD_free(md5); ++ EVP_MD_CTX_free(ctx); + fatal_fr(r, "digest failed"); +- ssh_digest_free(md); +- md = NULL; ++ } ++ EVP_MD_free(md5); md5 = NULL; ++ EVP_MD_CTX_free(ctx); ctx = NULL; + + encoded = xmalloc(ssh_digest_bytes(SSH_DIGEST_MD5) + * 2); diff --git a/openssh-9.9p1-scp-traversing.patch b/openssh-9.9p1-scp-traversing.patch new file mode 100644 index 0000000..2378b3a --- /dev/null +++ b/openssh-9.9p1-scp-traversing.patch @@ -0,0 +1,437 @@ +diff --color -ruNp a/regress/scp3.sh b/regress/scp3.sh +--- a/regress/scp3.sh 2024-09-20 00:20:48.000000000 +0200 ++++ b/regress/scp3.sh 2025-10-29 16:00:59.815068193 +0100 +@@ -6,6 +6,12 @@ tid="scp3" + COPY2=${OBJ}/copy2 + DIR=${COPY}.dd + DIR2=${COPY}.dd2 ++DIFFOPT="-rN" ++ ++# Figure out if diff does not understand "-N" ++if ! diff -N ${SRC}/scp.sh ${SRC}/scp.sh 2>/dev/null; then ++ DIFFOPT="-r" ++fi + + maybe_add_scp_path_to_sshd + +@@ -63,6 +69,15 @@ for mode in scp sftp ; do + echo b > ${COPY2} + $SCP $scpopts -3 hostA:${DATA} hostA:${COPY} hostB:${COPY2} + cmp ${COPY} ${COPY2} >/dev/null && fail "corrupt target" ++ ++ # scp /blah/.. is only supported via the sftp protocol. ++ # Original protocol scp just refuses it. ++ test $mode != sftp && continue ++ verbose "$tag: recursive .." ++ forest ++ $SCP $scpopts -r hostA:${DIR}/subdir/.. hostB:${DIR2} || \ ++ fail "copy failed" ++ diff ${DIFFOPT} ${DIR} ${DIR2} || fail "corrupted copy" + done + + scpclean +diff --color -ruNp a/regress/scp.sh b/regress/scp.sh +--- a/regress/scp.sh 2024-09-20 00:20:48.000000000 +0200 ++++ b/regress/scp.sh 2025-10-29 16:00:59.810765653 +0100 +@@ -199,6 +199,19 @@ for mode in scp sftp ; do + echo b > ${COPY2} + $SCP $scpopts ${DATA} ${COPY} ${COPY2} + cmp ${COPY} ${COPY2} >/dev/null && fail "corrupt target" ++ ++ # scp /blah/.. is only supported via the sftp protocol. ++ # Original protocol scp just refuses it. ++ test $mode != sftp && continue ++ verbose "$tag: recursive local .. to remote dir" ++ forest ++ $SCP $scpopts -r ${DIR}/subdir/.. somehost:${DIR2} || fail "copy failed" ++ diff ${DIFFOPT} ${DIR} ${DIR2} || fail "corrupted copy" ++ ++ verbose "$tag: recursive remote .. to local dir" ++ forest ++ $SCP $scpopts -r somehost:${DIR}/subdir/.. ${DIR2} || fail "copy failed" ++ diff ${DIFFOPT} ${DIR} ${DIR2} || fail "corrupted copy" + done + + scpclean +diff --color -ruNp a/regress/sftp-cmds.sh b/regress/sftp-cmds.sh +--- a/regress/sftp-cmds.sh 2024-09-20 00:20:48.000000000 +0200 ++++ b/regress/sftp-cmds.sh 2025-10-29 16:00:59.813392901 +0100 +@@ -7,6 +7,12 @@ + + tid="sftp commands" + ++DIFFOPT="-rN" ++# Figure out if diff does not understand "-N" ++if ! diff -N ${SRC}/sftp-cmds.sh ${SRC}/sftp-cmds.sh 2>/dev/null; then ++ DIFFOPT="-r" ++fi ++ + # test that these files are readable! + for i in `(cd /bin;echo l*)` + do +@@ -24,207 +30,246 @@ SPACECOPY_ARG="${COPY}\ this\ has\ space + # File with glob metacharacters + GLOBMETACOPY="${COPY} [metachar].txt" + ++sftpserver() { ++ ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 ++} ++ ++sftpserver_with_stdout() { ++ ${SFTP} -D ${SFTPSERVER} 2>&1 ++} ++ ++forest() { ++ rm -rf ${COPY}.dd/* ++ rm -rf ${COPY}.dd2 ++ mkdir -p ${COPY}.dd/a ${COPY}.dd/b ${COPY}.dd/c ${COPY}.dd/a/d ++ echo 'A' > ${COPY}.dd/a/A ++ echo 'B' > ${COPY}.dd/a/B ++ echo 'C' > ${COPY}.dd/a/C ++ echo 'D' > ${COPY}.dd/a/D ++} ++ + rm -rf ${COPY} ${COPY}.1 ${COPY}.2 ${COPY}.dd ${COPY}.dd2 + mkdir ${COPY}.dd + + verbose "$tid: lls" +-printf "lcd ${OBJ}\nlls\n" | ${SFTP} -D ${SFTPSERVER} 2>&1 | \ ++printf "lcd ${OBJ}\nlls\n" | sftpserver_with_stdout | \ + grep copy.dd >/dev/null || fail "lls failed" + + verbose "$tid: lls w/path" +-echo "lls ${OBJ}" | ${SFTP} -D ${SFTPSERVER} 2>&1 | \ ++echo "lls ${OBJ}" | sftpserver_with_stdout | \ + grep copy.dd >/dev/null || fail "lls w/path failed" + + verbose "$tid: ls" +-echo "ls ${OBJ}" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "ls failed" ++echo "ls ${OBJ}" | sftpserver || fail "ls failed" + # XXX always successful + + verbose "$tid: shell" +-echo "!echo hi there" | ${SFTP} -D ${SFTPSERVER} 2>&1 | \ ++echo "!echo hi there" | sftpserver_with_stdout | \ + egrep '^hi there$' >/dev/null || fail "shell failed" + + verbose "$tid: pwd" +-echo "pwd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "pwd failed" ++echo "pwd" | sftpserver || fail "pwd failed" + # XXX always successful + + verbose "$tid: lpwd" +-echo "lpwd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "lpwd failed" ++echo "lpwd" | sftpserver || fail "lpwd failed" + # XXX always successful + + verbose "$tid: quit" +-echo "quit" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "quit failed" ++echo "quit" | sftpserver || fail "quit failed" + # XXX always successful + + verbose "$tid: help" +-echo "help" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "help failed" ++echo "help" | sftpserver || fail "help failed" + # XXX always successful + + rm -f ${COPY} + verbose "$tid: get" +-echo "get $DATA $COPY" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "get failed" ++echo "get $DATA $COPY" | sftpserver || fail "get failed" + cmp $DATA ${COPY} || fail "corrupted copy after get" + + rm -f ${COPY} + verbose "$tid: get quoted" +-echo "get \"$DATA\" $COPY" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "get failed" ++echo "get \"$DATA\" $COPY" | sftpserver || fail "get failed" + cmp $DATA ${COPY} || fail "corrupted copy after get" + + rm -f ${QUOTECOPY} + cp $DATA ${QUOTECOPY} + verbose "$tid: get filename with quotes" +-echo "get \"$QUOTECOPY_ARG\" ${COPY}" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "get failed" ++echo "get \"$QUOTECOPY_ARG\" ${COPY}" | sftpserver || fail "get failed" + cmp ${COPY} ${QUOTECOPY} || fail "corrupted copy after get with quotes" + rm -f ${QUOTECOPY} ${COPY} + + rm -f "$SPACECOPY" ${COPY} + cp $DATA "$SPACECOPY" + verbose "$tid: get filename with spaces" +-echo "get ${SPACECOPY_ARG} ${COPY}" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "get failed" ++echo "get ${SPACECOPY_ARG} ${COPY}" | sftpserver || fail "get failed" + cmp ${COPY} "$SPACECOPY" || fail "corrupted copy after get with spaces" + + rm -f "$GLOBMETACOPY" ${COPY} + cp $DATA "$GLOBMETACOPY" + verbose "$tid: get filename with glob metacharacters" +-echo "get \"${GLOBMETACOPY}\" ${COPY}" | \ +- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "get failed" ++echo "get \"${GLOBMETACOPY}\" ${COPY}" | sftpserver || fail "get failed" + cmp ${COPY} "$GLOBMETACOPY" || \ + fail "corrupted copy after get with glob metacharacters" + +-rm -f ${COPY}.dd/* ++rm -rf ${COPY}.dd/* + verbose "$tid: get to directory" +-echo "get $DATA ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "get failed" ++echo "get $DATA ${COPY}.dd" | sftpserver || fail "get failed" + cmp $DATA ${COPY}.dd/${DATANAME} || fail "corrupted copy after get" + +-rm -f ${COPY}.dd/* ++rm -rf ${COPY}.dd/* + verbose "$tid: glob get to directory" +-echo "get /bin/l* ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "get failed" ++echo "get /bin/l* ${COPY}.dd" | sftpserver || fail "get failed" + for x in $GLOBFILES; do + cmp /bin/$x ${COPY}.dd/$x || fail "corrupted copy after get" + done + +-rm -f ${COPY}.dd/* ++rm -rf ${COPY}.dd/* + verbose "$tid: get to local dir" +-printf "lcd ${COPY}.dd\nget $DATA\n" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "get failed" ++printf "lcd ${COPY}.dd\nget $DATA\n" | sftpserver || fail "get failed" + cmp $DATA ${COPY}.dd/${DATANAME} || fail "corrupted copy after get" + +-rm -f ${COPY}.dd/* ++rm -rf ${COPY}.dd/* + verbose "$tid: glob get to local dir" +-printf "lcd ${COPY}.dd\nget /bin/l*\n" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "get failed" ++printf "lcd ${COPY}.dd\nget /bin/l*\n" | sftpserver || fail "get failed" + for x in $GLOBFILES; do + cmp /bin/$x ${COPY}.dd/$x || fail "corrupted copy after get" + done + ++forest ++verbose "$tid: get recursive absolute" ++echo "get -R ${COPY}.dd ${COPY}.dd2" | sftpserver || fail "get failed" ++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy" ++ ++forest ++verbose "$tid: get recursive relative src" ++printf "cd ${COPY}.dd\n get -R . ${COPY}.dd2\n" | sftpserver || \ ++ fail "get failed" ++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy" ++ ++forest ++verbose "$tid: get relative .." ++printf "cd ${COPY}.dd/b\n get -R .. ${COPY}.dd2\n" | sftpserver || \ ++ fail "get failed" ++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy" ++ ++forest ++mkdir ${COPY}.dd2 ++verbose "$tid: get recursive relative .." ++printf "cd ${COPY}.dd/b\n lcd ${COPY}.dd2\n get -R ..\n" | sftpserver || \ ++ fail "get failed" ++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy" ++ + rm -f ${COPY} + verbose "$tid: put" +-echo "put $DATA $COPY" | \ +- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "put failed" ++echo "put $DATA $COPY" | sftpserver || fail "put failed" + cmp $DATA ${COPY} || fail "corrupted copy after put" + + rm -f ${QUOTECOPY} + verbose "$tid: put filename with quotes" +-echo "put $DATA \"$QUOTECOPY_ARG\"" | \ +- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "put failed" ++echo "put $DATA \"$QUOTECOPY_ARG\"" | sftpserver || fail "put failed" + cmp $DATA ${QUOTECOPY} || fail "corrupted copy after put with quotes" + + rm -f "$SPACECOPY" + verbose "$tid: put filename with spaces" +-echo "put $DATA ${SPACECOPY_ARG}" | \ +- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "put failed" ++echo "put $DATA ${SPACECOPY_ARG}" | sftpserver || fail "put failed" + cmp $DATA "$SPACECOPY" || fail "corrupted copy after put with spaces" + +-rm -f ${COPY}.dd/* ++rm -rf ${COPY}.dd/* + verbose "$tid: put to directory" +-echo "put $DATA ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "put failed" ++echo "put $DATA ${COPY}.dd" | sftpserver || fail "put failed" + cmp $DATA ${COPY}.dd/${DATANAME} || fail "corrupted copy after put" + +-rm -f ${COPY}.dd/* ++rm -rf ${COPY}.dd/* + verbose "$tid: glob put to directory" +-echo "put /bin/l? ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "put failed" ++echo "put /bin/l? ${COPY}.dd" | sftpserver || fail "put failed" + for x in $GLOBFILES; do + cmp /bin/$x ${COPY}.dd/$x || fail "corrupted copy after put" + done + +-rm -f ${COPY}.dd/* ++rm -rf ${COPY}.dd/* + verbose "$tid: put to local dir" +-printf "cd ${COPY}.dd\nput $DATA\n" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "put failed" ++printf "cd ${COPY}.dd\nput $DATA\n" | sftpserver || fail "put failed" + cmp $DATA ${COPY}.dd/${DATANAME} || fail "corrupted copy after put" + +-rm -f ${COPY}.dd/* ++rm -rf ${COPY}.dd/* + verbose "$tid: glob put to local dir" +-printf "cd ${COPY}.dd\nput /bin/l*\n" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "put failed" ++printf "cd ${COPY}.dd\nput /bin/l*\n" | sftpserver || fail "put failed" + for x in $GLOBFILES; do + cmp /bin/$x ${COPY}.dd/$x || fail "corrupted copy after put" + done + ++forest ++verbose "$tid: put recursive absolute" ++echo "put -R ${COPY}.dd ${COPY}.dd2" | sftpserver || fail "put failed" ++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy" ++ ++forest ++verbose "$tid: put recursive relative src" ++printf "lcd ${COPY}.dd\n put -R . ${COPY}.dd2\n" | sftpserver || \ ++ fail "put failed" ++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy" ++ ++forest ++verbose "$tid: put recursive .." ++printf "lcd ${COPY}.dd/b\n put -R .. ${COPY}.dd2\n" | sftpserver || \ ++ fail "put failed" ++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy" ++ ++forest ++mkdir ${COPY}.dd2 ++verbose "$tid: put recursive .. relative" ++printf "lcd ${COPY}.dd/b\n cd ${COPY}.dd2\n put -R ..\n" | sftpserver || \ ++ fail "put failed" ++diff ${DIFFOPT} ${COPY}.dd ${COPY}.dd2 || fail "corrupted copy" ++ + verbose "$tid: rename" +-echo "rename $COPY ${COPY}.1" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "rename failed" ++echo "rename $COPY ${COPY}.1" | sftpserver || fail "rename failed" + test -f ${COPY}.1 || fail "missing file after rename" + cmp $DATA ${COPY}.1 >/dev/null 2>&1 || fail "corrupted copy after rename" + + verbose "$tid: rename directory" +-echo "rename ${COPY}.dd ${COPY}.dd2" | \ +- ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || \ ++rm -rf ${COPY}.dd2 ++echo "rename ${COPY}.dd ${COPY}.dd2" | sftpserver || \ + fail "rename directory failed" + test -d ${COPY}.dd && fail "oldname exists after rename directory" + test -d ${COPY}.dd2 || fail "missing newname after rename directory" + + verbose "$tid: ln" +-echo "ln ${COPY}.1 ${COPY}.2" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "ln failed" ++echo "ln ${COPY}.1 ${COPY}.2" | sftpserver || fail "ln failed" + test -f ${COPY}.2 || fail "missing file after ln" + cmp ${COPY}.1 ${COPY}.2 || fail "created file is not equal after ln" + + verbose "$tid: ln -s" + rm -f ${COPY}.2 +-echo "ln -s ${COPY}.1 ${COPY}.2" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "ln -s failed" ++echo "ln -s ${COPY}.1 ${COPY}.2" | sftpserver || fail "ln -s failed" + test -h ${COPY}.2 || fail "missing file after ln -s" + + verbose "$tid: cp" + rm -f ${COPY}.2 +-echo "cp ${COPY}.1 ${COPY}.2" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 || fail "cp failed" ++echo "cp ${COPY}.1 ${COPY}.2" | sftpserver || fail "cp failed" + cmp ${COPY}.1 ${COPY}.2 || fail "created file is not equal after cp" + + verbose "$tid: mkdir" +-echo "mkdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "mkdir failed" ++echo "mkdir ${COPY}.dd" | sftpserver || fail "mkdir failed" + test -d ${COPY}.dd || fail "missing directory after mkdir" + + # XXX do more here + verbose "$tid: chdir" +-echo "chdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "chdir failed" ++echo "chdir ${COPY}.dd" | sftpserver || fail "chdir failed" + + verbose "$tid: rmdir" +-echo "rmdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "rmdir failed" ++echo "rmdir ${COPY}.dd" | sftpserver || fail "rmdir failed" + test -d ${COPY}.1 && fail "present directory after rmdir" + + verbose "$tid: lmkdir" +-echo "lmkdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "lmkdir failed" ++echo "lmkdir ${COPY}.dd" | sftpserver || fail "lmkdir failed" + test -d ${COPY}.dd || fail "missing directory after lmkdir" + + # XXX do more here + verbose "$tid: lchdir" +-echo "lchdir ${COPY}.dd" | ${SFTP} -D ${SFTPSERVER} >/dev/null 2>&1 \ +- || fail "lchdir failed" ++echo "lchdir ${COPY}.dd" | sftpserver || fail "lchdir failed" + + rm -rf ${COPY} ${COPY}.1 ${COPY}.2 ${COPY}.dd ${COPY}.dd2 + rm -rf ${QUOTECOPY} "$SPACECOPY" "$GLOBMETACOPY" +diff --color -ruNp a/scp.c b/scp.c +--- a/scp.c 2025-10-29 15:49:51.795813258 +0100 ++++ b/scp.c 2025-10-29 16:00:59.817393000 +0100 +@@ -1362,6 +1362,10 @@ source_sftp(int argc, char *src, char *t + if ((filename = basename(src)) == NULL) + fatal("basename \"%s\": %s", src, strerror(errno)); + ++ /* Special handling for source of '..' */ ++ if (strcmp(filename, "..") == 0) ++ filename = "."; /* Upload to dest, not dest/.. */ ++ + /* + * No need to glob here - the local shell already took care of + * the expansions +@@ -1635,6 +1639,10 @@ sink_sftp(int argc, char *dst, const cha + goto out; + } + ++ /* Special handling for destination of '..' */ ++ if (strcmp(filename, "..") == 0) ++ filename = "."; /* Download to dest, not dest/.. */ ++ + if (dst_is_dir) + abs_dst = sftp_path_append(dst, filename); + else +diff --color -ruNp a/sftp.c b/sftp.c +--- a/sftp.c 2025-10-29 15:49:51.796907436 +0100 ++++ b/sftp.c 2025-10-29 16:00:59.819803948 +0100 +@@ -687,6 +687,10 @@ process_get(struct sftp_conn *conn, cons + goto out; + } + ++ /* Special handling for dest of '..' */ ++ if (strcmp(filename, "..") == 0) ++ filename = "."; /* Download to dest, not dest/.. */ ++ + if (g.gl_matchc == 1 && dst) { + if (local_is_dir(dst)) { + abs_dst = sftp_path_append(dst, filename); +@@ -781,6 +785,9 @@ process_put(struct sftp_conn *conn, cons + err = -1; + goto out; + } ++ /* Special handling for source of '..' */ ++ if (strcmp(filename, "..") == 0) ++ filename = "."; /* Upload to dest, not dest/.. */ + + free(abs_dst); + abs_dst = NULL; diff --git a/openssh-9.9p1-sshd-no-delegate-credentials.patch b/openssh-9.9p1-sshd-no-delegate-credentials.patch new file mode 100644 index 0000000..2b70103 --- /dev/null +++ b/openssh-9.9p1-sshd-no-delegate-credentials.patch @@ -0,0 +1,122 @@ +diff --git a/gss-serv.c b/gss-serv.c +index 5c0491cf1..e2c501d0c 100644 +--- a/gss-serv.c ++++ b/gss-serv.c +@@ -509,6 +509,11 @@ ssh_gssapi_cleanup_creds(void) + int + ssh_gssapi_storecreds(void) + { ++ if (options.gss_deleg_creds == 0) { ++ debug_f("delegate credential is disabled, doing nothing"); ++ return 0; ++ } ++ + if (gssapi_client.mech && gssapi_client.mech->storecreds) { + return (*gssapi_client.mech->storecreds)(&gssapi_client); + } else +diff --git a/servconf.c b/servconf.c +index aab653244..02a9888c9 100644 +--- a/servconf.c ++++ b/servconf.c +@@ -144,6 +144,7 @@ initialize_server_options(ServerOptions *options) + options->gss_authentication=-1; + options->gss_keyex = -1; + options->gss_cleanup_creds = -1; ++ options->gss_deleg_creds = -1; + options->gss_strict_acceptor = -1; + options->gss_store_rekey = -1; + options->gss_kex_algorithms = NULL; +@@ -403,6 +404,8 @@ fill_default_server_options(ServerOptions *options) + options->gss_keyex = 0; + if (options->gss_cleanup_creds == -1) + options->gss_cleanup_creds = 1; ++ if (options->gss_deleg_creds == -1) ++ options->gss_deleg_creds = 1; + if (options->gss_strict_acceptor == -1) + options->gss_strict_acceptor = 1; + if (options->gss_store_rekey == -1) +@@ -598,7 +601,8 @@ typedef enum { + sHostKeyAlgorithms, sPerSourceMaxStartups, sPerSourceNetBlockSize, + sPerSourcePenalties, sPerSourcePenaltyExemptList, + sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile, +- sGssAuthentication, sGssCleanupCreds, sGssEnablek5users, sGssStrictAcceptor, ++ sGssAuthentication, sGssCleanupCreds, sGssDelegateCreds, ++ sGssEnablek5users, sGssStrictAcceptor, + sGssKeyEx, sGssIndicators, sGssKexAlgorithms, sGssStoreRekey, + sAcceptEnv, sSetEnv, sPermitTunnel, + sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory, +@@ -690,6 +694,7 @@ static struct { + { "gssapiauthentication", sGssAuthentication, SSHCFG_ALL }, + { "gssapicleanupcredentials", sGssCleanupCreds, SSHCFG_GLOBAL }, + { "gssapicleanupcreds", sGssCleanupCreds, SSHCFG_GLOBAL }, ++ { "gssapidelegatecredentials", sGssDelegateCreds, SSHCFG_GLOBAL }, + { "gssapistrictacceptorcheck", sGssStrictAcceptor, SSHCFG_GLOBAL }, + { "gssapikeyexchange", sGssKeyEx, SSHCFG_GLOBAL }, + { "gssapistorecredentialsonrekey", sGssStoreRekey, SSHCFG_GLOBAL }, +@@ -700,6 +705,7 @@ static struct { + { "gssapiauthentication", sUnsupported, SSHCFG_ALL }, + { "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL }, + { "gssapicleanupcreds", sUnsupported, SSHCFG_GLOBAL }, ++ { "gssapidelegatecredentials", sUnsupported, SSHCFG_GLOBAL }, + { "gssapistrictacceptorcheck", sUnsupported, SSHCFG_GLOBAL }, + { "gssapikeyexchange", sUnsupported, SSHCFG_GLOBAL }, + { "gssapistorecredentialsonrekey", sUnsupported, SSHCFG_GLOBAL }, +@@ -1713,6 +1719,10 @@ process_server_config_line_depth(ServerOptions *options, char *line, + intptr = &options->gss_cleanup_creds; + goto parse_flag; + ++ case sGssDelegateCreds: ++ intptr = &options->gss_deleg_creds; ++ goto parse_flag; ++ + case sGssStrictAcceptor: + intptr = &options->gss_strict_acceptor; + goto parse_flag; +@@ -3359,6 +3369,7 @@ dump_config(ServerOptions *o) + #ifdef GSSAPI + dump_cfg_fmtint(sGssAuthentication, o->gss_authentication); + dump_cfg_fmtint(sGssCleanupCreds, o->gss_cleanup_creds); ++ dump_cfg_fmtint(sGssDelegateCreds, o->gss_deleg_creds); + dump_cfg_fmtint(sGssKeyEx, o->gss_keyex); + dump_cfg_fmtint(sGssStrictAcceptor, o->gss_strict_acceptor); + dump_cfg_fmtint(sGssStoreRekey, o->gss_store_rekey); +diff --git a/servconf.h b/servconf.h +index 7c41df417..6bfdf6305 100644 +--- a/servconf.h ++++ b/servconf.h +@@ -158,6 +158,7 @@ typedef struct { + int gss_authentication; /* If true, permit GSSAPI authentication */ + int gss_keyex; /* If true, permit GSSAPI key exchange */ + int gss_cleanup_creds; /* If true, destroy cred cache on logout */ ++ int gss_deleg_creds; /* If true, accept delegated GSS credentials */ + int gss_strict_acceptor; /* If true, restrict the GSSAPI acceptor name */ + int gss_store_rekey; + char *gss_kex_algorithms; /* GSSAPI kex methods to be offered by client. */ +diff --git a/sshd_config.0 b/sshd_config.0 +index 49349bb30..e798f4df5 100644 +--- a/sshd_config.0 ++++ b/sshd_config.0 +@@ -453,6 +453,9 @@ DESCRIPTION + Specifies whether to automatically destroy the user's credentials + cache on logout. The default is yes. + ++ GSSAPIDelegateCredentials ++ Accept delegated credentials on the server side. The default is yes. ++ + GSSAPIStrictAcceptorCheck + Determines whether to be strict about the identity of the GSSAPI + acceptor a client authenticates against. If set to yes then the +diff --git a/sshd_config.5 b/sshd_config.5 +index 90ab87edd..8c677bfd0 100644 +--- a/sshd_config.5 ++++ b/sshd_config.5 +@@ -733,6 +733,9 @@ Specifies whether to automatically destroy the user's credentials cache + on logout. + The default is + .Cm yes . ++.It Cm GSSAPIDelegateCredentials ++Accept delegated credentials on the server side. The default is ++.CM yes . + .It Cm GSSAPIEnablek5users + Specifies whether to look at .k5users file for GSSAPI authentication + access control. Further details are described in diff --git a/openssh.spec b/openssh.spec index b7f7042..2c0c600 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: 14%{?dist} +Release: 23%{?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 @@ -214,23 +214,33 @@ Patch1025: openssh-9.9p1-non-supported-keys-err-msg.patch Patch1026: openssh-9.9p1-bad-hostkey.patch # https://github.com/openssh/openssh-portable/pull/500 Patch1027: openssh-9.9p1-support-authentication-indicators-in-GSSAPI.patch +# +Patch1028: openssh-9.9p1-fips-gss.patch +#upstream 6432b9f6a216d0f5fb43df500e9bc30bebb3f58b +#upstream 4f14ca8633a2c8c0a1a19165663421f0ab32f6ab +Patch1029: openssh-9.9p1-scp-traversing.patch +Patch1030: openssh-9.9p1-canonical-match-user.patch +Patch1031: openssh-10.0-mlkem-nist.patch # upstream 35d5917652106aede47621bb3f64044604164043 -Patch1028: openssh-9.9p1-reject-cntrl-chars-in-username.patch +Patch1032: openssh-9.9p1-reject-cntrl-chars-in-username.patch # upstream 43b3bff47bb029f2299bacb6a36057981b39fdb0 -Patch1029: openssh-9.9p1-reject-null-char-in-url-string.patch +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 # upstream 487e8ac146f7d6616f65c125d5edb210519b833a -Patch1030: openssh-9.9p1-scp-clear-setuid.patch +Patch1036: openssh-9.9p1-scp-clear-setuid.patch # upstream c805b97b67c774e0bf922ffb29dfbcda9d7b5add -Patch1031: openssh-9.9p1-mux-askpass-check.patch +Patch1037: openssh-9.9p1-mux-askpass-check.patch # upstream fd1c7e131f331942d20f42f31e79912d570081fa -Patch1032: openssh-9.9p1-ecdsa-incomplete-application.patch +Patch1038: openssh-9.9p1-ecdsa-incomplete-application.patch # upstream fd1c7e131f331942d20f42f31e79912d570081fa -Patch1033: openssh-9.9p1-authorized-keys-principles-option.patch +Patch1039: openssh-9.9p1-authorized-keys-principles-option.patch # upstream 76685c9b09a66435cd2ad8373246adf1c53976d3 # upstream 0a0ef4515361143cad21afa072319823854c1cf6 # upstream 607bd871ec029e9aa22e632a22547250f3cae223 # upstream 1340d3fa8e4bb122906a82159c4c9b91584d65ce -Patch1034: openssh-9.9p1-proxyjump-username-validity-checks.patch +Patch1040: openssh-9.9p1-proxyjump-username-validity-checks.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 @@ -423,13 +433,19 @@ gpgv2 --quiet --keyring %{SOURCE3} %{SOURCE1} %{SOURCE0} %patch -P 1025 -p1 -b .non-supported-keys-err-msg %patch -P 1026 -p1 -b .bad-hostkey %patch -P 1027 -p1 -b .gss-indicators -%patch -P 1028 -p1 -b .reject-cntrl-chars-in-username -%patch -P 1029 -p1 -b .reject-null-char-in-url-string -%patch -P 1030 -p1 -b .scp-clear-setuid -%patch -P 1031 -p1 -b .mux-askpass-check -%patch -P 1032 -p1 -b .ecdsa-incomplete-application -%patch -P 1033 -p1 -b .authorized-keys-principles-option -%patch -P 1034 -p1 -b .proxyjump-username-validity-checks +%patch -P 1028 -p1 -b .gss-fips +%patch -P 1029 -p1 -b .scp-traversing +%patch -P 1030 -p1 -b .canonical-match-user +%patch -P 1031 -p1 -b .mlkem-nist +%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 1036 -p1 -b .scp-clear-setuid +%patch -P 1037 -p1 -b .mux-askpass-check +%patch -P 1038 -p1 -b .ecdsa-incomplete-application +%patch -P 1039 -p1 -b .authorized-keys-principles-option +%patch -P 1040 -p1 -b .proxyjump-username-validity-checks %patch -P 100 -p1 -b .coverity @@ -710,32 +726,69 @@ test -f %{sysconfig_anaconda} && \ %attr(0755,root,root) %{_libdir}/sshtest/sk-dummy.so %changelog -* Mon Apr 13 2026 Zoltan Fridrich - 9.9p1-14 +* Mon Apr 13 2026 Zoltan Fridrich - 9.9p1-23 - CVE-2026-35385: Fix privilege escalation via scp legacy protocol when not in preserving file mode - Resolves: RHEL-164738 + Resolves: RHEL-164739 - CVE-2026-35388: Add connection multiplexing confirmation for proxy-mode multiplexing sessions - Resolves: RHEL-166237 + Resolves: RHEL-166238 - CVE-2026-35387: Fix incomplete application of PubkeyAcceptedAlgorithms and HostbasedAcceptedAlgorithms with regard to ECDSA keys - Resolves: RHEL-166221 + Resolves: RHEL-166222 - CVE-2026-35414: Fix mishandling of authorized_keys principals option - Resolves: RHEL-166189 + Resolves: RHEL-166190 - CVE-2026-35386: Add validation rules to usernames and hostnames set for ProxyJump/-J on the commandline - Resolves: RHEL-166205 + Resolves: RHEL-166206 -* Mon Mar 16 2026 Zoltan Fridrich - 9.9p1-13 +* Thu Mar 26 2026 Zoltan Fridrich - 9.9p1-22 +- Version bump + +* Mon Mar 16 2026 Zoltan Fridrich - 9.9p1-21 - CVE-2026-3497: Fix information disclosure or denial of service due to uninitialized variables in gssapi-keyex - Resolves: RHEL-155811 + Resolves: RHEL-155812 -* Mon Dec 08 2025 Zoltan Fridrich - 9.9p1-12 +* Wed Feb 25 2026 Dmitry Belyavskiy - 9.9p1-20 +- Provide a way to skip unsupported ML-KEM hybrid algorithms in FIPS mode + Resolves: RHEL-151579 + +* 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 + +* Fri Dec 05 2025 Zoltan Fridrich - 9.9p1-17 - CVE-2025-61984: Reject usernames with control characters - Resolves: RHEL-128397 + Resolves: RHEL-128399 - CVE-2025-61985: Reject URL-strings with NULL characters - Resolves: RHEL-128387 + Resolves: RHEL-128388 + +* Mon Nov 03 2025 Dmitry Belyavskiy - 9.9p1-16 +- Implement mlkem768nistp256-sha256 and mlkem1024nistp384-sha384 KEX methods + Resolves: RHEL-70824 + +* Mon Oct 27 2025 Zoltan Fridrich - 9.9p1-15 +- Fix implicit destination path selection when source path ends with ".." + Resolves: RHEL-118406 +- Canonicalize username when matching a user + Resolves: RHEL-101440 + +* Mon Sep 15 2025 Dmitry Belyavskiy - 9.9p1-14 +- Relax GSS Kex restriction in FIPS mode + Resolves: RHEL-91181 + +* Mon Sep 01 2025 Dmitry Belyavskiy - 9.9p1-13 +- Allow non-cryptographical use of MD5 in GSS Kex in FIPS mode + Related: RHEL-91181 + +* Mon Aug 04 2025 Dmitry Belyavskiy - 9.9p1-12 +- Relax GSS Kex restriction in FIPS mode + Resolves: RHEL-91181 * Fri Jul 18 2025 Zoltan Fridrich - 9.9p1-11 - Move the redhat help message to debug1 log level