mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
start client public auth
This commit is contained in:
parent
157d6081b8
commit
85f1def4b5
9 changed files with 132 additions and 16 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -1214,6 +1214,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"users",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1481,6 +1482,16 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "users"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
@ -28,9 +29,9 @@ enum Subcommand {
|
||||||
},
|
},
|
||||||
/// Generate a new SSH key
|
/// Generate a new SSH key
|
||||||
Generate {
|
Generate {
|
||||||
#[arg(short, long = "type")]
|
#[arg(short, long = "type", default_value_t = KeyType::Ed25519)]
|
||||||
type_: KeyType,
|
type_: KeyType,
|
||||||
#[arg(short, long)]
|
#[arg(short, long, default_value_t = String::default())]
|
||||||
comment: String,
|
comment: String,
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
@ -55,6 +56,14 @@ enum KeyType {
|
||||||
Ed25519,
|
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<()> {
|
fn main() -> eyre::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
|
||||||
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
rpassword = "7.3.1"
|
rpassword = "7.3.1"
|
||||||
|
users = "0.11.0"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use std::io::Write;
|
use std::{collections::HashSet, io::Write};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use eyre::Context;
|
use eyre::{bail, Context, ContextCompat, OptionExt};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
use ssh_transport::{key::PublicKey, parse::Writer};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
|
|
@ -27,12 +28,19 @@ impl ssh_protocol::transport::SshRng for ThreadRngRand {
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(short = 'p', long, default_value_t = 22)]
|
#[arg(short = 'p', long, default_value_t = 22)]
|
||||||
port: u16,
|
port: u16,
|
||||||
|
#[arg(short, long)]
|
||||||
|
user: Option<String>,
|
||||||
destination: String,
|
destination: String,
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Operation {
|
enum Operation {
|
||||||
PasswordEntered(std::io::Result<String>),
|
PasswordEntered(std::io::Result<String>),
|
||||||
|
Signature {
|
||||||
|
key_alg_name: &'static str,
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
signature: Vec<u8>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[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"));
|
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
||||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
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))
|
let mut conn = TcpStream::connect(&format!("{}:{}", args.destination, args.port))
|
||||||
.await
|
.await
|
||||||
.wrap_err("connecting")?;
|
.wrap_err("connecting")?;
|
||||||
|
|
||||||
let username = "hans-peter";
|
|
||||||
|
|
||||||
let mut state = ssh_protocol::ClientConnection::new(
|
let mut state = ssh_protocol::ClientConnection::new(
|
||||||
transport::client::ClientConnection::new(ThreadRngRand),
|
transport::client::ClientConnection::new(ThreadRngRand),
|
||||||
ssh_protocol::auth::ClientAuth::new(username.as_bytes().to_vec()),
|
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() {
|
for req in auth.user_requests() {
|
||||||
match req {
|
match req {
|
||||||
ssh_protocol::auth::ClientUserRequest::Password => {
|
ssh_protocol::auth::ClientUserRequest::Password => {
|
||||||
let username = username.to_owned();
|
let username = username.clone();
|
||||||
let destination = args.destination.clone();
|
let destination = args.destination.clone();
|
||||||
let send_op = send_op.clone();
|
let send_op = send_op.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
|
@ -80,16 +105,50 @@ async fn main() -> eyre::Result<()> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ssh_protocol::auth::ClientUserRequest::PrivateKeySign {
|
ssh_protocol::auth::ClientUserRequest::PrivateKeySign {
|
||||||
session_identifier: _,
|
session_identifier,
|
||||||
} => {
|
} => {
|
||||||
|
// TODO: support agentless manual key opening
|
||||||
// TODO: move
|
// TODO: move
|
||||||
let mut agent = ssh_agent_client::SocketAgentConnection::from_env()
|
let mut agent = ssh_agent_client::SocketAgentConnection::from_env()
|
||||||
.await
|
.await
|
||||||
.wrap_err("failed to connect to SSH agent")?;
|
.wrap_err("failed to connect to SSH agent")?;
|
||||||
let identities = agent.list_identities().await?;
|
let identities = agent.list_identities().await?;
|
||||||
for identity in identities {
|
for identity in &identities {
|
||||||
debug!(comment = ?identity.comment, "Found identity");
|
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) => {
|
ssh_protocol::auth::ClientUserRequest::Banner(banner) => {
|
||||||
let banner = String::from_utf8_lossy(&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");
|
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 => {}
|
None => {}
|
||||||
}
|
}
|
||||||
state.progress();
|
state.progress();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
{ pkgs ? import <nixpkgs> { }, ... }: pkgs.rustPlatform.buildRustPackage {
|
{ pkgs ? import <nixpkgs> { }, ... }: pkgs.rustPlatform.buildRustPackage {
|
||||||
src = pkgs.lib.cleanSource ./.;
|
src = pkgs.lib.cleanSource ./.;
|
||||||
pname = "fakessh";
|
pname = "ssh";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
meta = {
|
|
||||||
mainProgram = "fakesshd";
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ pub mod auth {
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use ssh_transport::{numbers, packet::Packet, parse::NameList, peer_error, Result};
|
use ssh_transport::{numbers, packet::Packet, parse::NameList, peer_error, Result};
|
||||||
use tracing::info;
|
use tracing::{debug, info};
|
||||||
|
|
||||||
pub struct BadAuth {
|
pub struct BadAuth {
|
||||||
has_failed: bool,
|
has_failed: bool,
|
||||||
|
|
@ -368,6 +368,19 @@ pub mod auth {
|
||||||
self.packets_to_send.push_back(packet);
|
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<()> {
|
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()");
|
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()?;
|
let _partial_success = p.bool()?;
|
||||||
|
|
||||||
if authentications.iter().any(|item| item == "password") {
|
if authentications.iter().any(|item| item == "password") {
|
||||||
|
debug!("Received authentication failure, trying password");
|
||||||
self.user_requests.push_back(ClientUserRequest::Password);
|
self.user_requests.push_back(ClientUserRequest::Password);
|
||||||
} else if authentications.iter().any(|item| item == "publickey") {
|
} else if authentications.iter().any(|item| item == "publickey") {
|
||||||
|
debug!("Received authentication failure, trying publickey");
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc4252#section-7>
|
// <https://datatracker.ietf.org/doc/html/rfc4252#section-7>
|
||||||
// TODO: Ask the server whether there are any keys we can use instead of just yoloing the signature.
|
// TODO: Ask the server whether there are any keys we can use instead of just yoloing the signature.
|
||||||
self.user_requests
|
self.user_requests
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ impl PublicKey {
|
||||||
}
|
}
|
||||||
p.finish()
|
p.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn algorithm_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Ed25519 { .. } => "ssh-ed25519",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for PublicKey {
|
impl Display for PublicKey {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,15 @@ ctors! {
|
||||||
false_: bool,
|
false_: bool,
|
||||||
password: string,
|
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;
|
fn new_msg_userauth_failure(SSH_MSG_USERAUTH_FAILURE;
|
||||||
auth_options: name_list,
|
auth_options: name_list,
|
||||||
partial_success: bool,
|
partial_success: bool,
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore = "this is super annoying, use expect-test please"]
|
||||||
fn handshake() {
|
fn handshake() {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
let rng = vec![
|
let rng = vec![
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue