From 04758cb3ec076bc5c17e8c8dd689e2715c78b4ed Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 13 Mar 2021 10:53:53 +0000 Subject: [PATCH] Allow pre-storing host key fingerprints of all types. verify_ssh_manual_host_key() now takes an array of all key fingerprints instead of just the default type, which means that an expected key fingerprint stored in the session configuration can now be matched against any of them. --- misc.c | 33 +++++++++++++++++++++++---------- ssh.h | 3 +-- ssh1login.c | 14 +++++++------- ssh2kex-client.c | 14 +++++++++----- sshcommon.c | 36 ++++++++++++++++++------------------ 5 files changed, 58 insertions(+), 42 deletions(-) diff --git a/misc.c b/misc.c index ae146748..84de046d 100644 --- a/misc.c +++ b/misc.c @@ -22,6 +22,10 @@ #include "putty.h" #include "misc.h" +#define BASE64_CHARS_NOEQ \ + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/" +#define BASE64_CHARS_ALL BASE64_CHARS_NOEQ "=" + void seat_connection_fatal(Seat *seat, const char *fmt, ...) { va_list ap; @@ -129,22 +133,32 @@ bool validate_manual_hostkey(char *key) * Now q is our word. */ - if (strlen(q) == 16*3 - 1 && - q[strspn(q, "0123456789abcdefABCDEF:")] == 0) { + if (strstartswith(q, "SHA256:")) { + /* Test for a valid SHA256 key fingerprint. */ + r = q + 7; + if (strlen(r) == 43 && r[strspn(r, BASE64_CHARS_NOEQ)] == 0) + return true; + } + + r = q; + if (strstartswith(r, "MD5:")) + r += 4; + if (strlen(r) == 16*3 - 1 && + r[strspn(r, "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. + * Test for a valid MD5 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] == ':') + if (r[3*i] == ':' || r[3*i+1] == ':') goto not_fingerprint; /* sorry */ for (i = 0; i < 15; i++) - if (q[3*i+2] != ':') + if (r[3*i+2] != ':') goto not_fingerprint; /* sorry */ for (i = 0; i < 16*3 - 1; i++) - key[i] = tolower(q[i]); + key[i] = tolower(r[i]); key[16*3 - 1] = '\0'; return true; } @@ -161,8 +175,7 @@ bool validate_manual_hostkey(char *key) *s = '\0'; if (strlen(q) % 4 == 0 && strlen(q) > 2*4 && - q[strspn(q, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz+/=")] == 0) { + q[strspn(q, BASE64_CHARS_ALL)] == 0) { /* * Might be a base64-encoded SSH-2 public key blob. Check * that it starts with a sensible algorithm string. No diff --git a/ssh.h b/ssh.h index acffbe32..3e46b814 100644 --- a/ssh.h +++ b/ssh.h @@ -1686,8 +1686,7 @@ unsigned alloc_channel_id_general(tree234 *channels, size_t localid_offset); void add_to_commasep(strbuf *buf, const char *data); bool get_commasep_word(ptrlen *list, ptrlen *word); -int verify_ssh_manual_host_key( - Conf *conf, const char *fingerprint, ssh_key *key); +int verify_ssh_manual_host_key(Conf *conf, char **fingerprints, ssh_key *key); typedef struct ssh_transient_hostkey_cache ssh_transient_hostkey_cache; ssh_transient_hostkey_cache *ssh_transient_hostkey_cache_new(void); diff --git a/ssh1login.c b/ssh1login.c index cf191831..5556320b 100644 --- a/ssh1login.c +++ b/ssh1login.c @@ -244,14 +244,13 @@ static void ssh1_login_process_queue(PacketProtocolLayer *ppl) /* * First format the key into a string. */ - char *fingerprint; char *keystr = rsastr_fmt(&s->hostkey); - fingerprint = rsa_ssh1_fingerprint(&s->hostkey); + char **fingerprints = rsa_ssh1_fake_all_fingerprints(&s->hostkey); /* First check against manually configured host keys. */ - s->dlgret = verify_ssh_manual_host_key(s->conf, fingerprint, NULL); + s->dlgret = verify_ssh_manual_host_key(s->conf, fingerprints, NULL); if (s->dlgret == 0) { /* did not match */ - sfree(fingerprint); + ssh2_free_all_fingerprints(fingerprints); sfree(keystr); ssh_proto_error(s->ppl.ssh, "Host key did not appear in manually " "configured list"); @@ -259,8 +258,9 @@ static void ssh1_login_process_queue(PacketProtocolLayer *ppl) } else if (s->dlgret < 0) { /* none configured; use standard handling */ s->dlgret = seat_verify_ssh_host_key( s->ppl.seat, s->savedhost, s->savedport, - "rsa", keystr, fingerprint, ssh1_login_dialog_callback, s); - sfree(fingerprint); + "rsa", keystr, fingerprints[SSH_FPTYPE_DEFAULT], + ssh1_login_dialog_callback, s); + ssh2_free_all_fingerprints(fingerprints); sfree(keystr); #ifdef FUZZING s->dlgret = 1; @@ -273,7 +273,7 @@ static void ssh1_login_process_queue(PacketProtocolLayer *ppl) return; } } else { - sfree(fingerprint); + ssh2_free_all_fingerprints(fingerprints); sfree(keystr); } } diff --git a/ssh2kex-client.c b/ssh2kex-client.c index ca1ee0c1..91452e55 100644 --- a/ssh2kex-client.c +++ b/ssh2kex-client.c @@ -843,13 +843,16 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) * Authenticate remote host: verify host key. (We've already * checked the signature of the exchange hash.) */ - char *fingerprint = ssh2_fingerprint(s->hkey, SSH_FPTYPE_DEFAULT); + char **fingerprints = ssh2_all_fingerprints(s->hkey); + FingerprintType fptype_default = + ssh2_pick_default_fingerprint(fingerprints); ppl_logevent("Host key fingerprint is:"); - ppl_logevent("%s", fingerprint); + ppl_logevent("%s", fingerprints[fptype_default]); /* First check against manually configured host keys. */ s->dlgret = verify_ssh_manual_host_key( - s->conf, fingerprint, s->hkey); + s->conf, fingerprints, s->hkey); if (s->dlgret == 0) { /* did not match */ + ssh2_free_all_fingerprints(fingerprints); ssh_sw_abort(s->ppl.ssh, "Host key did not appear in manually " "configured list"); *aborted = true; @@ -857,9 +860,10 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) } else if (s->dlgret < 0) { /* none configured; use standard handling */ s->dlgret = seat_verify_ssh_host_key( s->ppl.seat, s->savedhost, s->savedport, - ssh_key_cache_id(s->hkey), s->keystr, fingerprint, + ssh_key_cache_id(s->hkey), s->keystr, + fingerprints[SSH_FPTYPE_DEFAULT], ssh2_transport_dialog_callback, s); - sfree(fingerprint); + ssh2_free_all_fingerprints(fingerprints); #ifdef FUZZING s->dlgret = 1; #endif diff --git a/sshcommon.c b/sshcommon.c index 9cf2cf6f..5485e33a 100644 --- a/sshcommon.c +++ b/sshcommon.c @@ -826,28 +826,28 @@ bool ssh2_bpp_check_unimplemented(BinaryPacketProtocol *bpp, PktIn *pktin) * Function to check a host key against any manually configured in Conf. */ -int verify_ssh_manual_host_key( - Conf *conf, const char *fingerprint, ssh_key *key) +int verify_ssh_manual_host_key(Conf *conf, char **fingerprints, ssh_key *key) { if (!conf_get_str_nthstrkey(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(conf, CONF_ssh_manual_hostkeys, fingerprint)) - return 1; /* success */ + if (fingerprints) { + for (size_t i = 0; i < SSH_N_FPTYPES; i++) { + /* + * Each 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 hash at the end + * of the string. + */ + const char *fingerprint = fingerprints[i]; + if (!fingerprint) + continue; + const char *p = strrchr(fingerprint, ' '); + fingerprint = p ? p+1 : fingerprint; + if (conf_get_str_str_opt(conf, CONF_ssh_manual_hostkeys, + fingerprint)) + return 1; /* success */ + } } if (key) {