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:
parent
e3d610e9ad
commit
545ec4ebc4
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
466
src/main.rs
466
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<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,
|
||||
|
|
|
@ -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(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue