diff --git a/ConfigureChecks.cmake b/ConfigureChecks.cmake index 7c1ebb4d..ead10182 100644 --- a/ConfigureChecks.cmake +++ b/ConfigureChecks.cmake @@ -234,6 +234,9 @@ if (GCRYPT_FOUND) set(HAVE_GCRYPT_CHACHA_POLY 1) set(HAVE_GCRYPT_CURVE25519 1) endif (NOT GCRYPT_VERSION VERSION_LESS "1.7.0") + if (GCRYPT_VERSION VERSION_GREATER_EQUAL "1.10.1") + set(HAVE_MLKEM 1) + endif () endif (GCRYPT_FOUND) if (MBEDTLS_FOUND) diff --git a/include/libssh/crypto.h b/include/libssh/crypto.h index b37f2bab..0014f0f6 100644 --- a/include/libssh/crypto.h +++ b/include/libssh/crypto.h @@ -160,7 +160,12 @@ struct ssh_crypto_struct { ssh_curve25519_pubkey curve25519_server_pubkey; #endif #ifdef HAVE_MLKEM +#ifdef HAVE_LIBGCRYPT + unsigned char *mlkem_privkey; + size_t mlkem_privkey_len; +#else EVP_PKEY *mlkem_privkey; +#endif ssh_string mlkem_client_pubkey; ssh_string mlkem_ciphertext; #endif diff --git a/include/libssh/mlkem.h b/include/libssh/mlkem.h index 94ef6ae4..dc318fd4 100644 --- a/include/libssh/mlkem.h +++ b/include/libssh/mlkem.h @@ -36,7 +36,12 @@ extern "C" { struct mlkem_type_info { size_t pubkey_size; size_t ciphertext_size; +#ifdef HAVE_LIBGCRYPT + size_t privkey_size; + enum gcry_kem_algos alg; +#else const char *name; +#endif }; extern const struct mlkem_type_info MLKEM768_INFO; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eca80e38..b2f9589b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -195,6 +195,13 @@ if (WITH_GCRYPT) curve25519_gcrypt.c ) endif(HAVE_GCRYPT_CURVE25519) + + if (HAVE_MLKEM) + set(libssh_SRCS + ${libssh_SRCS} + mlkem_gcrypt.c + ) + endif (HAVE_MLKEM) elseif (WITH_MBEDTLS) set(libssh_SRCS ${libssh_SRCS} @@ -248,6 +255,12 @@ else (WITH_GCRYPT) chachapoly.c ) endif (NOT HAVE_OPENSSL_EVP_CHACHA20) + if (HAVE_MLKEM) + set(libssh_SRCS + ${libssh_SRCS} + mlkem_crypto.c + ) + endif (HAVE_MLKEM) endif (WITH_GCRYPT) if (WITH_SFTP) @@ -304,7 +317,6 @@ if (HAVE_MLKEM) set(libssh_SRCS ${libssh_SRCS} hybrid_mlkem.c - mlkem_crypto.c mlkem.c ) endif (HAVE_MLKEM) diff --git a/src/ecdh_gcrypt.c b/src/ecdh_gcrypt.c index b2c61555..f5635053 100644 --- a/src/ecdh_gcrypt.c +++ b/src/ecdh_gcrypt.c @@ -36,16 +36,25 @@ /** @internal * @brief Map the given key exchange enum value to its curve name. */ -static const char *ecdh_kex_type_to_curve(enum ssh_key_exchange_e kex_type) { - if (kex_type == SSH_KEX_ECDH_SHA2_NISTP256 || - kex_type == SSH_GSS_KEX_ECDH_NISTP256_SHA256) { +static const char *ecdh_kex_type_to_curve(enum ssh_key_exchange_e kex_type) +{ + switch (kex_type) { + case SSH_KEX_ECDH_SHA2_NISTP256: + case SSH_GSS_KEX_ECDH_NISTP256_SHA256: +#ifdef HAVE_MLKEM + case SSH_KEX_MLKEM768NISTP256_SHA256: +#endif return "NIST P-256"; - } else if (kex_type == SSH_KEX_ECDH_SHA2_NISTP384) { + case SSH_KEX_ECDH_SHA2_NISTP384: +#ifdef HAVE_MLKEM + case SSH_KEX_MLKEM1024NISTP384_SHA384: +#endif return "NIST P-384"; - } else if (kex_type == SSH_KEX_ECDH_SHA2_NISTP521) { + case SSH_KEX_ECDH_SHA2_NISTP521: return "NIST P-521"; + default: + return NULL; } - return NULL; } /** @internal diff --git a/src/hybrid_mlkem.c b/src/hybrid_mlkem.c index 18a0744d..36c0bd0b 100644 --- a/src/hybrid_mlkem.c +++ b/src/hybrid_mlkem.c @@ -24,12 +24,14 @@ #include "config.h" -#include "libssh/bignum.h" #include "libssh/buffer.h" #include "libssh/hybrid_mlkem.h" #include "libssh/pki.h" #include "libssh/ssh2.h" +/* sorry, this needs to come last to avoid header dependency issues */ +#include "libssh/bignum.h" + static SSH_PACKET_CALLBACK(ssh_packet_client_hybrid_mlkem_reply); static ssh_packet_callback dh_client_callbacks[] = { diff --git a/src/mlkem_gcrypt.c b/src/mlkem_gcrypt.c new file mode 100644 index 00000000..9e5eec3a --- /dev/null +++ b/src/mlkem_gcrypt.c @@ -0,0 +1,202 @@ +/* + * This file is part of the SSH Library + * + * Copyright (c) 2025 by Red Hat, Inc. + * + * Author: Jakub Jelen + * + * 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/crypto.h" +#include "libssh/mlkem.h" +#include "libssh/session.h" + +#include + +const struct mlkem_type_info MLKEM768_INFO = { + .pubkey_size = GCRY_KEM_MLKEM768_PUBKEY_LEN, + .privkey_size = GCRY_KEM_MLKEM768_SECKEY_LEN, + .ciphertext_size = GCRY_KEM_MLKEM768_CIPHER_LEN, + .alg = GCRY_KEM_MLKEM768, +}; + +const struct mlkem_type_info MLKEM1024_INFO = { + .pubkey_size = GCRY_KEM_MLKEM1024_PUBKEY_LEN, + .privkey_size = GCRY_KEM_MLKEM1024_SECKEY_LEN, + .ciphertext_size = GCRY_KEM_MLKEM1024_CIPHER_LEN, + .alg = GCRY_KEM_MLKEM1024, +}; + +int ssh_mlkem_init(ssh_session session) +{ + int ret = SSH_ERROR; + struct ssh_crypto_struct *crypto = session->next_crypto; + const struct mlkem_type_info *mlkem_info = NULL; + ssh_string pubkey = NULL; + unsigned char *privkey = NULL, *pubkey_data = NULL; + gcry_error_t err; + + mlkem_info = kex_type_to_mlkem_info(crypto->kex_type); + if (mlkem_info == NULL) { + SSH_LOG(SSH_LOG_WARNING, "Unknown ML-KEM type"); + goto cleanup; + } + + privkey = malloc(mlkem_info->privkey_size); + if (privkey == NULL) { + ssh_set_error_oom(session); + goto cleanup; + } + + pubkey = ssh_string_new(mlkem_info->pubkey_size); + if (pubkey == NULL) { + ssh_set_error_oom(session); + goto cleanup; + } + + pubkey_data = ssh_string_data(pubkey); + err = gcry_kem_keypair(mlkem_info->alg, + pubkey_data, + mlkem_info->pubkey_size, + privkey, + mlkem_info->privkey_size); + if (err) { + SSH_LOG(SSH_LOG_TRACE, + "Failed to generate ML-KEM key: %s", + gpg_strerror(err)); + goto cleanup; + } + + ssh_string_free(crypto->mlkem_client_pubkey); + crypto->mlkem_client_pubkey = pubkey; + pubkey = NULL; + + free(crypto->mlkem_privkey); + crypto->mlkem_privkey = privkey; + crypto->mlkem_privkey_len = mlkem_info->privkey_size; + privkey = NULL; + + ret = SSH_OK; + +cleanup: + ssh_string_free(pubkey); + if (privkey != NULL) { + ssh_burn(privkey, mlkem_info->privkey_size); + free(privkey); + } + return ret; +} + +int ssh_mlkem_encapsulate(ssh_session session, + ssh_mlkem_shared_secret shared_secret) +{ + int ret = SSH_ERROR; + const struct mlkem_type_info *mlkem_info = NULL; + struct ssh_crypto_struct *crypto = session->next_crypto; + const unsigned char *pubkey_data = NULL; + unsigned char *ciphertext_data = NULL; + ssh_string ciphertext = NULL; + ssh_string pubkey = crypto->mlkem_client_pubkey; + gcry_error_t err; + + if (pubkey == NULL) { + SSH_LOG(SSH_LOG_WARNING, "Missing pubkey in session"); + return SSH_ERROR; + } + + mlkem_info = kex_type_to_mlkem_info(crypto->kex_type); + if (mlkem_info == NULL) { + SSH_LOG(SSH_LOG_WARNING, "Unknown ML-KEM type"); + return SSH_ERROR; + } + + ciphertext = ssh_string_new(mlkem_info->ciphertext_size); + if (ciphertext == NULL) { + ssh_set_error_oom(session); + return SSH_ERROR; + } + + pubkey_data = ssh_string_data(pubkey); + ciphertext_data = ssh_string_data(ciphertext); + err = gcry_kem_encap(mlkem_info->alg, + pubkey_data, + mlkem_info->pubkey_size, + ciphertext_data, + mlkem_info->ciphertext_size, + shared_secret, + MLKEM_SHARED_SECRET_SIZE, + NULL, + 0); + if (err) { + SSH_LOG(SSH_LOG_TRACE, + "Failed to encapsulate ML-KEM shared secret: %s", + gpg_strerror(err)); + goto cleanup; + } + + ssh_string_free(crypto->mlkem_ciphertext); + crypto->mlkem_ciphertext = ciphertext; + ciphertext = NULL; + + ret = SSH_OK; + +cleanup: + ssh_string_free(ciphertext); + return ret; +} + +int ssh_mlkem_decapsulate(const ssh_session session, + ssh_mlkem_shared_secret shared_secret) +{ + const struct mlkem_type_info *mlkem_info = NULL; + struct ssh_crypto_struct *crypto = session->next_crypto; + ssh_string ciphertext = NULL; + unsigned char *ciphertext_data = NULL; + gcry_error_t err; + + ciphertext = crypto->mlkem_ciphertext; + if (ciphertext == NULL) { + SSH_LOG(SSH_LOG_WARNING, "Missing ciphertext in session"); + return SSH_ERROR; + } + + mlkem_info = kex_type_to_mlkem_info(crypto->kex_type); + if (mlkem_info == NULL) { + SSH_LOG(SSH_LOG_WARNING, "Unknown ML-KEM type"); + return SSH_ERROR; + } + + ciphertext_data = ssh_string_data(ciphertext); + err = gcry_kem_decap(mlkem_info->alg, + crypto->mlkem_privkey, + mlkem_info->privkey_size, + ciphertext_data, + mlkem_info->ciphertext_size, + shared_secret, + MLKEM_SHARED_SECRET_SIZE, + NULL, + 0); + if (err) { + SSH_LOG(SSH_LOG_TRACE, + "Failed to decapsulate ML-KEM shared secret: %s", + gpg_strerror(err)); + return SSH_ERROR; + } + + return SSH_OK; +} diff --git a/src/wrapper.c b/src/wrapper.c index 3ecb607b..fb454849 100644 --- a/src/wrapper.c +++ b/src/wrapper.c @@ -231,7 +231,15 @@ void crypto_free(struct ssh_crypto_struct *crypto) } #ifdef HAVE_MLKEM +#ifdef HAVE_LIBGCRYPT + if (crypto->mlkem_privkey != NULL) { + ssh_burn(crypto->mlkem_privkey, crypto->mlkem_privkey_len); + SAFE_FREE(crypto->mlkem_privkey); + crypto->mlkem_privkey_len = 0; + } +#else EVP_PKEY_free(crypto->mlkem_privkey); +#endif ssh_string_burn(crypto->hybrid_shared_secret); ssh_string_free(crypto->mlkem_client_pubkey); ssh_string_free(crypto->mlkem_ciphertext);