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);
}
}
+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)
{
struct ttymodes_data *td;
struct environ_data *ed;
struct portfwd_data *pfd;
+ struct manual_hostkey_data *mh;
union control *c;
char *str;
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) {
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}
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
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
}
}
#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;
+}
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);
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) \
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)
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)
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.
*/
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);
+ }
}
}
* 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
#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"