kex: Implement remaining hybrid ML-KEM methods

This builds on top of a9c8f94. The pure ML-KEM
code is now separated from the hybrid parts,
with the hybrid implementation generalized to
support NIST curves.

Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
This commit is contained in:
Pavol Žáčik
2025-11-18 13:36:25 +01:00
committed by Jakub Jelen
parent 7911580304
commit 0ef79018b3
21 changed files with 1494 additions and 793 deletions

View File

@@ -19,7 +19,7 @@ the interesting functions as you go.
The libssh library provides:
- <strong>Key Exchange Methods</strong>: <i>sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, mlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521</i>, diffie-hellman-group1-sha1, diffie-hellman-group14-sha1
- <strong>Key Exchange Methods</strong>: <i>sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, mlkem768x25519-sha256, mlkem768nistp256-sha256, mlkem1024nistp384-sha384, curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521</i>, diffie-hellman-group1-sha1, diffie-hellman-group14-sha1
- <strong>Public Key Algorithms</strong>: ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, ssh-rsa, rsa-sha2-512, rsa-sha2-256
- <strong>Ciphers</strong>: <i>aes256-ctr, aes192-ctr, aes128-ctr</i>, aes256-cbc (rijndael-cbc@lysator.liu.se), aes192-cbc, aes128-cbc, 3des-cbc, blowfish-cbc
- <strong>Compression Schemes</strong>: zlib, <i>zlib@openssh.com</i>, none

View File

@@ -50,9 +50,6 @@
#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
@@ -93,6 +90,10 @@ enum ssh_key_exchange_e {
#ifdef HAVE_MLKEM
/* mlkem768x25519-sha256 */
SSH_KEX_MLKEM768X25519_SHA256,
/* mlkem768nistp256-sha256 */
SSH_KEX_MLKEM768NISTP256_SHA256,
/* mlkem1024nistp384-sha384 */
SSH_KEX_MLKEM1024NISTP384_SHA384,
#endif /* HAVE_MLKEM */
};
@@ -117,6 +118,9 @@ struct dh_ctx;
struct ssh_crypto_struct {
bignum shared_secret;
ssh_string hybrid_client_init;
ssh_string hybrid_server_reply;
ssh_string hybrid_shared_secret;
struct dh_ctx *dh_ctx;
#ifdef WITH_GEX
size_t dh_pmin; size_t dh_pn; size_t dh_pmax; /* preferred group parameters */
@@ -148,9 +152,9 @@ struct ssh_crypto_struct {
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;
EVP_PKEY *mlkem_privkey;
ssh_string mlkem_client_pubkey;
ssh_string mlkem_ciphertext;
#endif
#ifdef HAVE_SNTRUP761
ssh_sntrup761_privkey sntrup761_privkey;

View File

@@ -0,0 +1,48 @@
/*
* This file is part of the SSH Library
*
* Copyright (c) 2025 by Red Hat, Inc.
*
* Author: Sahana Prasad <sahana@redhat.com>
* Author: Pavol Žáčik <pzacik@redhat.com>
* 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 HYBRID_MLKEM_H_
#define HYBRID_MLKEM_H_
#include "libssh/mlkem.h"
#include "libssh/wrapper.h"
#include "config.h"
#ifdef __cplusplus
extern "C" {
#endif
int ssh_client_hybrid_mlkem_init(ssh_session session);
void ssh_client_hybrid_mlkem_remove_callbacks(ssh_session session);
#ifdef WITH_SERVER
void ssh_server_hybrid_mlkem_init(ssh_session session);
#endif /* WITH_SERVER */
#ifdef __cplusplus
}
#endif
#endif /* HYBRID_MLKEM_H_ */

64
include/libssh/mlkem.h Normal file
View File

@@ -0,0 +1,64 @@
/*
* This file is part of the SSH Library
*
* Copyright (c) 2025 by Red Hat, Inc.
*
* Author: Pavol Žáčik <pzacik@redhat.com>
*
* 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.
*/
#ifndef MLKEM_H_
#define MLKEM_H_
#include "libssh/crypto.h"
#include "libssh/libssh.h"
#include "libssh/session.h"
#include "config.h"
#ifdef __cplusplus
extern "C" {
#endif
struct mlkem_type_info {
size_t pubkey_size;
size_t ciphertext_size;
const char *name;
};
extern const struct mlkem_type_info MLKEM768_INFO;
extern const struct mlkem_type_info MLKEM1024_INFO;
#define MLKEM_SHARED_SECRET_SIZE 32
typedef unsigned char ssh_mlkem_shared_secret[MLKEM_SHARED_SECRET_SIZE];
const struct mlkem_type_info *
kex_type_to_mlkem_info(enum ssh_key_exchange_e kex_type);
int ssh_mlkem_init(ssh_session session);
int ssh_mlkem_encapsulate(ssh_session session,
ssh_mlkem_shared_secret shared_secret);
int ssh_mlkem_decapsulate(const ssh_session session,
ssh_mlkem_shared_secret shared_secret);
#ifdef __cplusplus
}
#endif
#endif /* MLKEM_H_ */

View File

@@ -1,63 +0,0 @@
/*
* This file is part of the SSH Library
*
* Copyright (c) 2025 by Red Hat, Inc.
*
* Author: Sahana Prasad <sahana@redhat.com>
* 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_ */

View File

@@ -302,7 +302,9 @@ endif (NOT WITH_NACL)
if (HAVE_MLKEM)
set(libssh_SRCS
${libssh_SRCS}
mlkem768.c
hybrid_mlkem.c
mlkem_crypto.c
mlkem.c
)
endif (HAVE_MLKEM)

View File

@@ -47,7 +47,7 @@
#include "libssh/pki.h"
#include "libssh/kex.h"
#ifdef HAVE_MLKEM
#include "libssh/mlkem768.h"
#include "libssh/hybrid_mlkem.h"
#endif
#ifndef _WIN32
@@ -301,7 +301,9 @@ int dh_handshake(ssh_session session)
#endif
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768X25519_SHA256:
rc = ssh_client_mlkem768x25519_init(session);
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
rc = ssh_client_hybrid_mlkem_init(session);
break;
#endif
default:

View File

@@ -51,18 +51,26 @@ static int ecdh_kex_type_to_curve(enum ssh_key_exchange_e kex_type) {
#else
static const char *ecdh_kex_type_to_curve(enum ssh_key_exchange_e kex_type) {
#endif /* OPENSSL_VERSION_NUMBER */
if (kex_type == SSH_KEX_ECDH_SHA2_NISTP256) {
return NISTP256;
} else if (kex_type == SSH_KEX_ECDH_SHA2_NISTP384) {
return NISTP384;
} else if (kex_type == SSH_KEX_ECDH_SHA2_NISTP521) {
return NISTP521;
}
#if OPENSSL_VERSION_NUMBER < 0x30000000L
return SSH_ERROR;
#else
return NULL;
switch (kex_type) {
case SSH_KEX_ECDH_SHA2_NISTP256:
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768NISTP256_SHA256:
#endif
return NISTP256;
case SSH_KEX_ECDH_SHA2_NISTP384:
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM1024NISTP384_SHA384:
#endif
return NISTP384;
case SSH_KEX_ECDH_SHA2_NISTP521:
return NISTP521;
default:
#if OPENSSL_VERSION_NUMBER < 0x30000000L
return SSH_ERROR;
#else
return NULL;
#endif
}
}
/* @internal

870
src/hybrid_mlkem.c Normal file
View File

@@ -0,0 +1,870 @@
/*
* This file is part of the SSH Library
*
* Copyright (c) 2025 by Red Hat, Inc.
*
* Author: Sahana Prasad <sahana@redhat.com>
* Author: Pavol Žáčik <pzacik@redhat.com>
* 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/hybrid_mlkem.h"
#include "libssh/pki.h"
#include "libssh/ssh2.h"
static SSH_PACKET_CALLBACK(ssh_packet_client_hybrid_mlkem_reply);
static ssh_packet_callback dh_client_callbacks[] = {
ssh_packet_client_hybrid_mlkem_reply,
};
static struct ssh_packet_callbacks_struct ssh_hybrid_mlkem_client_callbacks = {
.start = SSH2_MSG_KEX_HYBRID_REPLY,
.n_callbacks = 1,
.callbacks = dh_client_callbacks,
.user = NULL,
};
static ssh_string derive_ecdh_secret(ssh_session session)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
ssh_string secret = NULL;
size_t secret_size;
int rc;
switch (crypto->kex_type) {
case SSH_KEX_MLKEM768X25519_SHA256:
secret = ssh_string_new(CURVE25519_PUBKEY_SIZE);
if (secret == NULL) {
ssh_set_error_oom(session);
return NULL;
}
rc = ssh_curve25519_create_k(session, ssh_string_data(secret));
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Curve25519 secret derivation failed");
ssh_string_free(secret);
return NULL;
}
break;
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
rc = ecdh_build_k(session);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "ECDH secret derivation failed");
return NULL;
}
secret_size = bignum_num_bytes(crypto->shared_secret);
secret = ssh_string_new(secret_size);
if (secret == NULL) {
ssh_set_error_oom(session);
bignum_safe_free(crypto->shared_secret);
return NULL;
}
bignum_bn2bin(crypto->shared_secret, secret_size, ssh_string_data(secret));
bignum_safe_free(crypto->shared_secret);
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported KEX type");
return NULL;
}
return secret;
}
static int derive_hybrid_secret(ssh_session session,
ssh_mlkem_shared_secret mlkem_shared_secret,
ssh_string ecdh_shared_secret)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
ssh_buffer combined_secret = NULL;
int (*digest)(const unsigned char *, size_t, unsigned char *) = NULL;
size_t digest_len;
int rc, ret = SSH_ERROR;
switch (crypto->kex_type) {
case SSH_KEX_MLKEM768X25519_SHA256:
case SSH_KEX_MLKEM768NISTP256_SHA256:
digest = sha256;
digest_len = SHA256_DIGEST_LEN;
break;
case SSH_KEX_MLKEM1024NISTP384_SHA384:
digest = sha384;
digest_len = SHA384_DIGEST_LEN;
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported KEX type");
goto cleanup;
}
/* Concatenate the two shared secrets */
combined_secret = ssh_buffer_new();
if (combined_secret == NULL) {
ssh_set_error_oom(session);
goto cleanup;
}
ssh_buffer_set_secure(combined_secret);
rc = ssh_buffer_pack(combined_secret,
"PP",
MLKEM_SHARED_SECRET_SIZE,
mlkem_shared_secret,
ssh_string_len(ecdh_shared_secret),
ssh_string_data(ecdh_shared_secret));
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to concatenate shared secrets");
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("Concatenated shared secrets",
ssh_buffer_get(combined_secret),
ssh_buffer_get_len(combined_secret));
#endif
/* Store the hashed combined shared secrets */
ssh_string_burn(crypto->hybrid_shared_secret);
ssh_string_free(crypto->hybrid_shared_secret);
crypto->hybrid_shared_secret = ssh_string_new(digest_len);
if (crypto->hybrid_shared_secret == NULL) {
ssh_set_error_oom(session);
goto cleanup;
}
rc = digest(ssh_buffer_get(combined_secret),
ssh_buffer_get_len(combined_secret),
ssh_string_data(crypto->hybrid_shared_secret));
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Shared secret hashing failed");
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("Hybrid shared secret",
ssh_string_data(crypto->hybrid_shared_secret),
digest_len);
#endif
ret = SSH_OK;
cleanup:
ssh_buffer_free(combined_secret);
return ret;
}
int ssh_client_hybrid_mlkem_init(ssh_session session)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
ssh_buffer client_init_buffer = NULL;
int rc, ret = SSH_ERROR;
SSH_LOG(SSH_LOG_TRACE, "Initializing hybrid ML-KEM key exchange");
/* Prepare a buffer to concatenate ML-KEM + ECDH public keys */
client_init_buffer = ssh_buffer_new();
if (client_init_buffer == NULL) {
ssh_set_error_oom(session);
goto cleanup;
}
/* Generate an ML-KEM keypair */
rc = ssh_mlkem_init(session);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to generate an ML-KEM keypair");
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ML-KEM client pubkey",
ssh_string_data(crypto->mlkem_client_pubkey),
ssh_string_len(crypto->mlkem_client_pubkey));
#endif
/* Generate an ECDH keypair and concatenate the public keys */
switch (crypto->kex_type) {
case SSH_KEX_MLKEM768X25519_SHA256:
rc = ssh_curve25519_init(session);
if (rc != SSH_OK) {
ssh_set_error(session,
SSH_FATAL,
"Failed to generate a Curve25519 ECDH keypair");
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("Curve25519 client pubkey",
crypto->curve25519_client_pubkey,
CURVE25519_PUBKEY_SIZE);
#endif
rc = ssh_buffer_pack(client_init_buffer,
"PP",
ssh_string_len(crypto->mlkem_client_pubkey),
ssh_string_data(crypto->mlkem_client_pubkey),
CURVE25519_PUBKEY_SIZE,
crypto->curve25519_client_pubkey);
break;
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
rc = ssh_ecdh_init(session);
if (rc != SSH_OK) {
ssh_set_error(session,
SSH_FATAL,
"Failed to generate a NIST-curve ECDH keypair");
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ECDH client pubkey",
ssh_string_data(crypto->ecdh_client_pubkey),
ssh_string_len(crypto->ecdh_client_pubkey));
#endif
rc = ssh_buffer_pack(client_init_buffer,
"PP",
ssh_string_len(crypto->mlkem_client_pubkey),
ssh_string_data(crypto->mlkem_client_pubkey),
ssh_string_len(crypto->ecdh_client_pubkey),
ssh_string_data(crypto->ecdh_client_pubkey));
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported KEX type");
goto cleanup;
}
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to construct client init buffer");
goto cleanup;
}
/* Convert the client init buffer to an SSH string */
ssh_string_free(crypto->hybrid_client_init);
crypto->hybrid_client_init = ssh_string_new(ssh_buffer_get_len(client_init_buffer));
if (crypto->hybrid_client_init == NULL) {
ssh_set_error_oom(session);
goto cleanup;
}
rc = ssh_string_fill(crypto->hybrid_client_init,
ssh_buffer_get(client_init_buffer),
ssh_buffer_get_len(client_init_buffer));
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to convert client init to string");
goto cleanup;
}
rc = ssh_buffer_pack(session->out_buffer,
"bS",
SSH2_MSG_KEX_HYBRID_INIT,
crypto->hybrid_client_init);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to construct SSH_MSG_KEX_HYBRID_INIT");
goto cleanup;
}
ssh_packet_set_callbacks(session, &ssh_hybrid_mlkem_client_callbacks);
session->dh_handshake_state = DH_STATE_INIT_SENT;
rc = ssh_packet_send(session);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to send SSH_MSG_KEX_HYBRID_INIT");
goto cleanup;
}
ret = SSH_OK;
cleanup:
ssh_buffer_free(client_init_buffer);
return ret;
}
static SSH_PACKET_CALLBACK(ssh_packet_client_hybrid_mlkem_reply)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
const struct mlkem_type_info *mlkem_info = NULL;
ssh_string pubkey_blob = NULL;
ssh_string signature = NULL;
ssh_mlkem_shared_secret mlkem_shared_secret;
ssh_string ecdh_shared_secret = NULL;
ssh_buffer server_reply_buffer = NULL;
size_t read_len;
size_t ecdh_server_pubkey_size;
int rc;
(void)type;
(void)user;
SSH_LOG(SSH_LOG_TRACE, "Received ML-KEM hybrid server reply");
ssh_client_hybrid_mlkem_remove_callbacks(session);
mlkem_info = kex_type_to_mlkem_info(crypto->kex_type);
if (mlkem_info == NULL) {
ssh_set_error(session, SSH_FATAL, "Unknown ML-KEM type");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
pubkey_blob = ssh_buffer_get_ssh_string(packet);
if (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, pubkey_blob);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to import public key");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Get server reply containing ML-KEM ciphertext + ECDH public key */
ssh_string_free(crypto->hybrid_server_reply);
crypto->hybrid_server_reply = ssh_buffer_get_ssh_string(packet);
if (crypto->hybrid_server_reply == NULL) {
ssh_set_error(session, SSH_FATAL, "No server reply in packet");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
server_reply_buffer = ssh_buffer_new();
if (server_reply_buffer == NULL) {
ssh_set_error_oom(session);
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
rc = ssh_buffer_add_data(server_reply_buffer,
ssh_string_data(crypto->hybrid_server_reply),
ssh_string_len(crypto->hybrid_server_reply));
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to pack server reply to a buffer");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Store ML-KEM ciphertext for decapsulation and sessionid calculation */
ssh_string_free(crypto->mlkem_ciphertext);
crypto->mlkem_ciphertext = ssh_string_new(mlkem_info->ciphertext_size);
if (crypto->mlkem_ciphertext == NULL) {
ssh_set_error_oom(session);
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
read_len = ssh_buffer_get_data(server_reply_buffer,
ssh_string_data(crypto->mlkem_ciphertext),
mlkem_info->ciphertext_size);
if (read_len != mlkem_info->ciphertext_size) {
ssh_set_error(session,
SSH_FATAL,
"Could not read ML-KEM ciphertext from "
"the server reply buffer, buffer too short");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ML-KEM ciphertext",
ssh_string_data(crypto->mlkem_ciphertext),
ssh_string_len(crypto->mlkem_ciphertext));
#endif
/* Extract server ECDH public key */
switch (crypto->kex_type) {
case SSH_KEX_MLKEM768X25519_SHA256:
read_len = ssh_buffer_get_data(server_reply_buffer,
crypto->curve25519_server_pubkey,
CURVE25519_PUBKEY_SIZE);
if (read_len != CURVE25519_PUBKEY_SIZE) {
ssh_set_error(session,
SSH_FATAL,
"Could not read Curve25519 pubkey from "
"the server reply buffer, buffer too short");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
if (ssh_buffer_get_len(server_reply_buffer) > 0) {
ssh_set_error(session,
SSH_FATAL,
"Unrecognized data in the server reply buffer");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("Curve25519 server pubkey",
crypto->curve25519_server_pubkey,
CURVE25519_PUBKEY_SIZE);
#endif
break;
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
ecdh_server_pubkey_size = ssh_buffer_get_len(server_reply_buffer);
ssh_string_free(crypto->ecdh_server_pubkey);
crypto->ecdh_server_pubkey = ssh_string_new(ecdh_server_pubkey_size);
if (crypto->ecdh_server_pubkey == NULL) {
ssh_set_error_oom(session);
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
ssh_buffer_get_data(server_reply_buffer,
ssh_string_data(crypto->ecdh_server_pubkey),
ecdh_server_pubkey_size);
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ECDH server pubkey",
ssh_string_data(crypto->ecdh_server_pubkey),
ssh_string_len(crypto->ecdh_server_pubkey));
#endif
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported KEX type");
goto cleanup;
}
/* Decapsulate ML-KEM shared secret */
rc = ssh_mlkem_decapsulate(session, mlkem_shared_secret);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "ML-KEM decapsulation failed");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ML-KEM shared secret",
mlkem_shared_secret,
MLKEM_SHARED_SECRET_SIZE);
#endif
/* Derive the classical ECDH shared secret */
ecdh_shared_secret = derive_ecdh_secret(session);
if (ecdh_shared_secret == NULL) {
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ECDH shared secret",
ssh_string_data(ecdh_shared_secret),
ssh_string_len(ecdh_shared_secret));
#endif
/* Derive the final shared secret */
rc = derive_hybrid_secret(session, mlkem_shared_secret, ecdh_shared_secret);
if (rc != SSH_OK) {
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Get signature for verification */
signature = ssh_buffer_get_ssh_string(packet);
if (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 = signature;
/* Send the MSG_NEWKEYS */
rc = ssh_packet_send_newkeys(session);
if (rc != SSH_OK) {
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:
explicit_bzero(mlkem_shared_secret, sizeof(mlkem_shared_secret));
ssh_string_burn(ecdh_shared_secret);
ssh_string_free(ecdh_shared_secret);
ssh_string_free(pubkey_blob);
ssh_buffer_free(server_reply_buffer);
return SSH_PACKET_USED;
}
void ssh_client_hybrid_mlkem_remove_callbacks(ssh_session session)
{
ssh_packet_remove_callbacks(session, &ssh_hybrid_mlkem_client_callbacks);
}
#ifdef WITH_SERVER
static SSH_PACKET_CALLBACK(ssh_packet_server_hybrid_mlkem_init);
static ssh_packet_callback dh_server_callbacks[] = {
ssh_packet_server_hybrid_mlkem_init,
};
static struct ssh_packet_callbacks_struct ssh_hybrid_mlkem_server_callbacks = {
.start = SSH2_MSG_KEX_HYBRID_INIT,
.n_callbacks = 1,
.callbacks = dh_server_callbacks,
.user = NULL,
};
static SSH_PACKET_CALLBACK(ssh_packet_server_hybrid_mlkem_init)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
const struct mlkem_type_info *mlkem_info = NULL;
ssh_string ecdh_shared_secret = NULL;
ssh_mlkem_shared_secret mlkem_shared_secret;
ssh_buffer server_reply_buffer = NULL;
ssh_buffer client_init_buffer = NULL;
ssh_key privkey = NULL;
enum ssh_digest_e digest = SSH_DIGEST_AUTO;
ssh_string signature = NULL;
ssh_string pubkey_blob = NULL;
size_t ecdh_client_pubkey_size;
size_t read_len;
int rc;
(void)type;
(void)user;
SSH_LOG(SSH_LOG_TRACE, "Received ML-KEM hybrid client init");
ssh_packet_remove_callbacks(session, &ssh_hybrid_mlkem_server_callbacks);
mlkem_info = kex_type_to_mlkem_info(crypto->kex_type);
if (mlkem_info == NULL) {
ssh_set_error(session, SSH_FATAL, "Unknown ML-KEM type");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Generate an ECDH keypair */
switch (crypto->kex_type) {
case SSH_KEX_MLKEM768X25519_SHA256:
rc = ssh_curve25519_init(session);
if (rc != SSH_OK) {
ssh_set_error(session,
SSH_FATAL,
"Failed to generate a Curve25519 ECDH keypair");
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("Curve25519 server pubkey",
crypto->curve25519_server_pubkey,
CURVE25519_PUBKEY_SIZE);
#endif
break;
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
rc = ssh_ecdh_init(session);
if (rc != SSH_OK) {
ssh_set_error(session,
SSH_FATAL,
"Failed to generate a NIST-curve ECDH keypair");
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ECDH server pubkey",
ssh_string_data(crypto->ecdh_server_pubkey),
ssh_string_len(crypto->ecdh_server_pubkey));
#endif
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported KEX type");
goto cleanup;
}
/* Get client init: ML-KEM public key + ECDH public key */
ssh_string_free(crypto->hybrid_client_init);
crypto->hybrid_client_init = ssh_buffer_get_ssh_string(packet);
if (crypto->hybrid_client_init == NULL) {
ssh_set_error(session, SSH_FATAL, "No client public keys in packet");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
client_init_buffer = ssh_buffer_new();
if (client_init_buffer == NULL) {
ssh_set_error_oom(session);
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
rc = ssh_buffer_add_data(client_init_buffer,
ssh_string_data(crypto->hybrid_client_init),
ssh_string_len(crypto->hybrid_client_init));
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to pack client init to a buffer");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Extract client ML-KEM public key */
ssh_string_free(crypto->mlkem_client_pubkey);
crypto->mlkem_client_pubkey = ssh_string_new(mlkem_info->pubkey_size);
if (crypto->mlkem_client_pubkey == NULL) {
ssh_set_error_oom(session);
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
read_len = ssh_buffer_get_data(client_init_buffer,
ssh_string_data(crypto->mlkem_client_pubkey),
mlkem_info->pubkey_size);
if (read_len != mlkem_info->pubkey_size) {
ssh_set_error(session,
SSH_FATAL,
"Could not read ML-KEM pubkey from "
"the client init buffer, buffer too short");
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ML-KEM client pubkey",
ssh_string_data(crypto->mlkem_client_pubkey),
ssh_string_len(crypto->mlkem_client_pubkey));
#endif
/* Extract client ECDH public key */
switch (crypto->kex_type) {
case SSH_KEX_MLKEM768X25519_SHA256:
read_len = ssh_buffer_get_data(client_init_buffer,
crypto->curve25519_client_pubkey,
CURVE25519_PUBKEY_SIZE);
if (read_len != CURVE25519_PUBKEY_SIZE) {
ssh_set_error(session,
SSH_FATAL,
"Could not read Curve25519 pubkey from "
"the client init buffer, buffer too short");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
if (ssh_buffer_get_len(client_init_buffer) > 0) {
ssh_set_error(session,
SSH_FATAL,
"Unrecognized data in the client init buffer");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("Curve25519 client pubkey",
crypto->curve25519_client_pubkey,
CURVE25519_PUBKEY_SIZE);
#endif
break;
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
ecdh_client_pubkey_size = ssh_buffer_get_len(client_init_buffer);
ssh_string_free(crypto->ecdh_client_pubkey);
crypto->ecdh_client_pubkey = ssh_string_new(ecdh_client_pubkey_size);
if (crypto->ecdh_client_pubkey == NULL) {
ssh_set_error_oom(session);
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
ssh_buffer_get_data(client_init_buffer,
ssh_string_data(crypto->ecdh_client_pubkey),
ecdh_client_pubkey_size);
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ECDH client pubkey",
ssh_string_data(crypto->ecdh_client_pubkey),
ssh_string_len(crypto->ecdh_client_pubkey));
#endif
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported KEX type");
goto cleanup;
}
/* Encapsulate an ML-KEM shared secret using client's ML-KEM public key */
rc = ssh_mlkem_encapsulate(session, mlkem_shared_secret);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "ML-KEM encapsulation failed");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ML-KEM shared secret",
mlkem_shared_secret,
MLKEM_SHARED_SECRET_SIZE);
ssh_log_hexdump("ML-KEM ciphertext",
ssh_string_data(crypto->mlkem_ciphertext),
ssh_string_len(crypto->mlkem_ciphertext));
#endif
/* Derive the classical ECDH shared secret */
ecdh_shared_secret = derive_ecdh_secret(session);
if (ecdh_shared_secret == NULL) {
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
#ifdef DEBUG_CRYPTO
ssh_log_hexdump("ECDH shared secret",
ssh_string_data(ecdh_shared_secret),
ssh_string_len(ecdh_shared_secret));
#endif
/* Derive the final shared secret */
rc = derive_hybrid_secret(session, mlkem_shared_secret, ecdh_shared_secret);
if (rc != SSH_OK) {
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Create server reply: ML-KEM ciphertext + ECDH public key */
server_reply_buffer = ssh_buffer_new();
if (server_reply_buffer == NULL) {
ssh_set_error_oom(session);
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
switch (crypto->kex_type) {
case SSH_KEX_MLKEM768X25519_SHA256:
rc = ssh_buffer_pack(server_reply_buffer,
"PP",
ssh_string_len(crypto->mlkem_ciphertext),
ssh_string_data(crypto->mlkem_ciphertext),
CURVE25519_PUBKEY_SIZE,
crypto->curve25519_server_pubkey);
break;
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
rc = ssh_buffer_pack(server_reply_buffer,
"PP",
ssh_string_len(crypto->mlkem_ciphertext),
ssh_string_data(crypto->mlkem_ciphertext),
ssh_string_len(crypto->ecdh_server_pubkey),
ssh_string_data(crypto->ecdh_server_pubkey));
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported KEX type");
goto cleanup;
}
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to construct server reply buffer");
goto cleanup;
}
/* Convert the reply buffer to an SSH string for sending */
ssh_string_free(crypto->hybrid_server_reply);
crypto->hybrid_server_reply = ssh_string_new(ssh_buffer_get_len(server_reply_buffer));
if (crypto->hybrid_server_reply == NULL) {
ssh_set_error_oom(session);
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
rc = ssh_string_fill(crypto->hybrid_server_reply,
ssh_buffer_get(server_reply_buffer),
ssh_buffer_get_len(server_reply_buffer));
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to convert reply buffer to string");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Add MSG_KEX_ECDH_REPLY header */
rc = ssh_buffer_add_u8(session->out_buffer, SSH2_MSG_KEX_HYBRID_REPLY);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to add MSG_KEX_HYBRID_REPLY to buffer");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Get server host key */
rc = ssh_get_key_params(session, &privkey, &digest);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not get server key params");
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, &pubkey_blob);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not export server public key");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Add server public key to output */
rc = ssh_buffer_add_ssh_string(session->out_buffer, pubkey_blob);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to add server hostkey to buffer");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Add server reply */
rc = ssh_buffer_add_ssh_string(session->out_buffer, crypto->hybrid_server_reply);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to add server reply to buffer");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
/* Sign the exchange hash */
signature = ssh_srv_pki_do_sign_sessionid(session, privkey, digest);
if (signature == 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, signature);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to add signature to buffer");
session->session_state = SSH_SESSION_STATE_ERROR;
goto cleanup;
}
rc = ssh_packet_send(session);
if (rc != SSH_OK) {
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_OK) {
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:
explicit_bzero(mlkem_shared_secret, sizeof(mlkem_shared_secret));
ssh_string_burn(ecdh_shared_secret);
ssh_string_free(ecdh_shared_secret);
ssh_string_free(pubkey_blob);
ssh_string_free(signature);
ssh_buffer_free(client_init_buffer);
ssh_buffer_free(server_reply_buffer);
return SSH_PACKET_USED;
}
void ssh_server_hybrid_mlkem_init(ssh_session session)
{
SSH_LOG(SSH_LOG_TRACE, "Setting up ML-KEM hybrid server callbacks");
ssh_packet_set_callbacks(session, &ssh_hybrid_mlkem_server_callbacks);
}
#endif /* WITH_SERVER */

View File

@@ -42,7 +42,7 @@
#include "libssh/curve25519.h"
#include "libssh/sntrup761.h"
#ifdef HAVE_MLKEM
#include "libssh/mlkem768.h"
#include "libssh/hybrid_mlkem.h"
#endif
#include "libssh/knownhosts.h"
#include "libssh/misc.h"
@@ -106,9 +106,11 @@
#endif /* HAVE_SNTRUP761 */
#ifdef HAVE_MLKEM
#define MLKEM768X25519 "mlkem768x25519-sha256,"
#define HYBRID_MLKEM "mlkem768x25519-sha256," \
"mlkem768nistp256-sha256," \
"mlkem1024nistp384-sha384,"
#else
#define MLKEM768X25519 ""
#define HYBRID_MLKEM ""
#endif /* HAVE_MLKEM */
#ifdef HAVE_ECC
@@ -174,7 +176,7 @@
#define CHACHA20 "chacha20-poly1305@openssh.com,"
#define DEFAULT_KEY_EXCHANGE \
MLKEM768X25519 \
HYBRID_MLKEM \
SNTRUP761X25519 \
CURVE25519 \
ECDH \
@@ -939,6 +941,10 @@ kex_select_kex_type(const char *kex)
#ifdef HAVE_MLKEM
} else if (strcmp(kex, "mlkem768x25519-sha256") == 0) {
return SSH_KEX_MLKEM768X25519_SHA256;
} else if (strcmp(kex, "mlkem768nistp256-sha256") == 0) {
return SSH_KEX_MLKEM768NISTP256_SHA256;
} else if (strcmp(kex, "mlkem1024nistp384-sha384") == 0) {
return SSH_KEX_MLKEM1024NISTP384_SHA384;
#endif
}
/* should not happen. We should be getting only valid names at this stage */
@@ -988,7 +994,9 @@ static void revert_kex_callbacks(ssh_session session)
#endif
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768X25519_SHA256:
ssh_client_mlkem768x25519_remove_callbacks(session);
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
ssh_client_hybrid_mlkem_remove_callbacks(session);
break;
#endif
}
@@ -1576,22 +1584,16 @@ int ssh_make_sessionid(ssh_session session)
#endif /* HAVE_SNTRUP761 */
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768X25519_SHA256:
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
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);
"SS",
session->next_crypto->hybrid_client_init,
session->next_crypto->hybrid_server_reply);
if (rc != SSH_OK) {
ssh_set_error(session,
SSH_FATAL,
"Failed to pack ML-KEM768 individual components");
"Failed to pack ML-KEM individual components");
goto error;
}
break;
@@ -1612,10 +1614,9 @@ int ssh_make_sessionid(ssh_session session)
break;
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768X25519_SHA256:
rc = ssh_buffer_pack(buf,
"F",
session->next_crypto->shared_secret,
SHA256_DIGEST_LEN);
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
rc = ssh_buffer_pack(buf, "S", session->next_crypto->hybrid_shared_secret);
break;
#endif /* HAVE_MLKEM */
default:
@@ -1655,6 +1656,7 @@ int ssh_make_sessionid(ssh_session session)
case SSH_KEX_CURVE25519_SHA256_LIBSSH_ORG:
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768X25519_SHA256:
case SSH_KEX_MLKEM768NISTP256_SHA256:
#endif
#ifdef WITH_GEX
case SSH_KEX_DH_GEX_SHA256:
@@ -1670,6 +1672,9 @@ int ssh_make_sessionid(ssh_session session)
session->next_crypto->secret_hash);
break;
case SSH_KEX_ECDH_SHA2_NISTP384:
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM1024NISTP384_SHA384:
#endif
session->next_crypto->digest_len = SHA384_DIGEST_LENGTH;
session->next_crypto->digest_type = SSH_KDF_SHA384;
session->next_crypto->secret_hash = malloc(session->next_crypto->digest_len);
@@ -1831,12 +1836,16 @@ 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,
crypto->digest_len);
break;
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768X25519_SHA256:
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
k_string = ssh_string_copy(crypto->hybrid_shared_secret);
break;
#endif /* HAVE_MLKEM */
default:
k_string = ssh_make_bignum_string(crypto->shared_secret);
break;

38
src/mlkem.c Normal file
View File

@@ -0,0 +1,38 @@
/*
* This file is part of the SSH Library
*
* Copyright (c) 2025 by Red Hat, Inc.
*
* Author: Pavol Žáčik <pzacik@redhat.com>
*
* 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/mlkem.h"
const struct mlkem_type_info *kex_type_to_mlkem_info(enum ssh_key_exchange_e kex_type)
{
switch (kex_type) {
case SSH_KEX_MLKEM768X25519_SHA256:
case SSH_KEX_MLKEM768NISTP256_SHA256:
return &MLKEM768_INFO;
case SSH_KEX_MLKEM1024NISTP384_SHA384:
return &MLKEM1024_INFO;
default:
return NULL;
}
}

View File

@@ -1,679 +0,0 @@
/*
* 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 <sahana@redhat.com>
* 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 <string.h>
#include <openssl/evp.h>
#include <openssl/err.h>
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));
goto cleanup;
}
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));
goto cleanup;
}
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));
goto cleanup;
}
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));
goto cleanup;
}
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));
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_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 */

238
src/mlkem_crypto.c Normal file
View File

@@ -0,0 +1,238 @@
/*
* This file is part of the SSH Library
*
* Copyright (c) 2025 by Red Hat, Inc.
*
* Author: Pavol Žáčik <pzacik@redhat.com>
*
* 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 <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/ml_kem.h>
const struct mlkem_type_info MLKEM768_INFO = {
.pubkey_size = OSSL_ML_KEM_768_PUBLIC_KEY_BYTES,
.ciphertext_size = OSSL_ML_KEM_768_CIPHERTEXT_BYTES,
.name = LN_ML_KEM_768,
};
const struct mlkem_type_info MLKEM1024_INFO = {
.pubkey_size = OSSL_ML_KEM_1024_PUBLIC_KEY_BYTES,
.ciphertext_size = OSSL_ML_KEM_1024_CIPHERTEXT_BYTES,
.name = LN_ML_KEM_1024,
};
int ssh_mlkem_init(ssh_session session)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
EVP_PKEY_CTX *ctx = NULL;
EVP_PKEY *pkey = NULL;
int rc, ret = SSH_ERROR;
const struct mlkem_type_info *mlkem_info = NULL;
ssh_string pubkey = NULL;
size_t pubkey_size;
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;
}
ctx = EVP_PKEY_CTX_new_from_name(NULL, mlkem_info->name, NULL);
if (ctx == NULL) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to create ML-KEM 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 keygen: %s",
ERR_error_string(ERR_get_error(), NULL));
goto cleanup;
}
rc = EVP_PKEY_keygen(ctx, &pkey);
if (rc != 1) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to perform ML-KEM keygen: %s",
ERR_error_string(ERR_get_error(), NULL));
goto cleanup;
}
EVP_PKEY_free(crypto->mlkem_privkey);
crypto->mlkem_privkey = pkey;
pubkey_size = mlkem_info->pubkey_size;
pubkey = ssh_string_new(pubkey_size);
if (pubkey == NULL) {
ssh_set_error_oom(session);
goto cleanup;
}
rc = EVP_PKEY_get_raw_public_key(pkey,
ssh_string_data(pubkey),
&pubkey_size);
if (rc != 1) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to extract ML-KEM public key: %s",
ERR_error_string(ERR_get_error(), NULL));
goto cleanup;
}
ssh_string_free(crypto->mlkem_client_pubkey);
crypto->mlkem_client_pubkey = pubkey;
pubkey = NULL;
ret = SSH_OK;
cleanup:
ssh_string_free(pubkey);
EVP_PKEY_CTX_free(ctx);
return ret;
}
int ssh_mlkem_encapsulate(ssh_session session,
ssh_mlkem_shared_secret shared_secret)
{
EVP_PKEY *pkey = NULL;
EVP_PKEY_CTX *ctx = NULL;
int rc, ret = SSH_ERROR;
const struct mlkem_type_info *mlkem_info = NULL;
struct ssh_crypto_struct *crypto = session->next_crypto;
const unsigned char *pubkey = ssh_string_data(crypto->mlkem_client_pubkey);
const size_t pubkey_len = ssh_string_len(crypto->mlkem_client_pubkey);
size_t shared_secret_size = MLKEM_SHARED_SECRET_SIZE;
ssh_string ciphertext = NULL;
size_t ciphertext_size;
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;
}
pkey = EVP_PKEY_new_raw_public_key_ex(NULL,
mlkem_info->name,
NULL,
pubkey,
pubkey_len);
if (pkey == NULL) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to create ML-KEM 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 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 encapsulation: %s",
ERR_error_string(ERR_get_error(), NULL));
goto cleanup;
}
ciphertext_size = mlkem_info->ciphertext_size;
ciphertext = ssh_string_new(ciphertext_size);
if (ciphertext == NULL) {
ssh_set_error_oom(session);
goto cleanup;
}
rc = EVP_PKEY_encapsulate(ctx,
ssh_string_data(ciphertext),
&ciphertext_size,
shared_secret,
&shared_secret_size);
if (rc != 1) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to perform ML-KEM encapsulation: %s",
ERR_error_string(ERR_get_error(), NULL));
goto cleanup;
}
ssh_string_free(crypto->mlkem_ciphertext);
crypto->mlkem_ciphertext = ciphertext;
ciphertext = NULL;
ret = SSH_OK;
cleanup:
ssh_string_free(ciphertext);
EVP_PKEY_free(pkey);
EVP_PKEY_CTX_free(ctx);
return ret;
}
int ssh_mlkem_decapsulate(const ssh_session session,
ssh_mlkem_shared_secret shared_secret)
{
EVP_PKEY_CTX *ctx = NULL;
int rc, ret = SSH_ERROR;
size_t shared_secret_size = MLKEM_SHARED_SECRET_SIZE;
struct ssh_crypto_struct *crypto = session->next_crypto;
ctx = EVP_PKEY_CTX_new_from_pkey(NULL, crypto->mlkem_privkey, NULL);
if (ctx == NULL) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to create ML-KEM 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 decapsulation: %s",
ERR_error_string(ERR_get_error(), NULL));
goto cleanup;
}
rc = EVP_PKEY_decapsulate(ctx,
shared_secret,
&shared_secret_size,
ssh_string_data(crypto->mlkem_ciphertext),
ssh_string_len(crypto->mlkem_ciphertext));
if (rc != 1) {
SSH_LOG(SSH_LOG_WARNING,
"Failed to perform ML-KEM decapsulation: %s",
ERR_error_string(ERR_get_error(), NULL));
goto cleanup;
}
ret = SSH_OK;
cleanup:
EVP_PKEY_CTX_free(ctx);
return ret;
}

View File

@@ -491,6 +491,10 @@ const char* ssh_get_kex_algo(ssh_session session) {
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768X25519_SHA256:
return "mlkem768x25519-sha256";
case SSH_KEX_MLKEM768NISTP256_SHA256:
return "mlkem768nistp256-sha256";
case SSH_KEX_MLKEM1024NISTP384_SHA384:
return "mlkem1024nistp384-sha384";
#endif /* HAVE_MLKEM */
#ifdef WITH_GEX
case SSH_KEX_DH_GEX_SHA1:

View File

@@ -52,7 +52,7 @@
#include "libssh/ecdh.h"
#include "libssh/sntrup761.h"
#ifdef HAVE_MLKEM
#include "libssh/mlkem768.h"
#include "libssh/hybrid_mlkem.h"
#endif
static struct ssh_hmac_struct ssh_hmac_tab[] = {
@@ -229,6 +229,16 @@ void crypto_free(struct ssh_crypto_struct *crypto)
SAFE_FREE(crypto->kex_methods[i]);
}
#ifdef HAVE_MLKEM
EVP_PKEY_free(crypto->mlkem_privkey);
ssh_string_burn(crypto->hybrid_shared_secret);
ssh_string_free(crypto->mlkem_client_pubkey);
ssh_string_free(crypto->mlkem_ciphertext);
ssh_string_free(crypto->hybrid_client_init);
ssh_string_free(crypto->hybrid_server_reply);
ssh_string_free(crypto->hybrid_shared_secret);
#endif
explicit_bzero(crypto, sizeof(struct ssh_crypto_struct));
SAFE_FREE(crypto);
@@ -604,7 +614,9 @@ int crypt_set_algorithms_server(ssh_session session){
#endif
#ifdef HAVE_MLKEM
case SSH_KEX_MLKEM768X25519_SHA256:
ssh_server_mlkem768x25519_init(session);
case SSH_KEX_MLKEM768NISTP256_SHA256:
case SSH_KEX_MLKEM1024NISTP384_SHA384:
ssh_server_hybrid_mlkem_init(session);
break;
#endif
default:

View File

@@ -184,7 +184,8 @@ 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 mlkem768x25519-sha256
sntrup761x25519-sha512@openssh.com sntrup761x25519-sha512
mlkem768x25519-sha256 mlkem768nistp256-sha256 mlkem1024nistp384-sha384
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

View File

@@ -768,6 +768,38 @@ static void torture_algorithms_ecdh_mlkem768x25519_sha256(void **state)
}
#endif /* HAVE_MLKEM && defined(OPENSSH_MLKEM768X25519_SHA256) */
#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768NISTP256_SHA256)
static void torture_algorithms_ecdh_mlkem768nistp256_sha256(void **state)
{
struct torture_state *s = *state;
if (ssh_fips_mode()) {
skip();
}
test_algorithm(s->ssh.session,
"mlkem768nistp256-sha256",
NULL /*cipher*/,
NULL /*hmac*/);
}
#endif /* HAVE_MLKEM && defined(OPENSSH_MLKEM768NISTP256_SHA256) */
#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM1024NISTP384_SHA384)
static void torture_algorithms_ecdh_mlkem1024nistp384_sha384(void **state)
{
struct torture_state *s = *state;
if (ssh_fips_mode()) {
skip();
}
test_algorithm(s->ssh.session,
"mlkem1024nistp384-sha384",
NULL /*cipher*/,
NULL /*hmac*/);
}
#endif /* HAVE_MLKEM && defined(OPENSSH_MLKEM1024NISTP384_SHA384) */
static void torture_algorithms_dh_group1(void **state) {
struct torture_state *s = *state;
@@ -1050,6 +1082,16 @@ int torture_run_tests(void) {
session_setup,
session_teardown),
#endif /* HAVE_MLKEM && defined(OPENSSH_MLKEM768X25519_SHA256) */
#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768NISTP256_SHA256)
cmocka_unit_test_setup_teardown(torture_algorithms_ecdh_mlkem768nistp256_sha256,
session_setup,
session_teardown),
#endif /* defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768NISTP256_SHA256) */
#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM1024NISTP384_SHA384)
cmocka_unit_test_setup_teardown(torture_algorithms_ecdh_mlkem1024nistp384_sha384,
session_setup,
session_teardown),
#endif /* defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM1024NISTP384_SHA384) */
#if defined(HAVE_ECC)
cmocka_unit_test_setup_teardown(torture_algorithms_ecdh_sha2_nistp256,
session_setup,

View File

@@ -307,21 +307,45 @@ static int torture_pkd_setup_ecdsa_521(void **state) {
#endif
#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768X25519_SHA256)
#define PKDTESTS_KEX_MLKEM768(f, client, kexcmd) \
#define PKDTESTS_KEX_MLKEM768X25519(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) \
f(client, ed25519_mlkem768x25519_sha256, kexcmd("mlkem768x25519-sha256"), setup_ed25519, teardown)
#else
#define PKDTESTS_KEX_MLKEM768(f, client, kexcmd)
#define PKDTESTS_KEX_MLKEM768X25519(f, client, kexcmd)
#endif
#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM768NISTP256_SHA256)
#define PKDTESTS_KEX_MLKEM768NISTP256(f, client, kexcmd) \
f(client, rsa_mlkem768nistp256_sha256, kexcmd("mlkem768nistp256-sha256"), setup_rsa, teardown) \
f(client, ecdsa_256_mlkem768nistp256_sha256, kexcmd("mlkem768nistp256-sha256"), setup_ecdsa_256, teardown) \
f(client, ecdsa_384_mlkem768nistp256_sha256, kexcmd("mlkem768nistp256-sha256"), setup_ecdsa_384, teardown) \
f(client, ecdsa_521_mlkem768nistp256_sha256, kexcmd("mlkem768nistp256-sha256"), setup_ecdsa_521, teardown) \
f(client, ed25519_mlkem768nistp256_sha256, kexcmd("mlkem768nistp256-sha256"), setup_ed25519, teardown)
#else
#define PKDTESTS_KEX_MLKEM768NISTP256(f, client, kexcmd)
#endif
#if defined(HAVE_MLKEM) && defined(OPENSSH_MLKEM1024NISTP384_SHA384)
#define PKDTESTS_KEX_MLKEM1024NISTP384(f, client, kexcmd) \
f(client, rsa_mlkem1024nistp384_sha384, kexcmd("mlkem1024nistp384-sha384"), setup_rsa, teardown) \
f(client, ecdsa_256_mlkem1024nistp384_sha384, kexcmd("mlkem1024nistp384-sha384"), setup_ecdsa_256, teardown) \
f(client, ecdsa_384_mlkem1024nistp384_sha384, kexcmd("mlkem1024nistp384-sha384"), setup_ecdsa_384, teardown) \
f(client, ecdsa_521_mlkem1024nistp384_sha384, kexcmd("mlkem1024nistp384-sha384"), setup_ecdsa_521, teardown) \
f(client, ed25519_mlkem1024nistp384_sha384, kexcmd("mlkem1024nistp384-sha384"), setup_ed25519, teardown)
#else
#define PKDTESTS_KEX_MLKEM1024NISTP384(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) \
PKDTESTS_KEX_MLKEM768X25519(f, client, kexcmd) \
PKDTESTS_KEX_MLKEM768NISTP256(f, client, kexcmd) \
PKDTESTS_KEX_MLKEM1024NISTP384(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) \

View File

@@ -339,6 +339,71 @@ static void torture_algorithm_aes128gcm_with_no_hmac_overlap(void **state)
test_algorithm_no_hmac_overlap(state, "aes128-gcm@openssh.com");
}
#ifdef HAVE_MLKEM
/*
* Check the self-compatibility of a given key exchange method.
*/
static void test_kex_self_compat(void **state, const char *kex)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
ssh_session session = NULL;
int rc;
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
rc = start_server(state);
assert_int_equal(rc, 0);
rc = session_setup(state);
assert_int_equal(rc, 0);
session = s->ssh.session;
assert_non_null(session);
rc = ssh_options_set(session, SSH_OPTIONS_KEY_EXCHANGE, kex);
assert_int_equal(rc, SSH_OK);
rc = ssh_connect(session);
assert_int_equal(rc, SSH_OK);
rc = ssh_userauth_publickey_auto(session, NULL, NULL);
assert_ssh_return_code(session, rc);
rc = session_teardown(state);
assert_int_equal(rc, 0);
rc = stop_server(state);
assert_int_equal(rc, 0);
}
static void torture_algorithm_mlkem768x25119_self_compat(void **state)
{
if (ssh_fips_mode()) {
skip();
}
test_kex_self_compat(state, "mlkem768x25519-sha256");
}
static void torture_algorithm_mlkem768nistp256_self_compat(void **state)
{
if (ssh_fips_mode()) {
skip();
}
test_kex_self_compat(state, "mlkem768nistp256-sha256");
}
static void torture_algorithm_mlkem1024nistp384_self_compat(void **state)
{
if (ssh_fips_mode()) {
skip();
}
test_kex_self_compat(state, "mlkem1024nistp384-sha384");
}
#endif /* HAVE_MLKEM */
int torture_run_tests(void)
{
int rc;
@@ -349,6 +414,14 @@ int torture_run_tests(void)
setup_temp_dir, teardown_temp_dir),
cmocka_unit_test_setup_teardown(torture_algorithm_aes128gcm_with_no_hmac_overlap,
setup_temp_dir, teardown_temp_dir),
#ifdef HAVE_MLKEM
cmocka_unit_test_setup_teardown(torture_algorithm_mlkem768x25119_self_compat,
setup_temp_dir, teardown_temp_dir),
cmocka_unit_test_setup_teardown(torture_algorithm_mlkem768nistp256_self_compat,
setup_temp_dir, teardown_temp_dir),
cmocka_unit_test_setup_teardown(torture_algorithm_mlkem1024nistp384_self_compat,
setup_temp_dir, teardown_temp_dir),
#endif /* HAVE_MLKEM */
};
ssh_init();

View File

@@ -53,6 +53,8 @@
#cmakedefine OPENSSH_SNTRUP761X25519_SHA512 1
#cmakedefine OPENSSH_SNTRUP761X25519_SHA512_OPENSSH_COM 1
#cmakedefine OPENSSH_MLKEM768X25519_SHA256 1
#cmakedefine OPENSSH_MLKEM768NISTP256_SHA256 1
#cmakedefine OPENSSH_MLKEM1024NISTP384_SHA384 1
#cmakedefine OPENSSH_SSH_ED25519 1
#cmakedefine OPENSSH_SSH_ED25519_CERT_V01_OPENSSH_COM 1
#cmakedefine OPENSSH_SSH_RSA 1

View File

@@ -284,6 +284,8 @@ static void torture_options_get_key_exchange(void **state)
#ifdef HAVE_MLKEM
assert_string_equal(value,
"mlkem768x25519-sha256,"
"mlkem768nistp256-sha256,"
"mlkem1024nistp384-sha384,"
"sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,"
"curve25519-sha256,curve25519-sha256@libssh.org,"
"ecdh-sha2-nistp256,ecdh-sha2-nistp384,"