From 70ab076d839072357dab75590287d44961302de8 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 9 Sep 2014 11:46:24 +0000 Subject: [PATCH] New option to manually configure the expected host key(s). This option is available from the command line as '-hostkey', and is also configurable through the GUI. When enabled, it completely replaces all of the automated host key management: the server's host key will be checked against the manually configured list, and the connection will be allowed or disconnected on that basis, and the host key store in the registry will not be either consulted or updated. The main aim is to provide a means of automatically running Plink, PSCP or PSFTP deep inside Windows services where HKEY_CURRENT_USER isn't available to have stored the right host key in. But it also permits you to specify a list of multiple host keys, which means a second use case for the same mechanism will probably be round-robin DNS names that select one of several servers with different host keys. Host keys can be specified as the standard MD5 fingerprint or as an SSH-2 base64 blob, and are canonicalised on input. (The base64 blob is more unwieldy, especially with Windows command-line length limits, but provides a means of specifying the _whole_ public key in case you don't trust MD5. I haven't bothered to provide an analogous mechanism for SSH-1, on the basis that anyone worrying about MD5 should have stopped using SSH-1 already!) [originally from svn r10220] --- cmdline.c | 15 +++++ config.c | 104 ++++++++++++++++++++++++++++++ doc/config.but | 50 +++++++++++++++ doc/faq.but | 15 +++-- doc/using.but | 13 ++++ misc.c | 97 ++++++++++++++++++++++++++++ misc.h | 2 + putty.h | 6 ++ settings.c | 2 + ssh.c | 158 +++++++++++++++++++++++++++++++++------------- windows/winhelp.h | 1 + 11 files changed, 411 insertions(+), 52 deletions(-) diff --git a/cmdline.c b/cmdline.c index 0c7ef4c9..327a585a 100644 --- a/cmdline.c +++ b/cmdline.c @@ -236,6 +236,21 @@ int cmdline_process_param(char *p, char *value, int need_save, Conf *conf) SAVEABLE(0); conf_set_str(conf, CONF_loghost, value); } + if (!strcmp(p, "-hostkey")) { + char *dup; + RETURN(2); + UNAVAILABLE_IN(TOOLTYPE_NONNETWORK); + SAVEABLE(0); + dup = dupstr(value); + if (!validate_manual_hostkey(dup)) { + cmdline_error("'%s' is not a valid format for a manual host " + "key specification", value); + sfree(dup); + return ret; + } + conf_set_str_str(conf, CONF_ssh_manual_hostkeys, dup, ""); + sfree(dup); + } if ((!strcmp(p, "-L") || !strcmp(p, "-R") || !strcmp(p, "-D"))) { char type, *q, *qq, *key, *val; RETURN(2); diff --git a/config.c b/config.c index e4cfabf8..bfa9071b 100644 --- a/config.c +++ b/config.c @@ -1225,6 +1225,73 @@ static void portfwd_handler(union control *ctrl, void *dlg, } } +struct manual_hostkey_data { + union control *addbutton, *rembutton, *listbox, *keybox; +}; + +static void manual_hostkey_handler(union control *ctrl, void *dlg, + void *data, int event) +{ + Conf *conf = (Conf *)data; + struct manual_hostkey_data *mh = + (struct manual_hostkey_data *)ctrl->generic.context.p; + + if (event == EVENT_REFRESH) { + if (ctrl == mh->listbox) { + char *key, *val; + dlg_update_start(ctrl, dlg); + dlg_listbox_clear(ctrl, dlg); + for (val = conf_get_str_strs(conf, CONF_ssh_manual_hostkeys, + NULL, &key); + val != NULL; + val = conf_get_str_strs(conf, CONF_ssh_manual_hostkeys, + key, &key)) { + dlg_listbox_add(ctrl, dlg, key); + } + dlg_update_done(ctrl, dlg); + } + } else if (event == EVENT_ACTION) { + if (ctrl == mh->addbutton) { + char *key; + + key = dlg_editbox_get(mh->keybox, dlg); + if (!*key) { + dlg_error_msg(dlg, "You need to specify a host key or " + "fingerprint"); + sfree(key); + return; + } + + if (!validate_manual_hostkey(key)) { + dlg_error_msg(dlg, "Host key is not in a valid format"); + } else if (conf_get_str_str_opt(conf, CONF_ssh_manual_hostkeys, + key)) { + dlg_error_msg(dlg, "Specified host key is already listed"); + } else { + conf_set_str_str(conf, CONF_ssh_manual_hostkeys, key, ""); + } + + sfree(key); + dlg_refresh(mh->listbox, dlg); + } else if (ctrl == mh->rembutton) { + int i = dlg_listbox_index(mh->listbox, dlg); + if (i < 0) { + dlg_beep(dlg); + } else { + char *key; + + key = conf_get_str_nthstrkey(conf, CONF_ssh_manual_hostkeys, i); + if (key) { + dlg_editbox_set(mh->keybox, dlg, key); + /* And delete it */ + conf_del_str_str(conf, CONF_ssh_manual_hostkeys, key); + } + } + dlg_refresh(mh->listbox, dlg); + } + } +} + void setup_config_box(struct controlbox *b, int midsession, int protocol, int protcfginfo) { @@ -1235,6 +1302,7 @@ void setup_config_box(struct controlbox *b, int midsession, struct ttymodes_data *td; struct environ_data *ed; struct portfwd_data *pfd; + struct manual_hostkey_data *mh; union control *c; char *str; @@ -2163,6 +2231,42 @@ void setup_config_box(struct controlbox *b, int midsession, I(16)); ctrl_text(s, "(Use 1M for 1 megabyte, 1G for 1 gigabyte etc)", HELPCTX(ssh_kex_repeat)); + + s = ctrl_getset(b, "Connection/SSH/Kex", "hostkeys", + "Manually configure host keys for this connection"); + + ctrl_columns(s, 2, 75, 25); + c = ctrl_text(s, "Host keys or fingerprints to accept:", + HELPCTX(ssh_kex_manual_hostkeys)); + c->generic.column = 0; + /* You want to select from the list, _then_ hit Remove. So + * tab order should be that way round. */ + mh = (struct manual_hostkey_data *) + ctrl_alloc(b,sizeof(struct manual_hostkey_data)); + mh->rembutton = ctrl_pushbutton(s, "Remove", 'r', + HELPCTX(ssh_kex_manual_hostkeys), + manual_hostkey_handler, P(mh)); + mh->rembutton->generic.column = 1; + mh->rembutton->generic.tabdelay = 1; + mh->listbox = ctrl_listbox(s, NULL, NO_SHORTCUT, + HELPCTX(ssh_kex_manual_hostkeys), + manual_hostkey_handler, P(mh)); + /* This list box can't be very tall, because there's not + * much room in the pane on Windows at least. This makes + * it become really unhelpful if a horizontal scrollbar + * appears, so we suppress that. */ + mh->listbox->listbox.height = 2; + mh->listbox->listbox.hscroll = FALSE; + ctrl_tabdelay(s, mh->rembutton); + mh->keybox = ctrl_editbox(s, "Key", 'k', 80, + HELPCTX(ssh_kex_manual_hostkeys), + manual_hostkey_handler, P(mh), P(NULL)); + mh->keybox->generic.column = 0; + mh->addbutton = ctrl_pushbutton(s, "Add key", 'y', + HELPCTX(ssh_kex_manual_hostkeys), + manual_hostkey_handler, P(mh)); + mh->addbutton->generic.column = 1; + ctrl_columns(s, 1, 100); } if (!midsession || protcfginfo != 1) { diff --git a/doc/config.but b/doc/config.but index f355a489..7d93402c 100644 --- a/doc/config.but +++ b/doc/config.but @@ -2466,6 +2466,56 @@ 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. +\S{config-ssh-kex-manual-hostkeys} \ii{Manually configuring host keys} + +\cfg{winhelp-topic}{ssh.kex.manualhostkeys} + +In some situations, if PuTTY's automated host key management is not +doing what you need, you might need to manually configure PuTTY to +accept a specific host key, or one of a specific set of host keys. + +One reason why you might want to do this is because the host name +PuTTY is connecting to is using round-robin DNS to return one of +multiple actual servers, and they all have different host keys. In +that situation, you might need to configure PuTTY to accept any of a +list of host keys for the possible servers, while still rejecting any +key not in that list. + +Another reason is if PuTTY's automated host key management is +completely unavailable, e.g. because PuTTY (or Plink or PSFTP, etc) is +running in a Windows environment without access to the Registry. In +that situation, you will probably want to use the \cw{-hostkey} +command-line option to configure the expected host key(s); see FIXME. + +To configure manual host keys via the GUI, enter some text describing +the host key into the edit box in the \q{Manually configure host keys +for this connection} container, and press the \q{Add} button. The text +will appear in the {q Host keys or fingerprints to accept} list box. +You can remove keys again with the \q{Remove} button. + +The text describing a host key can be in one of the following formats: + +\b An MD5-based host key fingerprint of the form displayed in PuTTY's +Event Log and host key dialog boxes, i.e. sixteen 2-digit hex numbers +separated by colons. + +\b A base64-encoded blob describing an SSH-2 public key in the +standard way. This can be found in OpenSSH's one-line public key +format, or by concatenating all the lines of the public key section in +one of PuTTY's \cw{.ppk} files. Alternatively, you can load a key into +PuTTYgen, and paste out the OpenSSH-format public key line it +displays. + +If this box contains at least one host key or fingerprint when PuTTY +makes an SSH connection, then PuTTY's automated host key management is +completely bypassed: the connection will be permitted if and only if +the host key presented by the server is one of the keys listed in this +box, and the host key store in the Registry will be neither read +\e{nor written}. + +If the box is empty (as it usually is), then PuTTY's automated host +key management will work as normal. + \H{config-ssh-encryption} The Cipher panel \cfg{winhelp-topic}{ssh.ciphers} diff --git a/doc/faq.but b/doc/faq.but index a34973bd..b8650df9 100644 --- a/doc/faq.but +++ b/doc/faq.but @@ -151,13 +151,14 @@ military-strength cipher. That insignificant host key prompt really does make \e{that} much difference. If you're having a specific problem with host key checking - perhaps -you want an automated batch job to make use of PSCP or Plink, and -the interactive host key prompt is hanging the batch process - then -the right way to fix it is to add the correct host key to the -Registry in advance. That way, you retain the \e{important} feature -of host key checking: the right key will be accepted and the wrong -ones will not. Adding an option to turn host key checking off -completely is the wrong solution and we will not do it. +you want an automated batch job to make use of PSCP or Plink, and the +interactive host key prompt is hanging the batch process - then the +right way to fix it is to add the correct host key to the Registry in +advance, or if the Registry is not available, to use the \cw{-hostkey} +command-line option. That way, you retain the \e{important} feature of +host key checking: the right key will be accepted and the wrong ones +will not. Adding an option to turn host key checking off completely is +the wrong solution and we will not do it. If you have host keys available in the common \i\c{known_hosts} format, we have a script called diff --git a/doc/using.but b/doc/using.but index 27a1c2a8..fec03d32 100644 --- a/doc/using.but +++ b/doc/using.but @@ -913,6 +913,19 @@ connecting to). It can be a plain host name, or a host name followed by a colon and a port number. See \k{config-loghost} for more detail on this. +\S2{using-cmdline-hostkey} \i\c{-hostkey}: \I{manually configuring +host keys}manually specify an expected host key + +This option overrides PuTTY's normal SSH host key caching policy by +telling it exactly what host key to expect, which can be useful if the +normal automatic host key store in the Registry is unavailable. The +argument to this option should be either a host key fingerprint, or an +SSH-2 public key blob. See \k{config-ssh-kex-manual-hostkeys} for more +information. + +You can specify this option more than once if you want to configure +more than one key to be accepted. + \S2{using-cmdline-pgpfp} \i\c{-pgpfp}: display \i{PGP key fingerprint}s This option causes the PuTTY tools not to run as normal, but instead diff --git a/misc.c b/misc.c index fb7cc8c2..24e42eba 100644 --- a/misc.c +++ b/misc.c @@ -922,3 +922,100 @@ void smemclr(void *b, size_t n) { } } #endif + +/* + * Validate a manual host key specification (either entered in the + * GUI, or via -hostkey). If valid, we return TRUE, and update 'key' + * to contain a canonicalised version of the key string in 'key' + * (which is guaranteed to take up at most as much space as the + * original version), suitable for putting into the Conf. If not + * valid, we return FALSE. + */ +int validate_manual_hostkey(char *key) +{ + char *p, *q, *r, *s; + + /* + * Step through the string word by word, looking for a word that's + * in one of the formats we like. + */ + p = key; + while ((p += strspn(p, " \t"))[0]) { + q = p; + p += strcspn(p, " \t"); + if (p) *p++ = '\0'; + + /* + * Now q is our word. + */ + + if (strlen(q) == 16*3 - 1 && + q[strspn(q, "0123456789abcdefABCDEF:")] == 0) { + /* + * Might be a key fingerprint. Check the colons are in the + * right places, and if so, return the same fingerprint + * canonicalised into lowercase. + */ + int i; + for (i = 0; i < 16; i++) + if (q[3*i] == ':' || q[3*i+1] == ':') + goto not_fingerprint; /* sorry */ + for (i = 0; i < 15; i++) + if (q[3*i+2] != ':') + goto not_fingerprint; /* sorry */ + for (i = 0; i < 16*3 - 1; i++) + key[i] = tolower(q[i]); + key[16*3 - 1] = '\0'; + return TRUE; + } + not_fingerprint:; + + /* + * Before we check for a public-key blob, trim newlines out of + * the middle of the word, in case someone's managed to paste + * in a public-key blob _with_ them. + */ + for (r = s = q; *r; r++) + if (*r != '\n' && *r != '\r') + *s++ = *r; + *s = '\0'; + + if (strlen(q) % 4 == 0 && strlen(q) > 2*4 && + q[strspn(q, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz+/=")] == 0) { + /* + * Might be a base64-encoded SSH-2 public key blob. Check + * that it starts with a sensible algorithm string. No + * canonicalisation is necessary for this string type. + * + * The algorithm string must be at most 64 characters long + * (RFC 4251 section 6). + */ + unsigned char decoded[6]; + unsigned alglen; + int minlen; + int len = 0; + + len += base64_decode_atom(q, decoded+len); + if (len < 3) + goto not_ssh2_blob; /* sorry */ + len += base64_decode_atom(q+4, decoded+len); + if (len < 4) + goto not_ssh2_blob; /* sorry */ + + alglen = GET_32BIT_MSB_FIRST(decoded); + if (alglen > 64) + goto not_ssh2_blob; /* sorry */ + + minlen = ((alglen + 4) + 2) / 3; + if (strlen(q) < minlen) + goto not_ssh2_blob; /* sorry */ + + strcpy(key, q); + return TRUE; + } + not_ssh2_blob:; + } + + return FALSE; +} diff --git a/misc.h b/misc.h index 306b4a1e..a361d706 100644 --- a/misc.h +++ b/misc.h @@ -60,6 +60,8 @@ void bufchain_prefix(bufchain *ch, void **data, int *len); void bufchain_consume(bufchain *ch, int len); void bufchain_fetch(bufchain *ch, void *data, int len); +int validate_manual_hostkey(char *key); + struct tm ltime(void); void smemclr(void *b, size_t len); diff --git a/putty.h b/putty.h index a6578ae0..ff0f3156 100644 --- a/putty.h +++ b/putty.h @@ -848,6 +848,12 @@ void cleanup_exit(int); X(INT, NONE, ssh_connection_sharing) \ X(INT, NONE, ssh_connection_sharing_upstream) \ X(INT, NONE, ssh_connection_sharing_downstream) \ + /* + * ssh_manual_hostkeys is conceptually a set rather than a + * dictionary: the string subkeys are the important thing, and the + * actual values to which those subkeys map are all "". + */ \ + X(STR, STR, ssh_manual_hostkeys) \ /* Options for pterm. Should split out into platform-dependent part. */ \ X(INT, NONE, stamp_utmp) \ X(INT, NONE, login_shell) \ diff --git a/settings.c b/settings.c index aff6bf64..898c0dab 100644 --- a/settings.c +++ b/settings.c @@ -650,6 +650,7 @@ void save_open_settings(void *sesskey, Conf *conf) write_setting_i(sesskey, "ConnectionSharing", conf_get_int(conf, CONF_ssh_connection_sharing)); write_setting_i(sesskey, "ConnectionSharingUpstream", conf_get_int(conf, CONF_ssh_connection_sharing_upstream)); write_setting_i(sesskey, "ConnectionSharingDownstream", conf_get_int(conf, CONF_ssh_connection_sharing_downstream)); + wmap(sesskey, "SSHManualHostKeys", conf, CONF_ssh_manual_hostkeys, FALSE); } void load_settings(char *section, Conf *conf) @@ -996,6 +997,7 @@ void load_open_settings(void *sesskey, Conf *conf) gppi(sesskey, "ConnectionSharing", 0, conf, CONF_ssh_connection_sharing); gppi(sesskey, "ConnectionSharingUpstream", 1, conf, CONF_ssh_connection_sharing_upstream); gppi(sesskey, "ConnectionSharingDownstream", 1, conf, CONF_ssh_connection_sharing_downstream); + gppmap(sesskey, "SSHManualHostKeys", conf, CONF_ssh_manual_hostkeys); } void do_defaults(char *session, Conf *conf) diff --git a/ssh.c b/ssh.c index 893321e2..57ee2e8a 100644 --- a/ssh.c +++ b/ssh.c @@ -3677,6 +3677,59 @@ static void ssh_disconnect(Ssh ssh, char *client_reason, char *wire_reason, sfree(error); } +int verify_ssh_manual_host_key(Ssh ssh, const char *fingerprint, + const struct ssh_signkey *ssh2keytype, + void *ssh2keydata) +{ + if (!conf_get_str_nthstrkey(ssh->conf, CONF_ssh_manual_hostkeys, 0)) { + return -1; /* no manual keys configured */ + } + + if (fingerprint) { + /* + * The fingerprint string we've been given will have things + * like 'ssh-rsa 2048' at the front of it. Strip those off and + * narrow down to just the colon-separated hex block at the + * end of the string. + */ + const char *p = strrchr(fingerprint, ' '); + fingerprint = p ? p+1 : fingerprint; + /* Quick sanity checks, including making sure it's in lowercase */ + assert(strlen(fingerprint) == 16*3 - 1); + assert(fingerprint[2] == ':'); + assert(fingerprint[strspn(fingerprint, "0123456789abcdef:")] == 0); + + if (conf_get_str_str_opt(ssh->conf, CONF_ssh_manual_hostkeys, + fingerprint)) + return 1; /* success */ + } + + if (ssh2keydata) { + /* + * Construct the base64-encoded public key blob and see if + * that's listed. + */ + unsigned char *binblob; + char *base64blob; + int binlen, atoms, i; + binblob = ssh2keytype->public_blob(ssh2keydata, &binlen); + atoms = (binlen + 2) / 3; + base64blob = snewn(atoms * 4 + 1, char); + for (i = 0; i < atoms; i++) + base64_encode_atom(binblob + 3*i, binlen - 3*i, base64blob + 4*i); + base64blob[atoms * 4] = '\0'; + sfree(binblob); + if (conf_get_str_str_opt(ssh->conf, CONF_ssh_manual_hostkeys, + base64blob)) { + sfree(base64blob); + return 1; /* success */ + } + sfree(base64blob); + } + + return 0; +} + /* * Handle the key exchange and user authentication phases. */ @@ -3800,29 +3853,36 @@ static int do_ssh1_login(Ssh ssh, unsigned char *in, int inlen, rsastr_fmt(keystr, &s->hostkey); rsa_fingerprint(fingerprint, sizeof(fingerprint), &s->hostkey); - ssh_set_frozen(ssh, 1); - s->dlgret = verify_ssh_host_key(ssh->frontend, - ssh->savedhost, ssh->savedport, - "rsa", keystr, fingerprint, - ssh_dialog_callback, ssh); - sfree(keystr); - if (s->dlgret < 0) { - do { - crReturn(0); - if (pktin) { - bombout(("Unexpected data from server while waiting" - " for user host key response")); - crStop(0); - } - } while (pktin || inlen > 0); - s->dlgret = ssh->user_response; - } - ssh_set_frozen(ssh, 0); + /* First check against manually configured host keys. */ + s->dlgret = verify_ssh_manual_host_key(ssh, fingerprint, NULL, NULL); + if (s->dlgret == 0) { /* did not match */ + bombout(("Host key did not appear in manually configured list")); + crStop(0); + } else if (s->dlgret < 0) { /* none configured; use standard handling */ + ssh_set_frozen(ssh, 1); + s->dlgret = verify_ssh_host_key(ssh->frontend, + ssh->savedhost, ssh->savedport, + "rsa", keystr, fingerprint, + ssh_dialog_callback, ssh); + sfree(keystr); + if (s->dlgret < 0) { + do { + crReturn(0); + if (pktin) { + bombout(("Unexpected data from server while waiting" + " for user host key response")); + crStop(0); + } + } 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 verification", - NULL, 0, TRUE); - crStop(0); + if (s->dlgret == 0) { + ssh_disconnect(ssh, "User aborted at host key verification", + NULL, 0, TRUE); + crStop(0); + } } } @@ -6721,31 +6781,39 @@ static void do_ssh2_transport(Ssh ssh, void *vin, int inlen, * checked the signature of the exchange hash.) */ s->fingerprint = ssh->hostkey->fingerprint(s->hkey); - ssh_set_frozen(ssh, 1); - s->dlgret = verify_ssh_host_key(ssh->frontend, - ssh->savedhost, ssh->savedport, - ssh->hostkey->keytype, s->keystr, - s->fingerprint, - ssh_dialog_callback, ssh); - if (s->dlgret < 0) { - do { - crReturnV; - if (pktin) { - bombout(("Unexpected data from server while waiting" - " for user host key 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 verification", NULL, - 0, TRUE); - crStopV; - } logevent("Host key fingerprint is:"); logevent(s->fingerprint); + /* First check against manually configured host keys. */ + s->dlgret = verify_ssh_manual_host_key(ssh, s->fingerprint, + ssh->hostkey, s->hkey); + if (s->dlgret == 0) { /* did not match */ + bombout(("Host key did not appear in manually configured list")); + crStopV; + } else if (s->dlgret < 0) { /* none configured; use standard handling */ + ssh_set_frozen(ssh, 1); + s->dlgret = verify_ssh_host_key(ssh->frontend, + ssh->savedhost, ssh->savedport, + ssh->hostkey->keytype, s->keystr, + s->fingerprint, + ssh_dialog_callback, ssh); + if (s->dlgret < 0) { + do { + crReturnV; + if (pktin) { + bombout(("Unexpected data from server while waiting" + " for user host key response")); + crStopV; + } + } while (pktin || inlen > 0); + s->dlgret = ssh->user_response; + } + ssh_set_frozen(ssh, 0); + if (s->dlgret == 0) { + ssh_disconnect(ssh, "Aborted at host key verification", NULL, + 0, TRUE); + crStopV; + } + } sfree(s->fingerprint); /* * Save this host key, to check against the one presented in diff --git a/windows/winhelp.h b/windows/winhelp.h index dabd8b82..fc10a8f4 100644 --- a/windows/winhelp.h +++ b/windows/winhelp.h @@ -102,6 +102,7 @@ #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_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" #define WINHELP_CTX_ssh_auth_banner "ssh.auth.banner:config-ssh-banner" #define WINHELP_CTX_ssh_auth_privkey "ssh.auth.privkey:config-ssh-privkey" -- 2.45.2