I've just written this patch, it's undergone minimal testing and "works for me" and I'm after feedback as to acceptability of approach, anything I should be doing differently for the feature to be acceptable upstream and what I should be doing about automated testing. Use-case: you have the host's SSH fingerprints via an out-of-band mechanism which you trust and want to be able to connect and have verification use those known-good fingerprints and, if they match, update known_hosts. In our case, we use Amazon EC2, and I scripted up something which can use the AWS APIs to grab the serial console from a recently installed machine and grab the SSH host key fingerprints out of that. This provides an authenticated and tamper-proof path (provided that you trust the EC2 infrastructure APIs and, if you don't trust them as much as you do trust the SSH running in the VM, then I'd argue that you have a broken trust/threat model). In addition, we have a bastion host between the internal machines and the Internet, so ssh-keyscan is not, AFAIK, applicable. ----------------------------8< cut here >8------------------------------ % ./ssh -v -H fplist aws-cluster-foo-host-bar [...] debug1: Server host key: ECDSA 12:34:......................................:ef debug1: Have a new ECDSA host key for aws-cluster-foo-host-bar and checking fingerprint against fplist. debug1: fingerprint matches line 3. Warning: Permanently added 'aws-cluster-foo-host-bar' (ECDSA) to the list of known hosts. debug1: ssh_ecdsa_verify: signature correct ----------------------------8< cut here >8------------------------------ The file contained lines looking like: ----------------------------8< cut here >8------------------------------ rsa 11:22:33:...................................:00 root@ip-10-0-0-1 dsa ba:98:......................................:21 root@ip-10-0-0-1 ecdsa 12:34:......................................:ef root@ip-10-0-0-1 ----------------------------8< cut here >8------------------------------ (albeit with full fingerprints, obviously). Constructive feedback appreciated, and any pointers to any contributor docs that need legal signoff, or whatever. Thanks, -Phil From 5a0925ff19f6a80ec6cbf6cd5473de1d9ebf241d Mon Sep 17 00:00:00 2001 From: Phil Pennock <phil+git@xxxxxxxxxx> Date: Tue, 18 Feb 2014 03:19:26 -0500 Subject: [PATCH] Support -H/-o HashFile Use-case: have the expected host fingerprints via a trusted out-of-band mechanism (eg, console logs) and want to be able to connect and verify the fingerprints automatically. --- hostfile.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ hostfile.h | 2 ++ readconf.c | 8 +++++++- readconf.h | 1 + ssh.1 | 6 ++++++ ssh.c | 7 +++++-- ssh_config.5 | 7 +++++++ sshconnect.c | 21 +++++++++++++++++++-- 8 files changed, 99 insertions(+), 5 deletions(-) diff --git a/hostfile.c b/hostfile.c index 8bc9540..e5b834b 100644 --- a/hostfile.c +++ b/hostfile.c @@ -487,3 +487,55 @@ add_host_to_hostfile(const char *filename, const char *host, const Key *key, fclose(f); return success; } + +/* + * Checks if a fingerprint presented from a remote host can be found in the + * file. If found, returns line-number, starting with 1. If not found, + * returns -1. 0 should not be returned. + * This might be a pipe or other non-regular file, but we should only have + * one fingerprint to verify per process, so re-opening should not occur. + */ + +int +check_host_fingerprint(const char *type, const char *fp, const char *filename) +{ + char line[8192]; + FILE *f; + int match_line = -1; + u_long linenum = 0; + char *cp, *cp2; + + f = fopen(filename, "r"); + if (!f) + return -1; + + while (read_keyfile_line(f, filename, line, sizeof(line), &linenum) == 0) { + cp = line; + + /* Skip any leading whitespace, comments and empty lines. */ + for (; *cp == ' ' || *cp == '\t'; cp++) + ; + if (!*cp || *cp == '#' || *cp == '\n') + continue; + + /* expect: type <whitespace> fingerprint <optional-trailing-comments> */ + for (cp2 = cp; *cp2 && *cp2 != ' ' && *cp2 != '\t'; cp2++) + ; + if (!*cp) + continue; + *cp2 = '\0'; + if (strcasecmp(type, cp) != 0) + continue; + cp = cp2 + 1; + for (cp2 = cp; *cp2 && *cp2 != ' ' && *cp2 != '\t' && *cp2 != '\n'; cp2++) + ; + *cp2 = '\0'; + if (strcasecmp(fp, cp) != 0) + continue; + match_line = (int)linenum; + break; + } + + fclose(f); + return match_line; +} diff --git a/hostfile.h b/hostfile.h index 679c034..489a82d 100644 --- a/hostfile.h +++ b/hostfile.h @@ -51,4 +51,6 @@ int add_host_to_hostfile(const char *, const char *, const Key *, int); char *host_hash(const char *, const char *, u_int); +int check_host_fingerprint(const char *, const char *, const char *); + #endif diff --git a/readconf.c b/readconf.c index f80d1cc..af170e6 100644 --- a/readconf.c +++ b/readconf.c @@ -143,7 +143,7 @@ typedef enum { oAddressFamily, oGssAuthentication, oGssDelegateCreds, oServerAliveInterval, oServerAliveCountMax, oIdentitiesOnly, oSendEnv, oControlPath, oControlMaster, oControlPersist, - oHashKnownHosts, + oHashKnownHosts, oHashFile, oTunnel, oTunnelDevice, oLocalCommand, oPermitLocalCommand, oVisualHostKey, oUseRoaming, oKexAlgorithms, oIPQoS, oRequestTTY, oIgnoreUnknown, oProxyUseFdpass, @@ -246,6 +246,7 @@ static struct { { "controlmaster", oControlMaster }, { "controlpersist", oControlPersist }, { "hashknownhosts", oHashKnownHosts }, + { "hashfile", oHashFile }, { "tunnel", oTunnel }, { "tunneldevice", oTunnelDevice }, { "localcommand", oLocalCommand }, @@ -953,6 +954,10 @@ parse_char_array: max_entries = SSH_MAX_HOSTS_FILES; goto parse_char_array; + case oHashFile: + charptr = &options->hash_file; + goto parse_string; + case oHostName: charptr = &options->hostname; goto parse_string; @@ -1533,6 +1538,7 @@ initialize_options(Options * options) options->control_persist = -1; options->control_persist_timeout = 0; options->hash_known_hosts = -1; + options->hash_file = NULL; options->tun_open = -1; options->tun_local = -1; options->tun_remote = -1; diff --git a/readconf.h b/readconf.h index 9723da0..5b7d796 100644 --- a/readconf.h +++ b/readconf.h @@ -94,6 +94,7 @@ typedef struct { char *system_hostfiles[SSH_MAX_HOSTS_FILES]; u_int num_user_hostfiles; /* Path for $HOME/.ssh/known_hosts */ char *user_hostfiles[SSH_MAX_HOSTS_FILES]; + char *hash_file; /* when user has fingerprints for remote host */ char *preferred_authentications; char *bind_address; /* local socket address for connection to sshd */ char *pkcs11_provider; /* PKCS#11 provider */ diff --git a/ssh.1 b/ssh.1 index 27794e2..50e32e8 100644 --- a/ssh.1 +++ b/ssh.1 @@ -50,6 +50,7 @@ .Op Fl E Ar log_file .Op Fl e Ar escape_char .Op Fl F Ar configfile +.Op Fl H Ar hashfile .Op Fl I Ar pkcs11 .Op Fl i Ar identity_file .Op Fl L Oo Ar bind_address : Oc Ns Ar port : Ns Ar host : Ns Ar hostport @@ -267,6 +268,11 @@ will wait for all remote port forwards to be successfully established before placing itself in the background. .It Fl g Allows remote hosts to connect to local forwarded ports. +.It Fl H Ar hash_file +Provides a filename which contains expected fingerprints if the remote +host presents an unknown public key. If provided, the remote key must +match one of these, and if it does match, then it will be added to the +known hosts file. .It Fl I Ar pkcs11 Specify the PKCS#11 shared library .Nm diff --git a/ssh.c b/ssh.c index add760c..affd92f 100644 --- a/ssh.c +++ b/ssh.c @@ -198,7 +198,7 @@ usage(void) fprintf(stderr, "usage: ssh [-1246AaCfgKkMNnqsTtVvXxYy] [-b bind_address] [-c cipher_spec]\n" " [-D [bind_address:]port] [-E log_file] [-e escape_char]\n" -" [-F configfile] [-I pkcs11] [-i identity_file]\n" +" [-F configfile] [-H hash_file] [-I pkcs11] [-i identity_file]\n" " [-L [bind_address:]port:host:hostport] [-l login_name] [-m mac_spec]\n" " [-O ctl_cmd] [-o option] [-p port]\n" " [-Q cipher | cipher-auth | mac | kex | key]\n" @@ -455,7 +455,7 @@ main(int ac, char **av) again: while ((opt = getopt(ac, av, "1246ab:c:e:fgi:kl:m:no:p:qstvx" - "ACD:E:F:I:KL:MNO:PQ:R:S:TVw:W:XYy")) != -1) { + "ACD:E:F:H:I:KL:MNO:PQ:R:S:TVw:W:XYy")) != -1) { switch (opt) { case '1': options.protocol = SSH_PROTO_1; @@ -568,6 +568,9 @@ main(int ac, char **av) fprintf(stderr, "no support for PKCS#11.\n"); #endif break; + case 'H': + options.hash_file = xstrdup(optarg); + break; case 't': if (options.request_tty == REQUEST_TTY_YES) options.request_tty = REQUEST_TTY_FORCE; diff --git a/ssh_config.5 b/ssh_config.5 index 3cadcd7..1b6861d 100644 --- a/ssh_config.5 +++ b/ssh_config.5 @@ -681,6 +681,13 @@ Forward (delegate) credentials to the server. The default is .Dq no . Note that this option applies to protocol version 2 only. +.It Cm HashFile +Provides a file which should contain host fingerprint lines, in the same +format as reported by +.Xr ssh-keygen . +If the remote host's key is not known, but matches a fingerprint in this +file, then it will be automatically accepted. If it does not match, then +it will be automatically rejected. .It Cm HashKnownHosts Indicates that .Xr ssh 1 diff --git a/sshconnect.c b/sshconnect.c index 573d7a8..0ce3e52 100644 --- a/sshconnect.c +++ b/sshconnect.c @@ -811,7 +811,7 @@ check_host_key(char *hostname, struct sockaddr *hostaddr, u_short port, char msg[1024]; const char *type; const struct hostkey_entry *host_found, *ip_found; - int len, cancelled_forwarding = 0; + int len, line, cancelled_forwarding = 0; int local = sockaddr_is_local(hostaddr); int r, want_cert = key_is_cert(host_key), host_ip_differ = 0; struct hostkeys *host_hostkeys, *ip_hostkeys; @@ -936,7 +936,24 @@ check_host_key(char *hostname, struct sockaddr *hostaddr, u_short port, if (readonly || want_cert) goto fail; /* The host is new. */ - if (options.strict_host_key_checking == 1) { + if (options.hash_file) { + debug("Have a new %s host key for %.200s and checking " + "fingerprint against %s.", + type, host, options.hash_file); + fp = key_fingerprint(host_key, SSH_FP_MD5, SSH_FP_HEX); + line = check_host_fingerprint(type, fp, options.hash_file); + if (line >= 1) { + free(fp); + debug("fingerprint matches line %d.", line); + } else { + error("Explicit hash file given and host key " + "does not match.\n" + "Presented with: %s %s\n", + type, fp); + free(fp); + goto fail; + } + } else if (options.strict_host_key_checking == 1) { /* * User has requested strict host key checking. We * will not add the host key automatically. The only -- 1.8.5.4
Attachment:
pgpJsEg_qz1Xk.pgp
Description: PGP signature
_______________________________________________ openssh-unix-dev mailing list openssh-unix-dev@xxxxxxxxxxx https://lists.mindrot.org/mailman/listinfo/openssh-unix-dev