russh/keys/
known_hosts.rs

1use std::borrow::Cow;
2use std::fs::{File, OpenOptions};
3use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
4use std::path::{Path, PathBuf};
5
6use data_encoding::BASE64_MIME;
7use hmac::{Hmac, Mac};
8use log::debug;
9use sha1::Sha1;
10
11use crate::keys::Error;
12
13/// Check whether the host is known, from its standard location.
14pub fn check_known_hosts(
15    host: &str,
16    port: u16,
17    pubkey: &ssh_key::PublicKey,
18) -> Result<bool, Error> {
19    check_known_hosts_path(host, port, pubkey, known_hosts_path()?)
20}
21
22/// Check that a server key matches the one recorded in file `path`.
23pub fn check_known_hosts_path<P: AsRef<Path>>(
24    host: &str,
25    port: u16,
26    pubkey: &ssh_key::PublicKey,
27    path: P,
28) -> Result<bool, Error> {
29    let check = known_host_keys_path(host, port, path)?
30        .into_iter()
31        .map(|(line, recorded)| {
32            match (
33                pubkey.algorithm() == recorded.algorithm(),
34                *pubkey == recorded,
35            ) {
36                (true, true) => Ok(true),
37                (true, false) => Err(Error::KeyChanged { line }),
38                _ => Ok(false),
39            }
40        })
41        // If any Err was returned, we stop here
42        .collect::<Result<Vec<bool>, Error>>()?
43        .into_iter()
44        // Now we check the results for a match
45        .any(|x| x);
46
47    Ok(check)
48}
49
50#[cfg(target_os = "windows")]
51fn known_hosts_path() -> Result<PathBuf, Error> {
52    if let Some(home_dir) = home::home_dir() {
53        Ok(home_dir.join("ssh").join("known_hosts"))
54    } else {
55        Err(Error::NoHomeDir)
56    }
57}
58
59#[cfg(not(target_os = "windows"))]
60fn known_hosts_path() -> Result<PathBuf, Error> {
61    if let Some(home_dir) = home::home_dir() {
62        Ok(home_dir.join(".ssh").join("known_hosts"))
63    } else {
64        Err(Error::NoHomeDir)
65    }
66}
67
68/// Get the server key that matches the one recorded in the user's known_hosts file.
69pub fn known_host_keys(host: &str, port: u16) -> Result<Vec<(usize, ssh_key::PublicKey)>, Error> {
70    known_host_keys_path(host, port, known_hosts_path()?)
71}
72
73/// Get the server key that matches the one recorded in `path`.
74pub fn known_host_keys_path<P: AsRef<Path>>(
75    host: &str,
76    port: u16,
77    path: P,
78) -> Result<Vec<(usize, ssh_key::PublicKey)>, Error> {
79    use crate::keys::parse_public_key_base64;
80
81    let mut f = if let Ok(f) = File::open(path) {
82        BufReader::new(f)
83    } else {
84        return Ok(vec![]);
85    };
86    let mut buffer = String::new();
87
88    let host_port = if port == 22 {
89        Cow::Borrowed(host)
90    } else {
91        Cow::Owned(format!("[{}]:{}", host, port))
92    };
93    debug!("host_port = {:?}", host_port);
94    let mut line = 1;
95    let mut matches = vec![];
96    while f.read_line(&mut buffer)? > 0 {
97        {
98            if buffer.as_bytes().first() == Some(&b'#') {
99                buffer.clear();
100                continue;
101            }
102            debug!("line = {:?}", buffer);
103            let mut s = buffer.split(' ');
104            let hosts = s.next();
105            let _ = s.next();
106            let key = s.next();
107            if let (Some(h), Some(k)) = (hosts, key) {
108                debug!("{:?} {:?}", h, k);
109                if match_hostname(&host_port, h) {
110                    matches.push((line, parse_public_key_base64(k)?));
111                }
112            }
113        }
114        buffer.clear();
115        line += 1;
116    }
117    Ok(matches)
118}
119
120fn match_hostname(host: &str, pattern: &str) -> bool {
121    for entry in pattern.split(',') {
122        if entry.starts_with("|1|") {
123            let mut parts = entry.split('|').skip(2);
124            let Some(Ok(salt)) = parts.next().map(|p| BASE64_MIME.decode(p.as_bytes())) else {
125                continue;
126            };
127            let Some(Ok(hash)) = parts.next().map(|p| BASE64_MIME.decode(p.as_bytes())) else {
128                continue;
129            };
130            if let Ok(hmac) = Hmac::<Sha1>::new_from_slice(&salt) {
131                if hmac.chain_update(host).verify_slice(&hash).is_ok() {
132                    return true;
133                }
134            }
135        } else if host == entry {
136            return true;
137        }
138    }
139    false
140}
141
142/// Record a host's public key into the user's known_hosts file.
143pub fn learn_known_hosts(host: &str, port: u16, pubkey: &ssh_key::PublicKey) -> Result<(), Error> {
144    learn_known_hosts_path(host, port, pubkey, known_hosts_path()?)
145}
146
147/// Record a host's public key into a nonstandard location.
148pub fn learn_known_hosts_path<P: AsRef<Path>>(
149    host: &str,
150    port: u16,
151    pubkey: &ssh_key::PublicKey,
152    path: P,
153) -> Result<(), Error> {
154    if let Some(parent) = path.as_ref().parent() {
155        std::fs::create_dir_all(parent)?
156    }
157    let mut file = OpenOptions::new()
158        .read(true)
159        .append(true)
160        .create(true)
161        .open(path)?;
162
163    // Test whether the known_hosts file ends with a \n
164    let mut buf = [0; 1];
165    let mut ends_in_newline = false;
166    if file.seek(SeekFrom::End(-1)).is_ok() {
167        file.read_exact(&mut buf)?;
168        ends_in_newline = buf[0] == b'\n';
169    }
170
171    // Write the key.
172    file.seek(SeekFrom::End(0))?;
173    let mut file = std::io::BufWriter::new(file);
174    if !ends_in_newline {
175        file.write_all(b"\n")?;
176    }
177    if port != 22 {
178        write!(file, "[{}]:{} ", host, port)?
179    } else {
180        write!(file, "{} ", host)?
181    }
182    file.write_all(pubkey.to_openssh()?.as_bytes())?;
183    file.write_all(b"\n")?;
184    Ok(())
185}
186
187#[cfg(test)]
188mod test {
189    use std::fs::File;
190
191    use super::*;
192    use crate::keys::parse_public_key_base64;
193
194    #[test]
195    fn test_check_known_hosts() {
196        env_logger::try_init().unwrap_or(());
197        let dir = tempfile::tempdir().unwrap();
198        let path = dir.path().join("known_hosts");
199        {
200            let mut f = File::create(&path).unwrap();
201            f.write_all(b"[localhost]:13265 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ\n").unwrap();
202            f.write_all(b"#pijul.org,37.120.161.53 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G2sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X\n").unwrap();
203            f.write_all(b"pijul.org,37.120.161.53 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G1sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X\n").unwrap();
204            f.write_all(b"|1|O33ESRMWPVkMYIwJ1Uw+n877jTo=|nuuC5vEqXlEZ/8BXQR7m619W6Ak= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILIG2T/B0l0gaqj3puu510tu9N1OkQ4znY3LYuEm5zCF\n").unwrap();
205        }
206
207        // Valid key, non-standard port.
208        let host = "localhost";
209        let port = 13265;
210        let hostkey = parse_public_key_base64(
211            "AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ",
212        )
213        .unwrap();
214        assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap());
215
216        // Valid key, hashed.
217        let host = "example.com";
218        let port = 22;
219        let hostkey = parse_public_key_base64(
220            "AAAAC3NzaC1lZDI1NTE5AAAAILIG2T/B0l0gaqj3puu510tu9N1OkQ4znY3LYuEm5zCF",
221        )
222        .unwrap();
223        assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap());
224
225        // Valid key, several hosts, port 22
226        let host = "pijul.org";
227        let port = 22;
228        let hostkey = parse_public_key_base64(
229            "AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G1sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X",
230        )
231        .unwrap();
232        assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap());
233
234        // Now with the key in a comment above, check that it's not recognized
235        let host = "pijul.org";
236        let port = 22;
237        let hostkey = parse_public_key_base64(
238            "AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G2sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X",
239        )
240        .unwrap();
241        assert!(check_known_hosts_path(host, port, &hostkey, &path).is_err());
242    }
243}