[PATCH] verify against known fingerprints

Phil Pennock phil.pennock at globnix.org
Tue Feb 18 19:33:59 EST 2014


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 at ip-10-0-0-1
  dsa ba:98:......................................:21 root at ip-10-0-0-1
  ecdsa 12:34:......................................:ef root at 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 at apcera.com>
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

-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 455 bytes
Desc: not available
URL: <http://lists.mindrot.org/pipermail/openssh-unix-dev/attachments/20140218/6f95ca85/attachment-0001.bin>


More information about the openssh-unix-dev mailing list