mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-15 08:55:06 +01:00
615 lines
21 KiB
Rust
615 lines
21 KiB
Rust
use core::panic;
|
|
use std::collections::HashSet;
|
|
use std::mem;
|
|
|
|
use auth::AuthOption;
|
|
use cluelessh_connection::ChannelOperation;
|
|
use tracing::debug;
|
|
|
|
// Re-exports
|
|
pub use cluelessh_connection as connection;
|
|
pub use cluelessh_connection::{ChannelUpdate, ChannelUpdateKind};
|
|
pub use cluelessh_transport as transport;
|
|
pub use cluelessh_transport::{Result, SshStatus};
|
|
|
|
pub struct ThreadRngRand;
|
|
impl transport::SshRng for ThreadRngRand {
|
|
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
|
use rand::RngCore;
|
|
rand::thread_rng().fill_bytes(dest);
|
|
}
|
|
}
|
|
|
|
pub struct ServerConnection {
|
|
transport: cluelessh_transport::server::ServerConnection,
|
|
state: ServerConnectionState,
|
|
}
|
|
|
|
enum ServerConnectionState {
|
|
Setup(HashSet<AuthOption>, Option<String>),
|
|
Auth(auth::ServerAuth),
|
|
Open(cluelessh_connection::ChannelsState, String),
|
|
}
|
|
|
|
impl ServerConnection {
|
|
pub fn new(
|
|
transport: cluelessh_transport::server::ServerConnection,
|
|
auth_options: HashSet<AuthOption>,
|
|
auth_banner: Option<String>,
|
|
) -> Self {
|
|
Self {
|
|
transport,
|
|
state: ServerConnectionState::Setup(auth_options, auth_banner),
|
|
}
|
|
}
|
|
|
|
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
|
|
self.transport.recv_bytes(bytes)?;
|
|
|
|
if let ServerConnectionState::Setup(options, auth_banner) = &mut self.state {
|
|
if let Some(session_ident) = self.transport.is_open() {
|
|
self.state = ServerConnectionState::Auth(auth::ServerAuth::new(
|
|
mem::take(options),
|
|
auth_banner.take(),
|
|
session_ident,
|
|
));
|
|
}
|
|
}
|
|
|
|
while let Some(packet) = self.transport.next_plaintext_packet() {
|
|
match &mut self.state {
|
|
ServerConnectionState::Setup(_, _) => unreachable!(),
|
|
ServerConnectionState::Auth(auth) => {
|
|
auth.recv_packet(packet)?;
|
|
for to_send in auth.packets_to_send() {
|
|
self.transport.send_plaintext_packet(to_send);
|
|
}
|
|
}
|
|
ServerConnectionState::Open(con, _) => {
|
|
con.recv_packet(packet)?;
|
|
}
|
|
}
|
|
|
|
self.progress();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn next_msg_to_send(&mut self) -> Option<cluelessh_transport::Msg> {
|
|
self.transport.next_msg_to_send()
|
|
}
|
|
|
|
pub fn next_channel_update(&mut self) -> Option<cluelessh_connection::ChannelUpdate> {
|
|
match &mut self.state {
|
|
ServerConnectionState::Setup(..) | ServerConnectionState::Auth(_) => None,
|
|
ServerConnectionState::Open(con, _) => con.next_channel_update(),
|
|
}
|
|
}
|
|
|
|
pub fn do_operation(&mut self, op: ChannelOperation) {
|
|
match &mut self.state {
|
|
ServerConnectionState::Setup(..) | ServerConnectionState::Auth(_) => {
|
|
panic!("tried to get connection before it is ready")
|
|
}
|
|
ServerConnectionState::Open(con, _) => {
|
|
con.do_operation(op);
|
|
self.progress();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn progress(&mut self) {
|
|
match &mut self.state {
|
|
ServerConnectionState::Setup(..) => {}
|
|
ServerConnectionState::Auth(auth) => {
|
|
for to_send in auth.packets_to_send() {
|
|
self.transport.send_plaintext_packet(to_send);
|
|
}
|
|
if let Some(user) = auth.authenticated_user() {
|
|
self.state = ServerConnectionState::Open(
|
|
cluelessh_connection::ChannelsState::new(true),
|
|
user.to_owned(),
|
|
);
|
|
}
|
|
}
|
|
ServerConnectionState::Open(con, _) => {
|
|
for to_send in con.packets_to_send() {
|
|
self.transport.send_plaintext_packet(to_send);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn channels(&mut self) -> Option<&mut cluelessh_connection::ChannelsState> {
|
|
match &mut self.state {
|
|
ServerConnectionState::Open(channels, _) => Some(channels),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn auth(&mut self) -> Option<&mut auth::ServerAuth> {
|
|
match &mut self.state {
|
|
ServerConnectionState::Auth(auth) => Some(auth),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn authenticated_user(&self) -> Option<&str> {
|
|
match &self.state {
|
|
ServerConnectionState::Open(_, user) => Some(user),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ClientConnection {
|
|
transport: cluelessh_transport::client::ClientConnection,
|
|
state: ClientConnectionState,
|
|
}
|
|
|
|
enum ClientConnectionState {
|
|
Setup(Option<auth::ClientAuth>),
|
|
Auth(auth::ClientAuth),
|
|
Open(cluelessh_connection::ChannelsState),
|
|
}
|
|
|
|
impl ClientConnection {
|
|
pub fn new(
|
|
transport: cluelessh_transport::client::ClientConnection,
|
|
auth: auth::ClientAuth,
|
|
) -> Self {
|
|
Self {
|
|
transport,
|
|
state: ClientConnectionState::Setup(Some(auth)),
|
|
}
|
|
}
|
|
|
|
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
|
|
self.transport.recv_bytes(bytes)?;
|
|
|
|
if let ClientConnectionState::Setup(auth) = &mut self.state {
|
|
if let Some(session_ident) = self.transport.is_open() {
|
|
let mut auth = mem::take(auth).unwrap();
|
|
auth.set_session_identifier(session_ident);
|
|
|
|
debug!("Connection has been opened");
|
|
self.state = ClientConnectionState::Auth(auth);
|
|
self.progress();
|
|
}
|
|
}
|
|
|
|
while let Some(packet) = self.transport.next_plaintext_packet() {
|
|
match &mut self.state {
|
|
ClientConnectionState::Setup(_) => unreachable!("handled above"),
|
|
ClientConnectionState::Auth(auth) => {
|
|
auth.recv_packet(packet)?;
|
|
for to_send in auth.packets_to_send() {
|
|
self.transport.send_plaintext_packet(to_send);
|
|
}
|
|
if auth.is_authenticated() {
|
|
self.state = ClientConnectionState::Open(
|
|
cluelessh_connection::ChannelsState::new(false),
|
|
);
|
|
}
|
|
}
|
|
ClientConnectionState::Open(con) => {
|
|
con.recv_packet(packet)?;
|
|
for to_send in con.packets_to_send() {
|
|
self.transport.send_plaintext_packet(to_send);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn auth(&mut self) -> Option<&mut auth::ClientAuth> {
|
|
match &mut self.state {
|
|
ClientConnectionState::Auth(auth) => Some(auth),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn channels(&mut self) -> Option<&mut cluelessh_connection::ChannelsState> {
|
|
match &mut self.state {
|
|
ClientConnectionState::Open(channels) => Some(channels),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn is_open(&self) -> bool {
|
|
matches!(self.state, ClientConnectionState::Open(_))
|
|
}
|
|
|
|
pub fn next_msg_to_send(&mut self) -> Option<cluelessh_transport::Msg> {
|
|
self.transport.next_msg_to_send()
|
|
}
|
|
|
|
pub fn next_channel_update(&mut self) -> Option<cluelessh_connection::ChannelUpdate> {
|
|
match &mut self.state {
|
|
ClientConnectionState::Setup(_) => None,
|
|
ClientConnectionState::Auth(_) => None,
|
|
ClientConnectionState::Open(con) => con.next_channel_update(),
|
|
}
|
|
}
|
|
|
|
pub fn do_operation(&mut self, op: ChannelOperation) {
|
|
match &mut self.state {
|
|
ClientConnectionState::Setup(_) | ClientConnectionState::Auth(_) => {
|
|
panic!("tried to get connection during auth")
|
|
}
|
|
ClientConnectionState::Open(con) => {
|
|
con.do_operation(op);
|
|
self.progress();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn progress(&mut self) {
|
|
match &mut self.state {
|
|
ClientConnectionState::Setup(_) => {}
|
|
ClientConnectionState::Auth(auth) => {
|
|
for to_send in auth.packets_to_send() {
|
|
self.transport.send_plaintext_packet(to_send);
|
|
}
|
|
}
|
|
ClientConnectionState::Open(con) => {
|
|
for to_send in con.packets_to_send() {
|
|
self.transport.send_plaintext_packet(to_send);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <https://datatracker.ietf.org/doc/html/rfc4252>
|
|
pub mod auth {
|
|
use std::collections::{HashSet, VecDeque};
|
|
|
|
use cluelessh_format::{numbers, NameList};
|
|
use cluelessh_transport::{packet::Packet, peer_error, Result};
|
|
use tracing::{debug, info};
|
|
|
|
pub struct ServerAuth {
|
|
has_failed: bool,
|
|
packets_to_send: VecDeque<Packet>,
|
|
is_authenticated: Option<String>,
|
|
options: HashSet<AuthOption>,
|
|
banner: Option<String>,
|
|
server_requests: VecDeque<ServerRequest>,
|
|
session_ident: [u8; 32],
|
|
}
|
|
|
|
pub enum ServerRequest {
|
|
VerifyPassword(VerifyPassword),
|
|
/// Check whether a pubkey is usable.
|
|
CheckPubkey(CheckPubkey),
|
|
/// Verify the signature from a pubkey.
|
|
VerifySignature(VerifySignature),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct VerifyPassword {
|
|
pub user: String,
|
|
pub password: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckPubkey {
|
|
pub user: String,
|
|
pub session_identifier: [u8; 32],
|
|
pub pubkey_alg_name: String,
|
|
pub pubkey: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct VerifySignature {
|
|
pub user: String,
|
|
pub session_identifier: [u8; 32],
|
|
pub pubkey_alg_name: String,
|
|
pub pubkey: Vec<u8>,
|
|
pub signature: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
|
pub enum AuthOption {
|
|
Password,
|
|
PublicKey,
|
|
}
|
|
|
|
impl ServerAuth {
|
|
pub fn new(
|
|
options: HashSet<AuthOption>,
|
|
banner: Option<String>,
|
|
session_ident: [u8; 32],
|
|
) -> Self {
|
|
Self {
|
|
has_failed: false,
|
|
packets_to_send: VecDeque::new(),
|
|
options,
|
|
is_authenticated: None,
|
|
session_ident,
|
|
banner,
|
|
server_requests: VecDeque::new(),
|
|
}
|
|
}
|
|
|
|
pub fn recv_packet(&mut self, packet: Packet) -> Result<()> {
|
|
assert!(self.is_authenticated.is_none(), "Must not feed more packets to authentication after authentication is been completed, check with .is_authenticated()");
|
|
|
|
// This is a super simplistic implementation of RFC4252 SSH authentication.
|
|
// We ask for a public key, and always let that one pass.
|
|
// The reason for this is that this makes it a lot easier to test locally.
|
|
// It's not very good, but it's good enough for now.
|
|
let mut p = packet.payload_parser();
|
|
|
|
if p.u8()? != numbers::SSH_MSG_USERAUTH_REQUEST {
|
|
return Err(peer_error!("did not send SSH_MSG_SERVICE_REQUEST"));
|
|
}
|
|
let username = p.utf8_string()?;
|
|
let service_name = p.utf8_string()?;
|
|
let method_name = p.utf8_string()?;
|
|
|
|
if method_name != "none" {
|
|
info!(
|
|
%username,
|
|
%service_name,
|
|
%method_name,
|
|
"User trying to authenticate"
|
|
);
|
|
}
|
|
|
|
if service_name != "ssh-connection" {
|
|
return Err(peer_error!(
|
|
"client tried to unsupported service: {service_name}"
|
|
));
|
|
}
|
|
|
|
match method_name {
|
|
"password" => {
|
|
if !self.options.contains(&AuthOption::Password) {
|
|
self.has_failed = true;
|
|
self.send_failure();
|
|
}
|
|
|
|
let change_password = p.bool()?;
|
|
if change_password {
|
|
return Err(peer_error!("client tried to change password unprompted"));
|
|
}
|
|
let password = p.utf8_string()?;
|
|
|
|
self.server_requests
|
|
.push_back(ServerRequest::VerifyPassword(VerifyPassword {
|
|
user: username.to_owned(),
|
|
password: password.to_owned(),
|
|
}));
|
|
}
|
|
"publickey" => {
|
|
if !self.options.contains(&AuthOption::PublicKey) {
|
|
self.has_failed = true;
|
|
self.send_failure();
|
|
}
|
|
|
|
let has_signature = p.bool()?;
|
|
|
|
let pubkey_alg_name = p.utf8_string()?;
|
|
let public_key_blob = p.string()?;
|
|
|
|
// Whether the client is just checking whether the public key is allowed.
|
|
if !has_signature {
|
|
self.server_requests
|
|
.push_back(ServerRequest::CheckPubkey(CheckPubkey {
|
|
user: username.to_owned(),
|
|
session_identifier: self.session_ident,
|
|
pubkey_alg_name: pubkey_alg_name.to_owned(),
|
|
pubkey: public_key_blob.to_vec(),
|
|
}));
|
|
} else {
|
|
let signature = p.string()?;
|
|
self.server_requests
|
|
.push_back(ServerRequest::VerifySignature(VerifySignature {
|
|
user: username.to_owned(),
|
|
session_identifier: self.session_ident,
|
|
pubkey_alg_name: pubkey_alg_name.to_owned(),
|
|
pubkey: public_key_blob.to_vec(),
|
|
signature: signature.to_vec(),
|
|
}));
|
|
}
|
|
}
|
|
_ if self.has_failed => {
|
|
return Err(peer_error!(
|
|
"client tried unsupported method twice: {method_name}"
|
|
));
|
|
}
|
|
_ => {
|
|
// Initial:
|
|
if let Some(banner) = &self.banner {
|
|
self.queue_packet(Packet::new_msg_userauth_banner(banner.as_bytes(), b""));
|
|
}
|
|
self.send_failure();
|
|
// Stay in the same state
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn pubkey_check_result(&mut self, is_ok: bool, alg: &str, key_blob: &[u8]) {
|
|
if is_ok {
|
|
self.queue_packet(Packet::new_msg_userauth_pk_ok(alg.as_bytes(), key_blob));
|
|
} else {
|
|
self.send_failure();
|
|
// It's ok, don't treat this as a fatal failure.
|
|
}
|
|
}
|
|
|
|
// TODO: improve types with a newtype around an authenticated user
|
|
pub fn verification_result(&mut self, is_ok: bool, user: String) {
|
|
if is_ok {
|
|
self.queue_packet(Packet::new_msg_userauth_success());
|
|
self.is_authenticated = Some(user);
|
|
} else {
|
|
self.send_failure();
|
|
self.has_failed = true;
|
|
}
|
|
}
|
|
|
|
pub fn packets_to_send(&mut self) -> impl Iterator<Item = Packet> + '_ {
|
|
self.packets_to_send.drain(..)
|
|
}
|
|
|
|
pub fn authenticated_user(&self) -> Option<&str> {
|
|
self.is_authenticated.as_deref()
|
|
}
|
|
|
|
pub fn server_requests(&mut self) -> impl Iterator<Item = ServerRequest> + '_ {
|
|
self.server_requests.drain(..)
|
|
}
|
|
|
|
fn send_failure(&mut self) {
|
|
self.queue_packet(Packet::new_msg_userauth_failure(
|
|
NameList(&self.option_list()),
|
|
false,
|
|
));
|
|
}
|
|
|
|
fn option_list(&self) -> String {
|
|
self.options
|
|
.iter()
|
|
.map(|op| match op {
|
|
AuthOption::Password => "password",
|
|
AuthOption::PublicKey => "publickey",
|
|
})
|
|
.collect::<Vec<&str>>()
|
|
.join(",")
|
|
}
|
|
|
|
fn queue_packet(&mut self, packet: Packet) {
|
|
self.packets_to_send.push_back(packet);
|
|
}
|
|
}
|
|
|
|
pub struct ClientAuth {
|
|
username: Vec<u8>,
|
|
packets_to_send: VecDeque<Packet>,
|
|
user_requests: VecDeque<ClientUserRequest>,
|
|
is_authenticated: bool,
|
|
session_identifier: Option<[u8; 32]>,
|
|
}
|
|
|
|
pub enum ClientUserRequest {
|
|
Password,
|
|
PrivateKeySign { session_identifier: [u8; 32] },
|
|
Banner(Vec<u8>),
|
|
}
|
|
|
|
impl ClientAuth {
|
|
pub fn new(username: Vec<u8>) -> Self {
|
|
let mut packets_to_send = VecDeque::new();
|
|
let initial_useruath_req =
|
|
Packet::new_msg_userauth_request_none(&username, b"ssh-connection", b"none");
|
|
packets_to_send.push_back(initial_useruath_req);
|
|
|
|
Self {
|
|
packets_to_send,
|
|
username,
|
|
user_requests: VecDeque::new(),
|
|
is_authenticated: false,
|
|
session_identifier: None,
|
|
}
|
|
}
|
|
|
|
pub fn set_session_identifier(&mut self, ident: [u8; 32]) {
|
|
assert!(self.session_identifier.is_none());
|
|
self.session_identifier = Some(ident);
|
|
}
|
|
|
|
pub fn is_authenticated(&self) -> bool {
|
|
self.is_authenticated
|
|
}
|
|
|
|
pub fn packets_to_send(&mut self) -> impl Iterator<Item = Packet> + '_ {
|
|
self.packets_to_send.drain(..)
|
|
}
|
|
|
|
pub fn user_requests(&mut self) -> impl Iterator<Item = ClientUserRequest> + '_ {
|
|
self.user_requests.drain(..)
|
|
}
|
|
|
|
pub fn send_password(&mut self, password: &str) {
|
|
let packet = Packet::new_msg_userauth_request_password(
|
|
&self.username,
|
|
b"ssh-connection",
|
|
b"password",
|
|
false,
|
|
password.as_bytes(),
|
|
);
|
|
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()");
|
|
|
|
let mut p = packet.payload_parser();
|
|
let packet_type = p.u8()?;
|
|
|
|
match packet_type {
|
|
numbers::SSH_MSG_USERAUTH_BANNER => {
|
|
let banner = p.string()?;
|
|
let _lang = p.string()?;
|
|
|
|
self.user_requests
|
|
.push_back(ClientUserRequest::Banner(banner.to_vec()));
|
|
}
|
|
numbers::SSH_MSG_USERAUTH_FAILURE => {
|
|
let authentications = p.name_list()?;
|
|
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
|
|
.push_back(ClientUserRequest::PrivateKeySign {
|
|
session_identifier: self
|
|
.session_identifier
|
|
.expect("set_session_identifier has not been called"),
|
|
});
|
|
} else {
|
|
return Err(peer_error!(
|
|
"server does not support password authentication"
|
|
));
|
|
}
|
|
}
|
|
numbers::SSH_MSG_USERAUTH_SUCCESS => {
|
|
self.is_authenticated = true;
|
|
}
|
|
_ => {
|
|
return Err(peer_error!(
|
|
"unexpected packet: {}",
|
|
numbers::packet_type_to_string(packet_type)
|
|
))
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|