- djm@cvs.openbsd.org 2005/03/01 10:42:49
     [ssh-keygen.1 ssh-keygen.c ssh_config.5]
     add tools for managing known_hosts files with hashed hostnames, including
     hashing existing files and deleting hosts by name; ok markus@ deraadt@
diff --git a/ChangeLog b/ChangeLog
index fd30762..762f8dc 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -36,6 +36,10 @@
    - djm@cvs.openbsd.org 2005/03/01 10:41:28
      [ssh-keyscan.1 ssh-keyscan.c]
      option to hash hostnames output by ssh-keyscan; ok markus@ deraadt@
+   - djm@cvs.openbsd.org 2005/03/01 10:42:49
+     [ssh-keygen.1 ssh-keygen.c ssh_config.5]
+     add tools for managing known_hosts files with hashed hostnames, including
+     hashing existing files and deleting hosts by name; ok markus@ deraadt@
 
 20050226
  - (dtucker) [openbsd-compat/bsd-openpty.c openbsd-compat/inet_ntop.c]
@@ -2212,4 +2216,4 @@
    - (djm) Trim deprecated options from INSTALL. Mention UsePAM
    - (djm) Fix quote handling in sftp; Patch from admorten AT umich.edu
 
-$Id: ChangeLog,v 1.3674 2005/03/01 10:48:03 djm Exp $
+$Id: ChangeLog,v 1.3675 2005/03/01 10:48:35 djm Exp $
diff --git a/ssh-keygen.1 b/ssh-keygen.1
index c0f24dc..f4c5ebc 100644
--- a/ssh-keygen.1
+++ b/ssh-keygen.1
@@ -1,4 +1,4 @@
-.\"	$OpenBSD: ssh-keygen.1,v 1.63 2004/08/13 00:01:43 jmc Exp $
+.\"	$OpenBSD: ssh-keygen.1,v 1.64 2005/03/01 10:42:49 djm Exp $
 .\"
 .\"  -*- nroff -*-
 .\"
@@ -81,6 +81,15 @@
 .Nm ssh-keygen
 .Fl D Ar reader
 .Nm ssh-keygen
+.Fl F Ar hostname
+.Op Fl f Ar known_hosts_file
+.Nm ssh-keygen
+.Fl H
+.Op Fl f Ar known_hosts_file
+.Nm ssh-keygen
+.Fl R Ar hostname
+.Op Fl f Ar known_hosts_file
+.Nm ssh-keygen
 .Fl U Ar reader
 .Op Fl f Ar input_keyfile
 .Nm ssh-keygen
@@ -243,6 +252,38 @@
 .It Fl D Ar reader
 Download the RSA public key stored in the smartcard in
 .Ar reader .
+.It Fl F Ar hostname
+Search for the specified
+.Ar hostname
+in a
+.Pa known_hosts
+file, listing any occurances found.
+This option is useful to find hashed host names or addresses and may also be
+used in conjunction with the
+.Fl H
+option to print found keys in a hashed format.
+.It Fl H
+Hash a
+.Pa known_hosts
+file, printing the result to standard output.
+This replaces all hostnames and addresses with hashed representations.
+These hashes may be used normally by
+.Nm ssh
+and
+.Nm sshd ,
+but they do not reveal identifying information should the file's contents
+be disclosed.
+This option will not modify existing hashed hostnames and is therefore safe 
+to use on files that mix hashed and non-hashed names.
+.It Fl R Ar hostname
+Removes all keys belonging to
+.Ar hostname
+from a 
+.Pa known_hosts
+file.
+This option is useful to delete hashed hosts (see the 
+.Fl H
+option above).
 .It Fl G Ar output_file
 Generate candidate primes for DH-GEX.
 These primes must be screened for
diff --git a/ssh-keygen.c b/ssh-keygen.c
index 7ed62a3..00ddb90 100644
--- a/ssh-keygen.c
+++ b/ssh-keygen.c
@@ -12,7 +12,7 @@
  */
 
 #include "includes.h"
-RCSID("$OpenBSD: ssh-keygen.c,v 1.118 2004/12/23 17:38:07 markus Exp $");
+RCSID("$OpenBSD: ssh-keygen.c,v 1.119 2005/03/01 10:42:49 djm Exp $");
 
 #include <openssl/evp.h>
 #include <openssl/pem.h>
@@ -27,6 +27,8 @@
 #include "pathnames.h"
 #include "log.h"
 #include "misc.h"
+#include "match.h"
+#include "hostfile.h"
 
 #ifdef SMARTCARD
 #include "scard.h"
@@ -50,6 +52,13 @@
 
 int quiet = 0;
 
+/* Flag indicating that we want to hash a known_hosts file */
+int hash_hosts = 0;
+/* Flag indicating that we want lookup a host in known_hosts file */
+int find_host = 0;
+/* Flag indicating that we want to delete a host from a known_hosts file */
+int delete_host = 0;
+
 /* Flag indicating that we just want to see the key fingerprint */
 int print_fingerprint = 0;
 int print_bubblebabble = 0;
@@ -541,6 +550,194 @@
 	exit(0);
 }
 
+static void
+print_host(FILE *f, char *name, Key *public, int hash)
+{
+	if (hash && (name = host_hash(name, NULL, 0)) == NULL)
+		fatal("hash_host failed");
+	fprintf(f, "%s ", name);
+	if (!key_write(public, f))
+		fatal("key_write failed");
+	fprintf(f, "\n");
+}
+
+static void
+do_known_hosts(struct passwd *pw, const char *name)
+{
+	FILE *in, *out = stdout;
+	Key *public;
+	char *cp, *cp2, *kp, *kp2;
+	char line[16*1024], tmp[MAXPATHLEN], old[MAXPATHLEN];
+	int c, i, skip = 0, inplace = 0, num = 0, invalid = 0, has_unhashed = 0;
+
+	if (!have_identity) {
+		cp = tilde_expand_filename(_PATH_SSH_USER_HOSTFILE, pw->pw_uid);
+		if (strlcpy(identity_file, cp, sizeof(identity_file)) >=
+		    sizeof(identity_file))
+			fatal("Specified known hosts path too long");
+		xfree(cp);
+		have_identity = 1;
+	}
+	if ((in = fopen(identity_file, "r")) == NULL)
+		fatal("fopen: %s", strerror(errno));
+
+	/*
+	 * Find hosts goes to stdout, hash and deletions happen in-place
+	 * A corner case is ssh-keygen -HF foo, which should go to stdout
+	 */
+	if (!find_host && (hash_hosts || delete_host)) {
+		if (strlcpy(tmp, identity_file, sizeof(tmp)) >= sizeof(tmp) ||
+		    strlcat(tmp, ".XXXXXXXXXX", sizeof(tmp)) >= sizeof(tmp) ||
+		    strlcpy(old, identity_file, sizeof(old)) >= sizeof(old) ||
+		    strlcat(old, ".old", sizeof(old)) >= sizeof(old))
+			fatal("known_hosts path too long");
+		umask(077);
+		if ((c = mkstemp(tmp)) == -1)
+			fatal("mkstemp: %s", strerror(errno));
+		if ((out = fdopen(c, "w")) == NULL) {
+			c = errno;
+			unlink(tmp);
+			fatal("fdopen: %s", strerror(c));
+		}
+		inplace = 1;
+	}
+
+	while (fgets(line, sizeof(line), in)) {
+		num++;
+		i = strlen(line) - 1;
+		if (line[i] != '\n') {
+			error("line %d too long: %.40s...", num, line);
+			skip = 1;
+			invalid = 1;
+			continue;
+		}
+		if (skip) {
+			skip = 0;
+			continue;
+		}
+		line[i] = '\0';
+
+		/* Skip leading whitespace, empty and comment lines. */
+		for (cp = line; *cp == ' ' || *cp == '\t'; cp++)
+			;
+		if (!*cp || *cp == '\n' || *cp == '#') {
+			if (inplace)
+				fprintf(out, "%s\n", cp);
+			continue;
+		}
+		/* Find the end of the host name portion. */
+		for (kp = cp; *kp && *kp != ' ' && *kp != '\t'; kp++)
+			;
+		if (*kp == '\0' || *(kp + 1) == '\0') {
+			error("line %d missing key: %.40s...",
+			    num, line);
+			invalid = 1;
+			continue;
+		}
+		*kp++ = '\0';
+		kp2 = kp;
+
+		public = key_new(KEY_RSA1);
+		if (key_read(public, &kp) != 1) {
+			kp = kp2;
+			key_free(public);
+			public = key_new(KEY_UNSPEC);
+			if (key_read(public, &kp) != 1) {
+				error("line %d invalid key: %.40s...",
+				    num, line);
+				key_free(public);
+				invalid = 1;
+				continue;
+			}
+		}
+
+		if (*cp == HASH_DELIM) {
+			if (find_host || delete_host) {
+				cp2 = host_hash(name, cp, strlen(cp));
+				if (cp2 == NULL) {
+					error("line %d: invalid hashed "
+					    "name: %.64s...", num, line);
+					invalid = 1;
+					continue;
+				}
+				c = (strcmp(cp2, cp) == 0);
+				if (find_host && c) {
+					printf("# Host %s found: "
+					    "line %d type %s\n", name,
+					    num, key_type(public));
+					print_host(out, cp, public, 0);
+				}
+				if (delete_host && !c)
+					print_host(out, cp, public, 0);
+			} else if (hash_hosts)
+				print_host(out, cp, public, 0);
+		} else {
+			if (find_host || delete_host) {
+				c = (match_hostname(name, cp,
+				    strlen(cp)) == 1);
+				if (find_host && c) {
+					printf("# Host %s found: "
+					    "line %d type %s\n", name,
+					    num, key_type(public));
+					print_host(out, cp, public, hash_hosts);
+				}
+				if (delete_host && !c)
+					print_host(out, cp, public, 0);
+			} else if (hash_hosts) {
+				for(cp2 = strsep(&cp, ",");
+				    cp2 != NULL && *cp2 != '\0';
+				    cp2 = strsep(&cp, ","))
+					print_host(out, cp2, public, 1);
+				has_unhashed = 1;
+			}
+		}
+		key_free(public);
+	}
+	fclose(in);
+
+	if (invalid) {
+		fprintf(stderr, "%s is not a valid known_host file.\n",
+		    identity_file);
+		if (inplace) {
+			fprintf(stderr, "Not replacing existing known_hosts "
+			    "file beacuse of errors");
+			fclose(out);
+			unlink(tmp);
+		}
+		exit(1);
+	}
+
+	if (inplace) {
+		fclose(out);
+
+		/* Backup existing file */
+		if (unlink(old) == -1 && errno != ENOENT)
+			fatal("unlink %.100s: %s", old, strerror(errno));
+		if (link(identity_file, old) == -1)
+			fatal("link %.100s to %.100s: %s", identity_file, old,
+			    strerror(errno));
+		/* Move new one into place */
+		if (rename(tmp, identity_file) == -1) {
+			error("rename\"%s\" to \"%s\": %s", tmp, identity_file,
+			    strerror(errno));
+			unlink(tmp);
+			unlink(old);
+			exit(1);
+		}
+
+		fprintf(stderr, "%s updated.\n", identity_file);
+		fprintf(stderr, "Original contents retained as %s\n", old);
+		if (has_unhashed) {
+			fprintf(stderr, "WARNING: %s contains unhashed "
+			    "entries\n", old);
+			fprintf(stderr, "Delete this file to ensure privacy "
+			     "of hostnames\n");
+		}
+	}
+
+	exit(0);
+}
+
 /*
  * Perform changing a passphrase.  The argument is the passwd structure
  * for the current user.
@@ -767,6 +964,8 @@
 	fprintf(stderr, "  -y          Read private key file and print public key.\n");
 	fprintf(stderr, "  -t type     Specify type of key to create.\n");
 	fprintf(stderr, "  -B          Show bubblebabble digest of key file.\n");
+	fprintf(stderr, "  -H          Hash names in known_hosts file\n");
+	fprintf(stderr, "  -F hostname Find hostname in known hosts file\n");
 	fprintf(stderr, "  -C comment  Provide new comment.\n");
 	fprintf(stderr, "  -N phrase   Provide new passphrase.\n");
 	fprintf(stderr, "  -P phrase   Provide old passphrase.\n");
@@ -790,7 +989,7 @@
 {
 	char dotsshdir[MAXPATHLEN], comment[1024], *passphrase1, *passphrase2;
 	char out_file[MAXPATHLEN], *reader_id = NULL;
-	char *resource_record_hostname = NULL;
+	char *rr_hostname = NULL;
 	Key *private, *public;
 	struct passwd *pw;
 	struct stat st;
@@ -824,7 +1023,7 @@
 	}
 
 	while ((opt = getopt(ac, av,
-	    "degiqpclBRvxXyb:f:t:U:D:P:N:C:r:g:T:G:M:S:a:W:")) != -1) {
+	    "degiqpclBHvxXyF:b:f:t:U:D:P:N:C:r:g:R:T:G:M:S:a:W:")) != -1) {
 		switch (opt) {
 		case 'b':
 			bits = atoi(optarg);
@@ -833,6 +1032,17 @@
 				exit(1);
 			}
 			break;
+		case 'F':
+			find_host = 1;
+			rr_hostname = optarg;
+			break;
+		case 'H':
+			hash_hosts = 1;
+			break;
+		case 'R':
+			delete_host = 1;
+			rr_hostname = optarg;
+			break;
 		case 'l':
 			print_fingerprint = 1;
 			break;
@@ -864,10 +1074,6 @@
 		case 'q':
 			quiet = 1;
 			break;
-		case 'R':
-			/* unused */
-			exit(0);
-			break;
 		case 'e':
 		case 'x':
 			/* export key */
@@ -902,7 +1108,7 @@
 			}
 			break;
 		case 'r':
-			resource_record_hostname = optarg;
+			rr_hostname = optarg;
 			break;
 		case 'W':
 			generator_wanted = atoi(optarg);
@@ -945,6 +1151,8 @@
 		printf("Can only have one of -p and -c.\n");
 		usage();
 	}
+	if (delete_host || hash_hosts || find_host)
+		do_known_hosts(pw, rr_hostname);
 	if (print_fingerprint || print_bubblebabble)
 		do_fingerprint(pw);
 	if (change_passphrase)
@@ -957,8 +1165,8 @@
 		do_convert_from_ssh2(pw);
 	if (print_public)
 		do_print_public(pw);
-	if (resource_record_hostname != NULL) {
-		do_print_resource_record(pw, resource_record_hostname);
+	if (rr_hostname != NULL) {
+		do_print_resource_record(pw, rr_hostname);
 	}
 	if (reader_id != NULL) {
 #ifdef SMARTCARD
diff --git a/ssh_config.5 b/ssh_config.5
index 9077acb..2a85485 100644
--- a/ssh_config.5
+++ b/ssh_config.5
@@ -34,7 +34,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.\" $OpenBSD: ssh_config.5,v 1.44 2005/03/01 10:40:27 djm Exp $
+.\" $OpenBSD: ssh_config.5,v 1.45 2005/03/01 10:42:49 djm Exp $
 .Dd September 25, 1999
 .Dt SSH_CONFIG 5
 .Os
@@ -421,7 +421,8 @@
 The default is
 .Dq no .
 Note that hashing of names and addresses will not be retrospectively applied 
-to existing known hosts files.
+to existing known hosts files, but these may be manually hashed using
+.Xr ssh-keygen 1 .
 .It Cm HostbasedAuthentication
 Specifies whether to try rhosts based authentication with public key
 authentication.