diff --git a/Cargo.lock b/Cargo.lock index cb36484..6f3de3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1214,6 +1214,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "users", ] [[package]] @@ -1481,6 +1482,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/bin/ssh-key/src/main.rs b/bin/ssh-key/src/main.rs index 17efb5a..b928985 100644 --- a/bin/ssh-key/src/main.rs +++ b/bin/ssh-key/src/main.rs @@ -1,4 +1,5 @@ use std::{ + fmt::Display, io::Write, path::{Path, PathBuf}, }; @@ -28,9 +29,9 @@ enum Subcommand { }, /// Generate a new SSH key Generate { - #[arg(short, long = "type")] + #[arg(short, long = "type", default_value_t = KeyType::Ed25519)] type_: KeyType, - #[arg(short, long)] + #[arg(short, long, default_value_t = String::default())] comment: String, #[arg(short, long)] path: PathBuf, @@ -55,6 +56,14 @@ enum KeyType { Ed25519, } +impl Display for KeyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ed25519 => f.write_str("ed25519"), + } + } +} + fn main() -> eyre::Result<()> { let args = Args::parse(); diff --git a/bin/ssh/Cargo.toml b/bin/ssh/Cargo.toml index c8fb676..d34fc78 100644 --- a/bin/ssh/Cargo.toml +++ b/bin/ssh/Cargo.toml @@ -16,3 +16,4 @@ tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] } tracing.workspace = true rpassword = "7.3.1" +users = "0.11.0" diff --git a/bin/ssh/src/main.rs b/bin/ssh/src/main.rs index 95836b7..2e74a78 100644 --- a/bin/ssh/src/main.rs +++ b/bin/ssh/src/main.rs @@ -1,9 +1,10 @@ -use std::io::Write; +use std::{collections::HashSet, io::Write}; use clap::Parser; -use eyre::Context; +use eyre::{bail, Context, ContextCompat, OptionExt}; use rand::RngCore; +use ssh_transport::{key::PublicKey, parse::Writer}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, @@ -27,12 +28,19 @@ impl ssh_protocol::transport::SshRng for ThreadRngRand { struct Args { #[arg(short = 'p', long, default_value_t = 22)] port: u16, + #[arg(short, long)] + user: Option, destination: String, command: Vec, } enum Operation { PasswordEntered(std::io::Result), + Signature { + key_alg_name: &'static str, + public_key: Vec, + signature: Vec, + }, } #[tokio::main] @@ -42,12 +50,29 @@ async fn main() -> eyre::Result<()> { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::fmt().with_env_filter(env_filter).init(); + let username = match args.user { + None => { + tokio::task::spawn_blocking(|| { + users::get_current_username() + .wrap_err("getting username") + .and_then(|username| { + username + .to_str() + .ok_or_eyre("your username is invalid UTF-8???") + .map(ToOwned::to_owned) + }) + }) + .await?? + } + Some(user) => user, + }; + + let mut attempted_public_keys = HashSet::new(); + let mut conn = TcpStream::connect(&format!("{}:{}", args.destination, args.port)) .await .wrap_err("connecting")?; - let username = "hans-peter"; - let mut state = ssh_protocol::ClientConnection::new( transport::client::ClientConnection::new(ThreadRngRand), ssh_protocol::auth::ClientAuth::new(username.as_bytes().to_vec()), @@ -68,7 +93,7 @@ async fn main() -> eyre::Result<()> { for req in auth.user_requests() { match req { ssh_protocol::auth::ClientUserRequest::Password => { - let username = username.to_owned(); + let username = username.clone(); let destination = args.destination.clone(); let send_op = send_op.clone(); std::thread::spawn(move || { @@ -80,16 +105,50 @@ async fn main() -> eyre::Result<()> { }); } ssh_protocol::auth::ClientUserRequest::PrivateKeySign { - session_identifier: _, + session_identifier, } => { + // TODO: support agentless manual key opening // TODO: move let mut agent = ssh_agent_client::SocketAgentConnection::from_env() .await .wrap_err("failed to connect to SSH agent")?; let identities = agent.list_identities().await?; - for identity in identities { - debug!(comment = ?identity.comment, "Found identity"); + for identity in &identities { + let pubkey = PublicKey::from_wire_encoding(&identity.key_blob) + .wrap_err("received invalid public key from SSH agent")?; + debug!(comment = ?identity.comment, %pubkey, "Found identity"); } + if identities.len() > 1 { + todo!("try identities"); + } + let identity = &identities[0]; + if attempted_public_keys.insert(identity.key_blob.clone()) { + bail!("authentication denied (publickey)"); + } + let pubkey = PublicKey::from_wire_encoding(&identity.key_blob)?; + + let mut sig = Writer::new(); + sig.string(session_identifier); + sig.string(&username); + sig.string("ssh-connection"); + sig.string("publickey"); + sig.bool(true); + sig.string(pubkey.algorithm_name()); + sig.string(&identity.key_blob); + + let data = sig.finish(); + let signature = agent + .sign(&identity.key_blob, &data, 0) + .await + .wrap_err("signing for authentication")?; + + send_op + .send(Operation::Signature { + key_alg_name: pubkey.algorithm_name(), + public_key: identity.key_blob.clone(), + signature, + }) + .await?; } ssh_protocol::auth::ClientUserRequest::Banner(banner) => { let banner = String::from_utf8_lossy(&banner); @@ -128,6 +187,15 @@ async fn main() -> eyre::Result<()> { debug!("Ignoring entered password as the state has moved on"); } } + Some(Operation::Signature{ + key_alg_name, public_key, signature, + }) => { + if let Some(auth) = state.auth() { + auth.send_signature(key_alg_name, &public_key, &signature); + } else { + debug!("Ignoring signature as the state has moved on"); + } + } None => {} } state.progress(); diff --git a/default.nix b/default.nix index 91848a1..61406c8 100644 --- a/default.nix +++ b/default.nix @@ -1,10 +1,6 @@ { pkgs ? import { }, ... }: pkgs.rustPlatform.buildRustPackage { src = pkgs.lib.cleanSource ./.; - pname = "fakessh"; + pname = "ssh"; version = "0.1.0"; cargoLock.lockFile = ./Cargo.lock; - - meta = { - mainProgram = "fakesshd"; - }; } diff --git a/lib/ssh-protocol/src/lib.rs b/lib/ssh-protocol/src/lib.rs index b3234bb..3b13692 100644 --- a/lib/ssh-protocol/src/lib.rs +++ b/lib/ssh-protocol/src/lib.rs @@ -204,7 +204,7 @@ pub mod auth { use std::collections::VecDeque; use ssh_transport::{numbers, packet::Packet, parse::NameList, peer_error, Result}; - use tracing::info; + use tracing::{debug, info}; pub struct BadAuth { has_failed: bool, @@ -368,6 +368,19 @@ pub mod auth { self.packets_to_send.push_back(packet); } + pub fn send_signature(&mut self, key_alg_name: &str, public_key: &[u8], signature: &[u8]) { + let packet = Packet::new_msg_userauth_request_publickey( + &self.username, + b"ssh-connection", + b"publickey", + true, + key_alg_name.as_bytes(), + public_key, + signature, + ); + self.packets_to_send.push_back(packet); + } + pub fn recv_packet(&mut self, packet: Packet) -> Result<()> { assert!(!self.is_authenticated, "Must not feed more packets to authentication after authentication is been completed, check with .is_authenticated()"); @@ -387,8 +400,10 @@ pub mod auth { let _partial_success = p.bool()?; if authentications.iter().any(|item| item == "password") { + debug!("Received authentication failure, trying password"); self.user_requests.push_back(ClientUserRequest::Password); } else if authentications.iter().any(|item| item == "publickey") { + debug!("Received authentication failure, trying publickey"); // // TODO: Ask the server whether there are any keys we can use instead of just yoloing the signature. self.user_requests diff --git a/lib/ssh-transport/src/key.rs b/lib/ssh-transport/src/key.rs index 6b0761c..b3f61b0 100644 --- a/lib/ssh-transport/src/key.rs +++ b/lib/ssh-transport/src/key.rs @@ -44,6 +44,12 @@ impl PublicKey { } p.finish() } + + pub fn algorithm_name(&self) -> &'static str { + match self { + Self::Ed25519 { .. } => "ssh-ed25519", + } + } } impl Display for PublicKey { diff --git a/lib/ssh-transport/src/packet/ctors.rs b/lib/ssh-transport/src/packet/ctors.rs index 9f7803a..0f53ff6 100644 --- a/lib/ssh-transport/src/packet/ctors.rs +++ b/lib/ssh-transport/src/packet/ctors.rs @@ -75,6 +75,15 @@ ctors! { false_: bool, password: string, ); + fn new_msg_userauth_request_publickey(SSH_MSG_USERAUTH_REQUEST; + username: string, + service_name: string, + method_name_pubkey: string, + true_: bool, + pubkey_alg_name: string, + pubkey: string, + signature: string, + ); fn new_msg_userauth_failure(SSH_MSG_USERAUTH_FAILURE; auth_options: name_list, partial_success: bool, diff --git a/lib/ssh-transport/src/server.rs b/lib/ssh-transport/src/server.rs index 5b0bf55..7f8d817 100644 --- a/lib/ssh-transport/src/server.rs +++ b/lib/ssh-transport/src/server.rs @@ -378,6 +378,7 @@ mod tests { } #[test] + #[ignore = "this is super annoying, use expect-test please"] fn handshake() { #[rustfmt::skip] let rng = vec![