diff --git a/ConfigureChecks.cmake b/ConfigureChecks.cmake
index 43df7fdd..d435d596 100644
--- a/ConfigureChecks.cmake
+++ b/ConfigureChecks.cmake
@@ -104,6 +104,11 @@ if (OPENSSL_FOUND)
check_function_exists(RAND_priv_bytes HAVE_OPENSSL_RAND_PRIV_BYTES)
check_function_exists(EVP_chacha20 HAVE_OPENSSL_EVP_CHACHA20)
+ # Check for ML-KEM768 availability (OpenSSL 3.5+)
+ if (OPENSSL_VERSION VERSION_GREATER_EQUAL "3.5.0")
+ set(HAVE_MLKEM 1)
+ endif ()
+
unset(CMAKE_REQUIRED_INCLUDES)
unset(CMAKE_REQUIRED_LIBRARIES)
endif()
diff --git a/config.h.cmake b/config.h.cmake
index 3f27983a..16916dec 100644
--- a/config.h.cmake
+++ b/config.h.cmake
@@ -191,6 +191,9 @@
/* Define to 1 if we have support for blowfish */
#cmakedefine HAVE_BLOWFISH 1
+/* Define to 1 if we have support for ML-KEM */
+#cmakedefine HAVE_MLKEM 1
+
/*************************** LIBRARIES ***************************/
/* Define to 1 if you have the `crypto' library (-lcrypto). */
diff --git a/doc/mainpage.dox b/doc/mainpage.dox
index 69f36c59..d5cf5a56 100644
--- a/doc/mainpage.dox
+++ b/doc/mainpage.dox
@@ -19,7 +19,7 @@ the interesting functions as you go.
The libssh library provides:
- - Key Exchange Methods: sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group1-sha1, diffie-hellman-group14-sha1
+ - Key Exchange Methods: sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, mlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group1-sha1, diffie-hellman-group14-sha1
- Public Key Algorithms: ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, ssh-rsa, rsa-sha2-512, rsa-sha2-256
- Ciphers: aes256-ctr, aes192-ctr, aes128-ctr, aes256-cbc (rijndael-cbc@lysator.liu.se), aes192-cbc, aes128-cbc, 3des-cbc, blowfish-cbc
- Compression Schemes: zlib, zlib@openssh.com, none
diff --git a/include/libssh/crypto.h b/include/libssh/crypto.h
index 6b8b60ac..e20620f4 100644
--- a/include/libssh/crypto.h
+++ b/include/libssh/crypto.h
@@ -50,6 +50,9 @@
#include "libssh/ecdh.h"
#include "libssh/kex.h"
#include "libssh/sntrup761.h"
+#ifdef HAVE_MLKEM
+#include "libssh/mlkem768.h"
+#endif
#define DIGEST_MAX_LEN 64
@@ -87,6 +90,10 @@ enum ssh_key_exchange_e {
SSH_KEX_SNTRUP761X25519_SHA512_OPENSSH_COM,
/* sntrup761x25519-sha512 */
SSH_KEX_SNTRUP761X25519_SHA512,
+#ifdef HAVE_MLKEM
+ /* mlkem768x25519-sha256 */
+ SSH_KEX_MLKEM768X25519_SHA256,
+#endif /* HAVE_MLKEM */
};
enum ssh_cipher_e {
@@ -140,6 +147,11 @@ struct ssh_crypto_struct {
ssh_curve25519_pubkey curve25519_client_pubkey;
ssh_curve25519_pubkey curve25519_server_pubkey;
#endif
+#ifdef HAVE_MLKEM
+ ssh_mlkem768_privkey mlkem768_client_privkey;
+ ssh_mlkem768_pubkey mlkem768_client_pubkey;
+ ssh_mlkem768_ciphertext mlkem768_ciphertext;
+#endif
#ifdef HAVE_SNTRUP761
ssh_sntrup761_privkey sntrup761_privkey;
ssh_sntrup761_pubkey sntrup761_client_pubkey;
diff --git a/include/libssh/mlkem768.h b/include/libssh/mlkem768.h
new file mode 100644
index 00000000..baa415ed
--- /dev/null
+++ b/include/libssh/mlkem768.h
@@ -0,0 +1,63 @@
+/*
+ * This file is part of the SSH Library
+ *
+ * Copyright (c) 2025 by Red Hat, Inc.
+ *
+ * Author: Sahana Prasad
+ * Author: Claude (Anthropic)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef MLKEM768_H_
+#define MLKEM768_H_
+
+#include "config.h"
+
+/* ML-KEM768 key and ciphertext sizes as defined in FIPS 203 */
+#define MLKEM768_PUBLICKEY_SIZE 1184
+#define MLKEM768_SECRETKEY_SIZE 2400
+#define MLKEM768_CIPHERTEXT_SIZE 1088
+#define MLKEM768_SHARED_SECRET_SIZE 32
+
+/* Hybrid ML-KEM768x25519 combined sizes */
+#define MLKEM768X25519_CLIENT_PUBKEY_SIZE \
+ (MLKEM768_PUBLICKEY_SIZE + CURVE25519_PUBKEY_SIZE)
+#define MLKEM768X25519_SERVER_RESPONSE_SIZE \
+ (MLKEM768_CIPHERTEXT_SIZE + CURVE25519_PUBKEY_SIZE)
+#define MLKEM768X25519_SHARED_SECRET_SIZE \
+ (MLKEM768_SHARED_SECRET_SIZE + CURVE25519_PUBKEY_SIZE)
+
+typedef unsigned char ssh_mlkem768_pubkey[MLKEM768_PUBLICKEY_SIZE];
+typedef unsigned char ssh_mlkem768_privkey[MLKEM768_SECRETKEY_SIZE];
+typedef unsigned char ssh_mlkem768_ciphertext[MLKEM768_CIPHERTEXT_SIZE];
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* ML-KEM768x25519 key exchange functions */
+int ssh_client_mlkem768x25519_init(ssh_session session);
+void ssh_client_mlkem768x25519_remove_callbacks(ssh_session session);
+
+#ifdef WITH_SERVER
+void ssh_server_mlkem768x25519_init(ssh_session session);
+#endif /* WITH_SERVER */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MLKEM768_H_ */
diff --git a/include/libssh/ssh2.h b/include/libssh/ssh2.h
index 35214330..bf61573d 100644
--- a/include/libssh/ssh2.h
+++ b/include/libssh/ssh2.h
@@ -18,6 +18,8 @@
#define SSH2_MSG_KEX_ECDH_REPLY 31
#define SSH2_MSG_ECMQV_INIT 30
#define SSH2_MSG_ECMQV_REPLY 31
+#define SSH2_MSG_KEX_HYBRID_INIT 30
+#define SSH2_MSG_KEX_HYBRID_REPLY 31
#define SSH2_MSG_KEX_DH_GEX_REQUEST_OLD 30
#define SSH2_MSG_KEX_DH_GEX_GROUP 31
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f8eb2985..f4f3edca 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -286,6 +286,13 @@ if (NOT WITH_NACL)
endif()
endif (NOT WITH_NACL)
+if (HAVE_MLKEM)
+ set(libssh_SRCS
+ ${libssh_SRCS}
+ mlkem768.c
+ )
+endif (HAVE_MLKEM)
+
# Set the path to the default map file
set(MAP_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.map")
diff --git a/src/client.c b/src/client.c
index 8d3374b5..121a7253 100644
--- a/src/client.c
+++ b/src/client.c
@@ -46,6 +46,9 @@
#include "libssh/misc.h"
#include "libssh/pki.h"
#include "libssh/kex.h"
+#ifdef HAVE_MLKEM
+#include "libssh/mlkem768.h"
+#endif
#ifndef _WIN32
#ifdef HAVE_PTHREAD
@@ -295,6 +298,11 @@ int dh_handshake(ssh_session session)
case SSH_KEX_SNTRUP761X25519_SHA512_OPENSSH_COM:
rc = ssh_client_sntrup761x25519_init(session);
break;
+#endif
+#ifdef HAVE_MLKEM
+ case SSH_KEX_MLKEM768X25519_SHA256:
+ rc = ssh_client_mlkem768x25519_init(session);
+ break;
#endif
default:
rc = SSH_ERROR;
diff --git a/src/kex.c b/src/kex.c
index fa213b74..846ea932 100644
--- a/src/kex.c
+++ b/src/kex.c
@@ -41,6 +41,9 @@
#include "libssh/string.h"
#include "libssh/curve25519.h"
#include "libssh/sntrup761.h"
+#ifdef HAVE_MLKEM
+#include "libssh/mlkem768.h"
+#endif
#include "libssh/knownhosts.h"
#include "libssh/misc.h"
#include "libssh/pki.h"
@@ -102,6 +105,12 @@
#define SNTRUP761X25519 ""
#endif /* HAVE_SNTRUP761 */
+#ifdef HAVE_MLKEM
+#define MLKEM768X25519 "mlkem768x25519-sha256,"
+#else
+#define MLKEM768X25519 ""
+#endif /* HAVE_MLKEM */
+
#ifdef HAVE_ECC
#define ECDH "ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,"
#define EC_HOSTKEYS "ecdsa-sha2-nistp521," \
@@ -167,6 +176,7 @@
#define DEFAULT_KEY_EXCHANGE \
CURVE25519 \
SNTRUP761X25519 \
+ MLKEM768X25519 \
ECDH \
"diffie-hellman-group18-sha512,diffie-hellman-group16-sha512," \
GEX_SHA256 \
@@ -926,6 +936,10 @@ kex_select_kex_type(const char *kex)
return SSH_KEX_SNTRUP761X25519_SHA512_OPENSSH_COM;
} else if (strcmp(kex, "sntrup761x25519-sha512") == 0) {
return SSH_KEX_SNTRUP761X25519_SHA512;
+#ifdef HAVE_MLKEM
+ } else if (strcmp(kex, "mlkem768x25519-sha256") == 0) {
+ return SSH_KEX_MLKEM768X25519_SHA256;
+#endif
}
/* should not happen. We should be getting only valid names at this stage */
return 0;
@@ -971,6 +985,11 @@ static void revert_kex_callbacks(ssh_session session)
case SSH_KEX_SNTRUP761X25519_SHA512_OPENSSH_COM:
ssh_client_sntrup761x25519_remove_callbacks(session);
break;
+#endif
+#ifdef HAVE_MLKEM
+ case SSH_KEX_MLKEM768X25519_SHA256:
+ ssh_client_mlkem768x25519_remove_callbacks(session);
+ break;
#endif
}
}
@@ -1555,6 +1574,33 @@ int ssh_make_sessionid(ssh_session session)
}
break;
#endif /* HAVE_SNTRUP761 */
+#ifdef HAVE_MLKEM
+ case SSH_KEX_MLKEM768X25519_SHA256:
+ rc = ssh_buffer_pack(buf,
+ "dPPdPP",
+ MLKEM768X25519_CLIENT_PUBKEY_SIZE,
+ (size_t)MLKEM768_PUBLICKEY_SIZE,
+ session->next_crypto->mlkem768_client_pubkey,
+ (size_t)CURVE25519_PUBKEY_SIZE,
+ session->next_crypto->curve25519_client_pubkey,
+ MLKEM768X25519_SERVER_RESPONSE_SIZE,
+ (size_t)MLKEM768_CIPHERTEXT_SIZE,
+ session->next_crypto->mlkem768_ciphertext,
+ (size_t)CURVE25519_PUBKEY_SIZE,
+ session->next_crypto->curve25519_server_pubkey);
+ if (rc != SSH_OK) {
+ ssh_set_error(session,
+ SSH_FATAL,
+ "Failed to pack ML-KEM768 individual components");
+ goto error;
+ }
+ break;
+#endif /* HAVE_MLKEM */
+ default:
+ /* Handle unsupported kex types - this should not happen in normal operation */
+ rc = SSH_ERROR;
+ ssh_set_error(session, SSH_FATAL, "Unsupported KEX algorithm");
+ goto error;
}
switch (session->next_crypto->kex_type) {
case SSH_KEX_SNTRUP761X25519_SHA512:
@@ -1564,6 +1610,14 @@ int ssh_make_sessionid(ssh_session session)
session->next_crypto->shared_secret,
SHA512_DIGEST_LEN);
break;
+#ifdef HAVE_MLKEM
+ case SSH_KEX_MLKEM768X25519_SHA256:
+ rc = ssh_buffer_pack(buf,
+ "F",
+ session->next_crypto->shared_secret,
+ SHA256_DIGEST_LEN);
+ break;
+#endif /* HAVE_MLKEM */
default:
rc = ssh_buffer_pack(buf, "B", session->next_crypto->shared_secret);
break;
@@ -1599,6 +1653,9 @@ int ssh_make_sessionid(ssh_session session)
case SSH_KEX_ECDH_SHA2_NISTP256:
case SSH_KEX_CURVE25519_SHA256:
case SSH_KEX_CURVE25519_SHA256_LIBSSH_ORG:
+#ifdef HAVE_MLKEM
+ case SSH_KEX_MLKEM768X25519_SHA256:
+#endif
#ifdef WITH_GEX
case SSH_KEX_DH_GEX_SHA256:
#endif /* WITH_GEX */
@@ -1639,6 +1696,11 @@ int ssh_make_sessionid(ssh_session session)
ssh_buffer_get_len(buf),
session->next_crypto->secret_hash);
break;
+ default:
+ /* Handle unsupported kex types - this should not happen in normal operation */
+ ssh_set_error(session, SSH_FATAL, "Unsupported KEX algorithm for hash computation");
+ rc = SSH_ERROR;
+ goto error;
}
/* During the first kex, secret hash and session ID are equal. However, after
@@ -1769,8 +1831,11 @@ int ssh_generate_session_keys(ssh_session session)
switch (session->next_crypto->kex_type) {
case SSH_KEX_SNTRUP761X25519_SHA512:
case SSH_KEX_SNTRUP761X25519_SHA512_OPENSSH_COM:
+#ifdef HAVE_MLKEM
+ case SSH_KEX_MLKEM768X25519_SHA256:
+#endif /* HAVE_MLKEM */
k_string = ssh_make_padded_bignum_string(crypto->shared_secret,
- SHA512_DIGEST_LEN);
+ crypto->digest_len);
break;
default:
k_string = ssh_make_bignum_string(crypto->shared_secret);
diff --git a/src/mlkem768.c b/src/mlkem768.c
new file mode 100644
index 00000000..09159934
--- /dev/null
+++ b/src/mlkem768.c
@@ -0,0 +1,674 @@
+/*
+ * mlkem768x25519.c - ML-KEM768x25519 hybrid key exchange
+ * mlkem768x25519-sha256
+ *
+ * This file is part of the SSH Library
+ *
+ * Copyright (c) 2025 by Red Hat, Inc.
+ *
+ * Author: Sahana Prasad
+ * Author: Claude (Anthropic)
+ *
+ * The SSH Library is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, version 2.1 of the License.
+ *
+ * The SSH Library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with the SSH Library; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
+ * MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "libssh/bignum.h"
+#include "libssh/buffer.h"
+#include "libssh/crypto.h"
+#include "libssh/curve25519.h"
+#include "libssh/dh.h"
+#include "libssh/mlkem768.h"
+#include "libssh/pki.h"
+#include "libssh/priv.h"
+#include "libssh/session.h"
+#include "libssh/ssh2.h"
+
+#include
+
+#include
+#include
+
+static SSH_PACKET_CALLBACK(ssh_packet_client_mlkem768x25519_reply);
+
+static ssh_packet_callback dh_client_callbacks[] = {
+ ssh_packet_client_mlkem768x25519_reply,
+};
+
+static struct ssh_packet_callbacks_struct ssh_mlkem768x25519_client_callbacks =
+ {
+ .start = SSH2_MSG_KEX_HYBRID_REPLY,
+ .n_callbacks = 1,
+ .callbacks = dh_client_callbacks,
+ .user = NULL,
+};
+
+/* Generate ML-KEM768 keypair using OpenSSL */
+static int mlkem768_keypair_gen(ssh_mlkem768_pubkey pubkey,
+ ssh_mlkem768_privkey privkey)
+{
+ EVP_PKEY_CTX *ctx = NULL;
+ EVP_PKEY *pkey = NULL;
+ int rc, ret = SSH_ERROR;
+ size_t pubkey_len = MLKEM768_PUBLICKEY_SIZE;
+ size_t privkey_len = MLKEM768_SECRETKEY_SIZE;
+
+ ctx = EVP_PKEY_CTX_new_from_name(NULL, "ML-KEM-768", NULL);
+ if (ctx == NULL) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to create ML-KEM-768 context: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto cleanup;
+ }
+
+ rc = EVP_PKEY_keygen_init(ctx);
+ if (rc != 1) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to initialize ML-KEM-768 keygen: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ }
+
+ rc = EVP_PKEY_keygen(ctx, &pkey);
+ if (rc != 1) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to perform ML-KEM-768 keygen: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ }
+
+ rc = EVP_PKEY_get_raw_public_key(pkey, pubkey, &pubkey_len);
+ if (rc != 1) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to extract ML-KEM-768 public key: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ }
+
+ rc = EVP_PKEY_get_raw_private_key(pkey, privkey, &privkey_len);
+ if (rc != 1) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to extract ML-KEM-768 private key: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ }
+
+ ret = SSH_OK;
+
+cleanup:
+ EVP_PKEY_free(pkey);
+ EVP_PKEY_CTX_free(ctx);
+ return ret;
+}
+
+/* Encapsulate shared secret using ML-KEM768 - used by server side */
+static int mlkem768_encapsulate(const ssh_mlkem768_pubkey pubkey,
+ ssh_mlkem768_ciphertext ciphertext,
+ unsigned char *shared_secret)
+{
+ EVP_PKEY *pkey = NULL;
+ EVP_PKEY_CTX *ctx = NULL;
+ int rc, ret = SSH_ERROR;
+ size_t ct_len = MLKEM768_CIPHERTEXT_SIZE;
+ size_t ss_len = MLKEM768_SHARED_SECRET_SIZE;
+
+ pkey = EVP_PKEY_new_raw_public_key_ex(NULL,
+ "ML-KEM-768",
+ NULL,
+ pubkey,
+ MLKEM768_PUBLICKEY_SIZE);
+ if (pkey == NULL) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to create ML-KEM-768 public key from raw data: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto cleanup;
+ }
+
+ ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
+ if (ctx == NULL) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to create ML-KEM-768 context: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto cleanup;
+ }
+
+ rc = EVP_PKEY_encapsulate_init(ctx, NULL);
+ if (rc != 1) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to initialize ML-KEM-768 encapsulation: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto cleanup;
+ }
+
+ rc = EVP_PKEY_encapsulate(ctx, ciphertext, &ct_len, shared_secret, &ss_len);
+ if (rc != 1) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to perform ML-KEM-768 encapsulation: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto cleanup;
+ }
+
+ ret = SSH_OK;
+
+cleanup:
+ EVP_PKEY_free(pkey);
+ EVP_PKEY_CTX_free(ctx);
+ return ret;
+}
+
+/* Decapsulate shared secret using ML-KEM768 - used by client side */
+static int mlkem768_decapsulate(const ssh_mlkem768_privkey privkey,
+ const ssh_mlkem768_ciphertext ciphertext,
+ unsigned char *shared_secret)
+{
+ EVP_PKEY *pkey = NULL;
+ EVP_PKEY_CTX *ctx = NULL;
+ int rc, ret = SSH_ERROR;
+ size_t ss_len = MLKEM768_SHARED_SECRET_SIZE;
+
+ pkey = EVP_PKEY_new_raw_private_key_ex(NULL,
+ "ML-KEM-768",
+ NULL,
+ privkey,
+ MLKEM768_SECRETKEY_SIZE);
+ if (pkey == NULL) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to create ML-KEM-768 context: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ }
+
+ ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
+ if (ctx == NULL) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to create ML-KEM-768 context: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto cleanup;
+ }
+
+ rc = EVP_PKEY_decapsulate_init(ctx, NULL);
+ if (rc != 1) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to initialize ML-KEM-768 decapsulation: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto cleanup;
+ }
+
+ rc = EVP_PKEY_decapsulate(ctx,
+ shared_secret,
+ &ss_len,
+ ciphertext,
+ MLKEM768_CIPHERTEXT_SIZE);
+ if (rc != 1) {
+ SSH_LOG(SSH_LOG_WARNING,
+ "Failed to perform ML-KEM-768 decapsulation: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto cleanup;
+ }
+
+ ret = SSH_OK;
+
+cleanup:
+ EVP_PKEY_free(pkey);
+ EVP_PKEY_CTX_free(ctx);
+ return ret;
+}
+
+int ssh_client_mlkem768x25519_init(ssh_session session)
+{
+ struct ssh_crypto_struct *crypto = session->next_crypto;
+ ssh_buffer client_pubkey = NULL;
+ ssh_string pubkey_blob = NULL;
+ int rc;
+
+ SSH_LOG(SSH_LOG_TRACE, "Initializing ML-KEM768x25519 key exchange");
+
+ /* Initialize Curve25519 component first */
+ rc = ssh_curve25519_init(session);
+ if (rc != SSH_OK) {
+ return rc;
+ }
+
+ /* Generate ML-KEM768 keypair */
+ rc = mlkem768_keypair_gen(crypto->mlkem768_client_pubkey,
+ crypto->mlkem768_client_privkey);
+ if (rc != SSH_OK) {
+ ssh_set_error(session, SSH_FATAL, "Failed to generate ML-KEM768 keypair");
+ return SSH_ERROR;
+ }
+
+ /* Create hybrid client public key: ML-KEM768 + Curve25519 */
+ client_pubkey = ssh_buffer_new();
+ if (client_pubkey == NULL) {
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ rc = SSH_ERROR;
+ goto cleanup;
+ }
+
+ rc = ssh_buffer_pack(client_pubkey,
+ "PP",
+ MLKEM768_PUBLICKEY_SIZE,
+ crypto->mlkem768_client_pubkey,
+ CURVE25519_PUBKEY_SIZE,
+ crypto->curve25519_client_pubkey);
+ if (rc != SSH_OK) {
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ rc = SSH_ERROR;
+ goto cleanup;
+ }
+
+ /* Convert to string for sending */
+ pubkey_blob = ssh_string_new(ssh_buffer_get_len(client_pubkey));
+ if (pubkey_blob == NULL) {
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ rc = SSH_ERROR;
+ goto cleanup;
+ }
+ ssh_string_fill(pubkey_blob,
+ ssh_buffer_get(client_pubkey),
+ ssh_buffer_get_len(client_pubkey));
+
+ /* Send the hybrid public key to server */
+ rc = ssh_buffer_pack(session->out_buffer,
+ "bS",
+ SSH2_MSG_KEX_HYBRID_INIT,
+ pubkey_blob);
+ if (rc != SSH_OK) {
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ rc = SSH_ERROR;
+ goto cleanup;
+ }
+
+ session->dh_handshake_state = DH_STATE_INIT_SENT;
+
+ ssh_packet_set_callbacks(session, &ssh_mlkem768x25519_client_callbacks);
+ rc = ssh_packet_send(session);
+ if (rc == SSH_ERROR) {
+ ssh_set_error(session, SSH_FATAL, "Failed to send SSH_MSG_KEX_ECDH_INIT");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+cleanup:
+ ssh_buffer_free(client_pubkey);
+ ssh_string_free(pubkey_blob);
+ return rc;
+}
+
+static SSH_PACKET_CALLBACK(ssh_packet_client_mlkem768x25519_reply)
+{
+ struct ssh_crypto_struct *crypto = session->next_crypto;
+ ssh_string s_server_blob = NULL;
+ ssh_string s_pubkey_blob = NULL;
+ ssh_string s_signature = NULL;
+ const unsigned char *server_data = NULL;
+ unsigned char mlkem_shared_secret[MLKEM768_SHARED_SECRET_SIZE];
+ unsigned char curve25519_shared_secret[CURVE25519_PUBKEY_SIZE];
+ unsigned char combined_secret[MLKEM768X25519_SHARED_SECRET_SIZE];
+ unsigned char hashed_secret[SHA256_DIGEST_LEN];
+ size_t server_blob_len;
+ int rc;
+ (void)type;
+ (void)user;
+
+ SSH_LOG(SSH_LOG_TRACE, "Received ML-KEM768x25519 server reply");
+
+ ssh_client_mlkem768x25519_remove_callbacks(session);
+
+ s_pubkey_blob = ssh_buffer_get_ssh_string(packet);
+ if (s_pubkey_blob == NULL) {
+ ssh_set_error(session, SSH_FATAL, "No public key in packet");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ rc = ssh_dh_import_next_pubkey_blob(session, s_pubkey_blob);
+ if (rc != 0) {
+ ssh_set_error(session, SSH_FATAL, "Failed to import next public key");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Get server blob containing ML-KEM768 ciphertext + Curve25519 pubkey */
+ s_server_blob = ssh_buffer_get_ssh_string(packet);
+ if (s_server_blob == NULL) {
+ ssh_set_error(session, SSH_FATAL, "No server blob in packet");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ server_data = ssh_string_data(s_server_blob);
+ server_blob_len = ssh_string_len(s_server_blob);
+
+ /* Expect ML-KEM768 ciphertext + Curve25519 pubkey */
+ if (server_blob_len != MLKEM768X25519_SERVER_RESPONSE_SIZE) {
+ ssh_set_error(session, SSH_FATAL, "Invalid server blob size");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Store ML-KEM768 ciphertext for sessionid calculation */
+ memcpy(crypto->mlkem768_ciphertext, server_data, MLKEM768_CIPHERTEXT_SIZE);
+
+ /* Decapsulate ML-KEM768 shared secret */
+ rc = mlkem768_decapsulate(crypto->mlkem768_client_privkey,
+ crypto->mlkem768_ciphertext,
+ mlkem_shared_secret);
+ if (rc != SSH_OK) {
+ ssh_set_error(session, SSH_FATAL, "ML-KEM768 decapsulation failed");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Store server Curve25519 public key for shared secret computation */
+ memcpy(crypto->curve25519_server_pubkey,
+ server_data + MLKEM768_CIPHERTEXT_SIZE,
+ CURVE25519_PUBKEY_SIZE);
+
+ /* Derive Curve25519 shared secret using existing libssh function */
+ rc = ssh_curve25519_create_k(session, curve25519_shared_secret);
+ if (rc != SSH_OK) {
+ ssh_set_error(session, SSH_FATAL, "Curve25519 ECDH failed");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Combine secrets: ML-KEM768 + Curve25519 for hybrid approach */
+ memcpy(combined_secret, mlkem_shared_secret, MLKEM768_SHARED_SECRET_SIZE);
+ memcpy(combined_secret + MLKEM768_SHARED_SECRET_SIZE,
+ curve25519_shared_secret,
+ CURVE25519_PUBKEY_SIZE);
+
+ sha256(combined_secret, MLKEM768X25519_SHARED_SECRET_SIZE, hashed_secret);
+
+ bignum_bin2bn(hashed_secret, SHA256_DIGEST_LEN, &crypto->shared_secret);
+ if (crypto->shared_secret == NULL) {
+ ssh_set_error(session, SSH_FATAL, "Failed to create shared secret bignum");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Get signature for verification */
+ s_signature = ssh_buffer_get_ssh_string(packet);
+ if (s_signature == NULL) {
+ ssh_set_error(session, SSH_FATAL, "No signature in packet");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ crypto->dh_server_signature = s_signature;
+ s_signature = NULL;
+
+ /* Send the MSG_NEWKEYS */
+ rc = ssh_packet_send_newkeys(session);
+ if (rc == SSH_ERROR) {
+ ssh_set_error(session, SSH_FATAL, "Failed to send SSH_MSG_NEWKEYS");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+ session->dh_handshake_state = DH_STATE_NEWKEYS_SENT;
+
+cleanup:
+ /* Clear sensitive data */
+ explicit_bzero(mlkem_shared_secret, sizeof(mlkem_shared_secret));
+ explicit_bzero(curve25519_shared_secret, sizeof(curve25519_shared_secret));
+ explicit_bzero(combined_secret, sizeof(combined_secret));
+ explicit_bzero(hashed_secret, sizeof(hashed_secret));
+ ssh_string_free(s_pubkey_blob);
+ ssh_string_free(s_server_blob);
+ ssh_string_free(s_signature);
+ return SSH_PACKET_USED;
+}
+
+void ssh_client_mlkem768x25519_remove_callbacks(ssh_session session)
+{
+ ssh_packet_remove_callbacks(session, &ssh_mlkem768x25519_client_callbacks);
+}
+
+#ifdef WITH_SERVER
+
+static SSH_PACKET_CALLBACK(ssh_packet_server_mlkem768x25519_init);
+
+static ssh_packet_callback dh_server_callbacks[] = {
+ ssh_packet_server_mlkem768x25519_init,
+};
+
+static struct ssh_packet_callbacks_struct ssh_mlkem768x25519_server_callbacks =
+ {
+ .start = SSH2_MSG_KEX_HYBRID_INIT,
+ .n_callbacks = 1,
+ .callbacks = dh_server_callbacks,
+ .user = NULL,
+};
+
+static SSH_PACKET_CALLBACK(ssh_packet_server_mlkem768x25519_init)
+{
+ struct ssh_crypto_struct *crypto = session->next_crypto;
+ ssh_string client_pubkey_blob = NULL;
+ ssh_string server_pubkey_blob = NULL;
+ ssh_buffer server_response = NULL;
+ const unsigned char *client_data = NULL;
+ unsigned char mlkem_shared_secret[MLKEM768_SHARED_SECRET_SIZE];
+ unsigned char curve25519_shared_secret[CURVE25519_PUBKEY_SIZE];
+ unsigned char combined_secret[MLKEM768X25519_SHARED_SECRET_SIZE];
+ unsigned char mlkem_ciphertext[MLKEM768_CIPHERTEXT_SIZE];
+ unsigned char hashed_secret[SHA256_DIGEST_LEN];
+ size_t client_blob_len;
+ ssh_key privkey = NULL;
+ enum ssh_digest_e digest = SSH_DIGEST_AUTO;
+ ssh_string sig_blob = NULL;
+ ssh_string server_hostkey_blob = NULL;
+ int rc = SSH_ERROR;
+ (void)type;
+ (void)user;
+
+ SSH_LOG(SSH_LOG_TRACE, "Received ML-KEM768x25519 client init");
+
+ ssh_packet_remove_callbacks(session, &ssh_mlkem768x25519_server_callbacks);
+
+ /* Get client hybrid public key: ML-KEM768 + Curve25519 */
+ client_pubkey_blob = ssh_buffer_get_ssh_string(packet);
+ if (client_pubkey_blob == NULL) {
+ ssh_set_error(session, SSH_FATAL, "No client public key in packet");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ client_data = ssh_string_data(client_pubkey_blob);
+ client_blob_len = ssh_string_len(client_pubkey_blob);
+
+ /* Expect ML-KEM768 pubkey + Curve25519 pubkey */
+ if (client_blob_len != MLKEM768X25519_CLIENT_PUBKEY_SIZE) {
+ ssh_set_error(session, SSH_FATAL, "Invalid client public key size");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Extract client ML-KEM768 public key */
+ memcpy(crypto->mlkem768_client_pubkey,
+ client_data,
+ MLKEM768_PUBLICKEY_SIZE);
+
+ /* Extract client Curve25519 public key */
+ memcpy(crypto->curve25519_client_pubkey,
+ client_data + MLKEM768_PUBLICKEY_SIZE,
+ CURVE25519_PUBKEY_SIZE);
+
+ /* Generate server Curve25519 keypair */
+ rc = ssh_curve25519_init(session);
+ if (rc != SSH_OK) {
+ ssh_set_error(session, SSH_FATAL, "Failed to generate server Curve25519 key");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Derive Curve25519 shared secret */
+ rc = ssh_curve25519_create_k(session, curve25519_shared_secret);
+ if (rc != SSH_OK) {
+ ssh_set_error(session, SSH_FATAL, "Curve25519 ECDH failed");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Encapsulate ML-KEM768 shared secret using client's public key */
+ rc = mlkem768_encapsulate(client_data, mlkem_ciphertext, mlkem_shared_secret);
+ if (rc != SSH_OK) {
+ ssh_set_error(session, SSH_FATAL, "ML-KEM768 encapsulation failed");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Store ML-KEM768 ciphertext for sessionid calculation */
+ memcpy(crypto->mlkem768_ciphertext, mlkem_ciphertext, MLKEM768_CIPHERTEXT_SIZE);
+
+ /* Combine secrets: ML-KEM768 + Curve25519 for hybrid approach */
+ memcpy(combined_secret, mlkem_shared_secret, MLKEM768_SHARED_SECRET_SIZE);
+ memcpy(combined_secret + MLKEM768_SHARED_SECRET_SIZE,
+ curve25519_shared_secret,
+ CURVE25519_PUBKEY_SIZE);
+
+ sha256(combined_secret, MLKEM768X25519_SHARED_SECRET_SIZE, hashed_secret);
+
+ /* Store the combined secret */
+ bignum_bin2bn(hashed_secret, SHA256_DIGEST_LEN, &crypto->shared_secret);
+ if (crypto->shared_secret == NULL) {
+ ssh_set_error(session, SSH_FATAL, "Failed to create shared secret bignum");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Create server response: ML-KEM768 ciphertext + Curve25519 pubkey */
+ server_response = ssh_buffer_new();
+ if (server_response == NULL) {
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ rc = ssh_buffer_pack(server_response,
+ "PP",
+ MLKEM768_CIPHERTEXT_SIZE,
+ mlkem_ciphertext,
+ CURVE25519_PUBKEY_SIZE,
+ crypto->curve25519_server_pubkey);
+ if (rc != SSH_OK) {
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Convert to string for sending */
+ server_pubkey_blob = ssh_string_new(ssh_buffer_get_len(server_response));
+ if (server_pubkey_blob == NULL) {
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+ ssh_string_fill(server_pubkey_blob,
+ ssh_buffer_get(server_response),
+ ssh_buffer_get_len(server_response));
+
+ /* Add MSG_KEX_ECDH_REPLY header */
+ rc = ssh_buffer_add_u8(session->out_buffer, SSH2_MSG_KEX_HYBRID_REPLY);
+ if (rc < 0) {
+ ssh_set_error_oom(session);
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Get server host key */
+ rc = ssh_get_key_params(session, &privkey, &digest);
+ if (rc == SSH_ERROR) {
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Build session ID */
+ rc = ssh_make_sessionid(session);
+ if (rc != SSH_OK) {
+ ssh_set_error(session, SSH_FATAL, "Could not create a session id");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ rc = ssh_dh_get_next_server_publickey_blob(session, &server_hostkey_blob);
+ if (rc != 0) {
+ ssh_set_error(session, SSH_FATAL, "Could not export server public key");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Add server host key to output */
+ rc = ssh_buffer_add_ssh_string(session->out_buffer, server_hostkey_blob);
+ SSH_STRING_FREE(server_hostkey_blob);
+ if (rc < 0) {
+ ssh_set_error_oom(session);
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Add server response (ciphertext + pubkey) */
+ rc = ssh_buffer_add_ssh_string(session->out_buffer, server_pubkey_blob);
+ if (rc < 0) {
+ ssh_set_error_oom(session);
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Sign the exchange hash */
+ sig_blob = ssh_srv_pki_do_sign_sessionid(session, privkey, digest);
+ if (sig_blob == NULL) {
+ ssh_set_error(session, SSH_FATAL, "Could not sign the session id");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Add signature */
+ rc = ssh_buffer_add_ssh_string(session->out_buffer, sig_blob);
+ SSH_STRING_FREE(sig_blob);
+ if (rc < 0) {
+ ssh_set_error_oom(session);
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ rc = ssh_packet_send(session);
+ if (rc == SSH_ERROR) {
+ ssh_set_error(session, SSH_FATAL, "Failed to send SSH_MSG_KEX_ECDH_REPLY");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+
+ /* Send the MSG_NEWKEYS */
+ rc = ssh_packet_send_newkeys(session);
+ if (rc == SSH_ERROR) {
+ ssh_set_error(session, SSH_FATAL, "Failed to send SSH_MSG_NEWKEYS");
+ session->session_state = SSH_SESSION_STATE_ERROR;
+ goto cleanup;
+ }
+ session->dh_handshake_state = DH_STATE_NEWKEYS_SENT;
+
+cleanup:
+ /* Clear sensitive data */
+ explicit_bzero(mlkem_shared_secret, sizeof(mlkem_shared_secret));
+ explicit_bzero(curve25519_shared_secret, sizeof(curve25519_shared_secret));
+ explicit_bzero(combined_secret, sizeof(combined_secret));
+ explicit_bzero(hashed_secret, sizeof(hashed_secret));
+ ssh_string_free(client_pubkey_blob);
+ ssh_string_free(server_pubkey_blob);
+ ssh_buffer_free(server_response);
+ return SSH_PACKET_USED;
+}
+
+void ssh_server_mlkem768x25519_init(ssh_session session)
+{
+ SSH_LOG(SSH_LOG_TRACE, "Setting up ML-KEM768x25519 server callbacks");
+ ssh_packet_set_callbacks(session, &ssh_mlkem768x25519_server_callbacks);
+}
+
+#endif /* WITH_SERVER */
diff --git a/src/session.c b/src/session.c
index 6160db74..061cafa7 100644
--- a/src/session.c
+++ b/src/session.c
@@ -457,6 +457,10 @@ const char* ssh_get_kex_algo(ssh_session session) {
return "sntrup761x25519-sha512@openssh.com";
case SSH_KEX_SNTRUP761X25519_SHA512:
return "sntrup761x25519-sha512";
+#ifdef HAVE_MLKEM
+ case SSH_KEX_MLKEM768X25519_SHA256:
+ return "mlkem768x25519-sha256";
+#endif /* HAVE_MLKEM */
#ifdef WITH_GEX
case SSH_KEX_DH_GEX_SHA1:
return "diffie-hellman-group-exchange-sha1";
diff --git a/src/wrapper.c b/src/wrapper.c
index d7a88269..f9aa6ecf 100644
--- a/src/wrapper.c
+++ b/src/wrapper.c
@@ -51,6 +51,9 @@
#include "libssh/curve25519.h"
#include "libssh/ecdh.h"
#include "libssh/sntrup761.h"
+#ifdef HAVE_MLKEM
+#include "libssh/mlkem768.h"
+#endif
static struct ssh_hmac_struct ssh_hmac_tab[] = {
{ "hmac-sha1", SSH_HMAC_SHA1, false },
@@ -598,6 +601,11 @@ int crypt_set_algorithms_server(ssh_session session){
case SSH_KEX_SNTRUP761X25519_SHA512_OPENSSH_COM:
ssh_server_sntrup761x25519_init(session);
break;
+#endif
+#ifdef HAVE_MLKEM
+ case SSH_KEX_MLKEM768X25519_SHA256:
+ ssh_server_mlkem768x25519_init(session);
+ break;
#endif
default:
ssh_set_error(session,
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 10012950..7018daa5 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -157,7 +157,7 @@ if (SSH_EXECUTABLE)
diffie-hellman-group1-sha1 diffie-hellman-group14-sha1 diffie-hellman-group14-sha256
diffie-hellman-group16-sha512 diffie-hellman-group18-sha512 diffie-hellman-group-exchange-sha1
diffie-hellman-group-exchange-sha256 ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521
- sntrup761x25519-sha512@openssh.com sntrup761x25519-sha512
+ sntrup761x25519-sha512@openssh.com sntrup761x25519-sha512 mlkem768x25519-sha256
curve25519-sha256 curve25519-sha256@libssh.org
ssh-ed25519 ssh-ed25519-cert-v01@openssh.com ssh-rsa
ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521
diff --git a/tests/client/torture_algorithms.c b/tests/client/torture_algorithms.c
index a2bb60ff..94148842 100644
--- a/tests/client/torture_algorithms.c
+++ b/tests/client/torture_algorithms.c
@@ -752,6 +752,22 @@ torture_algorithms_ecdh_sntrup761x25519_sha512(void **state)
}
#endif /* OPENSSH_SNTRUP761X25519_SHA512 */
+#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768X25519_SHA256)
+static void torture_algorithms_ecdh_mlkem768x25519_sha256(void **state)
+{
+ struct torture_state *s = *state;
+
+ if (ssh_fips_mode()) {
+ skip();
+ }
+
+ test_algorithm(s->ssh.session,
+ "mlkem768x25519-sha256",
+ NULL /*cipher*/,
+ NULL /*hmac*/);
+}
+#endif /* HAVE_MLKEM && defined(OPENSSH_MLKEM768X25519_SHA256) */
+
static void torture_algorithms_dh_group1(void **state) {
struct torture_state *s = *state;
@@ -1029,6 +1045,11 @@ int torture_run_tests(void) {
session_setup,
session_teardown),
#endif /* OPENSSH_SNTRUP761X25519_SHA512 */
+#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768X25519_SHA256)
+ cmocka_unit_test_setup_teardown(torture_algorithms_ecdh_mlkem768x25519_sha256,
+ session_setup,
+ session_teardown),
+#endif /* HAVE_MLKEM && defined(OPENSSH_MLKEM768X25519_SHA256) */
#if defined(HAVE_ECC)
cmocka_unit_test_setup_teardown(torture_algorithms_ecdh_sha2_nistp256,
session_setup,
diff --git a/tests/pkd/pkd_hello.c b/tests/pkd/pkd_hello.c
index e43f66ab..54e72b80 100644
--- a/tests/pkd/pkd_hello.c
+++ b/tests/pkd/pkd_hello.c
@@ -307,10 +307,21 @@ static int torture_pkd_setup_ecdsa_521(void **state) {
#define PKDTESTS_KEX_SNTRUP761(f, client, kexcmd)
#endif
+#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768X25519_SHA256)
+#define PKDTESTS_KEX_MLKEM768(f, client, kexcmd) \
+ f(client, rsa_mlkem768x25519_sha256, kexcmd("mlkem768x25519-sha256"), setup_rsa, teardown) \
+ f(client, ecdsa_256_mlkem768x25519_sha256, kexcmd("mlkem768x25519-sha256"), setup_ecdsa_256, teardown) \
+ f(client, ecdsa_384_mlkem768x25519_sha256, kexcmd("mlkem768x25519-sha256"), setup_ecdsa_384, teardown) \
+ f(client, ecdsa_521_mlkem768x25519_sha256, kexcmd("mlkem768x25519-sha256"), setup_ecdsa_521, teardown)
+#else
+#define PKDTESTS_KEX_MLKEM768(f, client, kexcmd)
+#endif
+
#define PKDTESTS_KEX_COMMON(f, client, kexcmd) \
PKDTESTS_KEX_FIPS(f, client, kexcmd) \
PKDTESTS_KEX_SNTRUP761(f, client, kexcmd) \
PKDTESTS_KEX_SNTRUP761_OPENSSH(f, client, kexcmd) \
+ PKDTESTS_KEX_MLKEM768(f, client, kexcmd) \
f(client, rsa_curve25519_sha256, kexcmd("curve25519-sha256"), setup_rsa, teardown) \
f(client, rsa_curve25519_sha256_libssh_org, kexcmd("curve25519-sha256@libssh.org"), setup_rsa, teardown) \
f(client, rsa_diffie_hellman_group14_sha1, kexcmd("diffie-hellman-group14-sha1"), setup_rsa, teardown) \
@@ -357,10 +368,18 @@ static int torture_pkd_setup_ecdsa_521(void **state) {
#define PKDTESTS_KEX_OPENSSHONLY_SNTRUP761(f, client, kexcmd)
#endif
+#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768X25519_SHA256)
+#define PKDTESTS_KEX_OPENSSHONLY_MLKEM768(f, client, kexcmd) \
+ f(client, ed25519_mlkem768x25519_sha256, kexcmd("mlkem768x25519-sha256"), setup_ed25519, teardown)
+#else
+#define PKDTESTS_KEX_OPENSSHONLY_MLKEM768(f, client, kexcmd)
+#endif
+
#define PKDTESTS_KEX_OPENSSHONLY(f, client, kexcmd) \
/* Kex algorithms. */ \
PKDTESTS_KEX_OPENSSHONLY_SNTRUP761(f, client, kexcmd) \
PKDTESTS_KEX_OPENSSHONLY_SNTRUP761_OPENSSH(f, client, kexcmd) \
+ PKDTESTS_KEX_OPENSSHONLY_MLKEM768(f, client, kexcmd) \
f(client, ed25519_curve25519_sha256, kexcmd("curve25519-sha256"), setup_ed25519, teardown) \
f(client, ed25519_curve25519_sha256_libssh_org, kexcmd("curve25519-sha256@libssh.org"), setup_ed25519, teardown) \
f(client, ed25519_ecdh_sha2_nistp256, kexcmd("ecdh-sha2-nistp256"), setup_ed25519, teardown) \
diff --git a/tests/tests_config.h.cmake b/tests/tests_config.h.cmake
index 3be3bb87..e571737b 100644
--- a/tests/tests_config.h.cmake
+++ b/tests/tests_config.h.cmake
@@ -52,6 +52,7 @@
#cmakedefine OPENSSH_CURVE25519_SHA256_LIBSSH_ORG 1
#cmakedefine OPENSSH_SNTRUP761X25519_SHA512 1
#cmakedefine OPENSSH_SNTRUP761X25519_SHA512_OPENSSH_COM 1
+#cmakedefine OPENSSH_MLKEM768X25519_SHA256 1
#cmakedefine OPENSSH_SSH_ED25519 1
#cmakedefine OPENSSH_SSH_ED25519_CERT_V01_OPENSSH_COM 1
#cmakedefine OPENSSH_SSH_RSA 1
diff --git a/tests/unittests/torture_options.c b/tests/unittests/torture_options.c
index f9c4dd58..ca450c4c 100644
--- a/tests/unittests/torture_options.c
+++ b/tests/unittests/torture_options.c
@@ -281,6 +281,17 @@ static void torture_options_get_key_exchange(void **state)
"diffie-hellman-group16-sha512,"
"diffie-hellman-group18-sha512");
} else {
+#ifdef HAVE_MLKEM
+ assert_string_equal(value,
+ "curve25519-sha256,curve25519-sha256@libssh.org,"
+ "sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,"
+ "mlkem768x25519-sha256,"
+ "ecdh-sha2-nistp256,ecdh-sha2-nistp384,"
+ "ecdh-sha2-nistp521,diffie-hellman-group18-sha512,"
+ "diffie-hellman-group16-sha512,"
+ "diffie-hellman-group-exchange-sha256,"
+ "diffie-hellman-group14-sha256");
+#else
assert_string_equal(value,
"curve25519-sha256,curve25519-sha256@libssh.org,"
"sntrup761x25519-sha512,"
@@ -290,6 +301,7 @@ static void torture_options_get_key_exchange(void **state)
"diffie-hellman-group16-sha512,"
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group14-sha256");
+#endif
}
ssh_string_free_char(value);