start client public auth

This commit is contained in:
nora 2024-08-23 15:02:29 +02:00
parent 157d6081b8
commit 85f1def4b5
9 changed files with 132 additions and 16 deletions

11
Cargo.lock generated
View file

@ -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"

View file

@ -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();

View file

@ -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"

View file

@ -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<String>,
destination: String,
command: Vec<String>,
}
enum Operation {
PasswordEntered(std::io::Result<String>),
Signature {
key_alg_name: &'static str,
public_key: Vec<u8>,
signature: Vec<u8>,
},
}
#[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();

View file

@ -1,10 +1,6 @@
{ pkgs ? import <nixpkgs> { }, ... }: pkgs.rustPlatform.buildRustPackage {
src = pkgs.lib.cleanSource ./.;
pname = "fakessh";
pname = "ssh";
version = "0.1.0";
cargoLock.lockFile = ./Cargo.lock;
meta = {
mainProgram = "fakesshd";
};
}

View file

@ -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");
// <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.
self.user_requests

View file

@ -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 {

View file

@ -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,

View file

@ -378,6 +378,7 @@ mod tests {
}
#[test]
#[ignore = "this is super annoying, use expect-test please"]
fn handshake() {
#[rustfmt::skip]
let rng = vec![