]> asedeno.scripts.mit.edu Git - PuTTY.git/blobdiff - ssh.c
Simplifiy handling of stdin data in SSH-2.
[PuTTY.git] / ssh.c
diff --git a/ssh.c b/ssh.c
index cc503029eaf633b61f37365996291546fb1d8122..92dff019389d15f8abedfd765c0a6a41dfee4a2a 100644 (file)
--- a/ssh.c
+++ b/ssh.c
@@ -11,6 +11,7 @@
 
 #include "putty.h"
 #include "tree234.h"
+#include "storage.h"
 #include "ssh.h"
 #ifndef NO_GSSAPI
 #include "sshgssc.h"
@@ -383,7 +384,7 @@ static void ssh2_msg_something_unimplemented(Ssh ssh, struct Packet *pktin);
  *    ensure that the server never has any need to throttle its end
  *    of the connection), so we set this high as well.
  * 
- *  - OUR_V2_WINSIZE is the maximum window size we present on SSH-2
+ *  - OUR_V2_WINSIZE is the default window size we present on SSH-2
  *    channels.
  *
  *  - OUR_V2_BIGWIN is the window size we advertise for the only
@@ -407,16 +408,23 @@ static void ssh2_msg_something_unimplemented(Ssh ssh, struct Packet *pktin);
 #define OUR_V2_MAXPKT 0x4000UL
 #define OUR_V2_PACKETLIMIT 0x9000UL
 
-const static struct ssh_signkey *hostkey_algs[] = {
-    &ssh_ecdsa_ed25519,
-    &ssh_ecdsa_nistp256, &ssh_ecdsa_nistp384, &ssh_ecdsa_nistp521,
-    &ssh_rsa, &ssh_dss
+struct ssh_signkey_with_user_pref_id {
+    const struct ssh_signkey *alg;
+    int id;
+};
+const static struct ssh_signkey_with_user_pref_id hostkey_algs[] = {
+    { &ssh_ecdsa_ed25519, HK_ED25519 },
+    { &ssh_ecdsa_nistp256, HK_ECDSA },
+    { &ssh_ecdsa_nistp384, HK_ECDSA },
+    { &ssh_ecdsa_nistp521, HK_ECDSA },
+    { &ssh_dss, HK_DSA },
+    { &ssh_rsa, HK_RSA },
 };
 
-const static struct ssh_mac *macs[] = {
+const static struct ssh_mac *const macs[] = {
     &ssh_hmac_sha256, &ssh_hmac_sha1, &ssh_hmac_sha1_96, &ssh_hmac_md5
 };
-const static struct ssh_mac *buggymacs[] = {
+const static struct ssh_mac *const buggymacs[] = {
     &ssh_hmac_sha1_buggy, &ssh_hmac_sha1_96_buggy, &ssh_hmac_md5
 };
 
@@ -443,7 +451,7 @@ const static struct ssh_compress ssh_comp_none = {
     ssh_comp_none_disable, NULL
 };
 extern const struct ssh_compress ssh_zlib;
-const static struct ssh_compress *compressions[] = {
+const static struct ssh_compress *const compressions[] = {
     &ssh_zlib, &ssh_comp_none
 };
 
@@ -792,6 +800,7 @@ struct ssh_tag {
     int send_ok;
     int echoing, editing;
 
+    int session_started;
     void *frontend;
 
     int ospeed, ispeed;                       /* temporaries */
@@ -953,6 +962,24 @@ struct ssh_tag {
      */
     struct ssh_gss_liblist *gsslibs;
 #endif
+
+    /*
+     * The last list returned from get_specials.
+     */
+    struct telnet_special *specials;
+
+    /*
+     * List of host key algorithms for which we _don't_ have a stored
+     * host key. These are indices into the main hostkey_algs[] array
+     */
+    int uncert_hostkeys[lenof(hostkey_algs)];
+    int n_uncert_hostkeys;
+
+    /*
+     * Flag indicating that the current rekey is intended to finish
+     * with a newly cross-certified host key.
+     */
+    int cross_certifying;
 };
 
 #define logevent(s) logevent(ssh->frontend, s)
@@ -3070,6 +3097,8 @@ static int do_ssh_init(Ssh ssh, unsigned char c)
        crReturn(1);
     }
 
+    ssh->session_started = TRUE;
+
     s->vstrsize = sizeof(protoname) + 16;
     s->vstring = snewn(s->vstrsize, char);
     strcpy(s->vstring, protoname);
@@ -3109,15 +3138,21 @@ static int do_ssh_init(Ssh ssh, unsigned char c)
     /* Anything greater or equal to "1.99" means protocol 2 is supported. */
     s->proto2 = ssh_versioncmp(s->version, "1.99") >= 0;
 
-    if (conf_get_int(ssh->conf, CONF_sshprot) == 0 && !s->proto1) {
-       bombout(("SSH protocol version 1 required by configuration but "
-                "not provided by server"));
-       crStop(0);
-    }
-    if (conf_get_int(ssh->conf, CONF_sshprot) == 3 && !s->proto2) {
-       bombout(("SSH protocol version 2 required by configuration but "
-                "not provided by server"));
-       crStop(0);
+    if (conf_get_int(ssh->conf, CONF_sshprot) == 0) {
+       if (!s->proto1) {
+           bombout(("SSH protocol version 1 required by our configuration "
+                    "but not provided by server"));
+           crStop(0);
+       }
+    } else if (conf_get_int(ssh->conf, CONF_sshprot) == 3) {
+       if (!s->proto2) {
+           bombout(("SSH protocol version 2 required by our configuration "
+                    "but server only provides (old, insecure) SSH-1"));
+           crStop(0);
+       }
+    } else {
+       /* No longer support values 1 or 2 for CONF_sshprot */
+       assert(!"Unexpected value for CONF_sshprot");
     }
 
     if (s->proto2 && (conf_get_int(ssh->conf, CONF_sshprot) >= 2 || !s->proto1))
@@ -3452,34 +3487,20 @@ static void ssh_socket_log(Plug plug, int type, SockAddr addr, int port,
                            const char *error_msg, int error_code)
 {
     Ssh ssh = (Ssh) plug;
-    char addrbuf[256], *msg;
 
-    if (ssh->attempting_connshare) {
-        /*
-         * While we're attempting connection sharing, don't loudly log
-         * everything that happens. Real TCP connections need to be
-         * logged when we _start_ trying to connect, because it might
-         * be ages before they respond if something goes wrong; but
-         * connection sharing is local and quick to respond, and it's
-         * sufficient to simply wait and see whether it worked
-         * afterwards.
-         */
-    } else {
-        sk_getaddr(addr, addrbuf, lenof(addrbuf));
-
-        if (type == 0) {
-            if (sk_addr_needs_port(addr)) {
-                msg = dupprintf("Connecting to %s port %d", addrbuf, port);
-            } else {
-                msg = dupprintf("Connecting to %s", addrbuf);
-            }
-        } else {
-            msg = dupprintf("Failed to connect to %s: %s", addrbuf, error_msg);
-        }
+    /*
+     * While we're attempting connection sharing, don't loudly log
+     * everything that happens. Real TCP connections need to be logged
+     * when we _start_ trying to connect, because it might be ages
+     * before they respond if something goes wrong; but connection
+     * sharing is local and quick to respond, and it's sufficient to
+     * simply wait and see whether it worked afterwards.
+     */
 
-        logevent(msg);
-        sfree(msg);
-    }
+    if (!ssh->attempting_connshare)
+        backend_socket_log(ssh->frontend, type, addr, port,
+                           error_msg, error_code, ssh->conf,
+                           ssh->session_started);
 }
 
 void ssh_connshare_log(Ssh ssh, int event, const char *logtext,
@@ -3674,10 +3695,8 @@ static const char *connect_to_host(Ssh ssh, const char *host, int port,
          * Try to find host.
          */
         addressfamily = conf_get_int(ssh->conf, CONF_addressfamily);
-        logeventf(ssh, "Looking up host \"%s\"%s", host,
-                  (addressfamily == ADDRTYPE_IPV4 ? " (IPv4)" :
-                   (addressfamily == ADDRTYPE_IPV6 ? " (IPv6)" : "")));
-        addr = name_lookup(host, port, realhost, ssh->conf, addressfamily);
+        addr = name_lookup(host, port, realhost, ssh->conf, addressfamily,
+                           ssh->frontend, "SSH connection");
         if ((err = sk_addr_error(addr)) != NULL) {
             sk_addr_free(addr);
             return err;
@@ -3695,13 +3714,17 @@ static const char *connect_to_host(Ssh ssh, const char *host, int port,
     }
 
     /*
-     * If the SSH version number's fixed, set it now, and if it's SSH-2,
-     * send the version string too.
+     * The SSH version number is always fixed (since we no longer support
+     * fallback between versions), so set it now, and if it's SSH-2,
+     * send the version string now too.
      */
     sshprot = conf_get_int(ssh->conf, CONF_sshprot);
+    assert(sshprot == 0 || sshprot == 3);
     if (sshprot == 0)
+       /* SSH-1 only */
        ssh->version = 1;
     if (sshprot == 3 && !ssh->bare_connection) {
+       /* SSH-2 only */
        ssh->version = 2;
        ssh_send_verstring(ssh, "SSH-", NULL);
     }
@@ -5575,7 +5598,7 @@ static void ssh1_msg_port_open(Ssh ssh, struct Packet *pktin)
     ssh_pkt_getstring(pktin, &host, &hostsize);
     port = ssh_pkt_getuint32(pktin);
 
-    pf.dhost = dupprintf("%.*s", hostsize, host);
+    pf.dhost = dupprintf("%.*s", hostsize, NULLTOEMPTY(host));
     pf.dport = port;
     pfp = find234(ssh->rportfwds, &pf, NULL);
 
@@ -5728,6 +5751,48 @@ static void ssh1_msg_channel_close(Ssh ssh, struct Packet *pktin)
     }
 }
 
+/*
+ * Handle incoming data on an SSH-1 or SSH-2 agent-forwarding channel.
+ */
+static int ssh_agent_channel_data(struct ssh_channel *c, char *data,
+                                 int length)
+{
+    while (length > 0) {
+       if (c->u.a.lensofar < 4) {
+           unsigned int l = min(4 - c->u.a.lensofar, (unsigned)length);
+           memcpy(c->u.a.msglen + c->u.a.lensofar, data, l);
+           data += l;
+           length -= l;
+           c->u.a.lensofar += l;
+       }
+       if (c->u.a.lensofar == 4) {
+           c->u.a.totallen = 4 + GET_32BIT(c->u.a.msglen);
+           c->u.a.message = snewn(c->u.a.totallen, unsigned char);
+           memcpy(c->u.a.message, c->u.a.msglen, 4);
+       }
+       if (c->u.a.lensofar >= 4 && length > 0) {
+           unsigned int l = min(c->u.a.totallen - c->u.a.lensofar,
+                                (unsigned)length);
+           memcpy(c->u.a.message + c->u.a.lensofar, data, l);
+           data += l;
+           length -= l;
+           c->u.a.lensofar += l;
+       }
+       if (c->u.a.lensofar == c->u.a.totallen) {
+           void *reply;
+           int replylen;
+            c->u.a.outstanding_requests++;
+           if (agent_query(c->u.a.message, c->u.a.totallen, &reply, &replylen,
+                           ssh_agentf_callback, c))
+               ssh_agentf_callback(c, reply, replylen);
+           sfree(c->u.a.message);
+            c->u.a.message = NULL;
+           c->u.a.lensofar = 0;
+       }
+    }
+    return 0;   /* agent channels never back up */
+}
+
 static void ssh1_msg_channel_data(Ssh ssh, struct Packet *pktin)
 {
     /* Data sent down one of our channels. */
@@ -5749,47 +5814,7 @@ static void ssh1_msg_channel_data(Ssh ssh, struct Packet *pktin)
            bufsize = pfd_send(c->u.pfd.pf, p, len);
            break;
          case CHAN_AGENT:
-           /* Data for an agent message. Buffer it. */
-           while (len > 0) {
-               if (c->u.a.lensofar < 4) {
-                   unsigned int l = min(4 - c->u.a.lensofar, (unsigned)len);
-                   memcpy(c->u.a.msglen + c->u.a.lensofar, p,
-                          l);
-                   p += l;
-                   len -= l;
-                   c->u.a.lensofar += l;
-               }
-               if (c->u.a.lensofar == 4) {
-                   c->u.a.totallen =
-                       4 + GET_32BIT(c->u.a.msglen);
-                   c->u.a.message = snewn(c->u.a.totallen,
-                                          unsigned char);
-                   memcpy(c->u.a.message, c->u.a.msglen, 4);
-               }
-               if (c->u.a.lensofar >= 4 && len > 0) {
-                   unsigned int l =
-                       min(c->u.a.totallen - c->u.a.lensofar,
-                           (unsigned)len);
-                   memcpy(c->u.a.message + c->u.a.lensofar, p,
-                          l);
-                   p += l;
-                   len -= l;
-                   c->u.a.lensofar += l;
-               }
-               if (c->u.a.lensofar == c->u.a.totallen) {
-                   void *reply;
-                   int replylen;
-                    c->u.a.outstanding_requests++;
-                   if (agent_query(c->u.a.message,
-                                   c->u.a.totallen,
-                                   &reply, &replylen,
-                                   ssh_agentf_callback, c))
-                       ssh_agentf_callback(c, reply, replylen);
-                   sfree(c->u.a.message);
-                   c->u.a.lensofar = 0;
-               }
-           }
-           bufsize = 0;   /* agent channels never back up */
+           bufsize = ssh_agent_channel_data(c, p, len);
            break;
        }
        if (!c->throttling_conn && bufsize > SSH1_BUFFER_LIMIT) {
@@ -6058,7 +6083,7 @@ static void ssh1_msg_debug(Ssh ssh, struct Packet *pktin)
     int msglen;
 
     ssh_pkt_getstring(pktin, &msg, &msglen);
-    logeventf(ssh, "Remote debug message: %.*s", msglen, msg);
+    logeventf(ssh, "Remote debug message: %.*s", msglen, NULLTOEMPTY(msg));
 }
 
 static void ssh1_msg_disconnect(Ssh ssh, struct Packet *pktin)
@@ -6068,7 +6093,8 @@ static void ssh1_msg_disconnect(Ssh ssh, struct Packet *pktin)
     int msglen;
 
     ssh_pkt_getstring(pktin, &msg, &msglen);
-    bombout(("Server sent disconnect message:\n\"%.*s\"", msglen, msg));
+    bombout(("Server sent disconnect message:\n\"%.*s\"",
+             msglen, NULLTOEMPTY(msg)));
 }
 
 static void ssh_msg_ignore(Ssh ssh, struct Packet *pktin)
@@ -6235,7 +6261,10 @@ struct kexinit_algorithm {
            const struct ssh_kex *kex;
            int warn;
        } kex;
-       const struct ssh_signkey *hostkey;
+       struct {
+            const struct ssh_signkey *hostkey;
+            int warn;
+        } hk;
        struct {
            const struct ssh2_cipher *cipher;
            int warn;
@@ -6290,12 +6319,12 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
        "server-to-client compression method" };
     struct do_ssh2_transport_state {
        int crLine;
-       int nbits, pbits, warn_kex, warn_cscipher, warn_sccipher;
+       int nbits, pbits, warn_kex, warn_hk, warn_cscipher, warn_sccipher;
        Bignum p, g, e, f, K;
        void *our_kexinit;
        int our_kexinitlen;
        int kex_init_value, kex_reply_value;
-       const struct ssh_mac **maclist;
+       const struct ssh_mac *const *maclist;
        int nmacs;
        const struct ssh2_cipher *cscipher_tobe;
        const struct ssh2_cipher *sccipher_tobe;
@@ -6312,6 +6341,8 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
        unsigned char exchange_hash[SSH2_KEX_MAX_HASH_LEN];
        int n_preferred_kex;
        const struct ssh_kexes *preferred_kex[KEX_MAX];
+       int n_preferred_hk;
+       int preferred_hk[HK_MAX];
        int n_preferred_ciphers;
        const struct ssh2_ciphers *preferred_ciphers[CIPHER_MAX];
        const struct ssh_compress *preferred_comp;
@@ -6388,6 +6419,20 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
            }
        }
 
+       /*
+        * Set up the preferred host key types. These are just the ids
+        * in the enum in putty.h, so 'warn below here' is indicated
+        * by HK_WARN.
+        */
+       s->n_preferred_hk = 0;
+       for (i = 0; i < HK_MAX; i++) {
+            int id = conf_get_int_int(ssh->conf, CONF_ssh_hklist, i);
+            /* As above, don't bother with HK_WARN if it's last in the
+             * list */
+           if (id != HK_WARN || i < HK_MAX - 1)
+                s->preferred_hk[s->n_preferred_hk++] = id;
+       }
+
        /*
         * Set up the preferred ciphers. (NULL => warn below here)
         */
@@ -6464,20 +6509,43 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
              * In the first key exchange, we list all the algorithms
              * we're prepared to cope with, but prefer those algorithms
             * for which we have a host key for this host.
+             *
+             * If the host key algorithm is below the warning
+             * threshold, we warn even if we did already have a key
+             * for it, on the basis that if the user has just
+             * reconfigured that host key type to be warned about,
+             * they surely _do_ want to be alerted that a server
+             * they're actually connecting to is using it.
              */
-            for (i = 0; i < lenof(hostkey_algs); i++) {
-               if (have_ssh_host_key(ssh->savedhost, ssh->savedport,
-                                     hostkey_algs[i]->keytype)) {
-                   alg = ssh2_kexinit_addalg(s->kexlists[KEXLIST_HOSTKEY],
-                                             hostkey_algs[i]->name);
-                   alg->u.hostkey = hostkey_algs[i];
-               }
-           }
-            for (i = 0; i < lenof(hostkey_algs); i++) {
-               alg = ssh2_kexinit_addalg(s->kexlists[KEXLIST_HOSTKEY],
-                                         hostkey_algs[i]->name);
-               alg->u.hostkey = hostkey_algs[i];
+            warn = FALSE;
+            for (i = 0; i < s->n_preferred_hk; i++) {
+                if (s->preferred_hk[i] == HK_WARN)
+                    warn = TRUE;
+                for (j = 0; j < lenof(hostkey_algs); j++) {
+                    if (hostkey_algs[j].id != s->preferred_hk[i])
+                        continue;
+                    if (have_ssh_host_key(ssh->savedhost, ssh->savedport,
+                                          hostkey_algs[j].alg->keytype)) {
+                        alg = ssh2_kexinit_addalg(s->kexlists[KEXLIST_HOSTKEY],
+                                                  hostkey_algs[j].alg->name);
+                        alg->u.hk.hostkey = hostkey_algs[j].alg;
+                        alg->u.hk.warn = warn;
+                    }
+                }
            }
+            warn = FALSE;
+            for (i = 0; i < s->n_preferred_hk; i++) {
+                if (s->preferred_hk[i] == HK_WARN)
+                    warn = TRUE;
+                for (j = 0; j < lenof(hostkey_algs); j++) {
+                    if (hostkey_algs[j].id != s->preferred_hk[i])
+                        continue;
+                    alg = ssh2_kexinit_addalg(s->kexlists[KEXLIST_HOSTKEY],
+                                              hostkey_algs[j].alg->name);
+                    alg->u.hk.hostkey = hostkey_algs[j].alg;
+                    alg->u.hk.warn = warn;
+                }
+            }
         } else {
             /*
              * In subsequent key exchanges, we list only the kex
@@ -6489,7 +6557,8 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
             assert(ssh->kex);
            alg = ssh2_kexinit_addalg(s->kexlists[KEXLIST_HOSTKEY],
                                      ssh->hostkey->name);
-           alg->u.hostkey = ssh->hostkey;
+           alg->u.hk.hostkey = ssh->hostkey;
+            alg->u.hk.warn = FALSE;
         }
        /* List encryption algorithms (client->server then server->client). */
        for (k = KEXLIST_CSCIPHER; k <= KEXLIST_SCCIPHER; k++) {
@@ -6610,7 +6679,8 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
        s->scmac_tobe = NULL;
        s->cscomp_tobe = NULL;
        s->sccomp_tobe = NULL;
-       s->warn_kex = s->warn_cscipher = s->warn_sccipher = FALSE;
+       s->warn_kex = s->warn_hk = FALSE;
+        s->warn_cscipher = s->warn_sccipher = FALSE;
 
        pktin->savedpos += 16;          /* skip garbage cookie */
 
@@ -6654,7 +6724,8 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
                        ssh->kex = alg->u.kex.kex;
                        s->warn_kex = alg->u.kex.warn;
                    } else if (i == KEXLIST_HOSTKEY) {
-                       ssh->hostkey = alg->u.hostkey;
+                       ssh->hostkey = alg->u.hk.hostkey;
+                        s->warn_hk = alg->u.hk.warn;
                    } else if (i == KEXLIST_CSCIPHER) {
                        s->cscipher_tobe = alg->u.cipher.cipher;
                        s->warn_cscipher = alg->u.cipher.warn;
@@ -6678,10 +6749,37 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
                    in_commasep_string(alg->u.comp->delayed_name, str, len))
                    s->pending_compression = TRUE;  /* try this later */
            }
-           bombout(("Couldn't agree a %s ((available: %.*s)",
+           bombout(("Couldn't agree a %s (available: %.*s)",
                     kexlist_descr[i], len, str));
            crStopV;
          matched:;
+
+            if (i == KEXLIST_HOSTKEY) {
+                int j;
+
+                /*
+                 * In addition to deciding which host key we're
+                 * actually going to use, we should make a list of the
+                 * host keys offered by the server which we _don't_
+                 * have cached. These will be offered as cross-
+                 * certification options by ssh_get_specials.
+                 *
+                 * We also count the key we're currently using for KEX
+                 * as one we've already got, because by the time this
+                 * menu becomes visible, it will be.
+                 */
+                ssh->n_uncert_hostkeys = 0;
+
+                for (j = 0; j < lenof(hostkey_algs); j++) {
+                    if (hostkey_algs[j].alg != ssh->hostkey &&
+                        in_commasep_string(hostkey_algs[j].alg->name,
+                                           str, len) &&
+                        !have_ssh_host_key(ssh->savedhost, ssh->savedport,
+                                           hostkey_algs[j].alg->keytype)) {
+                        ssh->uncert_hostkeys[ssh->n_uncert_hostkeys++] = j;
+                    }
+                }
+            }
        }
 
        if (s->pending_compression) {
@@ -6726,6 +6824,73 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
            }
        }
 
+       if (s->warn_hk) {
+            int j, k;
+            char *betteralgs;
+
+           ssh_set_frozen(ssh, 1);
+
+            /*
+             * Change warning box wording depending on why we chose a
+             * warning-level host key algorithm. If it's because
+             * that's all we have *cached*, use the askhk mechanism,
+             * and list the host keys we could usefully cross-certify.
+             * Otherwise, use askalg for the standard wording.
+             */
+            betteralgs = NULL;
+            for (j = 0; j < ssh->n_uncert_hostkeys; j++) {
+                const struct ssh_signkey_with_user_pref_id *hktype =
+                    &hostkey_algs[ssh->uncert_hostkeys[j]];
+                int better = FALSE;
+                for (k = 0; k < HK_MAX; k++) {
+                    int id = conf_get_int_int(ssh->conf, CONF_ssh_hklist, k);
+                    if (id == HK_WARN) {
+                        break;
+                    } else if (id == hktype->id) {
+                        better = TRUE;
+                        break;
+                    }
+                }
+                if (better) {
+                    if (betteralgs) {
+                        char *old_ba = betteralgs;
+                        betteralgs = dupcat(betteralgs, ",",
+                                            hktype->alg->name,
+                                            (const char *)NULL);
+                        sfree(old_ba);
+                    } else {
+                        betteralgs = dupstr(hktype->alg->name);
+                    }
+                }
+            }
+            if (betteralgs) {
+                s->dlgret = askhk(ssh->frontend, ssh->hostkey->name,
+                                  betteralgs, ssh_dialog_callback, ssh);
+                sfree(betteralgs);
+            } else {
+                s->dlgret = askalg(ssh->frontend, "host key type",
+                                   ssh->hostkey->name,
+                                   ssh_dialog_callback, ssh);
+            }
+           if (s->dlgret < 0) {
+               do {
+                   crReturnV;
+                   if (pktin) {
+                       bombout(("Unexpected data from server while"
+                                " waiting for user response"));
+                       crStopV;
+                   }
+               } while (pktin || inlen > 0);
+               s->dlgret = ssh->user_response;
+           }
+           ssh_set_frozen(ssh, 0);
+           if (s->dlgret == 0) {
+               ssh_disconnect(ssh, "User aborted at host key warning", NULL,
+                              0, TRUE);
+               crStopV;
+           }
+       }
+
        if (s->warn_cscipher) {
            ssh_set_frozen(ssh, 1);
            s->dlgret = askalg(ssh->frontend,
@@ -7142,6 +7307,42 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
 
     s->keystr = ssh->hostkey->fmtkey(s->hkey);
     if (!s->got_session_id) {
+       /*
+        * Make a note of any other host key formats that are available.
+        */
+       {
+           int i, j, nkeys = 0;
+           char *list = NULL;
+           for (i = 0; i < lenof(hostkey_algs); i++) {
+               if (hostkey_algs[i].alg == ssh->hostkey)
+                   continue;
+
+                for (j = 0; j < ssh->n_uncert_hostkeys; j++)
+                    if (ssh->uncert_hostkeys[j] == i)
+                        break;
+
+                if (j < ssh->n_uncert_hostkeys) {
+                   char *newlist;
+                   if (list)
+                       newlist = dupprintf("%s/%s", list,
+                                           hostkey_algs[i].alg->name);
+                   else
+                       newlist = dupprintf("%s", hostkey_algs[i].alg->name);
+                   sfree(list);
+                   list = newlist;
+                   nkeys++;
+               }
+           }
+           if (list) {
+               logeventf(ssh,
+                         "Server also has %s host key%s, but we "
+                         "don't know %s", list,
+                         nkeys > 1 ? "s" : "",
+                         nkeys > 1 ? "any of them" : "it");
+               sfree(list);
+           }
+       }
+
         /*
          * Authenticate remote host: verify host key. (We've already
          * checked the signature of the exchange hash.)
@@ -7189,6 +7390,18 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
          * subsequent rekeys.
          */
         ssh->hostkey_str = s->keystr;
+    } else if (ssh->cross_certifying) {
+        s->fingerprint = ssh2_fingerprint(ssh->hostkey, s->hkey);
+        logevent("Storing additional host key for this host:");
+        logevent(s->fingerprint);
+        store_host_key(ssh->savedhost, ssh->savedport,
+                       ssh->hostkey->keytype, s->keystr);
+        ssh->cross_certifying = FALSE;
+        /*
+         * Don't forget to store the new key as the one we'll be
+         * re-checking in future normal rekeys.
+         */
+        ssh->hostkey_str = s->keystr;
     } else {
         /*
          * In a rekey, we never present an interactive host key
@@ -7312,14 +7525,14 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
      */
     if (ssh->sc_cipher_ctx)
        ssh->sccipher->free_context(ssh->sc_cipher_ctx);
-    if (ssh->sccipher) {
+    if (s->sccipher_tobe) {
        ssh->sccipher = s->sccipher_tobe;
        ssh->sc_cipher_ctx = ssh->sccipher->make_context();
     }
 
     if (ssh->sc_mac_ctx)
        ssh->scmac->free_context(ssh->sc_mac_ctx);
-    if (ssh->scmac) {
+    if (s->scmac_tobe) {
        ssh->scmac = s->scmac_tobe;
        ssh->scmac_etm = s->scmac_etm_tobe;
        ssh->sc_mac_ctx = ssh->scmac->make_context(ssh->sc_cipher_ctx);
@@ -7375,6 +7588,12 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
      */
     freebn(s->K);
 
+    /*
+     * Update the specials menu to list the remaining uncertified host
+     * keys.
+     */
+    update_specials_menu(ssh->frontend);
+
     /*
      * Key exchange is over. Loop straight back round if we have a
      * deferred rekey reason.
@@ -7862,48 +8081,7 @@ static void ssh2_msg_channel_data(Ssh ssh, struct Packet *pktin)
            bufsize = pfd_send(c->u.pfd.pf, data, length);
            break;
          case CHAN_AGENT:
-           while (length > 0) {
-               if (c->u.a.lensofar < 4) {
-                   unsigned int l = min(4 - c->u.a.lensofar,
-                                        (unsigned)length);
-                   memcpy(c->u.a.msglen + c->u.a.lensofar,
-                          data, l);
-                   data += l;
-                   length -= l;
-                   c->u.a.lensofar += l;
-               }
-               if (c->u.a.lensofar == 4) {
-                   c->u.a.totallen =
-                       4 + GET_32BIT(c->u.a.msglen);
-                   c->u.a.message = snewn(c->u.a.totallen,
-                                          unsigned char);
-                   memcpy(c->u.a.message, c->u.a.msglen, 4);
-               }
-               if (c->u.a.lensofar >= 4 && length > 0) {
-                   unsigned int l =
-                       min(c->u.a.totallen - c->u.a.lensofar,
-                           (unsigned)length);
-                   memcpy(c->u.a.message + c->u.a.lensofar,
-                          data, l);
-                   data += l;
-                   length -= l;
-                   c->u.a.lensofar += l;
-               }
-               if (c->u.a.lensofar == c->u.a.totallen) {
-                   void *reply;
-                   int replylen;
-                    c->u.a.outstanding_requests++;
-                   if (agent_query(c->u.a.message,
-                                   c->u.a.totallen,
-                                   &reply, &replylen,
-                                   ssh_agentf_callback, c))
-                       ssh_agentf_callback(c, reply, replylen);
-                   sfree(c->u.a.message);
-                    c->u.a.message = NULL;
-                   c->u.a.lensofar = 0;
-               }
-           }
-           bufsize = 0;
+           bufsize = ssh_agent_channel_data(c, data, length);
            break;
        }
        /*
@@ -8277,7 +8455,8 @@ static void ssh2_msg_channel_open_failure(Ssh ssh, struct Packet *pktin)
             reason_code = 0; /* ensure reasons[reason_code] in range */
         ssh_pkt_getstring(pktin, &reason_string, &reason_length);
         logeventf(ssh, "Forwarded connection refused by server: %s [%.*s]",
-                  reasons[reason_code], reason_length, reason_string);
+                  reasons[reason_code], reason_length,
+                  NULLTOEMPTY(reason_string));
 
         pfd_close(c->u.pfd.pf);
     } else if (c->type == CHAN_ZOMBIE) {
@@ -8573,9 +8752,7 @@ static void ssh2_msg_channel_open(Ssh ssh, struct Packet *pktin)
        char *addrstr;
 
        ssh_pkt_getstring(pktin, &peeraddr, &peeraddrlen);
-       addrstr = snewn(peeraddrlen+1, char);
-       memcpy(addrstr, peeraddr, peeraddrlen);
-       addrstr[peeraddrlen] = '\0';
+       addrstr = dupprintf("%.*s", peeraddrlen, NULLTOEMPTY(peeraddr));
        peerport = ssh_pkt_getuint32(pktin);
 
        logeventf(ssh, "Received X11 connect request from %s:%d",
@@ -8610,13 +8787,14 @@ static void ssh2_msg_channel_open(Ssh ssh, struct Packet *pktin)
        char *shost;
        int shostlen;
        ssh_pkt_getstring(pktin, &shost, &shostlen);/* skip address */
-        pf.shost = dupprintf("%.*s", shostlen, shost);
+        pf.shost = dupprintf("%.*s", shostlen, NULLTOEMPTY(shost));
        pf.sport = ssh_pkt_getuint32(pktin);
        ssh_pkt_getstring(pktin, &peeraddr, &peeraddrlen);
        peerport = ssh_pkt_getuint32(pktin);
        realpf = find234(ssh->rportfwds, &pf, NULL);
        logeventf(ssh, "Received remote port %s:%d open request "
-                 "from %s:%d", pf.shost, pf.sport, peeraddr, peerport);
+                 "from %.*s:%d", pf.shost, pf.sport,
+                  peeraddrlen, NULLTOEMPTY(peeraddr), peerport);
         sfree(pf.shost);
 
        if (realpf == NULL) {
@@ -9010,7 +9188,6 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen,
        int pklen, alglen, commentlen;
        int siglen, retlen, len;
        char *q, *agentreq, *ret;
-       int try_send;
        struct Packet *pktout;
        Filename *keyfile;
 #ifndef NO_GSSAPI
@@ -10280,7 +10457,7 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen,
                    s->cur_prompt->to_server = TRUE;
                    s->cur_prompt->name = dupstr("New SSH password");
                    s->cur_prompt->instruction =
-                       dupprintf("%.*s", prompt_len, prompt);
+                       dupprintf("%.*s", prompt_len, NULLTOEMPTY(prompt));
                    s->cur_prompt->instr_reqd = TRUE;
                    /*
                     * There's no explicit requirement in the protocol
@@ -10663,7 +10840,6 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen,
        ssh->send_ok = 1;
     while (1) {
        crReturnV;
-       s->try_send = FALSE;
        if (pktin) {
 
            /*
@@ -10679,17 +10855,7 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen,
             * We have spare data. Add it to the channel buffer.
             */
            ssh2_add_channel_data(ssh->mainchan, (char *)in, inlen);
-           s->try_send = TRUE;
-       }
-       if (s->try_send) {
-           int i;
-           struct ssh_channel *c;
-           /*
-            * Try to send data on all channels if we can.
-            */
-           for (i = 0; NULL != (c = index234(ssh->channels, i)); i++)
-                if (c->type != CHAN_SHARING)
-                    ssh2_try_send_and_unthrottle(ssh, c);
+           ssh2_try_send(ssh->mainchan);
        }
     }
 
@@ -10718,13 +10884,13 @@ static void ssh2_msg_disconnect(Ssh ssh, struct Packet *pktin)
     logevent(buf);
     sfree(buf);
     buf = dupprintf("Disconnection message text: %.*s",
-                   msglen, msg);
+                   msglen, NULLTOEMPTY(msg));
     logevent(buf);
     bombout(("Server sent disconnect message\ntype %d (%s):\n\"%.*s\"",
             reason,
             (reason > 0 && reason < lenof(ssh2_disconnect_reasons)) ?
             ssh2_disconnect_reasons[reason] : "unknown",
-            msglen, msg));
+            msglen, NULLTOEMPTY(msg)));
     sfree(buf);
 }
 
@@ -10738,7 +10904,7 @@ static void ssh2_msg_debug(Ssh ssh, struct Packet *pktin)
     ssh2_pkt_getbool(pktin);
     ssh_pkt_getstring(pktin, &msg, &msglen);
 
-    logeventf(ssh, "Remote debug message: %.*s", msglen, msg);
+    logeventf(ssh, "Remote debug message: %.*s", msglen, NULLTOEMPTY(msg));
 }
 
 static void ssh2_msg_transport(Ssh ssh, struct Packet *pktin)
@@ -11015,6 +11181,10 @@ static const char *ssh_init(void *frontend_handle, void **backend_handle,
     ssh->X11_fwd_enabled = FALSE;
     ssh->connshare = NULL;
     ssh->attempting_connshare = FALSE;
+    ssh->session_started = FALSE;
+    ssh->specials = NULL;
+    ssh->n_uncert_hostkeys = 0;
+    ssh->cross_certifying = FALSE;
 
     *backend_handle = ssh;
 
@@ -11162,6 +11332,7 @@ static void ssh_free(void *handle)
     sfree(ssh->v_s);
     sfree(ssh->fullhostname);
     sfree(ssh->hostkey_str);
+    sfree(ssh->specials);
     if (ssh->crcda_ctx) {
        crcda_free_context(ssh->crcda_ctx);
        ssh->crcda_ctx = NULL;
@@ -11375,19 +11546,24 @@ static const struct telnet_special *ssh_get_specials(void *handle)
     static const struct telnet_special specials_end[] = {
        {NULL, TS_EXITMENU}
     };
-    /* XXX review this length for any changes: */
-    static struct telnet_special ssh_specials[lenof(ssh2_ignore_special) +
-                                             lenof(ssh2_rekey_special) +
-                                             lenof(ssh2_session_specials) +
-                                             lenof(specials_end)];
+
+    struct telnet_special *specials = NULL;
+    int nspecials = 0, specialsize = 0;
+
     Ssh ssh = (Ssh) handle;
-    int i = 0;
-#define ADD_SPECIALS(name) \
-    do { \
-       assert((i + lenof(name)) <= lenof(ssh_specials)); \
-       memcpy(&ssh_specials[i], name, sizeof name); \
-       i += lenof(name); \
-    } while(0)
+
+    sfree(ssh->specials);
+
+#define ADD_SPECIALS(name) do                                           \
+    {                                                                   \
+        int len = lenof(name);                                          \
+        if (nspecials + len > specialsize) {                            \
+            specialsize = (nspecials + len) * 5 / 4 + 32;               \
+            specials = sresize(specials, specialsize, struct telnet_special); \
+        }                                                               \
+       memcpy(specials+nspecials, name, len*sizeof(struct telnet_special)); \
+        nspecials += len;                                               \
+    } while (0)
 
     if (ssh->version == 1) {
        /* Don't bother offering IGNORE if we've decided the remote
@@ -11402,11 +11578,37 @@ static const struct telnet_special *ssh_get_specials(void *handle)
            ADD_SPECIALS(ssh2_rekey_special);
        if (ssh->mainchan)
            ADD_SPECIALS(ssh2_session_specials);
+
+        if (ssh->n_uncert_hostkeys) {
+            static const struct telnet_special uncert_start[] = {
+                {NULL, TS_SEP},
+                {"Cache new host key type", TS_SUBMENU},
+            };
+            static const struct telnet_special uncert_end[] = {
+                {NULL, TS_EXITMENU},
+            };
+            int i;
+
+            ADD_SPECIALS(uncert_start);
+            for (i = 0; i < ssh->n_uncert_hostkeys; i++) {
+                struct telnet_special uncert[1];
+                const struct ssh_signkey *alg =
+                    hostkey_algs[ssh->uncert_hostkeys[i]].alg;
+                uncert[0].name = alg->name;
+                uncert[0].code = TS_LOCALSTART + ssh->uncert_hostkeys[i];
+                ADD_SPECIALS(uncert);
+            }
+            ADD_SPECIALS(uncert_end);
+        }
     } /* else we're not ready yet */
 
-    if (i) {
+    if (nspecials)
        ADD_SPECIALS(specials_end);
-       return ssh_specials;
+
+    ssh->specials = specials;
+
+    if (nspecials) {
+        return specials;
     } else {
        return NULL;
     }
@@ -11458,6 +11660,13 @@ static void ssh_special(void *handle, Telnet_Special code)
             ssh->version == 2) {
            do_ssh2_transport(ssh, "at user request", -1, NULL);
        }
+    } else if (code >= TS_LOCALSTART) {
+        ssh->hostkey = hostkey_algs[code - TS_LOCALSTART].alg;
+        ssh->cross_certifying = TRUE;
+       if (!ssh->kex_in_progress && !ssh->bare_connection &&
+            ssh->version == 2) {
+           do_ssh2_transport(ssh, "cross-certifying new host key", -1, NULL);
+       }
     } else if (code == TS_BRK) {
        if (ssh->state == SSH_STATE_CLOSED
            || ssh->state == SSH_STATE_PREPACKET) return;