mirror of
https://git.libssh.org/projects/libssh.git
synced 2025-12-03 13:31:11 +03:00
packet: Implement rekeying based on the recommendation from RFC's
The default rekeying recommendations are specified in RFC4344 Section 3 (First and Second Rekeying Recommendations). Additionally, the rekeying can be specified in configuration file/options allowing us to turn the rekeying off, base it on time or make it more strict. The code is highly inspired by the OpenSSH rekeying code. Signed-off-by: Jakub Jelen <jjelen@redhat.com> Reviewed-by: Daiki Ueno <dueno@redhat.com> Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
This commit is contained in:
committed by
Andreas Schneider
parent
c86a00d06b
commit
58cae2366a
@@ -46,5 +46,7 @@ const char *ssh_kex_get_supported_method(uint32_t algo);
|
||||
const char *ssh_kex_get_default_methods(uint32_t algo);
|
||||
const char *ssh_kex_get_description(uint32_t algo);
|
||||
char *ssh_client_select_hostkeys(ssh_session session);
|
||||
int ssh_send_rekex(ssh_session session);
|
||||
int server_set_kex(ssh_session session);
|
||||
|
||||
#endif /* KEX_H_ */
|
||||
|
||||
@@ -135,6 +135,8 @@ struct ssh_session_struct {
|
||||
ssh_buffer in_buffer;
|
||||
PACKET in_packet;
|
||||
ssh_buffer out_buffer;
|
||||
struct ssh_list *out_queue; /* This list is used for delaying packets
|
||||
when rekeying is required */
|
||||
|
||||
/* the states are used by the nonblocking stuff to remember */
|
||||
/* where it was before being interrupted */
|
||||
|
||||
55
src/kex.c
55
src/kex.c
@@ -444,7 +444,7 @@ SSH_PACKET_CALLBACK(ssh_packet_kexinit)
|
||||
(void)user;
|
||||
|
||||
if (session->session_state == SSH_SESSION_STATE_AUTHENTICATED) {
|
||||
SSH_LOG(SSH_LOG_WARNING, "Other side initiating key re-exchange");
|
||||
SSH_LOG(SSH_LOG_INFO, "Initiating key re-exchange");
|
||||
} else if (session->session_state != SSH_SESSION_STATE_INITIAL_KEX) {
|
||||
ssh_set_error(session,SSH_FATAL,"SSH_KEXINIT received in wrong state");
|
||||
goto error;
|
||||
@@ -564,6 +564,7 @@ SSH_PACKET_CALLBACK(ssh_packet_kexinit)
|
||||
}
|
||||
}
|
||||
|
||||
/* Note, that his overwrites authenticated state in case of rekeying */
|
||||
session->session_state = SSH_SESSION_STATE_KEXINIT_RECEIVED;
|
||||
session->dh_handshake_state = DH_STATE_INIT;
|
||||
session->ssh_connection_callback(session);
|
||||
@@ -880,6 +881,7 @@ int ssh_send_kex(ssh_session session, int server_kex) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
SSH_LOG(SSH_LOG_PACKET, "SSH_MSG_KEXINIT sent");
|
||||
return 0;
|
||||
error:
|
||||
ssh_buffer_reinit(session->out_buffer);
|
||||
@@ -889,6 +891,57 @@ error:
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Key re-exchange (rekey) is triggered by this function.
|
||||
* It can not be called again after the rekey is initialized!
|
||||
*/
|
||||
int ssh_send_rekex(ssh_session session)
|
||||
{
|
||||
int rc;
|
||||
|
||||
if (session->dh_handshake_state != DH_STATE_FINISHED) {
|
||||
/* Rekey/Key exchange is already in progress */
|
||||
SSH_LOG(SSH_LOG_PACKET, "Attempting rekey in bad state");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
if (session->current_crypto == NULL) {
|
||||
/* No current crypto used -- can not exchange it */
|
||||
SSH_LOG(SSH_LOG_PACKET, "No crypto to rekey");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
if (session->client) {
|
||||
rc = ssh_set_client_kex(session);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_PACKET, "Failed to set client kex");
|
||||
return rc;
|
||||
}
|
||||
} else {
|
||||
#ifdef WITH_SERVER
|
||||
rc = server_set_kex(session);
|
||||
if (rc == SSH_ERROR) {
|
||||
SSH_LOG(SSH_LOG_PACKET, "Failed to set server kex");
|
||||
return rc;
|
||||
}
|
||||
#else
|
||||
SSH_LOG(SSH_LOG_PACKET, "Invalid session state.");
|
||||
return SSH_ERROR;
|
||||
#endif /* WITH_SERVER */
|
||||
}
|
||||
|
||||
session->dh_handshake_state = DH_STATE_INIT;
|
||||
rc = ssh_send_kex(session, session->server);
|
||||
if (rc < 0) {
|
||||
SSH_LOG(SSH_LOG_PACKET, "Failed to send kex");
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Reset the handshake state */
|
||||
session->dh_handshake_state = DH_STATE_INIT_SENT;
|
||||
return SSH_OK;
|
||||
}
|
||||
|
||||
/* returns 1 if at least one of the name algos is in the default algorithms table */
|
||||
int ssh_verify_existing_algo(enum ssh_kex_types_e algo, const char *name)
|
||||
{
|
||||
|
||||
175
src/packet.c
175
src/packet.c
@@ -954,6 +954,65 @@ ssh_packet_get_current_crypto(ssh_session session,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#define MAX_PACKETS (1UL<<31)
|
||||
|
||||
static bool ssh_packet_need_rekey(ssh_session session,
|
||||
const uint32_t payloadsize)
|
||||
{
|
||||
struct ssh_crypto_struct *crypto = NULL;
|
||||
struct ssh_cipher_struct *out_cipher = NULL, *in_cipher = NULL;
|
||||
uint32_t next_blocks;
|
||||
|
||||
/* We can safely rekey only in authenticated state */
|
||||
if ((session->flags & SSH_SESSION_FLAG_AUTHENTICATED) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Do not rekey if the rekey/key-exchange is in progress */
|
||||
if (session->dh_handshake_state != DH_STATE_FINISHED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
crypto = ssh_packet_get_current_crypto(session, SSH_DIRECTION_BOTH);
|
||||
if (crypto == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out_cipher = crypto->out_cipher;
|
||||
in_cipher = crypto->in_cipher;
|
||||
|
||||
/* Make sure we can send at least something for very small limits */
|
||||
if ((out_cipher->packets == 0) && (in_cipher->packets == 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Time based rekeying */
|
||||
if (session->opts.rekey_time != 0 &&
|
||||
ssh_timeout_elapsed(&session->last_rekey_time,
|
||||
session->opts.rekey_time)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* RFC4344, Section 3.1 Recommends rekeying after 2^31 packets in either
|
||||
* direction to avoid possible information leakage through the MAC tag
|
||||
*/
|
||||
if (out_cipher->packets > MAX_PACKETS ||
|
||||
in_cipher->packets > MAX_PACKETS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Data-based rekeying:
|
||||
* * For outgoing packets we can still delay them
|
||||
* * Incoming packets need to be processed anyway, but we can
|
||||
* signalize our intention to rekey
|
||||
*/
|
||||
next_blocks = payloadsize / out_cipher->blocksize;
|
||||
return (out_cipher->max_blocks != 0 &&
|
||||
out_cipher->blocks + next_blocks > out_cipher->max_blocks) ||
|
||||
(in_cipher->max_blocks != 0 &&
|
||||
in_cipher->blocks + next_blocks > in_cipher->max_blocks);
|
||||
}
|
||||
|
||||
/* in nonblocking mode, socket_read will read as much as it can, and return */
|
||||
/* SSH_OK if it has read at least len bytes, otherwise, SSH_AGAIN. */
|
||||
/* in blocking mode, it will read at least len bytes and will block until it's ok. */
|
||||
@@ -984,6 +1043,7 @@ int ssh_packet_socket_callback(const void *data, size_t receivedlen, void *user)
|
||||
size_t processed = 0; /* number of byte processed from the callback */
|
||||
enum ssh_packet_filter_result_e filter_result;
|
||||
struct ssh_crypto_struct *crypto = NULL;
|
||||
bool ok;
|
||||
|
||||
crypto = ssh_packet_get_current_crypto(session, SSH_DIRECTION_IN);
|
||||
if (crypto != NULL) {
|
||||
@@ -1232,6 +1292,16 @@ int ssh_packet_socket_callback(const void *data, size_t receivedlen, void *user)
|
||||
processed += rc;
|
||||
}
|
||||
|
||||
ok = ssh_packet_need_rekey(session, 0);
|
||||
if (ok) {
|
||||
SSH_LOG(SSH_LOG_PACKET, "Incoming packet triggered rekey");
|
||||
rc = ssh_send_rekex(session);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_PACKET, "Rekey failed: rc = %d", rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
case PACKET_STATE_PROCESSING:
|
||||
SSH_LOG(SSH_LOG_PACKET, "Nested packet processing. Delaying.");
|
||||
@@ -1565,8 +1635,109 @@ error:
|
||||
return rc; /* SSH_OK, AGAIN or ERROR */
|
||||
}
|
||||
|
||||
int ssh_packet_send(ssh_session session) {
|
||||
return packet_send2(session);
|
||||
static bool
|
||||
ssh_packet_is_kex(unsigned char type)
|
||||
{
|
||||
return type >= SSH2_MSG_DISCONNECT &&
|
||||
type <= SSH2_MSG_KEX_DH_GEX_REQUEST &&
|
||||
type != SSH2_MSG_SERVICE_REQUEST &&
|
||||
type != SSH2_MSG_SERVICE_ACCEPT &&
|
||||
type != SSH2_MSG_IGNORE &&
|
||||
type != SSH2_MSG_EXT_INFO;
|
||||
}
|
||||
|
||||
static bool
|
||||
ssh_packet_in_rekey(ssh_session session)
|
||||
{
|
||||
/* We know we are rekeying if we are authenticated and the DH
|
||||
* status is not finished
|
||||
*/
|
||||
return (session->flags & SSH_SESSION_FLAG_AUTHENTICATED) &&
|
||||
(session->dh_handshake_state != DH_STATE_FINISHED);
|
||||
}
|
||||
|
||||
int ssh_packet_send(ssh_session session)
|
||||
{
|
||||
uint32_t payloadsize;
|
||||
uint8_t type, *payload;
|
||||
bool need_rekey, in_rekey;
|
||||
int rc;
|
||||
|
||||
payloadsize = ssh_buffer_get_len(session->out_buffer);
|
||||
if (payloadsize < 1) {
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
payload = (uint8_t *)ssh_buffer_get(session->out_buffer);
|
||||
type = payload[0]; /* type is the first byte of the packet now */
|
||||
need_rekey = ssh_packet_need_rekey(session, payloadsize);
|
||||
in_rekey = ssh_packet_in_rekey(session);
|
||||
|
||||
/* The rekey is triggered here. After that, only the key exchange
|
||||
* packets can be sent, until we send our NEWKEYS.
|
||||
*/
|
||||
if (need_rekey || (in_rekey && !ssh_packet_is_kex(type))) {
|
||||
if (need_rekey) {
|
||||
SSH_LOG(SSH_LOG_PACKET, "Outgoing packet triggered rekey");
|
||||
}
|
||||
/* Queue the current packet -- we will send it after the rekey */
|
||||
SSH_LOG(SSH_LOG_PACKET, "Queuing packet type %d", type);
|
||||
rc = ssh_list_append(session->out_queue, session->out_buffer);
|
||||
if (rc != SSH_OK) {
|
||||
return SSH_ERROR;
|
||||
}
|
||||
session->out_buffer = ssh_buffer_new();
|
||||
if (session->out_buffer == NULL) {
|
||||
ssh_set_error_oom(session);
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
if (need_rekey) {
|
||||
/* Send the KEXINIT packet instead.
|
||||
* This recursivelly calls the packet_send(), but it should
|
||||
* not get into rekeying again.
|
||||
* After that we need to handle the key exchange responses
|
||||
* up to the point where we can send the rest of the queue.
|
||||
*/
|
||||
return ssh_send_rekex(session);
|
||||
}
|
||||
return SSH_OK;
|
||||
}
|
||||
|
||||
/* Send the packet normally */
|
||||
rc = packet_send2(session);
|
||||
|
||||
/* We finished the key exchange so we can try to send our queue now */
|
||||
if (rc == SSH_OK && type == SSH2_MSG_NEWKEYS) {
|
||||
struct ssh_iterator *it;
|
||||
|
||||
for (it = ssh_list_get_iterator(session->out_queue);
|
||||
it != NULL;
|
||||
it = ssh_list_get_iterator(session->out_queue)) {
|
||||
struct ssh_buffer_struct *next_buffer = NULL;
|
||||
|
||||
/* Peek only -- do not remove from queue yet */
|
||||
next_buffer = (struct ssh_buffer_struct *)it->data;
|
||||
payloadsize = ssh_buffer_get_len(next_buffer);
|
||||
if (ssh_packet_need_rekey(session, payloadsize)) {
|
||||
/* Sigh ... we still can not send this packet. Repeat. */
|
||||
SSH_LOG(SSH_LOG_PACKET, "Queued packet triggered rekey");
|
||||
return ssh_send_rekex(session);
|
||||
}
|
||||
ssh_buffer_free(session->out_buffer);
|
||||
session->out_buffer = ssh_list_pop_head(struct ssh_buffer_struct *,
|
||||
session->out_queue);
|
||||
payload = (uint8_t *)ssh_buffer_get(session->out_buffer);
|
||||
type = payload[0];
|
||||
SSH_LOG(SSH_LOG_PACKET, "Dequeue packet type %d", type);
|
||||
rc = packet_send2(session);
|
||||
if (rc != SSH_OK) {
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void
|
||||
|
||||
@@ -82,7 +82,8 @@ static int dh_handshake_server(ssh_session session);
|
||||
* options that are currently set in the given ssh_session structure.
|
||||
*/
|
||||
|
||||
static int server_set_kex(ssh_session session) {
|
||||
int server_set_kex(ssh_session session)
|
||||
{
|
||||
struct ssh_kex_struct *server = &session->next_crypto->server_kex;
|
||||
int i, j, rc;
|
||||
const char *wanted;
|
||||
|
||||
@@ -85,6 +85,11 @@ ssh_session ssh_new(void) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
session->out_queue = ssh_list_new();
|
||||
if (session->out_queue == NULL) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
session->alive = 0;
|
||||
session->auth.supported_methods = 0;
|
||||
ssh_set_blocking(session, 1);
|
||||
@@ -166,9 +171,11 @@ err:
|
||||
* @see ssh_disconnect()
|
||||
* @see ssh_new()
|
||||
*/
|
||||
void ssh_free(ssh_session session) {
|
||||
void ssh_free(ssh_session session)
|
||||
{
|
||||
int i;
|
||||
struct ssh_iterator *it;
|
||||
struct ssh_iterator *it = NULL;
|
||||
struct ssh_buffer_struct *b = NULL;
|
||||
|
||||
if (session == NULL) {
|
||||
return;
|
||||
@@ -262,6 +269,12 @@ void ssh_free(ssh_session session) {
|
||||
ssh_list_free(session->opts.identity);
|
||||
}
|
||||
|
||||
while ((b = ssh_list_pop_head(struct ssh_buffer_struct *,
|
||||
session->out_queue)) != NULL) {
|
||||
ssh_buffer_free(b);
|
||||
}
|
||||
ssh_list_free(session->out_queue);
|
||||
|
||||
#ifndef _WIN32
|
||||
ssh_agent_state_free (session->agent_state);
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user