signstar_request_signature/ssh/
known_hosts.rs

1//! SSH `known_hosts` format utilities.
2
3use log::{info, warn};
4use russh::keys::{
5    PublicKey,
6    ssh_key::known_hosts::{Entry, HostPatterns},
7};
8
9/// Checks whether a set of server details can be found in SSH `known_hosts` data.
10///
11/// Based on a `host` and its `port`, this function evaluates whether a supplied `key` is part of a
12/// list of `entries` in the SSH known_hosts file format. Returns `true`, if the combination of
13/// `key`, `host` and `port` matches an entry in the list of `entries` and that entry is not a CA
14/// key or a revoked key. Returns `false` in all other cases.
15pub(crate) fn is_server_known<'a>(
16    entries: impl Iterator<Item = &'a Entry>,
17    host: &str,
18    port: u16,
19    key: &PublicKey,
20) -> bool {
21    for entry in entries {
22        if match entry.host_patterns() {
23            HostPatterns::Patterns(items) => items
24                .iter()
25                .any(|item| item == host || item == &format!("[{host}]:{port}")),
26            HostPatterns::HashedName { salt, hash } => {
27                use hmac::Mac;
28                if let Ok(mut mac) = hmac::Hmac::<sha1::Sha1>::new_from_slice(salt) {
29                    mac.update(host.as_bytes());
30                    mac.finalize().into_bytes()[..] == hash[..]
31                } else {
32                    warn!(
33                        "the salt {salt:?} was not of correct size so the entry for host {host} does not match"
34                    );
35                    false
36                }
37            }
38        } && entry.public_key() == key
39        {
40            return if let Some(marker) = entry.marker() {
41                info!("Found marker {marker} for host {host} but it is not supported.");
42                false
43            } else {
44                true
45            };
46        }
47    }
48    false
49}
50
51#[cfg(test)]
52mod tests {
53    use testresult::TestResult;
54
55    use super::*;
56
57    #[test]
58    fn test_single_entry() -> TestResult {
59        let entry: Entry = "gitlab.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy".parse()?;
60
61        assert!(
62            is_server_known(
63                [entry].iter(),
64                "gitlab.archlinux.org",
65                22,
66                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
67                    .parse()?
68            ),
69            "server should be known since there's one matching entry"
70        );
71
72        Ok(())
73    }
74
75    #[test]
76    fn test_single_entry_with_port() -> TestResult {
77        let entry: Entry = "[gitlab.archlinux.org]:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy".parse()?;
78
79        assert!(
80            is_server_known(
81                [entry].iter(),
82                "gitlab.archlinux.org",
83                22,
84                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
85                    .parse()?
86            ),
87            "server should be known since there's one matching entry"
88        );
89
90        Ok(())
91    }
92
93    #[test]
94    fn test_single_revoked_entry() -> TestResult {
95        let entry: Entry = "@revoked gitlab.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy".parse()?;
96
97        assert!(
98            !is_server_known(
99                [entry].iter(),
100                "gitlab.archlinux.org",
101                22,
102                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
103                    .parse()?
104            ),
105            "server should not be known since there's one matching entry but it is revoked"
106        );
107
108        Ok(())
109    }
110
111    #[test]
112    fn test_single_cert_authority_entry() -> TestResult {
113        let entry: Entry = "@cert-authority gitlab.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy".parse()?;
114
115        assert!(
116            !is_server_known(
117                [entry].iter(),
118                "gitlab.archlinux.org",
119                22,
120                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
121                    .parse()?
122            ),
123            "server should not be known since certification authorities are not supported"
124        );
125
126        Ok(())
127    }
128
129    #[test]
130    fn test_not_matching_entry() -> TestResult {
131        let entry: Entry = "gitlab.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K89dpNtltOK6vy".parse()?;
132
133        assert!(
134            !is_server_known(
135                [entry].iter(),
136                "gitlab.archlinux.org",
137                22,
138                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
139                    .parse()?
140            ),
141            "server should not be known since there are no matching entries"
142        );
143
144        Ok(())
145    }
146
147    #[test]
148    fn test_not_matching_port_entry() -> TestResult {
149        let entry: Entry = "[gitlab.archlinux.org]:23 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K89dpNtltOK6vy".parse()?;
150
151        assert!(
152            !is_server_known(
153                [entry].iter(),
154                "gitlab.archlinux.org",
155                22,
156                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICjT2SuA0k/xc5Cbyp+eBY5uN3bRL2K7GdpNtltOK6vy"
157                    .parse()?
158            ),
159            "server should not be known since there are no matching entries"
160        );
161
162        Ok(())
163    }
164
165    #[test]
166    fn test_hashed_entry() -> TestResult {
167        // entry generated using `ssh-keygen -H -F github.com`
168        let entry: Entry = "|1|b8LfkX9Y09oxr9MMnQyfC9CtciI=|MnTpZgaon9ON5+hrylyRlq/li3Q= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl".parse()?;
169        assert!(
170            is_server_known(
171                [entry].iter(),
172                "github.com",
173                22,
174                &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"
175                    .parse()?
176            ),
177            "server should be known since there's one matching entry"
178        );
179
180        Ok(())
181    }
182}