mirror of
https://git.libssh.org/projects/libssh.git
synced 2025-12-19 00:48:43 +09:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
48
include/libssh/hybrid_mlkem.h
Normal file
48
include/libssh/hybrid_mlkem.h
Normal 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
64
include/libssh/mlkem.h
Normal 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_ */
|
||||
@@ -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_ */
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
870
src/hybrid_mlkem.c
Normal 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 */
|
||||
57
src/kex.c
57
src/kex.c
@@ -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
38
src/mlkem.c
Normal 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;
|
||||
}
|
||||
}
|
||||
679
src/mlkem768.c
679
src/mlkem768.c
@@ -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
238
src/mlkem_crypto.c
Normal 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;
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,"
|
||||
|
||||
Reference in New Issue
Block a user