From a9c8f942a539953c128781254773bc6c241e2b35 Mon Sep 17 00:00:00 2001 From: Sahana Prasad Date: Thu, 11 Sep 2025 13:22:09 +0200 Subject: [PATCH] kex: Implement mlkem768x25519-sha256 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The implementation largely follows that of sntrup761x25519-sha512. Most of the work was done by Sahana with the help of Claude, Pavol provided fixes to match specs and did a final clean up. Co-Authored-By: Sahana Prasad Co-Authored-By: Pavol Žáčik Co-Authored-By: Claude Signed-off-by: Pavol Žáčik Reviewed-by: Jakub Jelen --- ConfigureChecks.cmake | 5 + config.h.cmake | 3 + doc/mainpage.dox | 2 +- include/libssh/crypto.h | 12 + include/libssh/mlkem768.h | 63 +++ include/libssh/ssh2.h | 2 + src/CMakeLists.txt | 7 + src/client.c | 8 + src/kex.c | 67 ++- src/mlkem768.c | 674 ++++++++++++++++++++++++++++++ src/session.c | 4 + src/wrapper.c | 8 + tests/CMakeLists.txt | 2 +- tests/client/torture_algorithms.c | 21 + tests/pkd/pkd_hello.c | 19 + tests/tests_config.h.cmake | 1 + tests/unittests/torture_options.c | 12 + 17 files changed, 907 insertions(+), 3 deletions(-) create mode 100644 include/libssh/mlkem768.h create mode 100644 src/mlkem768.c 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);