Skip to content

Commit

Permalink
Add known_hosts handling
Browse files Browse the repository at this point in the history
  • Loading branch information
mkj committed Mar 28, 2023
1 parent e1049a9 commit c97ff13
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 10 deletions.
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ debug = 1
[dependencies]
sunset-sshwire-derive = { version = "0.1", path = "sshwire-derive" }

snafu = { version = "0.7", default-features = false, features = ["rust_1_46"] }
snafu = { version = "0.7", default-features = false, features = ["rust_1_61"] }
# TODO: check that log macro calls disappear in no_std builds
log = { version = "0.4" }
heapless = "0.7.10"
Expand Down Expand Up @@ -60,7 +60,7 @@ embedded-io = { version = "0.4", optional = true }
pretty-hex = { version = "0.3", default-features = false }

[features]
std = ["snafu/std", "snafu/backtraces"]
std = ["snafu/std", "snafu/backtraces", "salty/alloc" ]
# allows conversion to/from OpenSSH key formats
openssh-key = ["dep:ssh-key"]
# implements embedded_io::Error for sunset::Error
Expand All @@ -72,7 +72,6 @@ snafu = { version = "0.7", default-features = true }
anyhow = { version = "1.0" }
pretty-hex = "0.3"
simplelog = { version = "0.12", features = ["test"] }
proptest = "1.0"

[patch.crates-io]
# needed for Default WakerRegistration, https://github.com/embassy-rs/embassy/commit/14a2d1524080593f7795fe14950a3f0ee6e2b409
Expand Down
3 changes: 3 additions & 0 deletions async/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ sunset-embassy = { path = "../embassy" }
log = { version = "0.4", features = ["release_max_level_info"] }
rpassword = "7.2"
argh = "0.1"
snafu = { version = "0.7", default-features = false, features = ["rust_1_61"] }

ssh-key = { version = "0.5", default-features = false, features = [ "std"] }

embassy-sync = { version = "0.1.0" }
embassy-futures = { version = "0.1.0" }
Expand Down
2 changes: 2 additions & 0 deletions async/examples/sshclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ async fn run(args: Args) -> Result<()> {
let ssh_task = spawn_local(async move {
let mut app = sunset_async::CmdlineClient::new(
args.username.as_ref().unwrap(),
&args.host,
args.port,
cmd,
wantpty,
);
Expand Down
26 changes: 21 additions & 5 deletions async/src/cmdline_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub struct CmdlineClient {
// to be passed to hooks
authkeys: VecDeque<SignKey>,
username: String,
host: String,
port: u16,

notify: Channel<SunsetRawMutex, Msg, 1>,
}
Expand All @@ -61,6 +63,8 @@ pub struct CmdlineRunner<'a> {
pub struct CmdlineHooks<'a> {
authkeys: VecDeque<SignKey>,
username: &'a str,
host: &'a str,
port: u16,

notify: Sender<'a, SunsetRawMutex, Msg, 1>,
}
Expand Down Expand Up @@ -257,7 +261,8 @@ impl<'a> CmdlineRunner<'a> {
}

impl CmdlineClient {
pub fn new(username: impl AsRef<str>, cmd: Option<impl AsRef<str>>, want_pty: bool) -> Self {
pub fn new(username: impl AsRef<str>, host: impl AsRef<str>, port: u16,
cmd: Option<impl AsRef<str>>, want_pty: bool) -> Self {
Self {

// TODO: shorthand for this?
Expand All @@ -267,13 +272,15 @@ impl CmdlineClient {
notify: Channel::new(),

username: username.as_ref().into(),
host: host.as_ref().into(),
port,
authkeys: Default::default(),
}
}

pub fn split(&mut self) -> (CmdlineHooks, CmdlineRunner) {
let ak = core::mem::replace(&mut self.authkeys, Default::default());
let hooks = CmdlineHooks::new(&self.username, ak, self.notify.sender());
let hooks = CmdlineHooks::new(&self.username, &self.host, self.port, ak, self.notify.sender());
let runner = CmdlineRunner::new(&self.cmd, self.want_pty, self.notify.receiver());
(hooks, runner)
}
Expand All @@ -284,10 +291,12 @@ impl CmdlineClient {
}

impl<'a> CmdlineHooks<'a> {
fn new(username: &'a str, authkeys: VecDeque<SignKey>, notify: Sender<'a, SunsetRawMutex, Msg, 1>) -> Self {
fn new(username: &'a str, host: &'a str, port: u16, authkeys: VecDeque<SignKey>, notify: Sender<'a, SunsetRawMutex, Msg, 1>) -> Self {
Self {
authkeys,
username,
host,
port,
notify,
}
}
Expand All @@ -306,8 +315,15 @@ impl<'a> sunset::CliBehaviour for CmdlineHooks<'a> {
}

fn valid_hostkey(&mut self, key: &sunset::PubKey) -> BhResult<bool> {
trace!("valid_hostkey for {key:?}");
Ok(true)
trace!("checking hostkey for {key:?}");

match known_hosts::check_known_hosts(self.host, self.port, key) {
Ok(()) => Ok(true),
Err(e) => {
debug!("Error for hostkey: {e:?}");
Ok(false)
}
}
}

fn next_authkey(&mut self) -> BhResult<Option<sunset::SignKey>> {
Expand Down
175 changes: 175 additions & 0 deletions async/src/known_hosts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#[allow(unused_imports)]
use log::{debug, error, info, log, trace, warn};

use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use std::io::{BufRead, Write};
use std::io;

use snafu::prelude::*;

use crate::*;
use sunset::packets::PubKey;

type OpenSSHKey = ssh_key::PublicKey;

#[derive(Snafu, Debug)]
#[snafu(context(suffix(false)))]
pub enum KnownHostsError {
/// Host Key Mismatch
Mismatch { path: PathBuf, line: usize, existing: OpenSSHKey },

/// New Host Key
NewHost { new_key: OpenSSHKey },

/// User didn't accept new key
NotAccepted,

/// Failure
Failure {
// The
// .map_err(|e| Box::new(e) as _).context(Failure)?;
// syntax is ugly, perhaps there's a better way
source: Box<dyn std::error::Error>
},

#[snafu(display("{msg}"))]
Other { msg: String },
}

const USER_KNOWN_HOSTS: &str = &".ssh/known_hosts";

fn user_known_hosts() -> Result<PathBuf, KnownHostsError> {
// home_dir() works fine on linux.
#[allow(deprecated)]
let p = std::env::home_dir().ok_or_else(|| KnownHostsError::Other {
msg: "Failed getting home directory".into(),
})?;
Ok(p.join(USER_KNOWN_HOSTS))
}

pub fn check_known_hosts(
host: &str,
port: u16,
key: &PubKey,
) -> Result<(), KnownHostsError> {
let p = user_known_hosts()?;
check_known_hosts_file(host, port, key, &p)
}

/// Returns a `(host, key)` entry from a known_hosts line, or `None` if not matching
fn line_entry(line: &str) -> Option<(String, String)> {
line.split_once(' ').map(|(h, k)| (h.into(), k.into()))
}

/// Returns the host string. Non-22 ports are appended.
fn host_part(host: &str, port: u16) -> String {
let mut host = host.to_lowercase();
if port != sunset::sshnames::SSH_PORT {
host = format!("[{host}]:{port}");
}
host
}

pub fn check_known_hosts_file(
host: &str,
port: u16,
key: &PubKey,
p: &Path,
) -> Result<(), KnownHostsError> {
let f = File::open(p)
.map_err(|e| Box::new(e) as _).context(Failure)?;
let f = io::BufReader::new(f);

let match_host = host_part(host, port);

let pubk: OpenSSHKey = key.try_into()
.map_err(|e| Box::new(e) as _).context(Failure)?;

for (line, (lh, lk)) in f.lines().enumerate()
.filter_map(|(num, l)| {
if let Ok(l) = l {
line_entry(&l).map(|entry| (num, entry))
} else {
None
}
}) {
let line = line + 1;

if lh != match_host {
continue;
}

let known_key = OpenSSHKey::from_openssh(&lk).map_err(|_| {
KnownHostsError::Other { msg: format!("Bad key format {}:{}", p.display(), line) }
})?;

if pubk.algorithm() != known_key.algorithm() {
debug!("Line {line}, Ignoring other-format existing key {known_key:?}")
} else {
if pubk.key_data() == known_key.key_data() {
debug!("Line {line}, found matching key");
return Ok(())
} else {
let fp = known_key.fingerprint(Default::default());
println!("\nHost key mismatch for {match_host} in ~/.ssh/known_hosts line {line}\n\
Existing key has fingerprint {fp}\n");
return Err(KnownHostsError::Mismatch { path: p.to_path_buf(), line, existing: known_key });
}
}
}

// no match, maybe add it
ask_to_confirm(host, port, key, p)
}

fn ask_to_confirm(
host: &str,
port: u16,
key: &PubKey,
p: &Path,
) -> Result<(), KnownHostsError> {

let k: OpenSSHKey = key.try_into().map_err(|e| Box::new(e) as _).context(Failure)?;
let fp = k.fingerprint(Default::default());
let h = host_part(host, port);
println!("\nHost {h} is not in ~/.ssh/known_hosts\nFingerprint {fp}\nDo you want to continue connecting? (y/n)");

let mut resp = String::new();
io::stdin().read_line(&mut resp)
.map_err(|e| Box::new(e) as _).context(Failure)?;

resp.make_ascii_lowercase();
if resp.starts_with('y') {
add_key(host, port, key, p)
} else {
Err(KnownHostsError::NotAccepted)
}
}

fn add_key(
host: &str,
port: u16,
key: &PubKey,
p: &Path,
) -> Result<(), KnownHostsError> {

let k: OpenSSHKey = key.try_into()
.map_err(|e| Box::new(e) as _).context(Failure)?;
// encode it
let k = k.to_openssh()
.map_err(|e| Box::new(e) as _).context(Failure)?;

let h = host_part(host, port);

let entry = format!("{h} {k}\n");

let mut f = std::fs::OpenOptions::new().append(true).open(p)
.map_err(|e| Box::new(e) as _).context(Failure)?;

f.write_all(entry.as_bytes())
.map_err(|e| Box::new(e) as _).context(Failure)?;

Ok(())
}

1 change: 1 addition & 0 deletions async/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

mod pty;
mod cmdline_client;
mod known_hosts;

#[cfg(unix)]
mod fdio;
Expand Down
4 changes: 2 additions & 2 deletions src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ pub struct Pty {
impl TryFrom<&packets::PtyReq<'_>> for Pty {
type Error = Error;
fn try_from(p: &packets::PtyReq) -> Result<Self, Self::Error> {
error!("TODO implement pty modes");
debug!("TODO implement pty modes");
let term = p.term.as_ascii()?.try_into().map_err(|_| Error::BadString)?;
Ok(Pty {
term,
Expand Down Expand Up @@ -513,7 +513,7 @@ impl Req {
let ty = match &self.details {
ReqDetails::Shell => ChannelReqType::Shell,
ReqDetails::Pty(pty) => {
error!("TODO implement pty modes");
debug!("TODO implement pty modes");
ChannelReqType::Pty(packets::PtyReq {
term: TextString(pty.term.as_bytes()),
cols: pty.cols,
Expand Down
13 changes: 13 additions & 0 deletions src/packets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,19 @@ impl PubKey<'_> {
}
}

#[cfg(feature = "openssh-key")]
impl TryFrom<&PubKey<'_>> for ssh_key::PublicKey {
type Error = Error;
fn try_from(k: &PubKey<'_>) -> Result<Self> {
match k {
PubKey::Ed25519(e) => {
let eb: &[u8; 32] = e.key.0.try_into().map_err(|_| Error::BadKey)?;
Ok(ssh_key::public::Ed25519PublicKey(*eb).into())
}
_ => Err(Error::msg("Unsupported OpenSSH key"))
}
}
}

#[derive(Debug, Clone, PartialEq, SSHEncode, SSHDecode)]
pub struct Ed25519PubKey<'a> {
Expand Down
2 changes: 2 additions & 0 deletions src/sshnames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,5 @@ pub enum ChanFail {
SSH_OPEN_RESOURCE_SHORTAGE = 4,
}

pub const SSH_PORT: u16 = 22;

2 changes: 2 additions & 0 deletions src/traffic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl<'a> TrafIn<'a> {
}

pub fn is_input_ready(&self) -> bool {
info!("is_input_ready {:?}", self.state);
match self.state {
| RxState::Idle
| RxState::ReadInitial { .. }
Expand All @@ -113,6 +114,7 @@ impl<'a> TrafIn<'a> {
buf: &[u8],
) -> Result<usize, Error> {
let mut inlen = 0;
info!("assert");
debug_assert!(self.is_input_ready());
if remote_version.version().is_none() && matches!(self.state, RxState::Idle) {
// Handle initial version string
Expand Down

0 comments on commit c97ff13

Please sign in to comment.