aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Jelen <jjelen@redhat.com>2018-11-15 13:43:18 +0100
committerAndreas Schneider <asn@cryptomilk.org>2019-01-09 10:31:49 +0100
commit58cae2366a801d6d3702d2fa8895976d4c169bd7 (patch)
tree7234181cbf462cf18938fd34f7f8d1bce7a0e800
parentc86a00d06b732c57153bdd5677a5d77f7f1be0a9 (diff)
downloadlibssh-58cae2366a801d6d3702d2fa8895976d4c169bd7.tar.gz
libssh-58cae2366a801d6d3702d2fa8895976d4c169bd7.tar.xz
libssh-58cae2366a801d6d3702d2fa8895976d4c169bd7.zip
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>
-rw-r--r--include/libssh/kex.h2
-rw-r--r--include/libssh/session.h2
-rw-r--r--src/kex.c55
-rw-r--r--src/packet.c175
-rw-r--r--src/server.c3
-rw-r--r--src/session.c17
6 files changed, 248 insertions, 6 deletions
diff --git a/include/libssh/kex.h b/include/libssh/kex.h
index a626d105..644a3956 100644
--- a/include/libssh/kex.h
+++ b/include/libssh/kex.h
@@ -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_ */
diff --git a/include/libssh/session.h b/include/libssh/session.h
index fb443b59..86540bbf 100644
--- a/include/libssh/session.h
+++ b/include/libssh/session.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 */
diff --git a/src/kex.c b/src/kex.c
index 7a1c89c7..c2e02ff6 100644
--- a/src/kex.c
+++ b/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)
{
diff --git a/src/packet.c b/src/packet.c
index c0e111f1..29824193 100644
--- a/src/packet.c
+++ b/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
diff --git a/src/server.c b/src/server.c
index 78e5ece6..c3e92ba6 100644
--- a/src/server.c
+++ b/src/server.c
@@ -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;
diff --git a/src/session.c b/src/session.c
index 28b748ef..01a96a32 100644
--- a/src/session.c
+++ b/src/session.c
@@ -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