import CS openssh-9.9p1-20.el10

This commit is contained in:
AlmaLinux RelEng Bot 2026-04-07 06:33:05 -04:00
parent 945a3f7589
commit 1269caa957
18 changed files with 3399 additions and 123 deletions

View File

@ -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 <openssl/fips.h>
/* 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 <openssl/crypto.h>
#include <openssl/fips.h>
#include <openssl/evp.h>
+#include <openssl/err.h>
#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 <openssl/bn.h>
+#include <openssl/ec.h>
#include <openssl/err.h>
#include <openssl/evp.h>
+#include <openssl/fips.h>
#include <stdio.h>
+#define FIPS_FALLBACK_PROPQ "provider=default,-fips"
+
static int
mlkem_keypair_gen(const char *algname, unsigned char *pubkeybuf, size_t pubkey_size,
unsigned char *privkeybuf, size_t privkey_size)
@@ -62,6 +67,14 @@ mlkem_keypair_gen(const char *algname, unsigned char *pubkeybuf, size_t pubkey_s
size_t got_pub_size = pubkey_size, got_priv_size = privkey_size;
ctx = EVP_PKEY_CTX_new_from_name(NULL, algname, NULL);
+
+ if (ctx == NULL && FIPS_mode()) {
+ /* We have filtered x25519 + ML-KEM in FIPS mode earlier
+ * so if we are in FIPS mode and ML-KEM is not available with default propq,
+ * we can fetch it from the default provider */
+ ctx = EVP_PKEY_CTX_new_from_name(NULL, algname, FIPS_FALLBACK_PROPQ);
+ }
+
if (ctx == NULL) {
ret = SSH_ERR_LIBCRYPTO_ERROR;
goto err;
@@ -121,6 +134,7 @@ mlkem_encap_secret(const char *mlkem_alg, const u_char *pubkeybuf, u_char *secre
EVP_PKEY_CTX *ctx = NULL;
int r = SSH_ERR_INTERNAL_ERROR;
size_t outlen, expected_outlen, publen, secretlen = crypto_kem_mlkem768_BYTES;
+ int fips_fallback = 0;
if (strcmp(mlkem_alg, "mlkem768") == 0) {
outlen = crypto_kem_mlkem768_CIPHERTEXTBYTES;
@@ -135,12 +149,17 @@ mlkem_encap_secret(const char *mlkem_alg, const u_char *pubkeybuf, u_char *secre
pkey = EVP_PKEY_new_raw_public_key_ex(NULL, mlkem_alg, NULL,
pubkeybuf, publen);
+ if (pkey == NULL && FIPS_mode()) {
+ pkey = EVP_PKEY_new_raw_public_key_ex(NULL, mlkem_alg, FIPS_FALLBACK_PROPQ,
+ pubkeybuf, publen);
+ fips_fallback = 1;
+ }
if (pkey == NULL) {
r = SSH_ERR_LIBCRYPTO_ERROR;
goto err;
}
- ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
+ ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, fips_fallback ? FIPS_FALLBACK_PROPQ : NULL);
if (ctx == NULL
|| EVP_PKEY_encapsulate_init(ctx, NULL) <= 0
|| EVP_PKEY_encapsulate(ctx, out, &expected_outlen, secret, &secretlen) <= 0
@@ -182,15 +201,21 @@ mlkem_decap_secret(const char *algname,
EVP_PKEY_CTX *ctx = NULL;
int r = SSH_ERR_INTERNAL_ERROR;
size_t secretlen = crypto_kem_mlkem768_BYTES;
+ int fips_fallback = 0;
pkey = EVP_PKEY_new_raw_private_key_ex(NULL, algname,
NULL, privkeybuf, privkey_len);
+ if (pkey == NULL && FIPS_mode()) {
+ pkey = EVP_PKEY_new_raw_private_key_ex(NULL, algname,
+ FIPS_FALLBACK_PROPQ, privkeybuf, privkey_len);
+ fips_fallback = 1;
+ }
if (pkey == NULL) {
r = SSH_ERR_LIBCRYPTO_ERROR;
goto err;
}
- ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
+ ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, fips_fallback ? FIPS_FALLBACK_PROPQ : NULL);
if (ctx == NULL
|| EVP_PKEY_decapsulate_init(ctx, NULL) <= 0
|| EVP_PKEY_decapsulate(ctx, secret, &secretlen, wrapped, wrapped_len) <= 0
@@ -744,6 +769,48 @@ nist_pkey_keygen(size_t pub_key_len)
return pkey;
}
+static size_t decompress_pub_key(void *pub, size_t compressed_len, size_t decompressed_len)
+{
+ EC_GROUP *group = NULL;
+ EC_POINT *point = NULL;
+ BN_CTX *ctx = NULL;
+ size_t len = 0;
+ int group_nid = NID_undef;
+
+ switch (compressed_len) {
+ case NIST_P256_COMPRESSED_LEN:
+ group_nid = NID_X9_62_prime256v1;
+ break;
+ case NIST_P384_COMPRESSED_LEN:
+ group_nid = NID_secp384r1;
+ break;
+ default:
+ return 0;
+ break;
+ }
+
+ ctx = BN_CTX_new();
+ group = EC_GROUP_new_by_curve_name(group_nid);
+ if (ctx == NULL || group == NULL)
+ goto err;
+
+ point = EC_POINT_new(group);
+ if (point == NULL)
+ goto err;
+
+ if (!EC_POINT_oct2point(group, point, pub, compressed_len, ctx))
+ goto err;
+
+ len = EC_POINT_point2oct(group, point, POINT_CONVERSION_UNCOMPRESSED, pub, decompressed_len, ctx);
+
+err:
+ EC_POINT_free(point);
+ EC_GROUP_free(group);
+ BN_CTX_free(ctx);
+
+ return len;
+}
+
static int
get_uncompressed_ec_pubkey(EVP_PKEY *pkey, unsigned char *buf, size_t buf_len)
{
@@ -757,16 +824,25 @@ get_uncompressed_ec_pubkey(EVP_PKEY *pkey, unsigned char *buf, size_t buf_len)
if (EVP_PKEY_set_params(pkey, params) <= 0
|| EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY,
- NULL, 0, &required_len) <= 0) {
+ buf, buf_len, &required_len) <= 0) {
return SSH_ERR_LIBCRYPTO_ERROR;
}
- if (required_len != buf_len)
- return SSH_ERR_INTERNAL_ERROR;
-
- if (EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY,
- buf, required_len, &out_len) <= 0) {
- return SSH_ERR_LIBCRYPTO_ERROR;
+ if (required_len != buf_len) {
+ /* Red Hat certified FIPS provider ignores OSSL_PKEY_PARAM_EC_POINT_CONVERSION_FORMAT
+ * We may have to perform the conversion manually */
+ if (len2curve_name(required_len) == len2curve_name(buf_len)) {
+ out_len = decompress_pub_key(buf, required_len, buf_len);
+ if (out_len != buf_len) {
+ debug_f("Error decompressing the compressed public key");
+ return SSH_ERR_LIBCRYPTO_ERROR;
+ } else {
+ return 0;
+ }
+ } else {
+ debug_f("Unexpected length of uncompressed public key: expected %d, got %d", buf_len, required_len);
+ return SSH_ERR_LIBCRYPTO_ERROR;
+ }
}
return 0;
diff --git a/myproposal.h b/myproposal.h
index 3e0ec6826..007347fc3 100644
--- a/myproposal.h
+++ b/myproposal.h
@@ -26,6 +26,8 @@
"sntrup761x25519-sha512," \
"sntrup761x25519-sha512@openssh.com," \
"mlkem768x25519-sha256," \
+ "mlkem768nistp256-sha256," \
+ "mlkem1024nistp384-sha384," \
"curve25519-sha256," \
"curve25519-sha256@libssh.org," \
"ecdh-sha2-nistp256," \
@@ -97,6 +99,8 @@
"aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se," \
"aes128-gcm@openssh.com,aes256-gcm@openssh.com"
#define KEX_DEFAULT_KEX_FIPS \
+ "mlkem768nistp256-sha256," \
+ "mlkem1024nistp384-sha384," \
"ecdh-sha2-nistp256," \
"ecdh-sha2-nistp384," \
"ecdh-sha2-nistp521," \

View File

@ -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 <stdio.h>
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;

View File

@ -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 <vis.h>
#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 <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
+#include <openssl/fips.h>
#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

View File

@ -1,38 +1,31 @@
diff -up openssh-8.7p1/ssh.c.xxx openssh-8.7p1/ssh.c
--- openssh-8.7p1/ssh.c.xxx 2024-09-11 14:24:06.711088878 +0200
+++ openssh-8.7p1/ssh.c 2024-09-11 14:35:12.883765718 +0200
@@ -175,6 +175,16 @@ extern int muxserver_sock;
diff --color -ruNp a/ssh.c b/ssh.c
--- a/ssh.c 2025-07-17 14:08:37.880551301 +0200
+++ b/ssh.c 2025-07-17 15:02:09.224627879 +0200
@@ -175,6 +175,17 @@ static int forward_confirms_pending = -1
extern int muxserver_sock;
extern u_int muxclient_command;
/* Prints a help message to the user. This function never returns. */
+static void
+redhat_usage(void)
+redhat_help_message(void)
+{
+ if(isatty(fileno(stderr))) {
+ fprintf(stderr,
+"\nYou can find some explanations for typical errors at this link:\n"
+" https://red.ht/support_rhel_ssh\n"
+ );
+ }
+ if (log_level_get() >= SYSLOG_LEVEL_DEBUG1 &&
+ isatty(fileno(stderr))) {
+ fprintf(stderr,
+"\nThe link below provides guidance on resolving common SSH errors:\n"
+" https://red.ht/support_rhel_ssh\n");
+ }
+}
+
/* Prints a help message to the user. This function never returns. */
static void
usage(void)
@@ -188,6 +196,7 @@ usage(void)
" destination [command [argument ...]]\n"
" ssh [-Q query_option]\n"
);
+ redhat_usage();
exit(255);
}
@@ -1609,8 +1618,10 @@ main(int ac, char **av)
@@ -1648,8 +1659,10 @@ main(int ac, char **av)
/* Open a connection to the remote host. */
if (ssh_connect(ssh, host, options.host_arg, addrs, &hostaddr,
options.port, options.connection_attempts,
- &timeout_ms, options.tcp_keep_alive) != 0)
+ &timeout_ms, options.tcp_keep_alive) != 0) {
+ redhat_usage();
+ redhat_help_message();
exit(255);
+ }

View File

@ -0,0 +1,68 @@
diff --color -ruNp a/hostfile.c b/hostfile.c
--- a/hostfile.c 2024-09-20 00:20:48.000000000 +0200
+++ b/hostfile.c 2025-04-30 15:52:02.792091018 +0200
@@ -63,6 +63,14 @@
#include "hmac.h"
#include "sshbuf.h"
+static int required_rsa_size = SSH_RSA_MINIMUM_MODULUS_SIZE;
+
+void
+hostfile_set_minimum_rsa_size(int size)
+{
+ required_rsa_size = size;
+}
+
/* XXX hmac is too easy to dictionary attack; use bcrypt? */
static int
@@ -233,6 +241,7 @@ record_hostkey(struct hostkey_foreach_li
struct load_callback_ctx *ctx = (struct load_callback_ctx *)_ctx;
struct hostkeys *hostkeys = ctx->hostkeys;
struct hostkey_entry *tmp;
+ int r = 0;
if (l->status == HKF_STATUS_INVALID) {
/* XXX make this verbose() in the future */
@@ -241,6 +250,12 @@ record_hostkey(struct hostkey_foreach_li
return 0;
}
+ if ((r = sshkey_check_rsa_length(l->key, required_rsa_size)) != 0) {
+ debug2_f("%s:%ld: ignoring hostkey: %s",
+ l->path, l->linenum, ssh_err(r));
+ return 0;
+ }
+
debug3_f("found %skey type %s in file %s:%lu",
l->marker == MRK_NONE ? "" :
(l->marker == MRK_CA ? "ca " : "revoked "),
diff --color -ruNp a/hostfile.h b/hostfile.h
--- a/hostfile.h 2024-09-20 00:20:48.000000000 +0200
+++ b/hostfile.h 2025-04-30 15:17:44.789206468 +0200
@@ -119,5 +119,6 @@ int hostkeys_foreach_file(const char *pa
const char *host, const char *ip, u_int options, u_int note);
void hostfile_create_user_ssh_dir(const char *, int);
+void hostfile_set_minimum_rsa_size(int);
#endif
diff --color -ruNp a/ssh.c b/ssh.c
--- a/ssh.c 2025-04-29 15:40:27.916735894 +0200
+++ b/ssh.c 2025-04-30 15:19:48.856855308 +0200
@@ -109,6 +109,7 @@
#include "ssherr.h"
#include "myproposal.h"
#include "utf8.h"
+#include "hostfile.h"
#ifdef ENABLE_PKCS11
#include "ssh-pkcs11.h"
@@ -1395,6 +1396,7 @@ main(int ac, char **av)
options.update_hostkeys = 0;
}
}
+ hostfile_set_minimum_rsa_size(options.required_rsa_size);
if (options.connection_attempts <= 0)
fatal("Invalid number of ConnectionAttempts");

View File

@ -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

View File

@ -0,0 +1,13 @@
diff --git a/readconf.c b/readconf.c
index 9f559269..777739d6 100644
--- a/readconf.c
+++ b/readconf.c
@@ -3410,6 +3410,8 @@ fmt_intarg(OpCodes code, int val)
switch (code) {
case oAddressFamily:
return fmt_multistate_int(val, multistate_addressfamily);
+ case oCompression:
+ return fmt_multistate_int(val, multistate_compression);
case oVerifyHostKeyDNS:
case oUpdateHostkeys:
return fmt_multistate_int(val, multistate_yesnoask);

View File

@ -0,0 +1,22 @@
diff --color -ruNp a/session.c b/session.c
--- a/session.c 2025-04-29 11:20:59.475107377 +0200
+++ b/session.c 2025-04-29 11:23:16.638538968 +0200
@@ -2284,7 +2284,8 @@ session_auth_agent_req(struct ssh *ssh,
if ((r = sshpkt_get_end(ssh)) != 0)
sshpkt_fatal(ssh, r, "%s: parse packet", __func__);
if (!auth_opts->permit_agent_forwarding_flag ||
- !options.allow_agent_forwarding) {
+ !options.allow_agent_forwarding ||
+ options.disable_forwarding) {
debug_f("agent forwarding disabled");
return 0;
}
@@ -2709,7 +2710,7 @@ session_setup_x11fwd(struct ssh *ssh, Se
ssh_packet_send_debug(ssh, "X11 forwarding disabled by key options.");
return 0;
}
- if (!options.x11_forwarding) {
+ if (!options.x11_forwarding || options.disable_forwarding) {
debug("X11 forwarding disabled in server configuration file.");
return 0;
}

View File

@ -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);

View File

@ -0,0 +1,12 @@
diff --color -ruNp a/sshkey.c b/sshkey.c
--- a/sshkey.c 2025-04-29 11:20:59.484832762 +0200
+++ b/sshkey.c 2025-04-29 11:28:32.349323029 +0200
@@ -3599,6 +3599,8 @@ translate_libcrypto_error(unsigned long
}
case ERR_LIB_ASN1:
return SSH_ERR_INVALID_FORMAT;
+ case ERR_LIB_OSSL_DECODER:
+ return SSH_ERR_INVALID_FORMAT;
}
return SSH_ERR_LIBCRYPTO_ERROR;
}

View File

@ -0,0 +1,385 @@
diff --git a/kex-names.c b/kex-names.c
index ec840c1f..c0d3be11 100644
--- a/kex-names.c
+++ b/kex-names.c
@@ -90,6 +90,19 @@ static const struct kexalg kexalgs[] = {
{ NULL, 0, -1, -1},
};
+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_free(mlkem768);
+ }
+
+ return is_fetched;
+}
+
static char *
kex_alg_list_internal(char sep, const struct kexalg *algs)
{
@@ -98,6 +111,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
+ && !is_mlkem768_available())
+ continue;
if (ret != NULL)
ret[rlen++] = sep;
nlen = strlen(k->name);
@@ -117,6 +133,10 @@ kex_alg_by_name(const char *name)
{
const struct kexalg *k;
+ if (strcmp(name, KEX_MLKEM768X25519_SHA256) == 0
+ && !is_mlkem768_available())
+ return NULL;
+
for (k = kexalgs; k->name != NULL; k++) {
if (strcmp(k->name, name) == 0)
return k;
diff --git a/kexmlkem768x25519.c b/kexmlkem768x25519.c
index 2b5d3960..670049dc 100644
--- a/kexmlkem768x25519.c
+++ b/kexmlkem768x25519.c
@@ -48,10 +48,127 @@
#ifdef USE_MLKEM768X25519
#include "libcrux_mlkem768_sha3.h"
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <stdio.h>
+
+static int
+mlkem768_keypair_gen(unsigned char *pubkeybuf, unsigned char *privkeybuf)
+{
+ 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;
+
+ ctx = EVP_PKEY_CTX_new_from_name(NULL, "mlkem768", NULL);
+ if (ctx == NULL) {
+ ret = SSH_ERR_LIBCRYPTO_ERROR;
+ goto err;
+ }
+
+ if (EVP_PKEY_keygen_init(ctx) <= 0
+ || EVP_PKEY_keygen(ctx, &pkey) <= 0) {
+ ret = SSH_ERR_LIBCRYPTO_ERROR;
+ 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) {
+ ret = SSH_ERR_LIBCRYPTO_ERROR;
+ goto err;
+ }
+
+ if (privkey_size != crypto_kem_mlkem768_SECRETKEYBYTES
+ || pubkey_size != crypto_kem_mlkem768_PUBLICKEYBYTES) {
+ ret = SSH_ERR_LIBCRYPTO_ERROR;
+ goto err;
+ }
+ ret = 0;
+
+ err:
+ EVP_PKEY_free(pkey);
+ EVP_PKEY_CTX_free(ctx);
+ if (ret == SSH_ERR_LIBCRYPTO_ERROR)
+ ERR_print_errors_fp(stderr);
+ return ret;
+}
+
+static int
+mlkem768_encap_secret(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;
+
+ pkey = EVP_PKEY_new_raw_public_key_ex(NULL, "mlkem768", NULL,
+ pubkeybuf, crypto_kem_mlkem768_PUBLICKEYBYTES);
+ if (pkey == NULL) {
+ r = SSH_ERR_LIBCRYPTO_ERROR;
+ goto err;
+ }
+
+ 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
+ || secretlen != crypto_kem_mlkem768_BYTES
+ || outlen != crypto_kem_mlkem768_CIPHERTEXTBYTES) {
+ r = SSH_ERR_LIBCRYPTO_ERROR;
+ goto err;
+ }
+ r = 0;
+
+ err:
+ EVP_PKEY_free(pkey);
+ EVP_PKEY_CTX_free(ctx);
+ if (r == SSH_ERR_LIBCRYPTO_ERROR)
+ ERR_print_errors_fp(stderr);
+
+ return r;
+}
+
+static int
+mlkem768_decap_secret(const u_char *privkeybuf, const u_char *wrapped, 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;
+
+ pkey = EVP_PKEY_new_raw_private_key_ex(NULL, "mlkem768", NULL,
+ privkeybuf, crypto_kem_mlkem768_SECRETKEYBYTES);
+ if (pkey == NULL) {
+ r = SSH_ERR_LIBCRYPTO_ERROR;
+ goto err;
+ }
+
+ 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
+ || secretlen != crypto_kem_mlkem768_BYTES) {
+ r = SSH_ERR_LIBCRYPTO_ERROR;
+ goto err;
+ }
+ r = 0;
+
+ err:
+ EVP_PKEY_free(pkey);
+ EVP_PKEY_CTX_free(ctx);
+
+ if (r == SSH_ERR_LIBCRYPTO_ERROR)
+ ERR_print_errors_fp(stderr);
+
+ return r;
+}
int
kex_kem_mlkem768x25519_keypair(struct kex *kex)
{
+#if 0
struct sshbuf *buf = NULL;
u_char rnd[LIBCRUX_ML_KEM_KEY_PAIR_PRNG_LEN], *cp = NULL;
size_t need;
@@ -86,6 +203,36 @@ kex_kem_mlkem768x25519_keypair(struct kex *kex)
explicit_bzero(rnd, sizeof(rnd));
sshbuf_free(buf);
return r;
+#else
+ struct sshbuf *buf = NULL;
+ u_char *cp = NULL;
+ size_t need;
+ int r = SSH_ERR_INTERNAL_ERROR;
+
+ if ((buf = sshbuf_new()) == NULL)
+ return SSH_ERR_ALLOC_FAIL;
+ need = crypto_kem_mlkem768_PUBLICKEYBYTES + CURVE25519_SIZE;
+ if ((r = sshbuf_reserve(buf, need, &cp)) != 0)
+ goto out;
+ if ((r = mlkem768_keypair_gen(cp, kex->mlkem768_client_key)) != 0)
+ goto out;
+#ifdef DEBUG_KEXECDH
+ dump_digest("client public key mlkem768:", cp,
+ crypto_kem_mlkem768_PUBLICKEYBYTES);
+#endif
+ cp += crypto_kem_mlkem768_PUBLICKEYBYTES;
+ kexc25519_keygen(kex->c25519_client_key, cp);
+#ifdef DEBUG_KEXECDH
+ dump_digest("client public key c25519:", cp, CURVE25519_SIZE);
+#endif
+ /* success */
+ r = 0;
+ kex->client_pub = buf;
+ buf = NULL;
+ out:
+ sshbuf_free(buf);
+ return r;
+#endif
}
int
@@ -93,6 +240,7 @@ kex_kem_mlkem768x25519_enc(struct kex *kex,
const struct sshbuf *client_blob, struct sshbuf **server_blobp,
struct sshbuf **shared_secretp)
{
+#if 0
struct sshbuf *server_blob = NULL;
struct sshbuf *buf = NULL;
const u_char *client_pub;
@@ -185,12 +333,97 @@ kex_kem_mlkem768x25519_enc(struct kex *kex,
sshbuf_free(server_blob);
sshbuf_free(buf);
return r;
+#else
+ struct sshbuf *server_blob = NULL;
+ struct sshbuf *buf = NULL;
+ const u_char *client_pub;
+ u_char server_pub[CURVE25519_SIZE], server_key[CURVE25519_SIZE];
+ u_char hash[SSH_DIGEST_MAX_LENGTH];
+ size_t need;
+ int r = SSH_ERR_INTERNAL_ERROR;
+ struct libcrux_mlkem768_enc_result enc; /* FIXME */
+
+ *server_blobp = NULL;
+ *shared_secretp = NULL;
+
+ /* client_blob contains both KEM and ECDH client pubkeys */
+ need = crypto_kem_mlkem768_PUBLICKEYBYTES + CURVE25519_SIZE;
+ if (sshbuf_len(client_blob) != need) {
+ r = SSH_ERR_SIGNATURE_INVALID;
+ goto out;
+ }
+ client_pub = sshbuf_ptr(client_blob);
+#ifdef DEBUG_KEXECDH
+ dump_digest("client public key mlkem768:", client_pub,
+ crypto_kem_mlkem768_PUBLICKEYBYTES);
+ dump_digest("client public key 25519:",
+ client_pub + crypto_kem_mlkem768_PUBLICKEYBYTES,
+ CURVE25519_SIZE);
+#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;
+ }
+ if (mlkem768_encap_secret(client_pub, enc.snd, enc.fst.value) != 0)
+ goto out;
+
+ /* generate ECDH key pair, store server pubkey after ciphertext */
+ kexc25519_keygen(server_key, server_pub);
+ if ((r = sshbuf_put(buf, enc.snd, sizeof(enc.snd))) != 0 ||
+ (r = sshbuf_put(server_blob, enc.fst.value, sizeof(enc.fst.value))) != 0 ||
+ (r = sshbuf_put(server_blob, server_pub, sizeof(server_pub))) != 0)
+ goto out;
+ /* append ECDH shared key */
+ client_pub += crypto_kem_mlkem768_PUBLICKEYBYTES;
+ if ((r = kexc25519_shared_key_ext(server_key, client_pub, buf, 1)) < 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 key 25519:", server_pub, CURVE25519_SIZE);
+ dump_digest("server cipher text:",
+ enc.fst.value, sizeof(enc.fst.value));
+ dump_digest("server kem key:", enc.snd, sizeof(enc.snd));
+ 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));
+ explicit_bzero(server_key, sizeof(server_key));
+ explicit_bzero(&enc, sizeof(enc));
+ sshbuf_free(server_blob);
+ sshbuf_free(buf);
+ return r;
+#endif
}
int
kex_kem_mlkem768x25519_dec(struct kex *kex,
const struct sshbuf *server_blob, struct sshbuf **shared_secretp)
{
+#if 0
struct sshbuf *buf = NULL;
u_char mlkem_key[crypto_kem_mlkem768_BYTES];
const u_char *ciphertext, *server_pub;
@@ -258,6 +491,64 @@ kex_kem_mlkem768x25519_dec(struct kex *kex,
explicit_bzero(mlkem_key, sizeof(mlkem_key));
sshbuf_free(buf);
return r;
+#else
+ struct sshbuf *buf = NULL;
+ const u_char *ciphertext, *server_pub;
+ u_char hash[SSH_DIGEST_MAX_LENGTH];
+ u_char decap[crypto_kem_mlkem768_BYTES];
+ size_t need;
+ int r;
+
+ *shared_secretp = NULL;
+
+ need = crypto_kem_mlkem768_CIPHERTEXTBYTES + CURVE25519_SIZE;
+ if (sshbuf_len(server_blob) != need) {
+ r = SSH_ERR_SIGNATURE_INVALID;
+ goto out;
+ }
+ ciphertext = sshbuf_ptr(server_blob);
+ server_pub = ciphertext + crypto_kem_mlkem768_CIPHERTEXTBYTES;
+ /* 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, crypto_kem_mlkem768_CIPHERTEXTBYTES);
+ dump_digest("server public key c25519:", server_pub, CURVE25519_SIZE);
+#endif
+ if ((r = mlkem768_decap_secret(kex->mlkem768_client_key, ciphertext, decap)) != 0)
+ goto out;
+ if ((r = sshbuf_put(buf, decap, sizeof(decap))) != 0)
+ goto out;
+ if ((r = kexc25519_shared_key_ext(kex->c25519_client_key, server_pub,
+ buf, 1)) < 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;
+#endif
}
#else /* USE_MLKEM768X25519 */
int

View File

@ -0,0 +1,59 @@
diff --color -ruNp a/ssh.c b/ssh.c
--- a/ssh.c 2025-12-03 15:22:36.754555231 +0100
+++ b/ssh.c 2025-12-03 16:12:16.715320349 +0100
@@ -662,6 +662,8 @@ valid_ruser(const char *s)
if (*s == '-')
return 0;
for (i = 0; s[i] != 0; i++) {
+ if (iscntrl((u_char)s[i]))
+ return 0;
if (strchr("'`\";&<>|(){}", s[i]) != NULL)
return 0;
/* Disallow '-' after whitespace */
@@ -683,6 +685,7 @@ main(int ac, char **av)
struct ssh *ssh = NULL;
int i, r, opt, exit_status, use_syslog, direct, timeout_ms;
int was_addr, config_test = 0, opt_terminated = 0, want_final_pass = 0;
+ int user_on_commandline = 0;
char *p, *cp, *line, *argv0, *logfile;
char cname[NI_MAXHOST], thishost[NI_MAXHOST];
struct stat st;
@@ -1039,8 +1042,10 @@ main(int ac, char **av)
}
break;
case 'l':
- if (options.user == NULL)
+ if (options.user == NULL) {
options.user = optarg;
+ user_on_commandline = 1;
+ }
break;
case 'L':
@@ -1143,6 +1148,7 @@ main(int ac, char **av)
if (options.user == NULL) {
options.user = tuser;
tuser = NULL;
+ user_on_commandline = 1;
}
free(tuser);
if (options.port == -1 && tport != -1)
@@ -1157,6 +1163,7 @@ main(int ac, char **av)
if (options.user == NULL) {
options.user = p;
p = NULL;
+ user_on_commandline = 1;
}
*cp++ = '\0';
host = xstrdup(cp);
@@ -1459,6 +1466,10 @@ main(int ac, char **av)
cinfo->locuser = xstrdup(pw->pw_name);
cinfo->jmphost = xstrdup(options.jump_host == NULL ?
"" : options.jump_host);
+
+ if (user_on_commandline && !valid_ruser(options.user))
+ fatal("remote username contains invalid characters");
+
cinfo->conn_hash_hex = ssh_connection_hash(cinfo->thishost,
cinfo->remhost, cinfo->portstr, cinfo->remuser, cinfo->jmphost);

View File

@ -0,0 +1,24 @@
diff --color -ruNp a/misc.c b/misc.c
--- a/misc.c 2025-12-03 16:19:11.255135131 +0100
+++ b/misc.c 2025-12-03 16:21:53.769590836 +0100
@@ -998,7 +998,7 @@ urldecode(const char *src)
size_t srclen;
if ((srclen = strlen(src)) >= SIZE_MAX)
- fatal_f("input too large");
+ return NULL;
ret = xmalloc(srclen + 1);
for (dst = ret; *src != '\0'; src++) {
switch (*src) {
@@ -1006,9 +1006,10 @@ urldecode(const char *src)
*dst++ = ' ';
break;
case '%':
+ /* note: don't allow \0 characters */
if (!isxdigit((unsigned char)src[1]) ||
!isxdigit((unsigned char)src[2]) ||
- (ch = hexchar(src + 1)) == -1) {
+ (ch = hexchar(src + 1)) == -1 || ch == 0) {
free(ret);
return NULL;
}

View File

@ -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;

View File

@ -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

View File

@ -0,0 +1,450 @@
From 5d5a66e96ad03132f65371070f4fa475f10207d9 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Mon, 10 Jun 2024 23:00:03 +0300
Subject: [PATCH] support authentication indicators in GSSAPI
RFC 6680 defines a set of GSSAPI extensions to handle attributes
associated with the GSSAPI names. MIT Kerberos and FreeIPA use
name attributes to add information about pre-authentication methods used
to acquire the initial Kerberos ticket. The attribute 'auth-indicators'
may contain list of strings that KDC has associated with the ticket
issuance process.
Use authentication indicators to authorise or deny access to SSH server.
GSSAPIIndicators setting allows to specify a list of possible indicators
that a Kerberos ticket presented must or must not contain. More details
on the syntax are provided in sshd_config(5) man page.
Fixes: https://bugzilla.mindrot.org/show_bug.cgi?id=2696
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
---
configure.ac | 1 +
gss-serv-krb5.c | 64 +++++++++++++++++++++++++++---
gss-serv.c | 103 +++++++++++++++++++++++++++++++++++++++++++++++-
servconf.c | 15 ++++++-
servconf.h | 2 +
ssh-gss.h | 7 ++++
sshd_config.5 | 44 +++++++++++++++++++++
7 files changed, 228 insertions(+), 8 deletions(-)
diff --git a/configure.ac b/configure.ac
index d92a85809..2cbe20bf3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -5004,6 +5004,7 @@ AC_ARG_WITH([kerberos5],
AC_CHECK_HEADERS([gssapi.h gssapi/gssapi.h])
AC_CHECK_HEADERS([gssapi_krb5.h gssapi/gssapi_krb5.h])
AC_CHECK_HEADERS([gssapi_generic.h gssapi/gssapi_generic.h])
+ AC_CHECK_HEADERS([gssapi_ext.h gssapi/gssapi_ext.h])
AC_SEARCH_LIBS([k_hasafs], [kafs], [AC_DEFINE([USE_AFS], [1],
[Define this if you want to use libkafs' AFS support])])
diff --git a/gss-serv-krb5.c b/gss-serv-krb5.c
index 03188d9b3..2c786ef14 100644
--- a/gss-serv-krb5.c
+++ b/gss-serv-krb5.c
@@ -43,6 +43,7 @@
#include "log.h"
#include "misc.h"
#include "servconf.h"
+#include "match.h"
#include "ssh-gss.h"
@@ -87,6 +88,32 @@ ssh_gssapi_krb5_init(void)
return 1;
}
+/* Check if any of the indicators in the Kerberos ticket match
+ * one of indicators in the list of allowed/denied rules.
+ * In case of the match, apply the decision from the rule.
+ * In case of no indicator from the ticket matching the rule, deny
+ */
+
+static int
+ssh_gssapi_check_indicators(ssh_gssapi_client *client, int *matched)
+{
+ int ret;
+ u_int i;
+
+ /* Check indicators */
+ for (i = 0; client->indicators[i] != NULL; i++) {
+ ret = match_pattern_list(client->indicators[i],
+ options.gss_indicators, 1);
+ /* negative or positive match */
+ if (ret != 0) {
+ *matched = i;
+ return ret;
+ }
+ }
+ /* No rule matched */
+ return 0;
+}
+
/* Check if this user is OK to login. This only works with krb5 - other
* GSSAPI mechanisms will need their own.
* Returns true if the user is OK to log in, otherwise returns 0
@@ -193,7 +220,7 @@ static int
ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name)
{
krb5_principal princ;
- int retval;
+ int retval, matched;
const char *errmsg;
int k5login_exists;
@@ -216,17 +243,42 @@ ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name)
if (k5login_exists &&
ssh_krb5_kuserok(krb_context, princ, name, k5login_exists)) {
retval = 1;
- logit("Authorized to %s, krb5 principal %s (krb5_kuserok)",
- name, (char *)client->displayname.value);
+ errmsg = "krb5_kuserok";
} else if (ssh_gssapi_krb5_cmdok(princ, client->exportedname.value,
name, k5login_exists)) {
retval = 1;
- logit("Authorized to %s, krb5 principal %s "
- "(ssh_gssapi_krb5_cmdok)",
- name, (char *)client->displayname.value);
+ errmsg = "ssh_gssapi_krb5_cmdok";
} else
retval = 0;
+ if ((retval == 1) && (options.gss_indicators != NULL)) {
+ /* At this point the configuration enforces presence of indicators
+ * so we drop the authorization result again */
+ retval = 0;
+ if (client->indicators) {
+ matched = -1;
+ retval = ssh_gssapi_check_indicators(client, &matched);
+ if (retval != 0) {
+ retval = (retval == 1);
+ logit("Ticket contains indicator %s, "
+ "krb5 principal %s is %s",
+ client->indicators[matched],
+ (char *)client->displayname.value,
+ retval ? "allowed" : "denied");
+ goto cont;
+ }
+ }
+ if (retval == 0) {
+ logit("GSSAPI authentication indicators enforced "
+ "but not matched. krb5 principal %s denied",
+ (char *)client->displayname.value);
+ }
+ }
+cont:
+ if (retval == 1) {
+ logit("Authorized to %s, krb5 principal %s (%s)",
+ name, (char *)client->displayname.value, errmsg);
+ }
krb5_free_principal(krb_context, princ);
return retval;
}
diff --git a/gss-serv.c b/gss-serv.c
index 9d5435eda..5c0491cf1 100644
--- a/gss-serv.c
+++ b/gss-serv.c
@@ -54,7 +54,7 @@ extern ServerOptions options;
static ssh_gssapi_client gssapi_client =
{ GSS_C_EMPTY_BUFFER, GSS_C_EMPTY_BUFFER, GSS_C_NO_CREDENTIAL,
- GSS_C_NO_NAME, NULL, {NULL, NULL, NULL, NULL, NULL}, 0, 0};
+ GSS_C_NO_NAME, NULL, {NULL, NULL, NULL, NULL, NULL}, 0, 0, NULL};
ssh_gssapi_mech gssapi_null_mech =
{ NULL, NULL, {0, NULL}, NULL, NULL, NULL, NULL, NULL};
@@ -296,6 +296,92 @@ ssh_gssapi_parse_ename(Gssctxt *ctx, gss_buffer_t ename, gss_buffer_t name)
return GSS_S_COMPLETE;
}
+
+/* Extract authentication indicators from the Kerberos ticket. Authentication
+ * indicators are GSSAPI name attributes for the name "auth-indicators".
+ * Multiple indicators might be present in the ticket.
+ * Each indicator is a utf8 string. */
+
+#define AUTH_INDICATORS_TAG "auth-indicators"
+
+/* Privileged (called from accept_secure_ctx) */
+static OM_uint32
+ssh_gssapi_getindicators(Gssctxt *ctx, gss_name_t gss_name, ssh_gssapi_client *client)
+{
+ gss_buffer_set_t attrs = GSS_C_NO_BUFFER_SET;
+ gss_buffer_desc value = GSS_C_EMPTY_BUFFER;
+ gss_buffer_desc display_value = GSS_C_EMPTY_BUFFER;
+ int is_mechname, authenticated, complete, more;
+ size_t count, i;
+
+ ctx->major = gss_inquire_name(&ctx->minor, gss_name,
+ &is_mechname, NULL, &attrs);
+ if (ctx->major != GSS_S_COMPLETE) {
+ return (ctx->major);
+ }
+
+ if (attrs == GSS_C_NO_BUFFER_SET) {
+ /* No indicators in the ticket */
+ return (0);
+ }
+
+ count = 0;
+ for (i = 0; i < attrs->count; i++) {
+ /* skip anything but auth-indicators */
+ if (((sizeof(AUTH_INDICATORS_TAG) - 1) != attrs->elements[i].length) ||
+ strncmp(AUTH_INDICATORS_TAG,
+ attrs->elements[i].value,
+ sizeof(AUTH_INDICATORS_TAG) - 1) != 0)
+ continue;
+ count++;
+ }
+
+ if (count == 0) {
+ /* No auth-indicators in the ticket */
+ (void) gss_release_buffer_set(&ctx->minor, &attrs);
+ return (0);
+ }
+
+ client->indicators = recallocarray(NULL, 0, count + 1, sizeof(char*));
+ count = 0;
+ for (i = 0; i < attrs->count; i++) {
+ authenticated = 0;
+ complete = 0;
+ more = -1;
+ /* skip anything but auth-indicators */
+ if (((sizeof(AUTH_INDICATORS_TAG) - 1) != attrs->elements[i].length) ||
+ strncmp(AUTH_INDICATORS_TAG,
+ attrs->elements[i].value,
+ sizeof(AUTH_INDICATORS_TAG) - 1) != 0)
+ continue;
+ /* retrieve all indicators */
+ while (more != 0) {
+ value.value = NULL;
+ display_value.value = NULL;
+ ctx->major = gss_get_name_attribute(&ctx->minor, gss_name,
+ &attrs->elements[i], &authenticated,
+ &complete, &value, &display_value, &more);
+ if (ctx->major != GSS_S_COMPLETE) {
+ goto out;
+ }
+
+ if ((value.value != NULL) && authenticated) {
+ client->indicators[count] = xmalloc(value.length + 1);
+ memcpy(client->indicators[count], value.value, value.length);
+ client->indicators[count][value.length] = '\0';
+ count++;
+ }
+ }
+ }
+
+out:
+ (void) gss_release_buffer(&ctx->minor, &value);
+ (void) gss_release_buffer(&ctx->minor, &display_value);
+ (void) gss_release_buffer_set(&ctx->minor, &attrs);
+ return (ctx->major);
+}
+
+
/* Extract the client details from a given context. This can only reliably
* be called once for a context */
@@ -385,6 +471,12 @@ ssh_gssapi_getclient(Gssctxt *ctx, ssh_gssapi_client *client)
}
gss_release_buffer(&ctx->minor, &ename);
+ /* Retrieve authentication indicators, if they exist */
+ if ((ctx->major = ssh_gssapi_getindicators(ctx,
+ ctx->client, client))) {
+ ssh_gssapi_error(ctx);
+ return (ctx->major);
+ }
/* We can't copy this structure, so we just move the pointer to it */
client->creds = ctx->client_creds;
@@ -447,6 +539,7 @@ int
ssh_gssapi_userok(char *user, struct passwd *pw, int kex)
{
OM_uint32 lmin;
+ size_t i;
(void) kex; /* used in privilege separation */
@@ -465,6 +558,14 @@ ssh_gssapi_userok(char *user, struct passwd *pw, int kex)
gss_release_buffer(&lmin, &gssapi_client.displayname);
gss_release_buffer(&lmin, &gssapi_client.exportedname);
gss_release_cred(&lmin, &gssapi_client.creds);
+
+ if (gssapi_client.indicators != NULL) {
+ for(i = 0; gssapi_client.indicators[i] != NULL; i++) {
+ free(gssapi_client.indicators[i]);
+ }
+ free(gssapi_client.indicators);
+ }
+
explicit_bzero(&gssapi_client,
sizeof(ssh_gssapi_client));
return 0;
diff --git a/servconf.c b/servconf.c
index e7e4ad046..aab653244 100644
--- a/servconf.c
+++ b/servconf.c
@@ -147,6 +147,7 @@ initialize_server_options(ServerOptions *options)
options->gss_strict_acceptor = -1;
options->gss_store_rekey = -1;
options->gss_kex_algorithms = NULL;
+ options->gss_indicators = NULL;
options->use_kuserok = -1;
options->enable_k5users = -1;
options->password_authentication = -1;
@@ -598,7 +599,7 @@ typedef enum {
sPerSourcePenalties, sPerSourcePenaltyExemptList,
sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile,
sGssAuthentication, sGssCleanupCreds, sGssEnablek5users, sGssStrictAcceptor,
- sGssKeyEx, sGssKexAlgorithms, sGssStoreRekey,
+ sGssKeyEx, sGssIndicators, sGssKexAlgorithms, sGssStoreRekey,
sAcceptEnv, sSetEnv, sPermitTunnel,
sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory,
sUsePrivilegeSeparation, sAllowAgentForwarding,
@@ -694,6 +695,7 @@ static struct {
{ "gssapistorecredentialsonrekey", sGssStoreRekey, SSHCFG_GLOBAL },
{ "gssapikexalgorithms", sGssKexAlgorithms, SSHCFG_GLOBAL },
{ "gssapienablek5users", sGssEnablek5users, SSHCFG_ALL },
+ { "gssapiindicators", sGssIndicators, SSHCFG_ALL },
#else
{ "gssapiauthentication", sUnsupported, SSHCFG_ALL },
{ "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL },
@@ -703,6 +705,7 @@ static struct {
{ "gssapistorecredentialsonrekey", sUnsupported, SSHCFG_GLOBAL },
{ "gssapikexalgorithms", sUnsupported, SSHCFG_GLOBAL },
{ "gssapienablek5users", sUnsupported, SSHCFG_ALL },
+ { "gssapiindicators", sUnsupported, SSHCFG_ALL },
#endif
{ "gssusesessionccache", sUnsupported, SSHCFG_GLOBAL },
{ "gssapiusesessioncredcache", sUnsupported, SSHCFG_GLOBAL },
@@ -1730,6 +1733,15 @@ process_server_config_line_depth(ServerOptions *options, char *line,
options->gss_kex_algorithms = xstrdup(arg);
break;
+ case sGssIndicators:
+ arg = argv_next(&ac, &av);
+ if (!arg || *arg == '\0')
+ fatal("%s line %d: %s missing argument.",
+ filename, linenum, keyword);
+ if (options->gss_indicators == NULL)
+ options->gss_indicators = xstrdup(arg);
+ break;
+
case sPasswordAuthentication:
intptr = &options->password_authentication;
goto parse_flag;
@@ -3351,6 +3363,7 @@ dump_config(ServerOptions *o)
dump_cfg_fmtint(sGssStrictAcceptor, o->gss_strict_acceptor);
dump_cfg_fmtint(sGssStoreRekey, o->gss_store_rekey);
dump_cfg_string(sGssKexAlgorithms, o->gss_kex_algorithms);
+ dump_cfg_string(sGssIndicators, o->gss_indicators);
#endif
dump_cfg_fmtint(sPasswordAuthentication, o->password_authentication);
dump_cfg_fmtint(sKbdInteractiveAuthentication,
diff --git a/servconf.h b/servconf.h
index 7c7e5d434..7c41df417 100644
--- a/servconf.h
+++ b/servconf.h
@@ -181,6 +181,7 @@ typedef struct {
char **allow_groups;
u_int num_deny_groups;
char **deny_groups;
+ char *gss_indicators;
u_int num_subsystems;
char **subsystem_name;
@@ -310,6 +311,7 @@ TAILQ_HEAD(include_list, include_item);
M_CP_STROPT(routing_domain); \
M_CP_STROPT(permit_user_env_allowlist); \
M_CP_STROPT(pam_service_name); \
+ M_CP_STROPT(gss_indicators); \
M_CP_STRARRAYOPT(authorized_keys_files, num_authkeys_files); \
M_CP_STRARRAYOPT(allow_users, num_allow_users); \
M_CP_STRARRAYOPT(deny_users, num_deny_users); \
diff --git a/ssh-gss.h b/ssh-gss.h
index a894e23c9..59cf46d47 100644
--- a/ssh-gss.h
+++ b/ssh-gss.h
@@ -34,6 +34,12 @@
#include <gssapi/gssapi.h>
#endif
+#ifdef HAVE_GSSAPI_EXT_H
+#include <gssapi_ext.h>
+#elif defined(HAVE_GSSAPI_GSSAPI_EXT_H)
+#include <gssapi/gssapi_ext.h>
+#endif
+
#ifdef KRB5
# ifndef HEIMDAL
# ifdef HAVE_GSSAPI_GENERIC_H
@@ -107,6 +113,7 @@ typedef struct {
ssh_gssapi_ccache store;
int used;
int updated;
+ char **indicators; /* auth indicators */
} ssh_gssapi_client;
typedef struct ssh_gssapi_mech_struct {
diff --git a/sshd_config.5 b/sshd_config.5
index 583a01cdb..90ab87edd 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -785,6 +785,50 @@ gss-nistp256-sha256-
gss-curve25519-sha256-
.Ed
This option only applies to connections using GSSAPI.
+.It Cm GSSAPIIndicators
+Specifies whether to accept or deny GSSAPI authenticated access if Kerberos
+mechanism is used and Kerberos ticket contains a particular set of
+authentication indicators. The values can be specified as a comma-separated list
+.Cm [!]name1,[!]name2,... .
+When indicator's name is prefixed with !, the authentication indicator 'name'
+will deny access to the system. Otherwise, one of non-negated authentication
+indicators must be present in the Kerberos ticket to allow access. If
+.Cm GSSAPIIndicators
+is defined, a Kerberos ticket that has indicators but does not match the
+policy will get denial. If at least one indicator is configured, whether for
+access or denial, tickets without authentication indicators will be explicitly
+rejected.
+.Pp
+By default systems using MIT Kerberos 1.17 or later will not assign any
+indicators. SPAKE and PKINIT methods add authentication indicators
+to all successful authentications. The SPAKE pre-authentication method is
+preferred over an encrypted timestamp pre-authentication when passwords used to
+authenticate user principals. Kerberos KDCs built with Heimdal Kerberos
+(including Samba AD DC built with Heimdal) do not add authentication
+indicators. However, OpenSSH built against Heimdal Kerberos library is able to
+inquire authentication indicators and thus can be used to check for their presence.
+.Pp
+Indicator name is case-sensitive and depends on the configuration of a
+particular Kerberos deployment. Indicators available in MIT Kerberos and
+FreeIPA environments:
+.Pp
+.Bl -tag -width XXXX -offset indent -compact
+.It Cm hardened
+SPAKE or encrypted timestamp pre-authentication mechanisms in MIT Kerberos and FreeIPA
+.It Cm pkinit
+smartcard or PKCS11 token-based pre-authentication in MIT Kerberos and FreeIPA
+.It Cm radius
+pre-authentication based on a RADIUS server in MIT Kerberos and FreeIPA
+.It Cm otp
+TOTP/HOTP-based two-factor pre-authentication in FreeIPA
+.It Cm idp
+OAuth2-based pre-authentication in FreeIPA using an external identity provider
+and device authorization grant flow
+.It Cm passkey
+FIDO2-based pre-authentication in FreeIPA, using FIDO2 USB and NFC tokens
+.El
+.Pp
+The default is to not use GSSAPI authentication indicators for access decisions.
.It Cm HostbasedAcceptedAlgorithms
The default is handled system-wide by
.Xr crypto-policies 7 .
--
2.49.0

View File

@ -39,12 +39,11 @@
%{?static_openssl:%global static_libcrypto 1}
%global openssh_ver 9.9p1
%global openssh_rel 7
Summary: An open source implementation of SSH protocol version 2
Name: openssh
Version: %{openssh_ver}
Release: %{openssh_rel}%{?dist}
Release: 20%{?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
@ -205,6 +204,29 @@ Patch1020: openssh-9.9p1-match-regression.patch
# upstream 6ce00f0c2ecbb9f75023dbe627ee6460bcec78c2
# upstream 0832aac79517611dd4de93ad0a83577994d9c907
Patch1021: openssh-9.9p2-error_processing.patch
# Downstream patch, OpenSSL based MLKEM implementation
Patch1022: openssh-9.9p1-openssl-mlkem.patch
# upstream 8eabd2ae2ca1d7756417a1ee5b41f09c5d997634
Patch1023: openssh-9.9p1-compression-directive.patch
# upstream fc86875e6acb36401dfc1dfb6b628a9d1460f367
Patch1024: openssh-9.9p1-disable-forwarding.patch
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
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
@ -391,6 +413,20 @@ gpgv2 --quiet --keyring %{SOURCE3} %{SOURCE1} %{SOURCE0}
%patch -P 1019 -p1 -b .mlkembe
%patch -P 1020 -p1 -b .match
%patch -P 1021 -p1 -b .errcode_set
%patch -P 1022 -p1 -b .openssl-mlkem
%patch -P 1023 -p1 -b .compression
%patch -P 1024 -p1 -b .disable-forwarding
%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 .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 100 -p1 -b .coverity
@ -489,7 +525,8 @@ popd
%endif
%check
OPENSSL_CONF=/dev/null %{SOURCE22} %{SOURCE23} # ./parallel_tests.sh parallel_tests.Makefile
%{SOURCE22} %{SOURCE23} # ./parallel_tests.sh parallel_tests.Makefile
#make tests
%install
rm -rf $RPM_BUILD_ROOT
@ -670,6 +707,70 @@ test -f %{sysconfig_anaconda} && \
%attr(0755,root,root) %{_libdir}/sshtest/sk-dummy.so
%changelog
* Wed Feb 25 2026 Dmitry Belyavskiy <dbelyavs@redhat.com> - 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 <dbelyavs@redhat.com> - 9.9p1-19
- Support of hybrid MLKEM key exchange methods in FIPS mode
Resolves: RHEL-125929
* Fri Dec 05 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-18
- Adding a mechanism to disable GSSAPIDelegateCredentials in sshd_config
Resolves: RHEL-5281
* Fri Dec 05 2025 Zoltan Fridrich <zfridric@redhat.com> - 9.9p1-17
- CVE-2025-61984: Reject usernames with control characters
Resolves: RHEL-128399
- CVE-2025-61985: Reject URL-strings with NULL characters
Resolves: RHEL-128388
* Mon Nov 03 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-16
- Implement mlkem768nistp256-sha256 and mlkem1024nistp384-sha384 KEX methods
Resolves: RHEL-70824
* Mon Oct 27 2025 Zoltan Fridrich <zfridric@redhat.com> - 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 <dbelyavs@redhat.com> - 9.9p1-14
- Relax GSS Kex restriction in FIPS mode
Resolves: RHEL-91181
* Mon Sep 01 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-13
- Allow non-cryptographical use of MD5 in GSS Kex in FIPS mode
Related: RHEL-91181
* Mon Aug 04 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-12
- Relax GSS Kex restriction in FIPS mode
Resolves: RHEL-91181
* Fri Jul 18 2025 Zoltan Fridrich <zfridric@redhat.com> - 9.9p1-11
- Move the redhat help message to debug1 log level
Resolves: RHEL-93957
* Thu Jun 26 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-10
- Support for authentication indicators in OpenSSH
Resolves: RHEL-40790
* Tue Apr 29 2025 Zoltan Fridrich <zfridric@redhat.com> - 9.9p1-9
- CVE-2025-32728: Fix logic error in DisableForwarding option
Resolves: RHEL-86819
- Provide better error for non-supported private keys
Resolves: RHEL-68124
- Ignore bad hostkeys in known_hosts file
Resolves: RHEL-83644
* Thu Mar 20 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-8
- OpenSSH should not use its own implementation of MLKEM
Resolves: RHEL-58252
- Correct processing of Compression directive
Resolves: RHEL-68346
- Supress systemd warning
Resolves: RHEL-84816
* Tue Feb 18 2025 Dmitry Belyavskiy <dbelyavs@redhat.com> - 9.9p1-7
- rebuilt
Related: RHEL-78699

View File

@ -8,3 +8,4 @@
#SSH_RSA_BITS=3072
#SSH_ECDSA_BITS=256
OPTIONS=""