diff --git a/Cargo.lock b/Cargo.lock index 9cd3087..1c695ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,20 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brisk" +version = "0.1.0" +source = "git+https://git.pfaff.dev/michael/brisk.git#41172e3adbde6de4727b1117a6e0cf631877cc99" + [[package]] name = "bumpalo" version = "3.13.0" @@ -102,6 +116,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "cache-padded" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" + [[package]] name = "cc" version = "1.0.79" @@ -165,6 +185,35 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.8.1" @@ -195,6 +244,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fast-hex" +version = "0.1.0" +source = "git+https://git.pfaff.dev/michael/fast-hex.git#9daecc56ea29fe2b500cd172759be707fe375943" +dependencies = [ + "brisk", + "cache-padded", +] + [[package]] name = "flume" version = "0.10.14" @@ -221,10 +279,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] -name = "getrandom" -version = "0.2.9" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "js-sys", @@ -286,9 +354,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.145" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc86cde3ff845662b8f4ef6cb50ea0e20c524eb3d29ae048287e06a1b3fa6a81" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" [[package]] name = "libloading" @@ -459,6 +527,29 @@ dependencies = [ "libc", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "paste" version = "1.0.12" @@ -527,29 +618,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quic-shell" -version = "0.1.0" -dependencies = [ - "anyhow", - "bytes", - "flume", - "libc", - "nix", - "pam-client", - "pin-project-lite", - "quinn", - "rcgen", - "rmp-serde", - "rpassword", - "rustls", - "serde", - "tokio", - "tracing", - "tracing-subscriber", - "triggered", -] - [[package]] name = "quinn" version = "0.10.1" @@ -598,6 +666,34 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "quinoa" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.2", + "bytes", + "fast-hex", + "flume", + "libc", + "nix", + "pam-client", + "parking_lot", + "pin-project-lite", + "quinn", + "rcgen", + "rmp-serde", + "rpassword", + "rustls", + "rustls-webpki", + "serde", + "sha2", + "tokio", + "tracing", + "tracing-subscriber", + "triggered", +] + [[package]] name = "quote" version = "1.0.28" @@ -650,14 +746,23 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.8.1" +name = "redox_syscall" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -677,9 +782,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "ring" @@ -861,6 +966,17 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1146,6 +1262,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce148eae0d1a376c1b94ae651fc3261d9cb8294788b962b7382066376503a2d1" +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicode-ident" version = "1.0.9" @@ -1176,6 +1298,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 9ec45f9..ed5c8e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "quic-shell" +name = "quinoa" version = "0.1.0" edition = "2021" @@ -7,18 +7,23 @@ edition = "2021" [dependencies] anyhow = "1.0.71" +base64 = "0.21.2" bytes = "1.4.0" +fast-hex = { git = "https://git.pfaff.dev/michael/fast-hex.git", version = "0.1.0" } flume = "0.10.14" libc = "0.2.145" nix = "0.26.2" pam-client = { version = "0.5.0", path = "../../../../../Users/michael/b/rust-pam-client", default-features = false, features = ["serde"] } +parking_lot = "0.12.1" pin-project-lite = "0.2.9" quinn = "0.10.1" rcgen = "0.10.0" rmp-serde = "1.1.1" rpassword = "7.2.0" -rustls = { version = "0.21.1", default-features = false } +rustls = { version = "0.21.1", default-features = false, features = ["dangerous_configuration"] } +rustls-webpki = "0.100.1" serde = { version = "1.0.163", features = ["derive"] } +sha2 = "0.10.6" #termion = "2.0.1" tokio = { version = "1.28.2", default-features = false, features = ["rt-multi-thread", "macros", "process", "io-util", "io-std", "time", "fs", "signal"] } tracing = "0.1.37" diff --git a/src/main.rs b/src/main.rs index 07d194a..ff656a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ #[deny(unused_must_use)] - #[macro_use] extern crate anyhow; #[macro_use] @@ -16,15 +15,19 @@ use std::net::SocketAddr; use std::os::fd::FromRawFd; use std::os::unix::process::CommandExt; use std::process::Stdio; +use std::str::FromStr; use std::sync::Arc; use std::task::Poll; use anyhow::{Context, Result}; +use base64::Engine as _; use pam_client::ConversationHandler; -use quinn::{RecvStream, SendStream, ReadExactError}; +use quinn::{ReadExactError, RecvStream, SendStream}; +use rustls::client::ServerCertVerifier; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; use tracing::Instrument; +use webpki::SubjectNameRef; #[derive(Debug, Clone, Serialize, Deserialize)] enum Stream { @@ -67,31 +70,39 @@ async fn run_cmd(mut args: std::env::Args) -> Result<()> { } } -const ALPN_QUIC_SHELL: &str = "quic-shell"; +const ALPN_QUINOA: &str = "quinoa"; struct ServerConfig { shell: String, listen: SocketAddr, } +struct ClientConfig { + known_hosts_file: String, + known_hosts: parking_lot::Mutex>>, +} + async fn run_server() -> Result<()> { let cfg = { - let opt_shell = std::env::var("SHELL") - .context("SHELL not defined")?; + let opt_shell = std::env::var("SHELL").context("SHELL not defined")?; let opt_listen = std::env::var("BIND_ADDR") .unwrap_or_else(|_| "127.0.0.1:8022".to_owned()) .parse()?; - &*Box::leak(Box::new(ServerConfig { - shell: opt_shell, - listen: opt_listen, - })) + &*Box::leak( + ServerConfig { + shell: opt_shell, + listen: opt_listen, + } + .into(), + ) }; let subject_alt_names = vec!["localhost".to_string()]; - - let (cert, key) = if !std::path::Path::new("cert.der").exists() || !std::path::Path::new("key.der").exists() { + let (cert, key) = if !std::path::Path::new("cert.der").exists() + || !std::path::Path::new("key.der").exists() + { let cert = rcgen::generate_simple_self_signed(subject_alt_names)?; let key = rustls::PrivateKey(cert.serialize_private_key_der()); let cert = rustls::Certificate(cert.serialize_der()?); @@ -109,7 +120,8 @@ async fn run_server() -> Result<()> { .with_no_client_auth() .with_single_cert(vec![cert], key) .unwrap(); - server_crypto.alpn_protocols = vec![ALPN_QUIC_SHELL.as_bytes().to_owned()]; + + server_crypto.alpn_protocols = vec![ALPN_QUINOA.as_bytes().to_owned()]; let mut transport = transport_config(); transport.max_concurrent_uni_streams(0_u8.into()); @@ -158,10 +170,11 @@ impl std::fmt::Display for FinishedEarly { } } -impl std::error::Error for FinishedEarly { -} +impl std::error::Error for FinishedEarly {} -async fn read_msg(recv: &mut RecvStream) -> Result> { +async fn read_msg( + recv: &mut RecvStream, +) -> Result> { let mut size = [0u8; 2]; match recv.read_exact(&mut size).await { Ok(()) => {} @@ -171,7 +184,9 @@ async fn read_msg(recv: &mut RecvStream) -> Resu let size = u16::from_le_bytes(size); let mut buf = Vec::with_capacity(size.into()); recv.take(size.into()).read_to_end(&mut buf).await?; - Ok(Ok(rmp_serde::from_slice(&buf).with_context(|| format!("reading a {} byte message", size))?)) + Ok(Ok(rmp_serde::from_slice(&buf).with_context(|| { + format!("reading a {} byte message", size) + })?)) } async fn write_msg(send: &mut SendStream, value: &T) -> Result<()> { @@ -182,9 +197,341 @@ async fn write_msg(send: &mut SendStream, value: &T) -> Res Ok(()) } +struct InformedServerCertVerifier { + cfg: &'static ClientConfig, +} + +impl InformedServerCertVerifier { + fn find_known_host<'a, 'b>( + known_hosts: &'b [KnownHost<'a>], + subject_name: webpki::SubjectNameRef<'_>, + ) -> Option<(usize, &'b KnownHost<'a>)> { + known_hosts + .iter() + .enumerate() + .find(|(_, h)| h.host.as_ref() == subject_name.as_ref()) + } + + fn inform( + &self, + known_hosts: &mut Vec>, + end_entity: &rustls::Certificate, + subject_name: webpki::SubjectNameRef<'_>, + known_as: Option, + ) -> Result<(), rustls::CertificateError> { + use rustls::CertificateError; + + let hash = Hash::new(&end_entity.0); + eprintln!( + "The authenticity of host {:?} can't be established.", + subject_name + ); + eprintln!("Certificate hash is {}", hash); + if let Some(known_as) = known_as.and_then(|i| known_hosts.get(i)) { + eprintln!("Previously known as {}", known_as.hash); + } + eprintln!("This key is not known by any other names (TODO: check)"); + loop { + eprintln!("Are you sure you want to continue connecting (yes/no)?"); + let mut s = String::new(); + std::io::stdin() + .read_line(&mut s) + .map_err(|e| CertificateError::Other(Arc::new(e)))?; + if s.ends_with('\n') { + s.pop(); + } + let yes = if s == "yes" { + true + } else if s == "no" { + false + } else { + continue; + }; + if yes { + let subject_name = match subject_name { + webpki::SubjectNameRef::DnsName(dns_name) => { + let dns_name = Box::leak(dns_name.as_ref().to_owned().into_boxed_slice()); + webpki::SubjectNameRef::DnsName( + webpki::DnsNameRef::try_from_ascii(&*dns_name).unwrap(), + ) + } + webpki::SubjectNameRef::IpAddress(ip_addr) => { + let ip_addr: &'static webpki::IpAddr = + Box::leak(Box::new(ip_addr.to_owned())); + let ip_addr: &'static str = ip_addr.as_ref(); + webpki::SubjectNameRef::IpAddress( + webpki::IpAddrRef::try_from_ascii_str(ip_addr).unwrap(), + ) + } //_ => return Err(CertificateError::NotValidForName.into()), + }; + let known_host = KnownHost { + host: subject_name, + hash, + }; + if known_as.is_some() { + while let Some((i, _)) = Self::find_known_host(&known_hosts, subject_name) { + known_hosts.remove(i); + } + } + known_hosts.push(known_host); + if let Err(e) = write_known_hosts(&self.cfg.known_hosts_file, &known_hosts) { + error!( + "Couldn't persist the new known-host. Continuing anyway.\n{}", + e + ); + } + return Ok(()); + } else { + eprintln!("Understood. Aborting..."); + return Err(CertificateError::NotValidForName.into()); + } + } + } +} + +impl ServerCertVerifier for InformedServerCertVerifier { + fn verify_server_cert( + &self, + end_entity: &rustls::Certificate, + _intermediates: &[rustls::Certificate], + server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: std::time::SystemTime, + ) -> std::result::Result { + use rustls::client::ServerCertVerified; + use rustls::CertificateError; + use rustls::ServerName; + + info!("starting verification"); + + fn pki_error(error: webpki::Error) -> rustls::Error { + use webpki::Error::*; + match error { + BadDer | BadDerTime => CertificateError::BadEncoding.into(), + CertNotValidYet => CertificateError::NotValidYet.into(), + CertExpired | InvalidCertValidity => CertificateError::Expired.into(), + UnknownIssuer => CertificateError::UnknownIssuer.into(), + CertNotValidForName => CertificateError::NotValidForName.into(), + + InvalidSignatureForPublicKey + | UnsupportedSignatureAlgorithm + | UnsupportedSignatureAlgorithmForPublicKey => { + CertificateError::BadSignature.into() + } + _ => CertificateError::Other(Arc::new(error)).into(), + } + } + + let cert = webpki::EndEntityCert::try_from(end_entity.0.as_ref()).map_err(pki_error)?; + + let ip_addr_slot; + let subject_name = match server_name { + ServerName::DnsName(dns_name) => webpki::SubjectNameRef::DnsName( + webpki::DnsNameRef::try_from_ascii_str(dns_name.as_ref()) + .map_err(|_| CertificateError::NotValidForName)?, + ), + ServerName::IpAddress(ip_addr) => { + ip_addr_slot = webpki::IpAddr::from(*ip_addr); + webpki::SubjectNameRef::IpAddress(webpki::IpAddrRef::from(&ip_addr_slot)) + } + _ => return Err(CertificateError::NotValidForName.into()), + }; + + cert.verify_is_valid_for_subject_name(subject_name) + .map_err(pki_error)?; + + let mut known_hosts = self.cfg.known_hosts.lock(); + if let Some((h_index, h)) = Self::find_known_host(&known_hosts, subject_name) { + if let Err(e) = h.hash.verify(&end_entity.0) { + debug!("verification failed: {}", e); + self.inform(&mut known_hosts, end_entity, subject_name, Some(h_index))?; + } else { + eprintln!("Host authenticity verified"); + } + } else { + self.inform(&mut known_hosts, end_entity, subject_name, None)?; + } + + Ok(ServerCertVerified::assertion()) + } +} + +#[derive(Debug, Clone)] +struct KnownHost<'a> { + host: SubjectNameRef<'a>, + hash: Hash, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Hash { + SHA2_512([u8; 64]), + + /// No hashing. Stores the certificate directly. + RAW(Vec), +} + +impl Hash { + pub fn new(data: &[u8]) -> Self { + Self::raw(data) + } + + pub fn sha2_512(data: &[u8]) -> Self { + use sha2::digest::FixedOutput; + use sha2::Digest; + let mut hsr = sha2::Sha512::new(); + hsr.update(data); + Self::SHA2_512(hsr.finalize_fixed().into()) + } + + pub fn raw(data: &[u8]) -> Self { + Self::RAW(data.to_owned()) + } + + pub fn name(&self) -> &'static str { + match self { + Hash::SHA2_512(_) => "SHA2_512", + Hash::RAW(_) => "RAW", + } + } + + pub fn bytes(&self) -> &[u8] { + match self { + Hash::SHA2_512(b) => b, + Hash::RAW(b) => b, + } + } + + pub fn verify(&self, data: &[u8]) -> Result<()> { + let matches = match self { + Hash::SHA2_512(_) => { + let hash = Self::sha2_512(data); + hash == *self + } + Hash::RAW(b) => data == b, + }; + if matches { + Ok(()) + } else { + Err(anyhow!( + "Certificate hash does not match hash in known_hosts file" + )) + } + } +} + +const B64: base64::engine::general_purpose::GeneralPurpose = + base64::engine::general_purpose::STANDARD_NO_PAD; + +impl std::fmt::Display for Hash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use fast_hex::Encode; + match self { + Hash::SHA2_512(b) => { + write!( + f, + "{}:{}", + self.name(), + fast_hex::Encoder::::display_sized(b) + ) + } + Hash::RAW(b) => { + write!(f, "{}:{}", self.name(), B64.encode(b)) + } + } + } +} + +impl FromStr for Hash { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + let (algo, hash) = s + .split_once(':') + .context("Expected an algorithm and raw hash")?; + let hash = match algo { + "SHA2_512" => Self::SHA2_512( + fast_hex::Decoder::decode_sized(hash.as_bytes().try_into()?) + .ok_or_else(|| anyhow!("Invalid hexadecimal"))?, + ), + "RAW" => Self::RAW(B64.decode(hash)?), + _ => bail!("Unrecognized algorithm: {}", algo), + }; + Ok(hash) + } +} + +impl KnownHost<'static> { + fn from_str(s: &'static str) -> Result { + let (host, s) = s.split_once(' ').context("Expected a host and hash")?; + let host = + webpki::SubjectNameRef::try_from_ascii_str(host).map_err(|e| anyhow!("{:?}", e))?; + let hash = s + .parse::() + .context("While parsing a known-host hash")?; + Ok(Self { host, hash }) + } +} + +/*fn append_known_host(file: &str, host: KnownHost<'_>) -> Result<()> { + use std::io::Write; + let mut file = std::fs::OpenOptions::new() + .read(false) + .append(true) + .create(true) + .open(file)?; + file.write_all(b"\n")?; + file.write_all(host.host.as_ref())?; + file.write_all(b" ")?; + file.write_all(host.hash.to_string().as_bytes())?; + Ok(()) +}*/ + +fn write_known_hosts(file: &str, hosts: &[KnownHost<'_>]) -> Result<()> { + use std::io::Write; + let mut file = std::fs::OpenOptions::new() + .read(false) + .write(true) + .truncate(true) + .create(true) + .open(file)?; + for host in hosts { + file.write_all(host.host.as_ref())?; + file.write_all(b" ")?; + file.write_all(host.hash.to_string().as_bytes())?; + file.write_all(b"\n")?; + } + Ok(()) +} + async fn run_client(mut args: std::env::Args) -> Result<()> { info!("running client"); + let mut cfg_dir = std::env::var("HOME")?; + cfg_dir.push_str("/.config/quinoa"); + tokio::fs::create_dir_all(&cfg_dir).await?; + let known_hosts_file = format!("{}/known_hosts", cfg_dir); + let known_hosts = if std::path::Path::new(&known_hosts_file).exists() { + let s = Box::leak( + tokio::fs::read_to_string(&known_hosts_file) + .await? + .into_boxed_str(), + ); + s.lines() + .filter(|s| !s.is_empty() && !s.starts_with('#')) + .map(KnownHost::from_str) + .collect::>>()? + } else { + Vec::new() + }; + let cfg = &*Box::leak( + ClientConfig { + known_hosts_file, + known_hosts: known_hosts.into(), + } + .into(), + ); + let conn_str = args.next().expect("USERNAME@HOST"); let (username, host) = conn_str.split_once('@').expect("USERNAME@HOST"); @@ -205,10 +552,11 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { let mut client_crypto = rustls::ClientConfig::builder() .with_safe_defaults() - .with_root_certificates(roots) + //.with_root_certificates(roots) + .with_custom_certificate_verifier(Arc::new(InformedServerCertVerifier { cfg }) as _) .with_no_client_auth(); - client_crypto.alpn_protocols = vec![ALPN_QUIC_SHELL.as_bytes().to_owned()]; + client_crypto.alpn_protocols = vec![ALPN_QUINOA.as_bytes().to_owned()]; let mut transport = transport_config(); transport.keep_alive_interval(Some(std::time::Duration::from_secs(5))); @@ -221,17 +569,19 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { info!("connecting"); - let conn = endpoint - .connect(host.parse()?, "localhost")? - .await?; + let conn = endpoint.connect(host.parse()?, "localhost")?.await?; // authenticating client { let (mut send, mut recv) = conn.open_bi().await?; - write_msg(&mut send, &auth::Hello { - username: username.to_owned(), - }).await?; + write_msg( + &mut send, + &auth::Hello { + username: username.to_owned(), + }, + ) + .await?; do_auth_prompt(&conn, &mut send, &mut recv).await?; } @@ -243,12 +593,14 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { struct Reset(Termios); impl Drop for Reset { fn drop(&mut self) { + //_ = crossterm::terminal::disable_raw_mode(); _ = tcsetattr(libc::STDIN_FILENO, SetArg::TCSAFLUSH, &self.0); - info!("termios reset!"); + println!("termios reset!"); } } let mut termios = tcgetattr(libc::STDIN_FILENO)?; + let reset = Reset(termios.clone()); termios.local_flags.remove(LocalFlags::ECHO); termios.local_flags.remove(LocalFlags::ICANON); termios.local_flags.remove(LocalFlags::ISIG); @@ -256,7 +608,6 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { termios.input_flags.remove(InputFlags::IXON); termios.input_flags.remove(InputFlags::ICRNL); termios.output_flags.remove(OutputFlags::OPOST); - let reset = Reset(termios.clone()); tcsetattr(libc::STDIN_FILENO, SetArg::TCSAFLUSH, &termios)?; reset }; @@ -270,9 +621,14 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { do_shell(&conn, &mut send, &mut recv).await } -async fn do_shell(conn: &quinn::Connection, send: &mut SendStream, recv: &mut RecvStream) -> Result<()> { +async fn do_shell( + conn: &quinn::Connection, + send: &mut SendStream, + recv: &mut RecvStream, +) -> Result<()> { let mut stdin = unsafe { ManuallyDrop::new(tokio::fs::File::from_raw_fd(libc::STDIN_FILENO)) }; - let mut stdout = unsafe { ManuallyDrop::new(tokio::fs::File::from_raw_fd(libc::STDOUT_FILENO)) }; + let mut stdout = + unsafe { ManuallyDrop::new(tokio::fs::File::from_raw_fd(libc::STDOUT_FILENO)) }; let mut stdin_buf = Vec::with_capacity(4096); //let mut stdout_buf = Vec::with_capacity(4096); let mut stdout_buf = vec![bytes::Bytes::new(); 128]; @@ -323,10 +679,15 @@ async fn do_shell(conn: &quinn::Connection, send: &mut SendStream, recv: &mut Re } } -async fn do_auth_prompt(conn: &quinn::Connection, send: &mut SendStream, recv: &mut RecvStream) -> Result<()> { +async fn do_auth_prompt( + conn: &quinn::Connection, + send: &mut SendStream, + recv: &mut RecvStream, +) -> Result<()> { use auth::*; - let mut stdout = unsafe { ManuallyDrop::new(tokio::fs::File::from_raw_fd(libc::STDOUT_FILENO)) }; + let mut stdout = + unsafe { ManuallyDrop::new(tokio::fs::File::from_raw_fd(libc::STDOUT_FILENO)) }; loop { tokio::select! { @@ -390,9 +751,7 @@ async fn greet_conn(cfg: &'static ServerConfig, conn: quinn::Connecting) -> Resu .map_or_else(|| "".into(), |x| String::from_utf8_lossy(&x).into_owned()) ); - if let Err(e) = authenticate_conn(cfg, &conn) - .instrument(span) - .await { + if let Err(e) = authenticate_conn(cfg, &conn).instrument(span).await { error!("handler failed: {reason}", reason = e.to_string()); conn.close(1u8.into(), b"handler error"); } @@ -410,10 +769,7 @@ mod auth { #[derive(Debug, Serialize, Deserialize)] pub enum Question { - Prompt { - prompt: CString, - echo: bool, - }, + Prompt { prompt: CString, echo: bool }, TextInfo(CString), ErrorMsg(CString), LoggedIn, @@ -446,16 +802,23 @@ async fn authenticate_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); fn ask(&self, question: Question) -> Result<(), pam_client::ErrorCode> { - self.send.send_timeout(question, Self::TIMEOUT).map_err(|_| pam_client::ErrorCode::ABORT) + self.send + .send_timeout(question, Self::TIMEOUT) + .map_err(|_| pam_client::ErrorCode::ABORT) } fn answer(&self) -> Result { - self.recv.recv_timeout(Self::TIMEOUT).map_err(|_| pam_client::ErrorCode::ABORT) + self.recv + .recv_timeout(Self::TIMEOUT) + .map_err(|_| pam_client::ErrorCode::ABORT) } } impl ConversationHandler for Conversation { - fn prompt_echo_on(&mut self, prompt: &CStr) -> std::result::Result { + fn prompt_echo_on( + &mut self, + prompt: &CStr, + ) -> std::result::Result { self.ask(Question::Prompt { prompt: prompt.to_owned(), echo: true, @@ -465,7 +828,10 @@ async fn authenticate_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) } } - fn prompt_echo_off(&mut self, prompt: &CStr) -> std::result::Result { + fn prompt_echo_off( + &mut self, + prompt: &CStr, + ) -> std::result::Result { self.ask(Question::Prompt { prompt: prompt.to_owned(), echo: false, @@ -485,10 +851,14 @@ async fn authenticate_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) } let hdl = tokio::task::spawn_blocking(move || { - let mut ctx = pam_client::Context::new("sshd", Some(&hello.username), Conversation { - send: q_send, - recv: a_recv, - })?; + let mut ctx = pam_client::Context::new( + "sshd", + Some(&hello.username), + Conversation { + send: q_send, + recv: a_recv, + }, + )?; info!("created context"); ctx.authenticate(pam_client::Flag::NONE)?; @@ -590,7 +960,6 @@ async fn authenticate_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) }*/ } - let (mut ctx, sess) = hdl.await??; let sess = ctx.unleak_session(sess); info!("logged in: {}", sess.envlist()); @@ -780,7 +1149,10 @@ async fn handle_stream_shell( impl<'a> Future for Wait<'a> { type Output = std::io::Result; - fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> Poll { + fn poll( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll { match self.proc.try_wait() { Ok(Some(code)) => Poll::Ready(Ok(code)), Ok(None) => Poll::Pending, diff --git a/src/pty.rs b/src/pty.rs index 39e0b82..3a87412 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -27,7 +27,7 @@ pub struct Child { impl Child { pub fn set_nodelay(&mut self) -> nix::Result<()> { - fcntl(self.pty.as_raw_fd(), FcntlArg::F_SETFL(OFlag::O_NDELAY)).map(|_|()) + fcntl(self.pty.as_raw_fd(), FcntlArg::F_SETFL(OFlag::O_NDELAY)).map(|_| ()) } }