1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use crate::session::SessionEvent;
use anyhow::{anyhow, Context};
use smol::channel::{bounded, Sender};
use ssh2::CheckResult;
use std::io::Write;
use std::path::Path;

#[derive(Debug)]
pub struct HostVerificationEvent {
    pub message: String,
    reply: Sender<bool>,
}

impl HostVerificationEvent {
    pub async fn answer(self, trust_host: bool) -> anyhow::Result<()> {
        Ok(self.reply.send(trust_host).await?)
    }
    pub fn try_answer(self, trust_host: bool) -> anyhow::Result<()> {
        Ok(self.reply.try_send(trust_host)?)
    }
}

impl crate::session::SessionInner {
    pub fn host_verification(
        &mut self,
        sess: &ssh2::Session,
        remote_host_name: &str,
        port: u16,
        remote_address: &str,
    ) -> anyhow::Result<()> {
        let mut known_hosts = sess.known_hosts().context("preparing known hosts")?;

        let known_hosts_files = self
            .config
            .get("userknownhostsfile")
            .unwrap()
            .split_whitespace()
            .map(|s| s.to_string());

        for file in known_hosts_files {
            let file = Path::new(&file);

            if !file.exists() {
                continue;
            }

            known_hosts
                .read_file(&file, ssh2::KnownHostFileKind::OpenSSH)
                .with_context(|| format!("reading known_hosts file {}", file.display()))?;

            let (key, key_type) = sess
                .host_key()
                .ok_or_else(|| anyhow!("failed to get ssh host key"))?;

            let fingerprint = sess
                .host_key_hash(ssh2::HashType::Sha256)
                .map(|fingerprint| {
                    format!(
                        "SHA256:{}",
                        base64::encode_config(
                            fingerprint,
                            base64::Config::new(base64::CharacterSet::Standard, false)
                        )
                    )
                })
                .or_else(|| {
                    // Querying for the Sha256 can fail if for example we were linked
                    // against libssh < 1.9, so let's fall back to Sha1 in that case.
                    sess.host_key_hash(ssh2::HashType::Sha1).map(|fingerprint| {
                        let mut res = vec![];
                        write!(&mut res, "SHA1").ok();
                        for b in fingerprint {
                            write!(&mut res, ":{:02x}", *b).ok();
                        }
                        String::from_utf8(res).unwrap()
                    })
                })
                .ok_or_else(|| anyhow!("failed to get host fingerprint"))?;

            match known_hosts.check_port(&remote_host_name, port, key) {
                CheckResult::Match => {}
                CheckResult::NotFound => {
                    let (reply, confirm) = bounded(1);
                    self.tx_event
                        .try_send(SessionEvent::HostVerify(HostVerificationEvent {
                            message: format!(
                                "SSH host {} is not yet trusted.\n\
                                {:?} Fingerprint: {}.\n\
                                Trust and continue connecting?",
                                remote_address, key_type, fingerprint
                            ),
                            reply,
                        }))
                        .context("sending HostVerify request to user")?;

                    let trusted = smol::block_on(confirm.recv())
                        .context("waiting for host verification confirmation from user")?;

                    if !trusted {
                        anyhow::bail!("user declined to trust host");
                    }

                    let host_and_port = if port != 22 {
                        format!("[{}]:{}", remote_host_name, port)
                    } else {
                        remote_host_name.to_string()
                    };

                    known_hosts
                        .add(&host_and_port, key, &remote_address, key_type.into())
                        .context("adding known_hosts entry in memory")?;

                    known_hosts
                        .write_file(&file, ssh2::KnownHostFileKind::OpenSSH)
                        .with_context(|| format!("writing known_hosts file {}", file.display()))?;
                }
                CheckResult::Mismatch => {
                    anyhow::bail!(
                        "host key mismatch for ssh server {}.\n\
                         Got fingerprint {} instead of expected value from known_hosts\n\
                         file {}.\n\
                         Refusing to connect.",
                        remote_address,
                        fingerprint,
                        file.display()
                    );
                }
                CheckResult::Failure => {
                    anyhow::bail!("failed to check the known hosts");
                }
            }
        }

        Ok(())
    }
}