From: Simon Tatham Date: Sat, 27 Nov 2004 14:29:20 +0000 (+0000) Subject: Implement client-initiated rekeys after an hour, or after 1Gb of X-Git-Tag: 0.58~346 X-Git-Url: https://asedeno.scripts.mit.edu/gitweb/?a=commitdiff_plain;h=812870d1b304d8f22633614f59c6690c05fcc029;p=PuTTY.git Implement client-initiated rekeys after an hour, or after 1Gb of data transfer in either direction (whichever comes first), or at explicit client request (nice idea Jacob). Have tested by lowering the limits, and it all seems solid enough; in particular, this has also allowed me to test the behaviour when connection-level data is received during rekey, and that looks fine too (at least it does _now_ :-). [originally from svn r4908] [this svn revision also touched putty-wishlist] --- diff --git a/ssh.c b/ssh.c index 1d8e6b8d..522d3467 100644 --- a/ssh.c +++ b/ssh.c @@ -517,6 +517,7 @@ struct Packet { unsigned char *body; long savedpos; long maxlen; + long encrypted_len; /* for SSH2 total-size counting */ /* * State associated with packet logging @@ -543,6 +544,9 @@ static void ssh_do_close(Ssh ssh); static unsigned long ssh_pkt_getuint32(struct Packet *pkt); static int ssh2_pkt_getbool(struct Packet *pkt); static void ssh_pkt_getstring(struct Packet *pkt, char **p, int *length); +static void ssh2_timer(void *ctx, long now); +static int do_ssh2_transport(Ssh ssh, unsigned char *in, int inlen, + struct Packet *pktin); struct rdpkt1_state_tag { long len, pad, biglen, to_read; @@ -703,8 +707,19 @@ struct ssh_tag { * This module deals with sending keepalives. */ Pinger pinger; + + /* + * Track incoming and outgoing data sizes and time, for + * size-based rekeys. + */ + unsigned long incoming_data_size, outgoing_data_size, deferred_data_size; + int kex_in_progress; + long next_rekey; }; +#define MAX_DATA_BEFORE_REKEY (0x40000000UL) +#define REKEY_TIMEOUT (3600 * TICKSPERSEC) + #define logevent(s) logevent(ssh->frontend, s) /* logevent, only printf-formatted. */ @@ -1099,6 +1114,8 @@ static struct Packet *ssh2_rdpkt(Ssh ssh, unsigned char **data, int *datalen) st->pktin->data + st->cipherblk, st->packetlen - st->cipherblk); + st->pktin->encrypted_len = st->packetlen; + /* * Check the MAC. */ @@ -1592,6 +1609,8 @@ static int ssh2_pkt_construct(Ssh ssh, struct Packet *pkt) ssh->cscipher->encrypt(ssh->cs_cipher_ctx, pkt->data, pkt->length + padding); + pkt->encrypted_len = pkt->length + padding; + /* Ready-to-send packet starts at pkt->data. We return length. */ return pkt->length + padding + maclen; } @@ -1636,6 +1655,13 @@ static void ssh2_pkt_send_noqueue(Ssh ssh, struct Packet *pkt) backlog = sk_write(ssh->s, (char *)pkt->data, len); if (backlog > SSH_MAX_BACKLOG) ssh_throttle_all(ssh, 1, backlog); + + ssh->outgoing_data_size += pkt->encrypted_len; + if (!ssh->kex_in_progress && + ssh->outgoing_data_size > MAX_DATA_BEFORE_REKEY) + do_ssh2_transport(ssh, "Initiating key re-exchange " + "(too much data sent)", -1, NULL); + ssh_free_packet(pkt); } @@ -1653,6 +1679,7 @@ static void ssh2_pkt_defer_noqueue(Ssh ssh, struct Packet *pkt) } memcpy(ssh->deferred_send_data + ssh->deferred_len, pkt->data, len); ssh->deferred_len += len; + ssh->deferred_data_size += pkt->encrypted_len; ssh_free_packet(pkt); } @@ -1718,6 +1745,13 @@ static void ssh_pkt_defersend(Ssh ssh) ssh->deferred_send_data = NULL; if (backlog > SSH_MAX_BACKLOG) ssh_throttle_all(ssh, 1, backlog); + + ssh->outgoing_data_size += ssh->deferred_data_size; + if (!ssh->kex_in_progress && + ssh->outgoing_data_size > MAX_DATA_BEFORE_REKEY) + do_ssh2_transport(ssh, "Initiating key re-exchange " + "(too much data sent)", -1, NULL); + ssh->deferred_data_size = 0; } /* @@ -4286,6 +4320,11 @@ static int do_ssh2_transport(Ssh ssh, unsigned char *in, int inlen, */ ssh->queueing = TRUE; + /* + * Flag that KEX is in progress. + */ + ssh->kex_in_progress = TRUE; + /* * Construct and send our key exchange packet. */ @@ -4637,11 +4676,62 @@ static int do_ssh2_transport(Ssh ssh, unsigned char *in, int inlen, sfree(s->keystr); ssh->hostkey->freekey(s->hkey); + /* + * The exchange hash from the very first key exchange is also + * the session id, used in session key construction and + * authentication. + */ + if (s->first_kex) + memcpy(ssh->v2_session_id, s->exchange_hash, + sizeof(s->exchange_hash)); + /* * Send SSH2_MSG_NEWKEYS. */ s->pktout = ssh2_pkt_init(SSH2_MSG_NEWKEYS); ssh2_pkt_send_noqueue(ssh, s->pktout); + ssh->outgoing_data_size = 0; /* start counting from here */ + + /* + * We've sent client NEWKEYS, so create and initialise + * client-to-servere session keys. + */ + if (ssh->cs_cipher_ctx) + ssh->cscipher->free_context(ssh->cs_cipher_ctx); + ssh->cscipher = s->cscipher_tobe; + ssh->cs_cipher_ctx = ssh->cscipher->make_context(); + + if (ssh->cs_mac_ctx) + ssh->csmac->free_context(ssh->cs_mac_ctx); + ssh->csmac = s->csmac_tobe; + ssh->cs_mac_ctx = ssh->csmac->make_context(); + + if (ssh->cs_comp_ctx) + ssh->cscomp->compress_cleanup(ssh->cs_comp_ctx); + ssh->cscomp = s->cscomp_tobe; + ssh->cs_comp_ctx = ssh->cscomp->compress_init(); + + /* + * Set IVs on client-to-server keys. Here we use the exchange + * hash from the _first_ key exchange. + */ + { + unsigned char keyspace[40]; + ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'C',keyspace); + ssh->cscipher->setkey(ssh->cs_cipher_ctx, keyspace); + ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'A',keyspace); + ssh->cscipher->setiv(ssh->cs_cipher_ctx, keyspace); + ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'E',keyspace); + ssh->csmac->setkey(ssh->cs_mac_ctx, keyspace); + } + + logeventf(ssh, "Initialised %.200s client->server encryption", + ssh->cscipher->text_name); + logeventf(ssh, "Initialised %.200s client->server MAC algorithm", + ssh->csmac->text_name); + if (ssh->cscomp->text_name) + logeventf(ssh, "Initialised %s compression", + ssh->cscomp->text_name); /* * Now our end of the key exchange is complete, we can send all @@ -4658,76 +4748,51 @@ static int do_ssh2_transport(Ssh ssh, unsigned char *in, int inlen, bombout(("expected new-keys packet from server")); crStop(0); } + ssh->incoming_data_size = 0; /* start counting from here */ /* - * Create and initialise session keys. + * We've seen server NEWKEYS, so create and initialise + * server-to-client session keys. */ - if (ssh->cs_cipher_ctx) - ssh->cscipher->free_context(ssh->cs_cipher_ctx); - ssh->cscipher = s->cscipher_tobe; - ssh->cs_cipher_ctx = ssh->cscipher->make_context(); - if (ssh->sc_cipher_ctx) ssh->sccipher->free_context(ssh->sc_cipher_ctx); ssh->sccipher = s->sccipher_tobe; ssh->sc_cipher_ctx = ssh->sccipher->make_context(); - if (ssh->cs_mac_ctx) - ssh->csmac->free_context(ssh->cs_mac_ctx); - ssh->csmac = s->csmac_tobe; - ssh->cs_mac_ctx = ssh->csmac->make_context(); - if (ssh->sc_mac_ctx) ssh->scmac->free_context(ssh->sc_mac_ctx); ssh->scmac = s->scmac_tobe; ssh->sc_mac_ctx = ssh->scmac->make_context(); - if (ssh->cs_comp_ctx) - ssh->cscomp->compress_cleanup(ssh->cs_comp_ctx); - ssh->cscomp = s->cscomp_tobe; - ssh->cs_comp_ctx = ssh->cscomp->compress_init(); - if (ssh->sc_comp_ctx) ssh->sccomp->decompress_cleanup(ssh->sc_comp_ctx); ssh->sccomp = s->sccomp_tobe; ssh->sc_comp_ctx = ssh->sccomp->decompress_init(); /* - * Set IVs after keys. Here we use the exchange hash from the - * _first_ key exchange. + * Set IVs on server-to-client keys. Here we use the exchange + * hash from the _first_ key exchange. */ { unsigned char keyspace[40]; - if (s->first_kex) - memcpy(ssh->v2_session_id, s->exchange_hash, - sizeof(s->exchange_hash)); - ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'C',keyspace); - ssh->cscipher->setkey(ssh->cs_cipher_ctx, keyspace); ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'D',keyspace); ssh->sccipher->setkey(ssh->sc_cipher_ctx, keyspace); - ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'A',keyspace); - ssh->cscipher->setiv(ssh->cs_cipher_ctx, keyspace); ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'B',keyspace); ssh->sccipher->setiv(ssh->sc_cipher_ctx, keyspace); - ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'E',keyspace); - ssh->csmac->setkey(ssh->cs_mac_ctx, keyspace); ssh2_mkkey(ssh,s->K,s->exchange_hash,ssh->v2_session_id,'F',keyspace); ssh->scmac->setkey(ssh->sc_mac_ctx, keyspace); } - logeventf(ssh, "Initialised %.200s client->server encryption", - ssh->cscipher->text_name); logeventf(ssh, "Initialised %.200s server->client encryption", ssh->sccipher->text_name); - logeventf(ssh, "Initialised %.200s client->server MAC algorithm", - ssh->csmac->text_name); logeventf(ssh, "Initialised %.200s server->client MAC algorithm", ssh->scmac->text_name); - if (ssh->cscomp->text_name) - logeventf(ssh, "Initialised %s compression", - ssh->cscomp->text_name); if (ssh->sccomp->text_name) logeventf(ssh, "Initialised %s decompression", ssh->sccomp->text_name); + + /* + * Free key exchange data. + */ freebn(s->f); freebn(s->K); if (ssh->kex == &ssh_diffiehellman_gex) { @@ -4735,6 +4800,12 @@ static int do_ssh2_transport(Ssh ssh, unsigned char *in, int inlen, freebn(s->p); } + /* + * Key exchange is over. Schedule a timer for our next rekey. + */ + ssh->kex_in_progress = FALSE; + ssh->next_rekey = schedule_timer(REKEY_TIMEOUT, ssh2_timer, ssh); + /* * If this is the first key exchange phase, we must pass the * SSH2_MSG_NEWKEYS packet to the next layer, not because it @@ -4753,11 +4824,21 @@ static int do_ssh2_transport(Ssh ssh, unsigned char *in, int inlen, * function so that other things can run on top of the * transport. If we ever see a KEXINIT, we must go back to the * start. + * + * We _also_ go back to the start if we see pktin==NULL and + * inlen==-1, because this is a special signal meaning + * `initiate client-driven rekey', and `in' contains a message + * giving the reason for the rekey. */ - while (!(pktin && pktin->type == SSH2_MSG_KEXINIT)) { + while (!((pktin && pktin->type == SSH2_MSG_KEXINIT) || + (!pktin && inlen == -1))) { crReturn(1); } - logevent("Server initiated key re-exchange"); + if (pktin) { + logevent("Server initiated key re-exchange"); + } else { + logevent((char *)in); + } goto begin_key_exchange; crFinish(1); @@ -6822,12 +6903,31 @@ static void ssh2_protocol_setup(Ssh ssh) ssh->packet_dispatch[SSH2_MSG_DEBUG] = ssh2_msg_debug; } +static void ssh2_timer(void *ctx, long now) +{ + Ssh ssh = (Ssh)ctx; + + if (!ssh->kex_in_progress && + now - ssh->next_rekey >= 0) { + do_ssh2_transport(ssh, "Initiating key re-exchange (timeout)", + -1, NULL); + } +} + static void ssh2_protocol(Ssh ssh, unsigned char *in, int inlen, struct Packet *pktin) { if (ssh->state == SSH_STATE_CLOSED) return; + if (pktin) { + ssh->incoming_data_size += pktin->encrypted_len; + if (!ssh->kex_in_progress && + ssh->incoming_data_size > MAX_DATA_BEFORE_REKEY) + do_ssh2_transport(ssh, "Initiating key re-exchange " + "(too much data received)", -1, NULL); + } + if (pktin && ssh->packet_dispatch[pktin->type]) { ssh->packet_dispatch[pktin->type](ssh, pktin); return; @@ -6943,6 +7043,10 @@ static const char *ssh_init(void *frontend_handle, void **backend_handle, ssh->pinger = NULL; + ssh->incoming_data_size = ssh->outgoing_data_size = + ssh->deferred_data_size = 0L; + ssh->kex_in_progress = FALSE; + p = connect_to_host(ssh, host, port, realhost, nodelay, keepalive); if (p != NULL) return p; @@ -7021,6 +7125,7 @@ static void ssh_free(void *handle) } if (ssh->s) ssh_do_close(ssh); + expire_timer_context(ssh); sfree(ssh); if (ssh->pinger) pinger_free(ssh->pinger); @@ -7139,6 +7244,7 @@ static const struct telnet_special *ssh_get_specials(void *handle) { static const struct telnet_special ignore_special[] = { {"IGNORE message", TS_NOP}, + {"Repeat key exchange", TS_REKEY}, }; static const struct telnet_special ssh2_session_specials[] = { {NULL, TS_SEP}, @@ -7180,7 +7286,6 @@ static const struct telnet_special *ssh_get_specials(void *handle) if (!(ssh->remote_bugs & BUG_CHOKES_ON_SSH1_IGNORE)) ADD_SPECIALS(ignore_special); } else if (ssh->version == 2) { - /* XXX add rekey, when implemented */ ADD_SPECIALS(ignore_special); if (ssh->mainchan) ADD_SPECIALS(ssh2_session_specials); @@ -7234,6 +7339,11 @@ static void ssh_special(void *handle, Telnet_Special code) ssh2_pkt_addstring_start(pktout); ssh2_pkt_send_noqueue(ssh, pktout); } + } else if (code == TS_REKEY) { + if (!ssh->kex_in_progress && ssh->version == 2) { + do_ssh2_transport(ssh, "Initiating key re-exchange at" + " user request", -1, NULL); + } } else if (code == TS_BRK) { if (ssh->state == SSH_STATE_CLOSED || ssh->state == SSH_STATE_PREPACKET) return;