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 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}