From 13c49524ba732299ae7c769f1b25e54ceef1e1b8 Mon Sep 17 00:00:00 2001 From: Noratrieb <48135649+Noratrieb@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:34:26 +0200 Subject: [PATCH] parse config file --- Cargo.lock | 84 ++++++++++++++- bin/cluelesshd/Cargo.toml | 2 + bin/cluelesshd/cluelesshd.toml | 13 +++ bin/cluelesshd/src/auth.rs | 63 ++++++++++++ bin/cluelesshd/src/config.rs | 62 +++++++++++ bin/cluelesshd/src/main.rs | 153 +++++++++------------------- lib/cluelessh-keys/src/host_keys.rs | 43 ++++++++ lib/cluelessh-keys/src/lib.rs | 1 + 8 files changed, 314 insertions(+), 107 deletions(-) create mode 100644 bin/cluelesshd/cluelesshd.toml create mode 100644 bin/cluelesshd/src/config.rs create mode 100644 lib/cluelessh-keys/src/host_keys.rs diff --git a/Cargo.lock b/Cargo.lock index c5bc2c3..224796c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -459,8 +459,10 @@ dependencies = [ "eyre", "futures", "rustix", + "serde", "thiserror", "tokio", + "toml", "tracing", "tracing-subscriber", "users", @@ -629,6 +631,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -803,6 +811,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "heck" version = "0.5.0" @@ -851,6 +865,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "inout" version = "0.1.3" @@ -1301,18 +1325,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.205" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.205" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -1331,6 +1355,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1493,6 +1526,40 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.40" @@ -1784,6 +1851,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/bin/cluelesshd/Cargo.toml b/bin/cluelesshd/Cargo.toml index b397fce..0175822 100644 --- a/bin/cluelesshd/Cargo.toml +++ b/bin/cluelesshd/Cargo.toml @@ -17,6 +17,8 @@ users = "0.11.0" futures = "0.3.30" thiserror = "1.0.63" cluelessh-keys = { version = "0.1.0", path = "../../lib/cluelessh-keys" } +serde = { version = "1.0.209", features = ["derive"] } +toml = "0.8.19" [lints] workspace = true diff --git a/bin/cluelesshd/cluelesshd.toml b/bin/cluelesshd/cluelesshd.toml new file mode 100644 index 0000000..183c086 --- /dev/null +++ b/bin/cluelesshd/cluelesshd.toml @@ -0,0 +1,13 @@ +log_level = "info" + +[net] +ip = "0.0.0.0" +port = 2223 + +[auth] +host_keys = [ + # "/etc/ssh/ssh_host_ed25519_key", + "./test_ed25519_key" +] +password_login = false +banner = "welcome to my server!!!\r\ni hope you enjoy your stay.\r\n" diff --git a/bin/cluelesshd/src/auth.rs b/bin/cluelesshd/src/auth.rs index f7d8162..9853be0 100644 --- a/bin/cluelesshd/src/auth.rs +++ b/bin/cluelesshd/src/auth.rs @@ -6,6 +6,9 @@ use cluelessh_keys::{ authorized_keys::{self, AuthorizedKeys}, public::{PublicKey, PublicKeyWithComment}, }; +use cluelessh_protocol::auth::{CheckPubkey, VerifySignature}; +use eyre::eyre; +use tracing::debug; use users::os::unix::UserExt; /// A known-authorized public key for a user. @@ -54,3 +57,63 @@ impl UserPublicKey { self.0.key.verify_signature(data, signature) } } + +pub async fn verify_signature(auth: VerifySignature) -> eyre::Result { + let Ok(public_key) = PublicKey::from_wire_encoding(&auth.pubkey) else { + return Ok(false); + }; + if auth.pubkey_alg_name != public_key.algorithm_name() { + return Ok(false); + } + + let result: std::result::Result = + UserPublicKey::for_user_and_key(auth.user.clone(), &public_key).await; + + debug!(user = %auth.user, err = ?result.as_ref().err(), "Attempting publickey signature"); + + match result { + Ok(user_key) => { + // Verify signature... + + let sign_data = cluelessh_keys::signature::signature_data( + auth.session_identifier, + &auth.user, + &public_key, + ); + + if user_key.verify_signature(&sign_data, &auth.signature) { + Ok(true) + } else { + Ok(false) + } + } + Err( + AuthError::UnknownUser + | AuthError::UnauthorizedPublicKey + | AuthError::NoAuthorizedKeys(_), + ) => Ok(false), + Err(AuthError::InvalidAuthorizedKeys(err)) => Err(eyre!(err)), + } +} + +pub async fn check_pubkey(auth: CheckPubkey) -> eyre::Result { + let Ok(public_key) = PublicKey::from_wire_encoding(&auth.pubkey) else { + return Ok(false); + }; + if auth.pubkey_alg_name != public_key.algorithm_name() { + return Ok(false); + } + let result = UserPublicKey::for_user_and_key(auth.user.clone(), &public_key).await; + + debug!(user = %auth.user, err = ?result.as_ref().err(), "Attempting publickey check"); + + match result { + Ok(_) => Ok(true), + Err( + AuthError::UnknownUser + | AuthError::UnauthorizedPublicKey + | AuthError::NoAuthorizedKeys(_), + ) => Ok(false), + Err(AuthError::InvalidAuthorizedKeys(err)) => Err(eyre!(err)), + } +} diff --git a/bin/cluelesshd/src/config.rs b/bin/cluelesshd/src/config.rs new file mode 100644 index 0000000..b0e8bd5 --- /dev/null +++ b/bin/cluelesshd/src/config.rs @@ -0,0 +1,62 @@ +use eyre::{Context, Result}; +use serde::Deserialize; +use std::{ + net::{IpAddr, Ipv4Addr}, + path::PathBuf, +}; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + #[serde(default = "default_info")] + pub log_level: String, + pub net: NetConfig, + pub auth: AuthConfig, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct NetConfig { + #[serde(default = "addr_default")] + pub ip: IpAddr, + #[serde(default = "port_default")] + pub port: u16, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct AuthConfig { + pub host_keys: Vec, + #[serde(default = "default_true")] + pub password_login: bool, + pub banner: Option, +} + +impl Config { + pub fn find() -> Result { + let path = + std::env::var("CLUELESSHD_CONFIG").unwrap_or_else(|_| "cluelesshd.toml".to_owned()); + + let content = std::fs::read_to_string(&path).wrap_err_with(|| { + format!("failed to open config file '{path}', refusing to start. you can change the config file path with the CLUELESSHD_CONFIG environment variable") + })?; + + toml::from_str(&content).wrap_err_with(|| format!("invalid config file '{path}'")) + } +} + +fn default_info() -> String { + "info".to_owned() +} + +fn default_true() -> bool { + true +} + +fn addr_default() -> IpAddr { + IpAddr::V4(Ipv4Addr::UNSPECIFIED) +} + +fn port_default() -> u16 { + 22 +} diff --git a/bin/cluelesshd/src/main.rs b/bin/cluelesshd/src/main.rs index 2b770bc..8d97106 100644 --- a/bin/cluelesshd/src/main.rs +++ b/bin/cluelesshd/src/main.rs @@ -1,19 +1,20 @@ mod auth; +mod config; mod pty; use std::{ io, net::SocketAddr, + path::PathBuf, pin::Pin, process::{ExitStatus, Stdio}, sync::Arc, }; -use auth::AuthError; -use cluelessh_keys::{private::EncryptedPrivateKeys, public::PublicKey}; +use cluelessh_keys::{host_keys::HostKeySet, private::EncryptedPrivateKeys}; use cluelessh_tokio::{server::ServerAuthVerify, Channel}; use cluelessh_transport::server::ServerConfig; -use eyre::{bail, eyre, Context, OptionExt, Result}; +use eyre::{bail, Context, OptionExt, Result}; use pty::Pty; use rustix::termios::Winsize; use tokio::{ @@ -34,115 +35,28 @@ use users::os::unix::UserExt; #[tokio::main(flavor = "current_thread")] async fn main() -> eyre::Result<()> { - let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + let config = config::Config::find()?; + + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level)); tracing_subscriber::fmt().with_env_filter(env_filter).init(); - let addr = "0.0.0.0:2223".to_owned(); - - let addr = addr - .parse::() - .wrap_err_with(|| format!("failed to parse listen addr '{addr}'"))?; - + let addr = SocketAddr::new(config.net.ip, config.net.port); info!(%addr, "Starting server"); - let listener = TcpListener::bind(addr).await.wrap_err("binding listener")?; + let listener = TcpListener::bind(addr) + .await + .wrap_err_with(|| format!("trying to listen on {addr}"))?; let auth_verify = ServerAuthVerify { - verify_password: None, - verify_signature: Some(Arc::new(|auth| { - Box::pin(async move { - let Ok(public_key) = PublicKey::from_wire_encoding(&auth.pubkey) else { - return Ok(false); - }; - if auth.pubkey_alg_name != public_key.algorithm_name() { - return Ok(false); - } - - let result: std::result::Result = - auth::UserPublicKey::for_user_and_key(auth.user.clone(), &public_key).await; - - debug!(user = %auth.user, err = ?result.as_ref().err(), "Attempting publickey signature"); - - match result { - Ok(user_key) => { - // Verify signature... - - let sign_data = cluelessh_keys::signature::signature_data( - auth.session_identifier, - &auth.user, - &public_key, - ); - - if user_key.verify_signature(&sign_data, &auth.signature) { - Ok(true) - } else { - Ok(false) - } - } - Err( - AuthError::UnknownUser - | AuthError::UnauthorizedPublicKey - | AuthError::NoAuthorizedKeys(_), - ) => Ok(false), - Err(AuthError::InvalidAuthorizedKeys(err)) => Err(eyre!(err)), - } - }) - })), - check_pubkey: Some(Arc::new(|auth| { - Box::pin(async move { - let Ok(public_key) = PublicKey::from_wire_encoding(&auth.pubkey) else { - return Ok(false); - }; - if auth.pubkey_alg_name != public_key.algorithm_name() { - return Ok(false); - } - let result = - auth::UserPublicKey::for_user_and_key(auth.user.clone(), &public_key).await; - - debug!(user = %auth.user, err = ?result.as_ref().err(), "Attempting publickey check"); - - match result { - Ok(_) => Ok(true), - Err( - AuthError::UnknownUser - | AuthError::UnauthorizedPublicKey - | AuthError::NoAuthorizedKeys(_), - ) => Ok(false), - Err(AuthError::InvalidAuthorizedKeys(err)) => Err(eyre!(err)), - } - }) - })), - auth_banner: Some("welcome to my server!!!\r\ni hope you enjoy your stay.\r\n".to_owned()), + verify_password: config.auth.password_login.then(|| todo!("password login")), + verify_signature: Some(Arc::new(|auth| Box::pin(auth::verify_signature(auth)))), + check_pubkey: Some(Arc::new(|auth| Box::pin(auth::check_pubkey(auth)))), + auth_banner: config.auth.banner, }; - let mut host_keys = Vec::new(); - - let host_key_locations = ["/etc/ssh/ssh_host_ed25519_key", "./test_ed25519_key"]; - - for host_key_location in host_key_locations { - match tokio::fs::read_to_string(host_key_location).await { - Ok(key) => { - let key = EncryptedPrivateKeys::parse(key.as_bytes()) - .wrap_err_with(|| format!("invalid {host_key_location}"))?; - if key.requires_passphrase() { - bail!("{host_key_location} must not require a passphrase"); - } - let mut key = key - .decrypt(None) - .wrap_err_with(|| format!("invalid {host_key_location}"))?; - if key.len() != 1 { - bail!("{host_key_location} must contain a single key"); - } - host_keys.push(key.remove(0)); - - info!(?host_key_location, "Loaded host key") - } - Err(err) => { - debug!(?err, ?host_key_location, "Failed to load host key") - } - } - } + let host_keys = load_host_keys(&config.auth.host_keys).await?.into_keys(); if host_keys.is_empty() { bail!("no host keys found"); @@ -173,6 +87,39 @@ async fn main() -> eyre::Result<()> { } } +async fn load_host_keys(keys: &[PathBuf]) -> Result { + let mut host_keys = HostKeySet::new(); + + for key_path in keys { + load_host_key(key_path, &mut host_keys) + .await + .wrap_err_with(|| format!("loading host key at '{}'", key_path.display()))?; + } + + Ok(host_keys) +} + +async fn load_host_key(key_path: &PathBuf, host_keys: &mut HostKeySet) -> Result<()> { + let key = tokio::fs::read_to_string(key_path) + .await + .wrap_err("failed to open")?; + let key = EncryptedPrivateKeys::parse(key.as_bytes()).wrap_err("failed to parse")?; + + if key.requires_passphrase() { + bail!("host key requires a passphrase, which is not allowed"); + } + let mut key = key.decrypt(None).wrap_err("failed to parse")?; + if key.len() != 1 { + bail!("host key must contain a single key"); + } + let key = key.remove(0); + let algorithm = key.private_key.algorithm_name(); + host_keys.insert(key)?; + + info!(?key_path, ?algorithm, "Loaded host key"); + Ok(()) +} + async fn handle_connection( mut conn: cluelessh_tokio::server::ServerConnection, ) -> Result<()> { diff --git a/lib/cluelessh-keys/src/host_keys.rs b/lib/cluelessh-keys/src/host_keys.rs new file mode 100644 index 0000000..4cfdc2a --- /dev/null +++ b/lib/cluelessh-keys/src/host_keys.rs @@ -0,0 +1,43 @@ +use std::collections::HashSet; + +use thiserror::Error; + +use crate::private::PlaintextPrivateKey; + +/// A set of host keys, ensuring there are no duplicated algorithms. +#[derive(Debug, Default)] +pub struct HostKeySet { + algs: HashSet<&'static str>, + keys: Vec, +} + +impl HostKeySet { + pub fn new() -> Self { + Self::default() + } + + pub fn into_keys(self) -> Vec { + self.keys + } + + pub fn insert(&mut self, key: PlaintextPrivateKey) -> Result<(), DuplicateHostKeyAlgorithm> { + let alg = key.private_key.algorithm_name(); + + let newly_inserted = self.algs.insert(alg); + if !newly_inserted { + return Err(DuplicateHostKeyAlgorithm { alg }); + } + + self.keys.push(key); + + Ok(()) + } +} + +#[derive(Debug, Error)] +#[error("another host key with algorithm {alg} has already been loaded")] +pub struct DuplicateHostKeyAlgorithm { + alg: &'static str, +} + +// TODO: write tests diff --git a/lib/cluelessh-keys/src/lib.rs b/lib/cluelessh-keys/src/lib.rs index 45ab540..441b177 100644 --- a/lib/cluelessh-keys/src/lib.rs +++ b/lib/cluelessh-keys/src/lib.rs @@ -1,5 +1,6 @@ pub mod authorized_keys; mod crypto; +pub mod host_keys; pub mod private; pub mod public; pub mod signature;