Rename, rudimentary host verification

- Rename to quinoa
- Implement rudimentary host verification (byte-for-byte equality check
  on certificate)
  - While the verification algorithm is rudimentary, the storage and
    handling/UI is completely acceptable, almost on par with that of SSH
- Fixed termios reset on exit
- There is to be a bug when using the fish shell that breaks things when
  navigating the history with the arrow keys
This commit is contained in:
Michael Pfaff 2023-06-06 23:33:02 -04:00
parent e3d610e9ad
commit 545ec4ebc4
Signed by: michael
GPG Key ID: CF402C4A012AA9D4
4 changed files with 589 additions and 84 deletions

196
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<Vec<KnownHost<'static>>>,
}
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<T: serde::de::DeserializeOwned>(recv: &mut RecvStream) -> Result<Result<T, FinishedEarly>> {
async fn read_msg<T: serde::de::DeserializeOwned>(
recv: &mut RecvStream,
) -> Result<Result<T, FinishedEarly>> {
let mut size = [0u8; 2];
match recv.read_exact(&mut size).await {
Ok(()) => {}
@ -171,7 +184,9 @@ async fn read_msg<T: serde::de::DeserializeOwned>(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<T: serde::Serialize>(send: &mut SendStream, value: &T) -> Result<()> {
@ -182,9 +197,341 @@ async fn write_msg<T: serde::Serialize>(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<KnownHost<'static>>,
end_entity: &rustls::Certificate,
subject_name: webpki::SubjectNameRef<'_>,
known_as: Option<usize>,
) -> 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<Item = &[u8]>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> std::result::Result<rustls::client::ServerCertVerified, rustls::Error> {
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<u8>),
}
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::<false>::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<Self, Self::Err> {
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<Self> {
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::<Hash>()
.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::<Result<Vec<_>>>()?
} 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(|| "<none>".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<Answer, pam_client::ErrorCode> {
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<std::ffi::CString, pam_client::ErrorCode> {
fn prompt_echo_on(
&mut self,
prompt: &CStr,
) -> std::result::Result<std::ffi::CString, pam_client::ErrorCode> {
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<std::ffi::CString, pam_client::ErrorCode> {
fn prompt_echo_off(
&mut self,
prompt: &CStr,
) -> std::result::Result<std::ffi::CString, pam_client::ErrorCode> {
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<i32>;
fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
fn poll(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> Poll<Self::Output> {
match self.proc.try_wait() {
Ok(Some(code)) => Poll::Ready(Ok(code)),
Ok(None) => Poll::Pending,

View File

@ -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(|_| ())
}
}