wezterm_ssh/
host.rs

1use crate::session::SessionEvent;
2use anyhow::Context;
3use smol::channel::{bounded, Sender};
4
5#[derive(Debug)]
6pub struct HostVerificationEvent {
7    pub message: String,
8    pub(crate) reply: Sender<bool>,
9}
10
11impl HostVerificationEvent {
12    pub async fn answer(self, trust_host: bool) -> anyhow::Result<()> {
13        Ok(self.reply.send(trust_host).await?)
14    }
15    pub fn try_answer(self, trust_host: bool) -> anyhow::Result<()> {
16        Ok(self.reply.try_send(trust_host)?)
17    }
18}
19
20impl crate::sessioninner::SessionInner {
21    #[cfg(feature = "libssh-rs")]
22    pub fn host_verification_libssh(
23        &mut self,
24        sess: &libssh_rs::Session,
25        hostname: &str,
26        port: u16,
27    ) -> anyhow::Result<()> {
28        let key = sess
29            .get_server_public_key()?
30            .get_public_key_hash_hexa(libssh_rs::PublicKeyHashType::Sha256)?;
31
32        match sess.is_known_server()? {
33            libssh_rs::KnownHosts::Ok => Ok(()),
34            libssh_rs::KnownHosts::NotFound | libssh_rs::KnownHosts::Unknown => {
35                let (reply, confirm) = bounded(1);
36                self.tx_event
37                    .try_send(SessionEvent::HostVerify(HostVerificationEvent {
38                        message: format!(
39                            "SSH host {}:{} is not yet trusted.\n\
40                                    Fingerprint: {}.\n\
41                                    Trust and continue connecting?",
42                            hostname, port, key
43                        ),
44                        reply,
45                    }))
46                    .context("sending HostVerify request to user")?;
47
48                let trusted = smol::block_on(confirm.recv())
49                    .context("waiting for host verification confirmation from user")?;
50
51                if !trusted {
52                    anyhow::bail!("user declined to trust host");
53                }
54
55                Ok(sess.update_known_hosts_file()?)
56            }
57            libssh_rs::KnownHosts::Changed => {
58                anyhow::bail!(
59                    "host key mismatch for ssh server {}:{}.\n\
60                         Got fingerprint {} instead of expected value from known_hosts\n\
61                         file.\n\
62                         Refusing to connect.",
63                    hostname,
64                    port,
65                    key,
66                );
67            }
68            libssh_rs::KnownHosts::Other => {
69                anyhow::bail!(
70                    "The host key for this server was not found, but another\n\
71            type of key exists. An attacker might change the default\n\
72            server key to confuse your client into thinking the key\n\
73            does not exist"
74                );
75            }
76        }
77    }
78
79    #[cfg(feature = "ssh2")]
80    pub fn host_verification(
81        &mut self,
82        sess: &ssh2::Session,
83        remote_host_name: &str,
84        port: u16,
85        remote_address: &str,
86    ) -> anyhow::Result<()> {
87        use anyhow::anyhow;
88        use std::io::Write;
89        use std::path::Path;
90
91        let mut known_hosts = sess.known_hosts().context("preparing known hosts")?;
92
93        let known_hosts_files = self
94            .config
95            .get("userknownhostsfile")
96            .unwrap()
97            .split_whitespace()
98            .map(|s| s.to_string());
99
100        for file in known_hosts_files {
101            let file = Path::new(&file);
102
103            if !file.exists() {
104                continue;
105            }
106
107            known_hosts
108                .read_file(&file, ssh2::KnownHostFileKind::OpenSSH)
109                .with_context(|| format!("reading known_hosts file {}", file.display()))?;
110
111            let (key, key_type) = sess
112                .host_key()
113                .ok_or_else(|| anyhow!("failed to get ssh host key"))?;
114
115            let fingerprint = sess
116                .host_key_hash(ssh2::HashType::Sha256)
117                .map(|fingerprint| {
118                    format!(
119                        "SHA256:{}",
120                        base64::encode_config(
121                            fingerprint,
122                            base64::Config::new(base64::CharacterSet::Standard, false)
123                        )
124                    )
125                })
126                .or_else(|| {
127                    // Querying for the Sha256 can fail if for example we were linked
128                    // against libssh < 1.9, so let's fall back to Sha1 in that case.
129                    sess.host_key_hash(ssh2::HashType::Sha1).map(|fingerprint| {
130                        let mut res = vec![];
131                        write!(&mut res, "SHA1").ok();
132                        for b in fingerprint {
133                            write!(&mut res, ":{:02x}", *b).ok();
134                        }
135                        String::from_utf8(res).unwrap()
136                    })
137                })
138                .ok_or_else(|| anyhow!("failed to get host fingerprint"))?;
139
140            match known_hosts.check_port(&remote_host_name, port, key) {
141                ssh2::CheckResult::Match => {}
142                ssh2::CheckResult::NotFound => {
143                    let (reply, confirm) = bounded(1);
144                    self.tx_event
145                        .try_send(SessionEvent::HostVerify(HostVerificationEvent {
146                            message: format!(
147                                "SSH host {} is not yet trusted.\n\
148                                {:?} Fingerprint: {}.\n\
149                                Trust and continue connecting?",
150                                remote_address, key_type, fingerprint
151                            ),
152                            reply,
153                        }))
154                        .context("sending HostVerify request to user")?;
155
156                    let trusted = smol::block_on(confirm.recv())
157                        .context("waiting for host verification confirmation from user")?;
158
159                    if !trusted {
160                        anyhow::bail!("user declined to trust host");
161                    }
162
163                    let host_and_port = if port != 22 {
164                        format!("[{}]:{}", remote_host_name, port)
165                    } else {
166                        remote_host_name.to_string()
167                    };
168
169                    known_hosts
170                        .add(&host_and_port, key, &remote_address, key_type.into())
171                        .context("adding known_hosts entry in memory")?;
172
173                    known_hosts
174                        .write_file(&file, ssh2::KnownHostFileKind::OpenSSH)
175                        .with_context(|| format!("writing known_hosts file {}", file.display()))?;
176                }
177                ssh2::CheckResult::Mismatch => {
178                    anyhow::bail!(
179                        "host key mismatch for ssh server {}.\n\
180                         Got fingerprint {} instead of expected value from known_hosts\n\
181                         file {}.\n\
182                         Refusing to connect.",
183                        remote_address,
184                        fingerprint,
185                        file.display()
186                    );
187                }
188                ssh2::CheckResult::Failure => {
189                    anyhow::bail!("failed to check the known hosts");
190                }
191            }
192        }
193
194        Ok(())
195    }
196}