]> asedeno.scripts.mit.edu Git - PuTTY.git/blobdiff - ssh.c
Update the specials menu as keys are cross-certified.
[PuTTY.git] / ssh.c
diff --git a/ssh.c b/ssh.c
index f1d62e83b51926bffb128a312d199165d6b2eeab..643e5df6a316acd453339543e6a06a0b492b0a5b 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"
@@ -364,6 +365,7 @@ static void do_ssh2_authconn(Ssh ssh, const unsigned char *in, int inlen,
                             struct Packet *pktin);
 static void ssh2_channel_check_close(struct ssh_channel *c);
 static void ssh_channel_destroy(struct ssh_channel *c);
+static void ssh2_msg_something_unimplemented(Ssh ssh, struct Packet *pktin);
 
 /*
  * Buffer management constants. There are several of these for
@@ -791,6 +793,7 @@ struct ssh_tag {
     int send_ok;
     int echoing, editing;
 
+    int session_started;
     void *frontend;
 
     int ospeed, ispeed;                       /* temporaries */
@@ -952,6 +955,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)
@@ -1834,6 +1855,15 @@ static struct Packet *ssh2_rdpkt(Ssh ssh, const unsigned char **data,
        }
     }
 
+    /*
+     * RFC 4253 doesn't explicitly say that completely empty packets
+     * with no type byte are forbidden, so treat them as deserving
+     * an SSH_MSG_UNIMPLEMENTED.
+     */
+    if (st->pktin->length <= 5) { /* == 5 we hope, but robustness */
+        ssh2_msg_something_unimplemented(ssh, st->pktin);
+        crStop(NULL);
+    }
     /*
      * pktin->body and pktin->length should identify the semantic
      * content of the packet, excluding the initial type byte.
@@ -3007,6 +3037,10 @@ static void ssh_send_verstring(Ssh ssh, const char *protoname, char *svers)
     }
 
     ssh_fix_verstring(verstring + strlen(protoname));
+#ifdef FUZZING
+    /* FUZZING make PuTTY insecure, so make live use difficult. */
+    verstring[0] = 'I';
+#endif
 
     if (ssh->version == 2) {
        size_t len;
@@ -3056,6 +3090,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);
@@ -3438,34 +3474,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,
@@ -3549,35 +3571,20 @@ static void ssh_sent(Plug plug, int bufsize)
        ssh_throttle_all(ssh, 0, bufsize);
 }
 
-/*
- * Connect to specified host and port.
- * Returns an error message, or NULL on success.
- * Also places the canonical host name into `realhost'. It must be
- * freed by the caller.
- */
-static const char *connect_to_host(Ssh ssh, const char *host, int port,
-                                  char **realhost, int nodelay, int keepalive)
+static void ssh_hostport_setup(const char *host, int port, Conf *conf,
+                               char **savedhost, int *savedport,
+                               char **loghost_ret)
 {
-    static const struct plug_function_table fn_table = {
-       ssh_socket_log,
-       ssh_closing,
-       ssh_receive,
-       ssh_sent,
-       NULL
-    };
+    char *loghost = conf_get_str(conf, CONF_loghost);
+    if (loghost_ret)
+        *loghost_ret = loghost;
 
-    SockAddr addr;
-    const char *err;
-    char *loghost;
-    int addressfamily, sshprot;
-    
-    loghost = conf_get_str(ssh->conf, CONF_loghost);
     if (*loghost) {
        char *tmphost;
         char *colon;
 
         tmphost = dupstr(loghost);
-       ssh->savedport = 22;           /* default ssh port */
+       *savedport = 22;               /* default ssh port */
 
        /*
         * A colon suffix on the hostname string also lets us affect
@@ -3588,17 +3595,58 @@ static const char *connect_to_host(Ssh ssh, const char *host, int port,
        if (colon && colon == host_strchr(tmphost, ':')) {
            *colon++ = '\0';
            if (*colon)
-               ssh->savedport = atoi(colon);
+               *savedport = atoi(colon);
        }
 
-        ssh->savedhost = host_strduptrim(tmphost);
+        *savedhost = host_strduptrim(tmphost);
         sfree(tmphost);
     } else {
-       ssh->savedhost = host_strduptrim(host);
+       *savedhost = host_strduptrim(host);
        if (port < 0)
            port = 22;                 /* default ssh port */
-       ssh->savedport = port;
+       *savedport = port;
     }
+}
+
+static int ssh_test_for_upstream(const char *host, int port, Conf *conf)
+{
+    char *savedhost;
+    int savedport;
+    int ret;
+
+    random_ref(); /* platform may need this to determine share socket name */
+    ssh_hostport_setup(host, port, conf, &savedhost, &savedport, NULL);
+    ret = ssh_share_test_for_upstream(savedhost, savedport, conf);
+    sfree(savedhost);
+    random_unref();
+
+    return ret;
+}
+
+/*
+ * Connect to specified host and port.
+ * Returns an error message, or NULL on success.
+ * Also places the canonical host name into `realhost'. It must be
+ * freed by the caller.
+ */
+static const char *connect_to_host(Ssh ssh, const char *host, int port,
+                                  char **realhost, int nodelay, int keepalive)
+{
+    static const struct plug_function_table fn_table = {
+       ssh_socket_log,
+       ssh_closing,
+       ssh_receive,
+       ssh_sent,
+       NULL
+    };
+
+    SockAddr addr;
+    const char *err;
+    char *loghost;
+    int addressfamily, sshprot;
+
+    ssh_hostport_setup(host, port, ssh->conf,
+                       &ssh->savedhost, &ssh->savedport, &loghost);
 
     ssh->fn = &fn_table;               /* make 'ssh' usable as a Plug */
 
@@ -3634,10 +3682,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;
@@ -4014,6 +4060,9 @@ static int do_ssh1_login(Ssh ssh, const unsigned char *in, int inlen,
                                             "rsa", keystr, fingerprint,
                                             ssh_dialog_callback, ssh);
             sfree(keystr);
+#ifdef FUZZING
+           s->dlgret = 1;
+#endif
             if (s->dlgret < 0) {
                 do {
                     crReturn(0);
@@ -5532,7 +5581,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);
 
@@ -6015,7 +6064,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)
@@ -6025,7 +6074,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)
@@ -6127,16 +6177,24 @@ static void ssh2_pkt_addstring_commasep(struct Packet *pkt, const char *data)
 
 
 /*
- * SSH-2 key creation method.
- * (Currently assumes 2 lots of any hash are sufficient to generate
- * keys/IVs for any cipher/MAC. SSH2_MKKEY_ITERS documents this assumption.)
+ * SSH-2 key derivation (RFC 4253 section 7.2).
  */
-#define SSH2_MKKEY_ITERS (2)
-static void ssh2_mkkey(Ssh ssh, Bignum K, unsigned char *H, char chr,
-                      unsigned char *keyspace)
+static unsigned char *ssh2_mkkey(Ssh ssh, Bignum K, unsigned char *H,
+                                 char chr, int keylen)
 {
     const struct ssh_hash *h = ssh->kex->hash;
-    void *s;
+    int keylen_padded;
+    unsigned char *key;
+    void *s, *s2;
+
+    if (keylen == 0)
+        return NULL;
+
+    /* Round up to the next multiple of hash length. */
+    keylen_padded = ((keylen + h->hlen - 1) / h->hlen) * h->hlen;
+
+    key = snewn(keylen_padded, unsigned char);
+
     /* First hlen bytes. */
     s = h->init();
     if (!(ssh->remote_bugs & BUG_SSH2_DERIVEKEY))
@@ -6144,14 +6202,33 @@ static void ssh2_mkkey(Ssh ssh, Bignum K, unsigned char *H, char chr,
     h->bytes(s, H, h->hlen);
     h->bytes(s, &chr, 1);
     h->bytes(s, ssh->v2_session_id, ssh->v2_session_id_len);
-    h->final(s, keyspace);
-    /* Next hlen bytes. */
-    s = h->init();
-    if (!(ssh->remote_bugs & BUG_SSH2_DERIVEKEY))
-       hash_mpint(h, s, K);
-    h->bytes(s, H, h->hlen);
-    h->bytes(s, keyspace, h->hlen);
-    h->final(s, keyspace + h->hlen);
+    h->final(s, key);
+
+    /* Subsequent blocks of hlen bytes. */
+    if (keylen_padded > h->hlen) {
+        int offset;
+
+        s = h->init();
+        if (!(ssh->remote_bugs & BUG_SSH2_DERIVEKEY))
+            hash_mpint(h, s, K);
+        h->bytes(s, H, h->hlen);
+
+        for (offset = h->hlen; offset < keylen_padded; offset += h->hlen) {
+            h->bytes(s, key + offset - h->hlen, h->hlen);
+            s2 = h->copy(s);
+            h->final(s2, key + offset);
+        }
+
+        h->free(s);
+    }
+
+    /* Now clear any extra bytes of key material beyond the length
+     * we're officially returning, because the caller won't know to
+     * smemclr those. */
+    if (keylen_padded > keylen)
+        smemclr(key + keylen, keylen_padded - keylen);
+
+    return key;
 }
 
 /*
@@ -6424,6 +6501,11 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
        /* List encryption algorithms (client->server then server->client). */
        for (k = KEXLIST_CSCIPHER; k <= KEXLIST_SCCIPHER; k++) {
            warn = FALSE;
+#ifdef FUZZING
+           alg = ssh2_kexinit_addalg(s->kexlists[k], "none");
+           alg->u.cipher.cipher = NULL;
+           alg->u.cipher.warn = warn;
+#endif /* FUZZING */
            for (i = 0; i < s->n_preferred_ciphers; i++) {
                const struct ssh2_ciphers *c = s->preferred_ciphers[i];
                if (!c) warn = TRUE;
@@ -6437,6 +6519,11 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
        }
        /* List MAC algorithms (client->server then server->client). */
        for (j = KEXLIST_CSMAC; j <= KEXLIST_SCMAC; j++) {
+#ifdef FUZZING
+           alg = ssh2_kexinit_addalg(s->kexlists[j], "none");
+           alg->u.mac.mac = NULL;
+           alg->u.mac.etm = FALSE;
+#endif /* FUZZING */
            for (i = 0; i < s->nmacs; i++) {
                alg = ssh2_kexinit_addalg(s->kexlists[j], s->maclist[i]->name);
                alg->u.mac.mac = s->maclist[i];
@@ -6541,6 +6628,24 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
                bombout(("KEXINIT packet was incomplete"));
                crStopV;
            }
+
+            /* If we've already selected a cipher which requires a
+             * particular MAC, then just select that, and don't even
+             * bother looking through the server's KEXINIT string for
+             * MACs. */
+            if (i == KEXLIST_CSMAC && s->cscipher_tobe &&
+                s->cscipher_tobe->required_mac) {
+                s->csmac_tobe = s->cscipher_tobe->required_mac;
+                s->csmac_etm_tobe = !!(s->csmac_tobe->etm_name);
+                goto matched;
+            }
+            if (i == KEXLIST_SCMAC && s->sccipher_tobe &&
+                s->sccipher_tobe->required_mac) {
+                s->scmac_tobe = s->sccipher_tobe->required_mac;
+                s->scmac_etm_tobe = !!(s->scmac_tobe->etm_name);
+                goto matched;
+            }
+
            for (j = 0; j < MAXKEXLIST; j++) {
                struct kexinit_algorithm *alg = &s->kexlists[i][j];
                if (alg->name == NULL) break;
@@ -6584,17 +6689,33 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
                     kexlist_descr[i], len, str));
            crStopV;
          matched:;
-       }
 
-        /* If the cipher over-rides the mac, then pick it */
-        if (s->cscipher_tobe && s->cscipher_tobe->required_mac) {
-            s->csmac_tobe = s->cscipher_tobe->required_mac;
-           s->csmac_etm_tobe = !!(s->csmac_tobe->etm_name);
-        }
-        if (s->sccipher_tobe && s->sccipher_tobe->required_mac) {
-            s->scmac_tobe = s->sccipher_tobe->required_mac;
-           s->scmac_etm_tobe = !!(s->scmac_tobe->etm_name);
-        }
+            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] != ssh->hostkey &&
+                        in_commasep_string(hostkey_algs[j]->name, str, len) &&
+                        !have_ssh_host_key(ssh->savedhost, ssh->savedport,
+                                           hostkey_algs[j]->keytype)) {
+                        ssh->uncert_hostkeys[ssh->n_uncert_hostkeys++] = j;
+                    }
+                }
+            }
+       }
 
        if (s->pending_compression) {
            logevent("Server supports delayed compression; "
@@ -6701,8 +6822,8 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
         {
             int csbits, scbits;
 
-            csbits = s->cscipher_tobe->keylen;
-            scbits = s->sccipher_tobe->keylen;
+            csbits = s->cscipher_tobe ? s->cscipher_tobe->real_keybits : 0;
+            scbits = s->sccipher_tobe ? s->sccipher_tobe->real_keybits : 0;
             s->nbits = (csbits > scbits ? csbits : scbits);
         }
         /* The keys only have hlen-bit entropy, since they're based on
@@ -7038,12 +7159,18 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
     dmemdump(s->exchange_hash, ssh->kex->hash->hlen);
 #endif
 
-    if (!s->hkey ||
-       !ssh->hostkey->verifysig(s->hkey, s->sigdata, s->siglen,
+    if (!s->hkey) {
+       bombout(("Server's host key is invalid"));
+       crStopV;
+    }
+
+    if (!ssh->hostkey->verifysig(s->hkey, s->sigdata, s->siglen,
                                 (char *)s->exchange_hash,
                                 ssh->kex->hash->hlen)) {
+#ifndef FUZZING
        bombout(("Server's host key did not match the signature supplied"));
        crStopV;
+#endif
     }
 
     s->keystr = ssh->hostkey->fmtkey(s->hkey);
@@ -7068,6 +7195,9 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
                                             ssh->hostkey->keytype, s->keystr,
                                             s->fingerprint,
                                             ssh_dialog_callback, ssh);
+#ifdef FUZZING
+           s->dlgret = 1;
+#endif
             if (s->dlgret < 0) {
                 do {
                     crReturnV;
@@ -7092,6 +7222,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
@@ -7100,8 +7242,10 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
          * the one we saw before.
          */
         if (strcmp(ssh->hostkey_str, s->keystr)) {
+#ifndef FUZZING
             bombout(("Host key was different in repeat key exchange"));
             crStopV;
+#endif
         }
         sfree(s->keystr);
     }
@@ -7135,13 +7279,14 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
     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->cscipher) 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->csmac_etm = s->csmac_etm_tobe;
-    ssh->cs_mac_ctx = ssh->csmac->make_context(ssh->cs_cipher_ctx);
+    if (ssh->csmac)
+        ssh->cs_mac_ctx = ssh->csmac->make_context(ssh->cs_cipher_ctx);
 
     if (ssh->cs_comp_ctx)
        ssh->cscomp->compress_cleanup(ssh->cs_comp_ctx);
@@ -7152,30 +7297,39 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
      * Set IVs on client-to-server keys. Here we use the exchange
      * hash from the _first_ key exchange.
      */
-    {
-       unsigned char keyspace[SSH2_KEX_MAX_HASH_LEN * SSH2_MKKEY_ITERS];
-       assert(sizeof(keyspace) >= ssh->kex->hash->hlen * SSH2_MKKEY_ITERS);
-       ssh2_mkkey(ssh,s->K,s->exchange_hash,'C',keyspace);
-       assert((ssh->cscipher->keylen+7) / 8 <=
-              ssh->kex->hash->hlen * SSH2_MKKEY_ITERS);
-       ssh->cscipher->setkey(ssh->cs_cipher_ctx, keyspace);
-       ssh2_mkkey(ssh,s->K,s->exchange_hash,'A',keyspace);
-       assert(ssh->cscipher->blksize <=
-              ssh->kex->hash->hlen * SSH2_MKKEY_ITERS);
-       ssh->cscipher->setiv(ssh->cs_cipher_ctx, keyspace);
-       ssh2_mkkey(ssh,s->K,s->exchange_hash,'E',keyspace);
-       assert(ssh->csmac->keylen <=
-              ssh->kex->hash->hlen * SSH2_MKKEY_ITERS);
-       ssh->csmac->setkey(ssh->cs_mac_ctx, keyspace);
-       smemclr(keyspace, sizeof(keyspace));
-    }
-
-    logeventf(ssh, "Initialised %.200s client->server encryption",
-             ssh->cscipher->text_name);
-    logeventf(ssh, "Initialised %.200s client->server MAC algorithm%s%s",
-             ssh->csmac->text_name,
-              ssh->csmac_etm ? " (in ETM mode)" : "",
-              ssh->cscipher->required_mac ? " (required by cipher)" : "");
+    if (ssh->cscipher) {
+       unsigned char *key;
+
+       key = ssh2_mkkey(ssh, s->K, s->exchange_hash, 'C',
+                         ssh->cscipher->padded_keybytes);
+       ssh->cscipher->setkey(ssh->cs_cipher_ctx, key);
+        smemclr(key, ssh->cscipher->padded_keybytes);
+        sfree(key);
+
+       key = ssh2_mkkey(ssh, s->K, s->exchange_hash, 'A',
+                         ssh->cscipher->blksize);
+       ssh->cscipher->setiv(ssh->cs_cipher_ctx, key);
+        smemclr(key, ssh->cscipher->blksize);
+        sfree(key);
+    }
+    if (ssh->csmac) {
+       unsigned char *key;
+
+       key = ssh2_mkkey(ssh, s->K, s->exchange_hash, 'E',
+                         ssh->csmac->keylen);
+       ssh->csmac->setkey(ssh->cs_mac_ctx, key);
+        smemclr(key, ssh->csmac->keylen);
+        sfree(key);
+    }
+
+    if (ssh->cscipher)
+       logeventf(ssh, "Initialised %.200s client->server encryption",
+                 ssh->cscipher->text_name);
+    if (ssh->csmac)
+       logeventf(ssh, "Initialised %.200s client->server MAC algorithm%s%s",
+                 ssh->csmac->text_name,
+                 ssh->csmac_etm ? " (in ETM mode)" : "",
+                 ssh->cscipher->required_mac ? " (required by cipher)" : "");
     if (ssh->cscomp->text_name)
        logeventf(ssh, "Initialised %s compression",
                  ssh->cscomp->text_name);
@@ -7203,14 +7357,18 @@ 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);
-    ssh->sccipher = s->sccipher_tobe;
-    ssh->sc_cipher_ctx = ssh->sccipher->make_context();
+    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);
-    ssh->scmac = s->scmac_tobe;
-    ssh->scmac_etm = s->scmac_etm_tobe;
-    ssh->sc_mac_ctx = ssh->scmac->make_context(ssh->sc_cipher_ctx);
+    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);
+    }
 
     if (ssh->sc_comp_ctx)
        ssh->sccomp->decompress_cleanup(ssh->sc_comp_ctx);
@@ -7221,29 +7379,38 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
      * Set IVs on server-to-client keys. Here we use the exchange
      * hash from the _first_ key exchange.
      */
-    {
-       unsigned char keyspace[SSH2_KEX_MAX_HASH_LEN * SSH2_MKKEY_ITERS];
-       assert(sizeof(keyspace) >= ssh->kex->hash->hlen * SSH2_MKKEY_ITERS);
-       ssh2_mkkey(ssh,s->K,s->exchange_hash,'D',keyspace);
-       assert((ssh->sccipher->keylen+7) / 8 <=
-              ssh->kex->hash->hlen * SSH2_MKKEY_ITERS);
-       ssh->sccipher->setkey(ssh->sc_cipher_ctx, keyspace);
-       ssh2_mkkey(ssh,s->K,s->exchange_hash,'B',keyspace);
-       assert(ssh->sccipher->blksize <=
-              ssh->kex->hash->hlen * SSH2_MKKEY_ITERS);
-       ssh->sccipher->setiv(ssh->sc_cipher_ctx, keyspace);
-       ssh2_mkkey(ssh,s->K,s->exchange_hash,'F',keyspace);
-       assert(ssh->scmac->keylen <=
-              ssh->kex->hash->hlen * SSH2_MKKEY_ITERS);
-       ssh->scmac->setkey(ssh->sc_mac_ctx, keyspace);
-       smemclr(keyspace, sizeof(keyspace));
-    }
-    logeventf(ssh, "Initialised %.200s server->client encryption",
-             ssh->sccipher->text_name);
-    logeventf(ssh, "Initialised %.200s server->client MAC algorithm%s%s",
-             ssh->scmac->text_name,
-              ssh->scmac_etm ? " (in ETM mode)" : "",
-              ssh->sccipher->required_mac ? " (required by cipher)" : "");
+    if (ssh->sccipher) {
+       unsigned char *key;
+
+       key = ssh2_mkkey(ssh, s->K, s->exchange_hash, 'D',
+                         ssh->sccipher->padded_keybytes);
+       ssh->sccipher->setkey(ssh->sc_cipher_ctx, key);
+        smemclr(key, ssh->sccipher->padded_keybytes);
+        sfree(key);
+
+       key = ssh2_mkkey(ssh, s->K, s->exchange_hash, 'B',
+                         ssh->sccipher->blksize);
+       ssh->sccipher->setiv(ssh->sc_cipher_ctx, key);
+        smemclr(key, ssh->sccipher->blksize);
+        sfree(key);
+    }
+    if (ssh->scmac) {
+       unsigned char *key;
+
+       key = ssh2_mkkey(ssh, s->K, s->exchange_hash, 'F',
+                         ssh->scmac->keylen);
+       ssh->scmac->setkey(ssh->sc_mac_ctx, key);
+        smemclr(key, ssh->scmac->keylen);
+        sfree(key);
+    }
+    if (ssh->sccipher)
+       logeventf(ssh, "Initialised %.200s server->client encryption",
+                 ssh->sccipher->text_name);
+    if (ssh->scmac)
+       logeventf(ssh, "Initialised %.200s server->client MAC algorithm%s%s",
+                 ssh->scmac->text_name,
+                 ssh->scmac_etm ? " (in ETM mode)" : "",
+                 ssh->sccipher->required_mac ? " (required by cipher)" : "");
     if (ssh->sccomp->text_name)
        logeventf(ssh, "Initialised %s decompression",
                  ssh->sccomp->text_name);
@@ -7253,6 +7420,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.
@@ -8155,7 +8328,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) {
@@ -8451,9 +8625,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",
@@ -8488,13 +8660,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) {
@@ -10158,7 +10331,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
@@ -10596,13 +10769,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);
 }
 
@@ -10616,7 +10789,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)
@@ -10893,6 +11066,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;
 
@@ -11040,6 +11217,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;
@@ -11253,19 +11431,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
@@ -11280,11 +11463,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]];
+                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;
     }
@@ -11336,6 +11545,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];
+        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;
@@ -11591,6 +11807,7 @@ Backend ssh_backend = {
     ssh_provide_logctx,
     ssh_unthrottle,
     ssh_cfg_info,
+    ssh_test_for_upstream,
     "ssh",
     PROT_SSH,
     22