[RFC] Preferentially TOFU certificate authorities rather than host keys

Matthew Garrett mjg59 at srcf.ucam.org
Tue Oct 15 02:29:56 AEDT 2024


There's currently no way to express trust for an SSH certificate CA other
than by manually adding it to known_hosts. This patch modifies the automatic
key write-out behaviour on user verification to associate the hostname with
the CA rather than the host key, allowing environments making use of
certificates to update (potentially compromised) host keys without needing
to modify client configuration or force users to update their known_hosts.
---

Posting at this point primarily for discussion rather than submission - 
if this is something that seems desirable then we probably also want the 
ability for servers to revoke old certificates. I have some patches for 
that, but don't want to spend too much time cleaning them up unless this 
seems like something that stands some chance of being accepted.

This was inspired by 
https://github.blog/news-insights/company-news/we-updated-our-rsa-ssh-host-key/ 
- github accidentally leaked their RSA private key into a public repo 
and immediately rolled over to a new one. This created significant 
disruption to client systems which fired noisy warnings about host keys 
having changed, and organisations were forced to reassure their users 
that in this specific case they should go ahead and delete the old 
fingerprint but should still in general not do that. Everyone had a bad 
day.

If Github had been using certificates, and if we had a way to engender 
trust in certificate CAs, they could have rolled to a new key and 
certificate signed with the same (hopefully offline) CA key with zero 
impact on users. Ideally they'd also be able to push a signed revocation 
statement that would invalidate the old certificate.

 hostfile.c   |  9 +++++++--
 sshconnect.c | 30 +++++++++++++++++++++++-------
 sshkey.c     |  6 ++++++
 sshkey.h     |  1 +
 4 files changed, 37 insertions(+), 9 deletions(-)

diff --git a/hostfile.c b/hostfile.c
index c5669c703..462ed8357 100644
--- a/hostfile.c
+++ b/hostfile.c
@@ -437,12 +437,15 @@ static int
 write_host_entry(FILE *f, const char *host, const char *ip,
     const struct sshkey *key, int store_hash)
 {
-	int r, success = 0;
+	int r, success = 0, cert = sshkey_is_cert(key);
 	char *hashed_host = NULL, *lhost;
 
 	lhost = xstrdup(host);
 	lowercase(lhost);
 
+	if (cert)
+		fprintf(f, "%s ", CA_MARKER);
+
 	if (store_hash) {
 		if ((hashed_host = host_hash(lhost, NULL, 0)) == NULL) {
 			error_f("host_hash failed");
@@ -457,7 +460,9 @@ write_host_entry(FILE *f, const char *host, const char *ip,
 	}
 	free(hashed_host);
 	free(lhost);
-	if ((r = sshkey_write(key, f)) == 0)
+	if ((cert && (r = sshca_write(key, f)) == 0))
+		success = 1;
+	else if ((r = sshkey_write(key, f) == 0))
 		success = 1;
 	else
 		error_fr(r, "sshkey_write");
diff --git a/sshconnect.c b/sshconnect.c
index 7cf6b6386..72bdc7d1f 100644
--- a/sshconnect.c
+++ b/sshconnect.c
@@ -964,7 +964,7 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
 	HostStatus host_status = -1, ip_status = -1;
 	struct sshkey *raw_key = NULL;
 	char *ip = NULL, *host = NULL;
-	char hostline[1000], *hostp, *fp, *ra;
+	char hostline[1000], *hostp, *fp, *cafp, *ra;
 	char msg[1024];
 	const char *type, *fail_reason = NULL;
 	const struct hostkey_entry *host_found = NULL, *ip_found = NULL;
@@ -973,6 +973,7 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
 	int r, want_cert = sshkey_is_cert(host_key), host_ip_differ = 0;
 	int hostkey_trusted = 0; /* Known or explicitly accepted by user */
 	struct hostkeys *host_hostkeys, *ip_hostkeys;
+	struct sshkey *cert = NULL;
 	u_int i;
 
 	/*
@@ -1189,13 +1190,20 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
 				    "type are already known for this host.");
 			} else
 				xextendf(&msg1, "", ".");
-
 			fp = sshkey_fingerprint(host_key,
 			    options.fingerprint_hash, SSH_FP_DEFAULT);
 			ra = sshkey_fingerprint(host_key,
 			    options.fingerprint_hash, SSH_FP_RANDOMART);
 			if (fp == NULL || ra == NULL)
 				fatal_f("sshkey_fingerprint failed");
+			if (cert) {
+				cafp = sshkey_fingerprint(cert->cert->signature_key,
+				     options.fingerprint_hash, SSH_FP_DEFAULT);
+				if (cafp == NULL)
+					fatal_f("sshkey_fingerprint failed");
+				xextendf(&msg1, "\n", "%s CA certificate fingerprint is %s.",
+				type, cafp);
+			}
 			xextendf(&msg1, "\n", "%s key fingerprint is %s.",
 			    type, fp);
 			if (options.visual_host_key)
@@ -1229,19 +1237,26 @@ check_host_key(char *hostname, const struct ssh_conn_info *cinfo,
 		 * If in "new" or "off" strict mode, add the key automatically
 		 * to the local known_hosts file.
 		 */
+		if (cert)
+			host_key = cert;
 		if (options.check_host_ip && ip_status == HOST_NEW) {
 			snprintf(hostline, sizeof(hostline), "%s,%s", host, ip);
 			hostp = hostline;
 			if (options.hash_known_hosts) {
 				/* Add hash of host and IP separately */
 				r = add_host_to_hostfile(user_hostfiles[0],
-				    host, host_key, options.hash_known_hosts) &&
-				    add_host_to_hostfile(user_hostfiles[0], ip,
-				    host_key, options.hash_known_hosts);
+				    host, host_key, options.hash_known_hosts);
+				/* Don't add an IP entry if we're writing out a cert */
+				if (!r && !cert) {
+				    r = add_host_to_hostfile(user_hostfiles[0], ip,
+				        host_key, options.hash_known_hosts);
+				}
 			} else {
-				/* Add unhashed "host,ip" */
+				if (cert)
+				  /* Certificates are host-specific */
+				  hostp = host;
 				r = add_host_to_hostfile(user_hostfiles[0],
-				    hostline, host_key,
+				    hostp, host_key,
 				    options.hash_known_hosts);
 			}
 		} else {
@@ -1453,6 +1468,7 @@ fail:
 			fatal_fr(r, "decode key");
 		if ((r = sshkey_drop_cert(raw_key)) != 0)
 			fatal_r(r, "Couldn't drop certificate");
+		cert = host_key;
 		host_key = raw_key;
 		goto retry;
 	}
diff --git a/sshkey.c b/sshkey.c
index 73fb89ac2..903611937 100644
--- a/sshkey.c
+++ b/sshkey.c
@@ -1427,6 +1427,12 @@ sshkey_format_text(const struct sshkey *key, struct sshbuf *b)
 	return r;
 }
 
+int
+sshca_write(const struct sshkey *key, FILE *f)
+{
+	return sshkey_write(key->cert->signature_key, f);
+}
+
 int
 sshkey_write(const struct sshkey *key, FILE *f)
 {
diff --git a/sshkey.h b/sshkey.h
index d0cdea0ce..71a111b8b 100644
--- a/sshkey.h
+++ b/sshkey.h
@@ -212,6 +212,7 @@ int		 sshkey_fingerprint_raw(const struct sshkey *k,
 const char	*sshkey_type(const struct sshkey *);
 const char	*sshkey_cert_type(const struct sshkey *);
 int		 sshkey_format_text(const struct sshkey *, struct sshbuf *);
+int		 sshca_write(const struct sshkey *, FILE *);
 int		 sshkey_write(const struct sshkey *, FILE *);
 int		 sshkey_read(struct sshkey *, char **);
 u_int		 sshkey_size(const struct sshkey *);
-- 
2.46.2



More information about the openssh-unix-dev mailing list