mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
parse config file
This commit is contained in:
parent
187478464c
commit
13c49524ba
8 changed files with 314 additions and 107 deletions
84
Cargo.lock
generated
84
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
13
bin/cluelesshd/cluelesshd.toml
Normal file
13
bin/cluelesshd/cluelesshd.toml
Normal file
|
|
@ -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"
|
||||
|
|
@ -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<bool> {
|
||||
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, AuthError> =
|
||||
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<bool> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
62
bin/cluelesshd/src/config.rs
Normal file
62
bin/cluelesshd/src/config.rs
Normal file
|
|
@ -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<PathBuf>,
|
||||
#[serde(default = "default_true")]
|
||||
pub password_login: bool,
|
||||
pub banner: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn find() -> Result<Self> {
|
||||
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
|
||||
}
|
||||
|
|
@ -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::<SocketAddr>()
|
||||
.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, AuthError> =
|
||||
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<HostKeySet> {
|
||||
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<TcpStream>,
|
||||
) -> Result<()> {
|
||||
|
|
|
|||
43
lib/cluelessh-keys/src/host_keys.rs
Normal file
43
lib/cluelessh-keys/src/host_keys.rs
Normal file
|
|
@ -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<PlaintextPrivateKey>,
|
||||
}
|
||||
|
||||
impl HostKeySet {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn into_keys(self) -> Vec<PlaintextPrivateKey> {
|
||||
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
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod authorized_keys;
|
||||
mod crypto;
|
||||
pub mod host_keys;
|
||||
pub mod private;
|
||||
pub mod public;
|
||||
pub mod signature;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue