diff --git a/config.c b/config.c index c48493be..b32cfae5 100644 --- 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); diff --git a/doc/config.but b/doc/config.but index f0ccc150..c8e68113 100644 --- a/doc/config.but +++ b/doc/config.but @@ -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 bdef6142..5f46b066 100644 --- 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) \ diff --git a/settings.c b/settings.c index 23e0ec39..9c772980 100644 --- a/settings.c +++ b/settings.c @@ -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 ee9798af..d03f2967 100644 --- 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) { diff --git a/windows/winhelp.h b/windows/winhelp.h index 9b21c92c..2e40938e 100644 --- a/windows/winhelp.h +++ b/windows/winhelp.h @@ -101,6 +101,7 @@ #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"