From fc2cb0014927b4862cc0a7df015c685733906157 Mon Sep 17 00:00:00 2001
From: Hayden Stainsby <hds@caffeineconcepts.com>
Date: Fri, 27 Jan 2023 14:42:22 +0100
Subject: [PATCH 1/6] Add partial support for SSH known hosts markers

The SSH `known_hosts` file parsing in Cargo did not previously support
markers. Markers are modifiers on the lines (`@cert-authority` and
`@revoked`) which denote special behavior for the details on that line.
Lines were skipped entirely.

This silent skipping of marker lines can be confusing to a user, who
sees that their command line Git/SSH client works for some repository,
but Cargo reports that no host key is found.

This change adds support for the `@revoked` marker. This marker denotes
that a key should be rejected outright. It is of limited use without
`@cert-authority` marker support. However, if it is present in a user's
`known_hosts` file, then Cargo definitely shouldn't accept that key and
probably shouldn't suggest that the user add it to their `known_hosts`
either.

The change also adds support for detecting `@cert-authority` markers in
`known_hosts` files. These lines cannot yet be used for host key
verification, but if one is found for a matching host, the user will be
informed that Cargo doesn't support `@cert-authority` markers in the
error message. Additionally, the user will be advised to use the
`net.git-fetch-with-cli` config option to use the command line git
client for fetching crates from Git.

Refs: #11577
---
 src/cargo/sources/git/known_hosts.rs | 320 ++++++++++++++++++++++-----
 1 file changed, 268 insertions(+), 52 deletions(-)

diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs
index c8466d6079c..370c8bf58f0 100644
--- a/src/cargo/sources/git/known_hosts.rs
+++ b/src/cargo/sources/git/known_hosts.rs
@@ -24,7 +24,7 @@ use git2::cert::{Cert, SshHostKeyType};
 use git2::CertificateCheckStatus;
 use hmac::Mac;
 use std::collections::HashSet;
-use std::fmt::Write;
+use std::fmt::{Display, Write};
 use std::path::{Path, PathBuf};
 
 /// These are host keys that are hard-coded in cargo to provide convenience.
@@ -62,6 +62,19 @@ enum KnownHostError {
         remote_host_key: String,
         remote_fingerprint: String,
     },
+    /// The host key was found with a @revoked marker, it must not be accepted.
+    HostKeyRevoked {
+        hostname: String,
+        key_type: SshHostKeyType,
+        remote_host_key: String,
+        location: KnownHostLocation,
+    },
+    /// The host key was not found, but there was a matching known host with a
+    /// @cert-authority marker (which Cargo doesn't yet support).
+    HostHasOnlyCertAuthority {
+        hostname: String,
+        location: KnownHostLocation,
+    },
 }
 
 impl From<anyhow::Error> for KnownHostError {
@@ -81,6 +94,21 @@ enum KnownHostLocation {
     Bundled,
 }
 
+impl Display for KnownHostLocation {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let loc = match self {
+            KnownHostLocation::File { path, lineno } => {
+                format!("{} line {lineno}", path.display())
+            }
+            KnownHostLocation::Config { definition } => {
+                format!("config value from {definition}")
+            }
+            KnownHostLocation::Bundled => format!("bundled with cargo"),
+        };
+        f.write_str(&loc)
+    }
+}
+
 /// The git2 callback used to validate a certificate (only ssh known hosts are validated).
 pub fn certificate_check(
     cert: &Cert<'_>,
@@ -133,16 +161,13 @@ pub fn certificate_check(
                     but is associated with a different host:\n",
                 );
                 for known_host in other_hosts {
-                    let loc = match known_host.location {
-                        KnownHostLocation::File { path, lineno } => {
-                            format!("{} line {lineno}", path.display())
-                        }
-                        KnownHostLocation::Config { definition } => {
-                            format!("config value from {definition}")
-                        }
-                        KnownHostLocation::Bundled => format!("bundled with cargo"),
-                    };
-                    write!(msg, "    {loc}: {}\n", known_host.patterns).unwrap();
+                    write!(
+                        msg,
+                        "    {loc}: {patterns}\n",
+                        loc = known_host.location,
+                        patterns = known_host.patterns
+                    )
+                    .unwrap();
                 }
                 msg
             };
@@ -220,6 +245,39 @@ pub fn certificate_check(
                 for more information.\n\
                 ")
         }
+        Err(KnownHostError::HostKeyRevoked {
+            hostname,
+            key_type,
+            remote_host_key,
+            location,
+        }) => {
+            let key_type_short_name = key_type.short_name();
+            format!(
+                "error: Key has been revoked for `{hostname}`\n\
+                **************************************\n\
+                * WARNING: REVOKED HOST KEY DETECTED *\n\
+                **************************************\n\
+                This may indicate that the key provided by this host has been\n\
+                compromised and should not be accepted.
+                \n\
+                The host key {key_type_short_name} {remote_host_key} is revoked\n\
+                in {location} and has been rejected.\n\
+                "
+            )
+        }
+        Err(KnownHostError::HostHasOnlyCertAuthority { hostname, location }) => {
+            format!("error: Found a `@cert-authority` marker for `{hostname}`\n\
+                \n\
+                Cargo doesn't support certificate authorities for host key verification. It is\n\
+                recommended that the command line Git client is used instead. This can be achieved\n\
+                by setting `net.git-fetch-with-cli` to `true` in the Cargo config.\n\
+                \n
+                The `@cert-authority` line was found in {location}.\n\
+                \n\
+                See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts \
+                for more information.\n\
+                ")
+        }
     };
     Err(git2::Error::new(
         git2::ErrorCode::GenericError,
@@ -285,6 +343,7 @@ fn check_ssh_known_hosts(
                 patterns: patterns.to_string(),
                 key_type: key_type.to_string(),
                 key,
+                line_type: KnownHostLineType::Key,
             });
         }
     }
@@ -298,12 +357,23 @@ fn check_ssh_known_hosts_loaded(
     remote_key_type: SshHostKeyType,
     remote_host_key: &[u8],
 ) -> Result<(), KnownHostError> {
-    // `changed_key` keeps track of any entries where the key has changed.
-    let mut changed_key = None;
+    // `latent_error` keeps track of a potential error that will be returned
+    // in case a matching host key isn't found.
+    let mut latent_errors: Vec<KnownHostError> = Vec::new();
+
     // `other_hosts` keeps track of any entries that have an identical key,
     // but a different hostname.
     let mut other_hosts = Vec::new();
 
+    // Older versions of OpenSSH (before 6.8, March 2015) showed MD5
+    // fingerprints (see FingerprintHash ssh config option). Here we only
+    // support SHA256.
+    let mut remote_fingerprint = cargo_util::Sha256::new();
+    remote_fingerprint.update(remote_host_key.clone());
+    let remote_fingerprint =
+        base64::encode_config(remote_fingerprint.finish(), base64::STANDARD_NO_PAD);
+    let remote_host_key_encoded = base64::encode(remote_host_key);
+
     for known_host in known_hosts {
         // The key type from libgit2 needs to match the key type from the host file.
         if known_host.key_type != remote_key_type.name() {
@@ -316,42 +386,81 @@ fn check_ssh_known_hosts_loaded(
             }
             continue;
         }
-        if key_matches {
-            return Ok(());
+        match known_host.line_type {
+            KnownHostLineType::Key => {
+                if key_matches {
+                    return Ok(());
+                }
+
+                // The host and key type matched, but the key itself did not.
+                // This indicates the key has changed.
+                // This is only reported as an error if no subsequent lines have a
+                // correct key.
+                latent_errors.push(KnownHostError::HostKeyHasChanged {
+                    hostname: host.to_string(),
+                    key_type: remote_key_type,
+                    old_known_host: known_host.clone(),
+                    remote_host_key: remote_host_key_encoded.clone(),
+                    remote_fingerprint: remote_fingerprint.clone(),
+                });
+            }
+            KnownHostLineType::Revoked => {
+                if key_matches {
+                    return Err(KnownHostError::HostKeyRevoked {
+                        hostname: host.to_string(),
+                        key_type: remote_key_type,
+                        remote_host_key: remote_host_key_encoded,
+                        location: known_host.location.clone(),
+                    });
+                }
+            }
+            KnownHostLineType::CertAuthority => {
+                // The host matches a @cert-authority line, which is unsupported.
+                latent_errors.push(KnownHostError::HostHasOnlyCertAuthority {
+                    hostname: host.to_string(),
+                    location: known_host.location.clone(),
+                });
+            }
         }
-        // The host and key type matched, but the key itself did not.
-        // This indicates the key has changed.
-        // This is only reported as an error if no subsequent lines have a
-        // correct key.
-        changed_key = Some(known_host.clone());
     }
-    // Older versions of OpenSSH (before 6.8, March 2015) showed MD5
-    // fingerprints (see FingerprintHash ssh config option). Here we only
-    // support SHA256.
-    let mut remote_fingerprint = cargo_util::Sha256::new();
-    remote_fingerprint.update(remote_host_key);
-    let remote_fingerprint =
-        base64::encode_config(remote_fingerprint.finish(), base64::STANDARD_NO_PAD);
-    let remote_host_key = base64::encode(remote_host_key);
-    // FIXME: Ideally the error message should include the IP address of the
-    // remote host (to help the user validate that they are connecting to the
-    // host they were expecting to). However, I don't see a way to obtain that
-    // information from libgit2.
-    match changed_key {
-        Some(old_known_host) => Err(KnownHostError::HostKeyHasChanged {
-            hostname: host.to_string(),
-            key_type: remote_key_type,
-            old_known_host,
-            remote_host_key,
-            remote_fingerprint,
-        }),
-        None => Err(KnownHostError::HostKeyNotFound {
+
+    if latent_errors.len() == 0 {
+        // FIXME: Ideally the error message should include the IP address of the
+        // remote host (to help the user validate that they are connecting to the
+        // host they were expecting to). However, I don't see a way to obtain that
+        // information from libgit2.
+        Err(KnownHostError::HostKeyNotFound {
             hostname: host.to_string(),
             key_type: remote_key_type,
-            remote_host_key,
+            remote_host_key: remote_host_key_encoded,
             remote_fingerprint,
             other_hosts,
-        }),
+        })
+    } else {
+        // We're going to take the first HostKeyHasChanged error if
+        // we find one, otherwise we'll take the first error (which
+        // we expect to be a CertAuthority error).
+        for err in &latent_errors {
+            if let KnownHostError::HostKeyHasChanged {
+                hostname,
+                key_type,
+                old_known_host,
+                remote_host_key,
+                remote_fingerprint,
+            } = err
+            {
+                return Err(KnownHostError::HostKeyHasChanged {
+                    hostname: hostname.clone(),
+                    key_type: key_type.clone(),
+                    old_known_host: old_known_host.clone(),
+                    remote_host_key: remote_host_key.clone(),
+                    remote_fingerprint: remote_fingerprint.clone(),
+                });
+            }
+        }
+        // Otherwise, we take the first error (which we expect to be
+        // a CertAuthority error).
+        Err(latent_errors.pop().unwrap())
     }
 }
 
@@ -422,6 +531,13 @@ fn user_known_host_location_to_add(diagnostic_home_config: &str) -> String {
 
 const HASH_HOSTNAME_PREFIX: &str = "|1|";
 
+#[derive(Clone)]
+enum KnownHostLineType {
+    Key,
+    CertAuthority,
+    Revoked,
+}
+
 /// A single known host entry.
 #[derive(Clone)]
 struct KnownHost {
@@ -430,6 +546,7 @@ struct KnownHost {
     patterns: String,
     key_type: String,
     key: Vec<u8>,
+    line_type: KnownHostLineType,
 }
 
 impl KnownHost {
@@ -488,15 +605,31 @@ fn load_hostfile_contents(path: &Path, contents: &str) -> Vec<KnownHost> {
 
 fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<KnownHost> {
     let line = line.trim();
-    // FIXME: @revoked and @cert-authority is currently not supported.
-    if line.is_empty() || line.starts_with(['#', '@']) {
+    if line.is_empty() || line.starts_with('#') {
         return None;
     }
     let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty());
+
+    let line_type = if line.starts_with("@") {
+        let line_type = parts.next()?;
+
+        if line_type == "@cert-authority" {
+            KnownHostLineType::CertAuthority
+        } else if line_type == "@revoked" {
+            KnownHostLineType::Revoked
+        } else {
+            // No other markers are defined
+            return None;
+        }
+    } else {
+        KnownHostLineType::Key
+    };
+
     let patterns = parts.next()?;
     let key_type = parts.next()?;
     let key = parts.next().map(base64::decode)?.ok()?;
     Some(KnownHost {
+        line_type,
         location,
         patterns: patterns.to_string(),
         key_type: key_type.to_string(),
@@ -517,8 +650,10 @@ mod tests {
         nistp256.example.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ4iYGCcJrUIfrHfzlsv8e8kaF36qpcUpe3VNAKVCZX/BDptIdlEe8u8vKNRTPgUO9jqS0+tjTcPiQd8/8I9qng= eric@host
         nistp384.example.org ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNuGT3TqMz2rcwOt2ZqkiNqq7dvWPE66W2qPCoZsh0pQhVU3BnhKIc6nEr6+Wts0Z3jdF3QWwxbbTjbVTVhdr8fMCFhDCWiQFm9xLerYPKnu9qHvx9K87/fjc5+0pu4hLA== eric@host
         nistp521.example.org ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAD35HH6OsK4DN75BrKipVj/GvZaUzjPNa1F8wMjUdPB1JlVcUfgzJjWSxrhmaNN3u0soiZw8WNRFINsGPCw5E7DywF1689WcIj2Ye2rcy99je15FknScTzBBD04JgIyOI50mCUaPCBoF14vFlN6BmO00cFo+yzy5N8GuQ2sx9kr21xmFQ== eric@host
-        # Revoked not yet supported.
-        @revoked * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host
+        # Revoked is supported, but without Cert-Authority support, it will only negate some other fixed key.
+        @revoked revoked.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host
+        # Cert-Authority is not supported (below key should not be valid anyway)
+        @cert-authority ca.example.com ssh-rsa AABBB5Wm
         example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host
         192.168.42.12 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
         |1|QxzZoTXIWLhUsuHAXjuDMIV3FjQ=|M6NCOIkjiWdCWqkh5+Q+/uFLGjs= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host
@@ -530,7 +665,7 @@ mod tests {
     fn known_hosts_parse() {
         let kh_path = Path::new("/home/abc/.known_hosts");
         let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
-        assert_eq!(khs.len(), 10);
+        assert_eq!(khs.len(), 12);
         match &khs[0].location {
             KnownHostLocation::File { path, lineno } => {
                 assert_eq!(path, kh_path);
@@ -551,7 +686,7 @@ mod tests {
         }
         assert_eq!(khs[2].patterns, "[example.net]:2222");
         assert_eq!(khs[3].patterns, "nistp256.example.org");
-        assert_eq!(khs[7].patterns, "192.168.42.12");
+        assert_eq!(khs[9].patterns, "192.168.42.12");
     }
 
     #[test]
@@ -565,9 +700,9 @@ mod tests {
         assert!(!khs[0].host_matches("example.net"));
         assert!(khs[2].host_matches("[example.net]:2222"));
         assert!(!khs[2].host_matches("example.net"));
-        assert!(khs[8].host_matches("hashed.example.com"));
-        assert!(!khs[8].host_matches("example.com"));
-        assert!(!khs[9].host_matches("neg.example.com"));
+        assert!(khs[10].host_matches("hashed.example.com"));
+        assert!(!khs[10].host_matches("example.com"));
+        assert!(!khs[11].host_matches("neg.example.com"));
     }
 
     #[test]
@@ -626,4 +761,85 @@ mod tests {
             _ => panic!("unexpected"),
         }
     }
+
+    #[test]
+    fn revoked() {
+        let kh_path = Path::new("/home/abc/.known_hosts");
+        let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
+
+        match check_ssh_known_hosts_loaded(
+            &khs,
+            "revoked.example.com",
+            SshHostKeyType::Ed255219,
+            &khs[6].key,
+        ) {
+            Err(KnownHostError::HostKeyRevoked {
+                hostname, location, ..
+            }) => {
+                assert_eq!("revoked.example.com", hostname);
+                assert!(matches!(
+                    location,
+                    KnownHostLocation::File { lineno: 11, .. }
+                ));
+            }
+            _ => panic!("Expected key to be revoked for revoked.example.com."),
+        }
+    }
+
+    #[test]
+    fn cert_authority() {
+        let kh_path = Path::new("/home/abc/.known_hosts");
+        let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
+
+        match check_ssh_known_hosts_loaded(
+            &khs,
+            "ca.example.com",
+            SshHostKeyType::Rsa,
+            &khs[0].key, // The key should not matter
+        ) {
+            Err(KnownHostError::HostHasOnlyCertAuthority {
+                hostname, location, ..
+            }) => {
+                assert_eq!("ca.example.com", hostname);
+                assert!(matches!(
+                    location,
+                    KnownHostLocation::File { lineno: 13, .. }
+                ));
+            }
+            Err(KnownHostError::HostKeyNotFound { hostname, .. }) => {
+                panic!("host key not found... {}", hostname);
+            }
+            _ => panic!("Expected host to only have @cert-authority line (which is unsupported)."),
+        }
+    }
+
+    #[test]
+    fn multiple_errors() {
+        let contents = r#"
+        not-used.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host
+        # Cert-authority and changed key for the same host - changed key error should prevail
+        @cert-authority example.com ssh-ed25519 AABBB5Wm
+        example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
+        "#;
+
+        let kh_path = Path::new("/home/abc/.known_hosts");
+        let khs = load_hostfile_contents(kh_path, contents);
+
+        match check_ssh_known_hosts_loaded(
+            &khs,
+            "example.com",
+            SshHostKeyType::Ed255219,
+            &khs[0].key,
+        ) {
+            Err(KnownHostError::HostKeyHasChanged { hostname, old_known_host, remote_host_key, .. }) => {
+                assert_eq!("example.com", hostname);
+                assert_eq!("AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY", remote_host_key);
+                assert!(matches!(
+                    old_known_host.location,
+                    KnownHostLocation::File { lineno: 5, .. }
+                ));
+            }
+            _ => panic!("Expected error to be of type HostKeyHasChanged."),
+        }
+    }
 }

From 605506ab67bb28a3d890ea8e19307bd1275d0da1 Mon Sep 17 00:00:00 2001
From: Hayden Stainsby <hds@caffeineconcepts.com>
Date: Fri, 27 Jan 2023 19:48:34 +0100
Subject: [PATCH 2/6] Fix format

---
 src/cargo/sources/git/known_hosts.rs | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs
index 370c8bf58f0..8380344e6ed 100644
--- a/src/cargo/sources/git/known_hosts.rs
+++ b/src/cargo/sources/git/known_hosts.rs
@@ -831,9 +831,17 @@ mod tests {
             SshHostKeyType::Ed255219,
             &khs[0].key,
         ) {
-            Err(KnownHostError::HostKeyHasChanged { hostname, old_known_host, remote_host_key, .. }) => {
+            Err(KnownHostError::HostKeyHasChanged {
+                hostname,
+                old_known_host,
+                remote_host_key,
+                ..
+            }) => {
                 assert_eq!("example.com", hostname);
-                assert_eq!("AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY", remote_host_key);
+                assert_eq!(
+                    "AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY",
+                    remote_host_key
+                );
                 assert!(matches!(
                     old_known_host.location,
                     KnownHostLocation::File { lineno: 5, .. }

From a871a90d8b35deac0d68fd21e1166d2555289f62 Mon Sep 17 00:00:00 2001
From: Hayden Stainsby <hds@caffeineconcepts.com>
Date: Mon, 30 Jan 2023 10:45:53 +0100
Subject: [PATCH 3/6] Apply suggestions from code review

Co-authored-by: Eric Huss <eric@huss.org>
---
 src/cargo/sources/git/known_hosts.rs | 31 +++++++++-------------------
 1 file changed, 10 insertions(+), 21 deletions(-)

diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs
index 8380344e6ed..956c8039386 100644
--- a/src/cargo/sources/git/known_hosts.rs
+++ b/src/cargo/sources/git/known_hosts.rs
@@ -424,7 +424,7 @@ fn check_ssh_known_hosts_loaded(
         }
     }
 
-    if latent_errors.len() == 0 {
+    if latent_errors.is_empty() {
         // FIXME: Ideally the error message should include the IP address of the
         // remote host (to help the user validate that they are connecting to the
         // host they were expecting to). However, I don't see a way to obtain that
@@ -440,27 +440,16 @@ fn check_ssh_known_hosts_loaded(
         // We're going to take the first HostKeyHasChanged error if
         // we find one, otherwise we'll take the first error (which
         // we expect to be a CertAuthority error).
-        for err in &latent_errors {
-            if let KnownHostError::HostKeyHasChanged {
-                hostname,
-                key_type,
-                old_known_host,
-                remote_host_key,
-                remote_fingerprint,
-            } = err
-            {
-                return Err(KnownHostError::HostKeyHasChanged {
-                    hostname: hostname.clone(),
-                    key_type: key_type.clone(),
-                    old_known_host: old_known_host.clone(),
-                    remote_host_key: remote_host_key.clone(),
-                    remote_fingerprint: remote_fingerprint.clone(),
-                });
-            }
+        if let Some(index) = latent_errors
+            .iter()
+            .position(|e| matches!(e, KnownHostError::HostKeyHasChanged { .. }))
+        {
+            return Err(latent_errors.remove(index));
+        } else {
+            // Otherwise, we take the first error (which we expect to be
+            // a CertAuthority error).
+            Err(latent_errors.pop().unwrap())
         }
-        // Otherwise, we take the first error (which we expect to be
-        // a CertAuthority error).
-        Err(latent_errors.pop().unwrap())
     }
 }
 

From 26e08ab0cd4e594026a5c282689b1eef3e7008c2 Mon Sep 17 00:00:00 2001
From: Hayden Stainsby <hds@caffeineconcepts.com>
Date: Tue, 31 Jan 2023 16:08:03 +0100
Subject: [PATCH 4/6] Fix host file checking to not return success until all
 lines have been processed

Since a @revoked line might deny access to a key which would otherwise
be accepted, we need to process all lines before we decide that a host
key should be accepted.
---
 src/cargo/sources/git/known_hosts.rs | 44 +++++++++++++++++++++++++++-
 1 file changed, 43 insertions(+), 1 deletion(-)

diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs
index 8380344e6ed..d39cc2f7315 100644
--- a/src/cargo/sources/git/known_hosts.rs
+++ b/src/cargo/sources/git/known_hosts.rs
@@ -365,6 +365,11 @@ fn check_ssh_known_hosts_loaded(
     // but a different hostname.
     let mut other_hosts = Vec::new();
 
+    // `accepted_known_host_found` keeps track of whether we've found a matching
+    // line in the `known_hosts` file that we would accept. We can't return that
+    // immediately, because there may be a subsequent @revoked key.
+    let mut accepted_known_host_found = false;
+
     // Older versions of OpenSSH (before 6.8, March 2015) showed MD5
     // fingerprints (see FingerprintHash ssh config option). Here we only
     // support SHA256.
@@ -389,7 +394,7 @@ fn check_ssh_known_hosts_loaded(
         match known_host.line_type {
             KnownHostLineType::Key => {
                 if key_matches {
-                    return Ok(());
+                    accepted_known_host_found = true;
                 }
 
                 // The host and key type matched, but the key itself did not.
@@ -424,6 +429,11 @@ fn check_ssh_known_hosts_loaded(
         }
     }
 
+    // We have an accepted host key and it hasn't been revoked.
+    if accepted_known_host_found {
+        return Ok(());
+    }
+
     if latent_errors.len() == 0 {
         // FIXME: Ideally the error message should include the IP address of the
         // remote host (to help the user validate that they are connecting to the
@@ -850,4 +860,36 @@ mod tests {
             _ => panic!("Expected error to be of type HostKeyHasChanged."),
         }
     }
+
+    #[test]
+    fn known_host_and_revoked() {
+        let contents = r#"
+        example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
+        # Later in the file the same host key is revoked
+        @revoked example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
+        "#;
+
+        let kh_path = Path::new("/home/abc/.known_hosts");
+        let khs = load_hostfile_contents(kh_path, contents);
+
+        match check_ssh_known_hosts_loaded(
+            &khs,
+            "example.com",
+            SshHostKeyType::Ed255219,
+            &khs[0].key,
+        ) {
+            Err(KnownHostError::HostKeyRevoked { hostname, remote_host_key, location, .. }) => {
+                assert_eq!("example.com", hostname);
+                assert_eq!(
+                    "AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR",
+                    remote_host_key
+                );
+                assert!(matches!(
+                    location,
+                    KnownHostLocation::File { lineno: 4, .. }
+                ));
+            },
+            _ => panic!("Expected host key to be reject with error HostKeyRevoked."),
+        }
+    }
 }

From 7a6ff7f068acde98230d712637e934c58fc84805 Mon Sep 17 00:00:00 2001
From: Hayden Stainsby <hds@caffeineconcepts.com>
Date: Tue, 31 Jan 2023 17:57:10 +0100
Subject: [PATCH 5/6] cargo fmt

---
 src/cargo/sources/git/known_hosts.rs | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs
index 8b4da1b6bd1..fd34c427417 100644
--- a/src/cargo/sources/git/known_hosts.rs
+++ b/src/cargo/sources/git/known_hosts.rs
@@ -867,7 +867,12 @@ mod tests {
             SshHostKeyType::Ed255219,
             &khs[0].key,
         ) {
-            Err(KnownHostError::HostKeyRevoked { hostname, remote_host_key, location, .. }) => {
+            Err(KnownHostError::HostKeyRevoked {
+                hostname,
+                remote_host_key,
+                location,
+                ..
+            }) => {
                 assert_eq!("example.com", hostname);
                 assert_eq!(
                     "AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR",
@@ -877,7 +882,7 @@ mod tests {
                     location,
                     KnownHostLocation::File { lineno: 4, .. }
                 ));
-            },
+            }
             _ => panic!("Expected host key to be reject with error HostKeyRevoked."),
         }
     }

From 0acf2bf7584a7eef6426bb910858dbc45a7cad12 Mon Sep 17 00:00:00 2001
From: Hayden Stainsby <hds@caffeineconcepts.com>
Date: Tue, 31 Jan 2023 22:15:23 +0100
Subject: [PATCH 6/6] avoid recording incorrect host key has changed errors

---
 src/cargo/sources/git/known_hosts.rs | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs
index fd34c427417..e50b7a5043a 100644
--- a/src/cargo/sources/git/known_hosts.rs
+++ b/src/cargo/sources/git/known_hosts.rs
@@ -395,19 +395,19 @@ fn check_ssh_known_hosts_loaded(
             KnownHostLineType::Key => {
                 if key_matches {
                     accepted_known_host_found = true;
+                } else {
+                    // The host and key type matched, but the key itself did not.
+                    // This indicates the key has changed.
+                    // This is only reported as an error if no subsequent lines have a
+                    // correct key.
+                    latent_errors.push(KnownHostError::HostKeyHasChanged {
+                        hostname: host.to_string(),
+                        key_type: remote_key_type,
+                        old_known_host: known_host.clone(),
+                        remote_host_key: remote_host_key_encoded.clone(),
+                        remote_fingerprint: remote_fingerprint.clone(),
+                    });
                 }
-
-                // The host and key type matched, but the key itself did not.
-                // This indicates the key has changed.
-                // This is only reported as an error if no subsequent lines have a
-                // correct key.
-                latent_errors.push(KnownHostError::HostKeyHasChanged {
-                    hostname: host.to_string(),
-                    key_type: remote_key_type,
-                    old_known_host: known_host.clone(),
-                    remote_host_key: remote_host_key_encoded.clone(),
-                    remote_fingerprint: remote_fingerprint.clone(),
-                });
             }
             KnownHostLineType::Revoked => {
                 if key_matches {