From 1da353e64936ea7425d43c48eb0b9c4c72d8b629 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 13 Mar 2021 09:52:56 +0000 Subject: [PATCH] Introduce OpenSSH-compatible SHA256 key fingerprinting. There's a new enumeration of fingerprint types, and you tell ssh2_fingerprint() or ssh2_fingerprint_blob() which of them to use. So far, this is only implemented behind the scenes, and exposed for testcrypt to test. All the call sites of ssh2_fingerprint pass a fixed default fptype, which is still set to the old MD5. That will change shortly. --- cmdgen.c | 4 +-- pageant.c | 22 ++++++++------ ssh.h | 13 +++++++-- ssh2kex-client.c | 8 +++--- sshpubk.c | 72 ++++++++++++++++++++++++++++++---------------- test/cryptsuite.py | 30 +++++++++++++++++++ test/testcrypt.py | 2 +- testcrypt.c | 19 ++++++++++++ testcrypt.h | 2 ++ windows/winpgen.c | 4 +-- windows/winpgnt.c | 2 +- 11 files changed, 133 insertions(+), 45 deletions(-) diff --git a/cmdgen.c b/cmdgen.c index e891070e..b500afca 100644 --- a/cmdgen.c +++ b/cmdgen.c @@ -1182,11 +1182,11 @@ int main(int argc, char **argv) fingerprint = rsa_ssh1_fingerprint(ssh1key); } else { if (ssh2key) { - fingerprint = ssh2_fingerprint(ssh2key->key); + fingerprint = ssh2_fingerprint(ssh2key->key, SSH_FPTYPE_DEFAULT); } else { assert(ssh2blob); fingerprint = ssh2_fingerprint_blob( - ptrlen_from_strbuf(ssh2blob)); + ptrlen_from_strbuf(ssh2blob), SSH_FPTYPE_DEFAULT); } } diff --git a/pageant.c b/pageant.c index 8e153261..e12aabef 100644 --- a/pageant.c +++ b/pageant.c @@ -703,7 +703,8 @@ static PageantAsyncOp *pageant_make_op( int i; ssh2_userkey *skey; for (i = 0; NULL != (skey = pageant_nth_ssh2_key(i)); i++) { - char *fingerprint = ssh2_fingerprint(skey->key); + char *fingerprint = ssh2_fingerprint( + skey->key, SSH_FPTYPE_DEFAULT); pageant_client_log(pc, reqid, "returned key: %s %s", fingerprint, skey->comment); sfree(fingerprint); @@ -812,7 +813,8 @@ static PageantAsyncOp *pageant_make_op( have_flags = true; if (!pc->suppress_logging) { - char *fingerprint = ssh2_fingerprint_blob(keyblob); + char *fingerprint = ssh2_fingerprint_blob( + keyblob, SSH_FPTYPE_DEFAULT); pageant_client_log(pc, reqid, "requested key: %s", fingerprint); sfree(fingerprint); } @@ -927,7 +929,7 @@ static PageantAsyncOp *pageant_make_op( } if (!pc->suppress_logging) { - char *fingerprint = ssh2_fingerprint(key->key); + char *fingerprint = ssh2_fingerprint(key->key, SSH_FPTYPE_DEFAULT); pageant_client_log(pc, reqid, "submitted key: %s %s", fingerprint, key->comment); sfree(fingerprint); @@ -1019,7 +1021,8 @@ static PageantAsyncOp *pageant_make_op( } if (!pc->suppress_logging) { - char *fingerprint = ssh2_fingerprint_blob(blob); + char *fingerprint = ssh2_fingerprint_blob( + blob, SSH_FPTYPE_DEFAULT); pageant_client_log(pc, reqid, "unwanted key: %s", fingerprint); sfree(fingerprint); } @@ -1132,7 +1135,7 @@ static PageantAsyncOp *pageant_make_op( if (!pc->suppress_logging) { char *fingerprint = ssh2_fingerprint_blob( - ptrlen_from_strbuf(public_blob)); + ptrlen_from_strbuf(public_blob), SSH_FPTYPE_DEFAULT); pageant_client_log(pc, reqid, "add-ppk: %s %s", fingerprint, comment); sfree(fingerprint); @@ -1234,7 +1237,8 @@ static PageantAsyncOp *pageant_make_op( } if (!pc->suppress_logging) { - char *fingerprint = ssh2_fingerprint_blob(blob); + char *fingerprint = ssh2_fingerprint_blob( + blob, SSH_FPTYPE_DEFAULT); pageant_client_log(pc, reqid, "key to re-encrypt: %s", fingerprint); sfree(fingerprint); @@ -1316,7 +1320,8 @@ static PageantAsyncOp *pageant_make_op( int i; ssh2_userkey *skey; for (i = 0; NULL != (skey = pageant_nth_ssh2_key(i)); i++) { - char *fingerprint = ssh2_fingerprint(skey->key); + char *fingerprint = ssh2_fingerprint( + skey->key, SSH_FPTYPE_DEFAULT); pageant_client_log(pc, reqid, "returned key: %s %s", fingerprint, skey->comment); sfree(fingerprint); @@ -2224,7 +2229,8 @@ int pageant_enum_keys(pageant_key_enum_fn_t callback, void *callback_ctx, cbkey.comment = mkstr(kl2->keys[i].comment); cbkey.ssh_version = 2; - char *fingerprint = ssh2_fingerprint_blob(kl2->keys[i].blob); + char *fingerprint = ssh2_fingerprint_blob(kl2->keys[i].blob, + SSH_FPTYPE_DEFAULT); callback(callback_ctx, fingerprint, cbkey.comment, kl2->keys[i].flags, &cbkey); diff --git a/ssh.h b/ssh.h index 99c51da1..378dcb90 100644 --- a/ssh.h +++ b/ssh.h @@ -1328,14 +1328,23 @@ enum { SSH_KEYTYPE_SSH2_PUBLIC_RFC4716, SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH }; + +typedef enum { + SSH_FPTYPE_MD5, + SSH_FPTYPE_SHA256, +} FingerprintType; + +#define SSH_FPTYPE_DEFAULT SSH_FPTYPE_MD5 +#define SSH_N_FPTYPES (SSH_FPTYPE_SHA256 + 1) + char *ssh1_pubkey_str(RSAKey *ssh1key); void ssh1_write_pubkey(FILE *fp, RSAKey *ssh1key); char *ssh2_pubkey_openssh_str(ssh2_userkey *key); void ssh2_write_pubkey(FILE *fp, const char *comment, const void *v_pub_blob, int pub_len, int keytype); -char *ssh2_fingerprint_blob(ptrlen); -char *ssh2_fingerprint(ssh_key *key); +char *ssh2_fingerprint_blob(ptrlen, FingerprintType); +char *ssh2_fingerprint(ssh_key *key, FingerprintType); int key_type(const Filename *filename); int key_type_s(BinarySource *src); const char *key_type_to_str(int type); diff --git a/ssh2kex-client.c b/ssh2kex-client.c index 61a01646..b4387b0a 100644 --- a/ssh2kex-client.c +++ b/ssh2kex-client.c @@ -721,7 +721,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) * host key, store it. */ if (s->hkey) { - s->fingerprint = ssh2_fingerprint(s->hkey); + s->fingerprint = ssh2_fingerprint(s->hkey, SSH_FPTYPE_DEFAULT); ppl_logevent("GSS kex provided fallback host key:"); ppl_logevent("%s", s->fingerprint); sfree(s->fingerprint); @@ -779,7 +779,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) * triggered on purpose to populate the transient cache. */ assert(s->hkey); /* only KEXTYPE_GSS lets this be null */ - s->fingerprint = ssh2_fingerprint(s->hkey); + s->fingerprint = ssh2_fingerprint(s->hkey, SSH_FPTYPE_DEFAULT); if (s->need_gss_transient_hostkey) { ppl_logevent("Post-GSS rekey provided fallback host key:"); @@ -843,7 +843,7 @@ 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.) */ - s->fingerprint = ssh2_fingerprint(s->hkey); + s->fingerprint = ssh2_fingerprint(s->hkey, SSH_FPTYPE_DEFAULT); ppl_logevent("Host key fingerprint is:"); ppl_logevent("%s", s->fingerprint); /* First check against manually configured host keys. */ @@ -882,7 +882,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) assert(s->hkey); assert(ssh_key_alg(s->hkey) == s->cross_certifying); - s->fingerprint = ssh2_fingerprint(s->hkey); + s->fingerprint = ssh2_fingerprint(s->hkey, SSH_FPTYPE_DEFAULT); ppl_logevent("Storing additional host key for this host:"); ppl_logevent("%s", s->fingerprint); sfree(s->fingerprint); diff --git a/sshpubk.c b/sshpubk.c index d5f5c70d..031093d6 100644 --- a/sshpubk.c +++ b/sshpubk.c @@ -1732,51 +1732,73 @@ void ssh2_write_pubkey(FILE *fp, const char *comment, /* ---------------------------------------------------------------------- * Utility functions to compute SSH-2 fingerprints in a uniform way. */ -char *ssh2_fingerprint_blob(ptrlen blob) +static void ssh2_fingerprint_blob_md5(ptrlen blob, strbuf *sb) { unsigned char digest[16]; - char fingerprint_str[16*3]; - ptrlen algname; - const ssh_keyalg *alg; - int i; - BinarySource src[1]; - /* - * The fingerprint hash itself is always just the MD5 of the blob. - */ hash_simple(&ssh_md5, blob, digest); - for (i = 0; i < 16; i++) - sprintf(fingerprint_str + i*3, "%02x%s", digest[i], i==15 ? "" : ":"); + for (unsigned i = 0; i < 16; i++) + strbuf_catf(sb, "%02x%s", digest[i], i==15 ? "" : ":"); +} + +static void ssh2_fingerprint_blob_sha256(ptrlen blob, strbuf *sb) +{ + unsigned char digest[32]; + hash_simple(&ssh_sha256, blob, digest); + + put_datapl(sb, PTRLEN_LITERAL("SHA256:")); + + for (unsigned i = 0; i < 32; i += 3) { + char buf[5]; + unsigned len = 32-i; + if (len > 3) + len = 3; + base64_encode_atom(digest + i, len, buf); + put_data(sb, buf, 4); + } + strbuf_chomp(sb, '='); +} + +char *ssh2_fingerprint_blob(ptrlen blob, FingerprintType fptype) +{ + strbuf *sb = strbuf_new(); /* * Identify the key algorithm, if possible. + * + * If we can't do that, then we have a seriously confused key + * blob, in which case we return only the hash. */ + BinarySource src[1]; BinarySource_BARE_INIT_PL(src, blob); - algname = get_string(src); + ptrlen algname = get_string(src); if (!get_err(src)) { - alg = find_pubkey_alg_len(algname); + const ssh_keyalg *alg = find_pubkey_alg_len(algname); if (alg) { int bits = ssh_key_public_bits(alg, blob); - return dupprintf("%.*s %d %s", PTRLEN_PRINTF(algname), - bits, fingerprint_str); + strbuf_catf(sb, "%.*s %d ", PTRLEN_PRINTF(algname), bits); } else { - return dupprintf("%.*s %s", PTRLEN_PRINTF(algname), - fingerprint_str); + strbuf_catf(sb, "%.*s ", PTRLEN_PRINTF(algname)); } - } else { - /* - * No algorithm available (which means a seriously confused - * key blob, but there we go). Return only the hash. - */ - return dupstr(fingerprint_str); } + + switch (fptype) { + case SSH_FPTYPE_MD5: + ssh2_fingerprint_blob_md5(blob, sb); + break; + case SSH_FPTYPE_SHA256: + ssh2_fingerprint_blob_sha256(blob, sb); + break; + } + + return strbuf_to_str(sb); } -char *ssh2_fingerprint(ssh_key *data) +char *ssh2_fingerprint(ssh_key *data, FingerprintType fptype) { strbuf *blob = strbuf_new(); ssh_key_public_blob(data, BinarySink_UPCAST(blob)); - char *ret = ssh2_fingerprint_blob(ptrlen_from_strbuf(blob)); + char *ret = ssh2_fingerprint_blob(ptrlen_from_strbuf(blob), fptype); strbuf_free(blob); return ret; } diff --git a/test/cryptsuite.py b/test/cryptsuite.py index 3aaf4a04..b1eb2818 100755 --- a/test/cryptsuite.py +++ b/test/cryptsuite.py @@ -1146,6 +1146,36 @@ class crypt(MyTestBase): self.assertEqual( fp, b"768 96:12:c8:bc:e6:03:75:86:e8:c7:b9:af:d8:0c:15:75") + def testSSH2Fingerprints(self): + # A sensible key blob that we can make sense of. + sensible_blob = base64.decodebytes( + b'AAAAC3NzaC1lZDI1NTE5AAAAICWiV0VAD4lQ7taUN7vZ5Rkc' + b'SLJBW5ubn6ZINwCOzpn3') + self.assertEqual(ssh2_fingerprint_blob(sensible_blob, "sha256"), + b'ssh-ed25519 255 SHA256:' + b'E4VmaHW0sUF7SUgSEOmMJ8WBtt0e/j3zbsKvyqfFnu4') + self.assertEqual(ssh2_fingerprint_blob(sensible_blob, "md5"), + b'ssh-ed25519 255 ' + b'35:73:80:df:a3:2c:1a:f2:2c:a6:5c:84:ce:48:6a:7e') + + # A key blob with an unknown algorithm name, so that we can't + # extract the bit count. + silly_blob = ssh_string(b'foo') + ssh_string(b'key data') + self.assertEqual(ssh2_fingerprint_blob(silly_blob, "sha256"), + b'foo SHA256:' + b'mvfJTB4PaRI7hxYaYwn0sH8G6zW1HbLkbWnZE2YIKc4') + self.assertEqual(ssh2_fingerprint_blob(silly_blob, "md5"), + b'foo ' + b'5f:5f:97:94:97:be:01:5c:f6:3f:e3:6e:55:46:ea:52') + + # A key blob without even a valid algorithm-name string at the start. + very_silly_blob = b'foo' + self.assertEqual(ssh2_fingerprint_blob(very_silly_blob, "sha256"), + b'SHA256:' + b'LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564') + self.assertEqual(ssh2_fingerprint_blob(very_silly_blob, "md5"), + b'ac:bd:18:db:4c:c2:f8:5c:ed:ef:65:4f:cc:c4:a4:d8') + def testAES(self): # My own test cases, generated by a mostly independent # reference implementation of AES in Python. ('Mostly' diff --git a/test/testcrypt.py b/test/testcrypt.py index 973b90ee..686302c8 100644 --- a/test/testcrypt.py +++ b/test/testcrypt.py @@ -178,7 +178,7 @@ def make_argword(arg, argtype, fnname, argindex, to_preserve): if typename in { "hashalg", "macalg", "keyalg", "cipheralg", "dh_group", "ecdh_alg", "rsaorder", "primegenpolicy", - "argon2flavour"}: + "argon2flavour", "fptype"}: arg = coerce_to_bytes(arg) if isinstance(arg, bytes) and b" " not in arg: return arg diff --git a/testcrypt.c b/testcrypt.c index 6dc063ce..67627752 100644 --- a/testcrypt.c +++ b/testcrypt.c @@ -433,6 +433,24 @@ static Argon2Flavour get_argon2flavour(BinarySource *in) fatal_error("Argon2 flavour '%.*s': not found", PTRLEN_PRINTF(name)); } +static FingerprintType get_fptype(BinarySource *in) +{ + static const struct { + const char *key; + FingerprintType value; + } ids[] = { + {"md5", SSH_FPTYPE_MD5}, + {"sha256", SSH_FPTYPE_SHA256}, + }; + + ptrlen name = get_word(in); + for (size_t i = 0; i < lenof(ids); i++) + if (ptrlen_eq_string(name, ids[i].key)) + return ids[i].value; + + fatal_error("fingerprint type '%.*s': not found", PTRLEN_PRINTF(name)); +} + static uintmax_t get_uint(BinarySource *in) { ptrlen word = get_word(in); @@ -1310,6 +1328,7 @@ typedef const PrimeGenerationPolicy *TD_primegenpolicy; typedef struct mpint_list TD_mpint_list; typedef PockleStatus TD_pocklestatus; typedef Argon2Flavour TD_argon2flavour; +typedef FingerprintType TD_fptype; #define FUNC0(rettype, function) \ static void handle_##function(BinarySource *in, strbuf *out) { \ diff --git a/testcrypt.h b/testcrypt.h index be58d9b8..298abc0f 100644 --- a/testcrypt.h +++ b/testcrypt.h @@ -261,6 +261,8 @@ FUNC5(int, rsa1_load_s, val_string_binarysource, val_rsa, out_opt_val_string_asc FUNC8(val_string, ppk_save_sb, val_key, opt_val_string_asciz, opt_val_string_asciz, uint, argon2flavour, uint, uint, uint) FUNC3(val_string, rsa1_save_sb, val_rsa, opt_val_string_asciz, opt_val_string_asciz) +FUNC2(val_string_asciz, ssh2_fingerprint_blob, val_string_ptrlen, fptype) + /* * Password hashing. */ diff --git a/windows/winpgen.c b/windows/winpgen.c index 7ac86c18..ea04a79b 100644 --- a/windows/winpgen.c +++ b/windows/winpgen.c @@ -1023,7 +1023,7 @@ void load_key_file(HWND hwnd, struct MainDlgState *state, savecomment = state->ssh2key.comment; state->ssh2key.comment = NULL; - fp = ssh2_fingerprint(state->ssh2key.key); + fp = ssh2_fingerprint(state->ssh2key.key, SSH_FPTYPE_DEFAULT); state->ssh2key.comment = savecomment; SetDlgItemText(hwnd, IDC_FINGERPRINT, fp); @@ -1796,7 +1796,7 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, savecomment = *state->commentptr; *state->commentptr = NULL; if (state->ssh2) - fp = ssh2_fingerprint(state->ssh2key.key); + fp = ssh2_fingerprint(state->ssh2key.key, SSH_FPTYPE_DEFAULT); else fp = rsa_ssh1_fingerprint(&state->key); SetDlgItemText(hwnd, IDC_FINGERPRINT, fp); diff --git a/windows/winpgnt.c b/windows/winpgnt.c index 239e0a59..96edc2e2 100644 --- a/windows/winpgnt.c +++ b/windows/winpgnt.c @@ -353,7 +353,7 @@ void keylist_update(void) * stop and leave out a tab character. Urgh. */ - p = ssh2_fingerprint(skey->key); + p = ssh2_fingerprint(skey->key, SSH_FPTYPE_DEFAULT); listentry = dupprintf("%s\t%s", p, skey->comment); sfree(p);