]> asedeno.scripts.mit.edu Git - PuTTY.git/commitdiff
Configurable preference list for SSH host key types.
authorSimon Tatham <anakin@pobox.com>
Fri, 25 Mar 2016 15:56:31 +0000 (15:56 +0000)
committerSimon Tatham <anakin@pobox.com>
Fri, 25 Mar 2016 16:32:17 +0000 (16:32 +0000)
Now we actually have enough of them to worry about, and especially
since some of the types we support are approved by organisations that
people might make their own decisions about whether to trust, it seems
worth having a config list for host keys the same way we have one for
kex types and ciphers.

To make room for this, I've created an SSH > Host Keys config panel,
and moved the existing host-key related configuration (manually
specified fingerprints) into there from the Kex panel.

config.c
doc/config.but
putty.h
settings.c
ssh.c
windows/winhelp.h

index c48493befc8ba42b7291c3746e2a9e3e9046b32f..b32cfae5b3566e3df38e7ccf57920d857c2241cc 100644 (file)
--- a/config.c
+++ b/config.c
@@ -466,6 +466,49 @@ static void kexlist_handler(union control *ctrl, void *dlg,
     }
 }
 
+static void hklist_handler(union control *ctrl, void *dlg,
+                            void *data, int event)
+{
+    Conf *conf = (Conf *)data;
+    if (event == EVENT_REFRESH) {
+        int i;
+
+        static const struct { const char *s; int k; } hks[] = {
+            { "Ed25519",               HK_ED25519 },
+            { "ECDSA",                 HK_ECDSA },
+            { "DSA",                   HK_DSA },
+            { "RSA",                   HK_RSA },
+            { "-- warn below here --", HK_WARN }
+        };
+
+        /* Set up the "host key preference" box. */
+        /* (hklist assumed to contain all algorithms) */
+        dlg_update_start(ctrl, dlg);
+        dlg_listbox_clear(ctrl, dlg);
+        for (i = 0; i < HK_MAX; i++) {
+            int k = conf_get_int_int(conf, CONF_ssh_hklist, i);
+            int j;
+            const char *kstr = NULL;
+            for (j = 0; j < lenof(hks); j++) {
+                if (hks[j].k == k) {
+                    kstr = hks[j].s;
+                    break;
+                }
+            }
+            dlg_listbox_addwithid(ctrl, dlg, kstr, k);
+        }
+        dlg_update_done(ctrl, dlg);
+
+    } else if (event == EVENT_VALCHANGE) {
+        int i;
+
+        /* Update array to match the list box. */
+        for (i=0; i < HK_MAX; i++)
+            conf_set_int_int(conf, CONF_ssh_hklist, i,
+                             dlg_listbox_getid(ctrl, dlg, i));
+    }
+}
+
 static void printerbox_handler(union control *ctrl, void *dlg,
                               void *data, int event)
 {
@@ -2249,13 +2292,28 @@ void setup_config_box(struct controlbox *b, int midsession,
                      HELPCTX(ssh_kex_repeat));
        }
 
+       /*
+        * The 'Connection/SSH/Host keys' panel.
+        */
+       if (protcfginfo != 1 && protcfginfo != -1) {
+           ctrl_settitle(b, "Connection/SSH/Host keys",
+                         "Options controlling SSH host keys");
+
+           s = ctrl_getset(b, "Connection/SSH/Host keys", "main",
+                           "Host key algorithm preference");
+           c = ctrl_draglist(s, "Algorithm selection policy:", 's',
+                             HELPCTX(ssh_hklist),
+                             hklist_handler, P(NULL));
+           c->listbox.height = 5;
+       }
+
        /*
         * Manual host key configuration is irrelevant mid-session,
         * as we enforce that the host key for rekeys is the
         * same as that used at the start of the session.
         */
        if (!midsession) {
-           s = ctrl_getset(b, "Connection/SSH/Kex", "hostkeys",
+           s = ctrl_getset(b, "Connection/SSH/Host keys", "hostkeys",
                            "Manually configure host keys for this connection");
 
             ctrl_columns(s, 2, 75, 25);
index f0ccc1503b1abff2e066496eebbb7f0333c4e242..c8e68113895dfa211681d696ad08b4a58347be64 100644 (file)
@@ -2483,6 +2483,47 @@ when the SSH connection is idle, so they shouldn't cause the same
 problems.  The SSH-1 protocol, incidentally, has even weaker integrity
 protection than SSH-2 without rekeys.
 
+\H{config-ssh-hostkey} The Host Keys panel
+
+The Host Keys panel allows you to configure options related to SSH-2
+host key management.
+
+Host keys are used to prove the server's identity, and assure you that
+the server is not being spoofed (either by a man-in-the-middle attack
+or by completely replacing it on the network).
+
+This entire panel is only relevant to SSH protocol version 2; none of
+these settings affect SSH-1 at all.
+
+\S{config-ssh-hostkey-order} \ii{Host key type} selection
+
+\cfg{winhelp-topic}{ssh.hostkey.order}
+
+PuTTY supports a variety of SSH-2 host key types, and allows you to
+choose which one you prefer to use to identify the server.
+Configuration is similar to cipher selection (see
+\k{config-ssh-encryption}).
+
+PuTTY currently supports the following host key types:
+
+\b \q{Ed25519}: \i{Edwards-curve} \i{DSA} using a twisted Edwards
+curve with modulus \cw{2^255-19}.
+
+\b \q{ECDSA}: \i{elliptic curve} \i{DSA} using one of the
+NIST-standardised elliptic curves.
+
+\b \q{DSA}: straightforward \i{DSA} using modular exponentiation.
+
+\b \q{RSA}: the ordinary \i{RSA} algorithm.
+
+If PuTTY already has a host key stored for the server, it will prefer
+to use the one it already has. If not, it will choose an algorithm
+based on the preference order you specify in the configuration.
+
+If the first algorithm PuTTY finds is below the \q{warn below here}
+line, you will see a warning box when you make the connection, similar
+to that for cipher selection (see \k{config-ssh-encryption}).
+
 \S{config-ssh-kex-manual-hostkeys} \ii{Manually configuring host keys}
 
 \cfg{winhelp-topic}{ssh.kex.manualhostkeys}
diff --git a/putty.h b/putty.h
index bdef61423dab08b78a24a5615c82ca781e624bfc..5f46b066f2ac6e48fcf9ec19f899ae55001d9106 100644 (file)
--- a/putty.h
+++ b/putty.h
@@ -266,6 +266,18 @@ enum {
     KEX_MAX
 };
 
+enum {
+    /*
+     * SSH-2 host key algorithms
+     */
+    HK_WARN,
+    HK_RSA,
+    HK_DSA,
+    HK_ECDSA,
+    HK_ED25519,
+    HK_MAX
+};
+
 enum {
     /*
      * SSH ciphers (both SSH-1 and SSH-2)
@@ -695,6 +707,7 @@ void cleanup_exit(int);
     X(INT, NONE, nopty) \
     X(INT, NONE, compression) \
     X(INT, INT, ssh_kexlist) \
+    X(INT, INT, ssh_hklist) \
     X(INT, NONE, ssh_rekey_time) /* in minutes */ \
     X(STR, NONE, ssh_rekey_data) /* string encoding e.g. "100K", "2M", "1G" */ \
     X(INT, NONE, tryagent) \
index 23e0ec3921384a633ea4cb10c29dfbff4f6089b5..9c772980c274c9e109cf566beabd2e65d92ebbed 100644 (file)
@@ -28,6 +28,14 @@ static const struct keyvalwhere kexnames[] = {
     { "WARN",               KEX_WARN,       -1, -1 }
 };
 
+static const struct keyvalwhere hknames[] = {
+    { "ed25519",    HK_ED25519,             -1, +1 },
+    { "ecdsa",      HK_ECDSA,               -1, -1 },
+    { "dsa",        HK_DSA,                 -1, -1 },
+    { "rsa",        HK_RSA,                 -1, -1 },
+    { "WARN",       HK_WARN,                -1, -1 },
+};
+
 /*
  * All the terminal modes that we know about for the "TerminalModes"
  * setting. (Also used by config.c for the drop-down list.)
@@ -493,6 +501,7 @@ void save_open_settings(void *sesskey, Conf *conf)
     write_setting_i(sesskey, "ChangeUsername", conf_get_int(conf, CONF_change_username));
     wprefs(sesskey, "Cipher", ciphernames, CIPHER_MAX, conf, CONF_ssh_cipherlist);
     wprefs(sesskey, "KEX", kexnames, KEX_MAX, conf, CONF_ssh_kexlist);
+    wprefs(sesskey, "HostKey", hknames, HK_MAX, conf, CONF_ssh_hklist);
     write_setting_i(sesskey, "RekeyTime", conf_get_int(conf, CONF_ssh_rekey_time));
     write_setting_s(sesskey, "RekeyBytes", conf_get_str(conf, CONF_ssh_rekey_data));
     write_setting_i(sesskey, "SshNoAuth", conf_get_int(conf, CONF_ssh_no_userauth));
@@ -789,6 +798,8 @@ void load_open_settings(void *sesskey, Conf *conf)
        gprefs(sesskey, "KEX", default_kexes,
               kexnames, KEX_MAX, conf, CONF_ssh_kexlist);
     }
+    gprefs(sesskey, "HostKey", "ed25519,ecdsa,rsa,dsa,WARN",
+           hknames, HK_MAX, conf, CONF_ssh_hklist);
     gppi(sesskey, "RekeyTime", 60, conf, CONF_ssh_rekey_time);
     gpps(sesskey, "RekeyBytes", "1G", conf, CONF_ssh_rekey_data);
     /* SSH-2 only by default */
diff --git a/ssh.c b/ssh.c
index ee9798af5e6d6485440afc544a8dc9c25968c416..d03f2967523ca40ccf0ffbecce4c0faa2dd6a961 100644 (file)
--- a/ssh.c
+++ b/ssh.c
@@ -408,10 +408,17 @@ 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[] = {
@@ -6242,7 +6249,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;
@@ -6297,7 +6307,7 @@ 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;
@@ -6319,6 +6329,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;
@@ -6395,6 +6407,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)
         */
@@ -6471,20 +6497,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
@@ -6496,7 +6545,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++) {
@@ -6617,7 +6667,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 */
 
@@ -6661,7 +6712,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;
@@ -6707,10 +6759,11 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
                 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) &&
+                    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]->keytype)) {
+                                           hostkey_algs[j].alg->keytype)) {
                         ssh->uncert_hostkeys[ssh->n_uncert_hostkeys++] = j;
                     }
                 }
@@ -6759,6 +6812,30 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
            }
        }
 
+       if (s->warn_hk) {
+           ssh_set_frozen(ssh, 1);
+           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,
@@ -7183,16 +7260,16 @@ static void do_ssh2_transport(Ssh ssh, const void *vin, int inlen,
            int i, j = 0;
            char *list = NULL;
            for (i = 0; i < lenof(hostkey_algs); i++) {
-               if (hostkey_algs[i] == ssh->hostkey)
+               if (hostkey_algs[i].alg == ssh->hostkey)
                    /* Not worth mentioning key types we wouldn't use */
                    break;
                else if (ssh->uncert_hostkeys[j] == i) {
                    char *newlist;
                    if (list)
                        newlist = dupprintf("%s/%s", list,
-                                           hostkey_algs[i]->name);
+                                           hostkey_algs[i].alg->name);
                    else
-                       newlist = dupprintf("%s", hostkey_algs[i]->name);
+                       newlist = dupprintf("%s", hostkey_algs[i].alg->name);
                    sfree(list);
                    list = newlist;
                    j++;
@@ -11518,7 +11595,7 @@ static const struct telnet_special *ssh_get_specials(void *handle)
             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]];
+                    hostkey_algs[ssh->uncert_hostkeys[i]].alg;
                 uncert[0].name = alg->name;
                 uncert[0].code = TS_LOCALSTART + ssh->uncert_hostkeys[i];
                 ADD_SPECIALS(uncert);
@@ -11586,7 +11663,7 @@ static void ssh_special(void *handle, Telnet_Special code)
            do_ssh2_transport(ssh, "at user request", -1, NULL);
        }
     } else if (code >= TS_LOCALSTART) {
-        ssh->hostkey = hostkey_algs[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) {
index 9b21c92c17f131242bc6c95bf4fc6249bfb2a581..2e40938e5ca73d87f1bc3e72b0707806121041da 100644 (file)
 #define WINHELP_CTX_ssh_compress "ssh.compress:config-ssh-comp"
 #define WINHELP_CTX_ssh_share "ssh.sharing:config-ssh-sharing"
 #define WINHELP_CTX_ssh_kexlist "ssh.kex.order:config-ssh-kex-order"
+#define WINHELP_CTX_ssh_hklist "ssh.hostkey.order:config-ssh-hostkey-order"
 #define WINHELP_CTX_ssh_kex_repeat "ssh.kex.repeat:config-ssh-kex-rekey"
 #define WINHELP_CTX_ssh_kex_manual_hostkeys "ssh.kex.manualhostkeys:config-ssh-kex-manual-hostkeys"
 #define WINHELP_CTX_ssh_auth_bypass "ssh.auth.bypass:config-ssh-noauth"