This commit is contained in:
nora 2024-08-11 23:38:35 +02:00
parent ae5db1642c
commit 0efd08dd5c
12 changed files with 268 additions and 155 deletions

19
Cargo.lock generated
View file

@ -225,7 +225,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"eyre", "eyre",
"hex-literal", "hex-literal",
"ssh-transport", "ssh-protocol",
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@ -663,6 +663,23 @@ dependencies = [
"der", "der",
] ]
[[package]]
name = "ssh-connection"
version = "0.1.0"
dependencies = [
"ssh-transport",
"tracing",
]
[[package]]
name = "ssh-protocol"
version = "0.1.0"
dependencies = [
"ssh-connection",
"ssh-transport",
"tracing",
]
[[package]] [[package]]
name = "ssh-transport" name = "ssh-transport"
version = "0.1.0" version = "0.1.0"

View file

@ -1,5 +1,5 @@
[workspace] [workspace]
members = [ "ssh-transport"] members = ["ssh-connection", "ssh-protocol", "ssh-transport"]
[package] [package]
name = "fakessh" name = "fakessh"
@ -9,7 +9,7 @@ edition = "2021"
[dependencies] [dependencies]
eyre = "0.6.12" eyre = "0.6.12"
hex-literal = "0.4.1" hex-literal = "0.4.1"
ssh-transport = { path = "./ssh-transport" } ssh-protocol = { path = "./ssh-protocol" }
tokio = { version = "1.39.2", features = ["full"] } tokio = { version = "1.39.2", features = ["full"] }
tracing = "0.1.40" tracing = "0.1.40"

View file

@ -7,7 +7,10 @@ use tokio::{
}; };
use tracing::{error, info}; use tracing::{error, info};
use ssh_transport::{ServerConnection, SshStatus, ThreadRngRand}; use ssh_protocol::{
transport::{self, ThreadRngRand},
ServerConnection, SshStatus,
};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
#[tokio::main] #[tokio::main]
@ -53,7 +56,7 @@ async fn handle_connection(next: (TcpStream, SocketAddr)) -> Result<()> {
// } // }
//} //}
let mut state = ServerConnection::new(ThreadRngRand); let mut state = ServerConnection::new(transport::ServerConnection::new(ThreadRngRand));
loop { loop {
let mut buf = [0; 1024]; let mut buf = [0; 1024];
@ -80,8 +83,6 @@ async fn handle_connection(next: (TcpStream, SocketAddr)) -> Result<()> {
} }
} }
while let Some(channel_update) = state.next_channel_update() {}
while let Some(msg) = state.next_msg_to_send() { while let Some(msg) = state.next_msg_to_send() {
conn.write_all(&msg.to_bytes()) conn.write_all(&msg.to_bytes())
.await .await

View file

@ -0,0 +1,8 @@
[package]
name = "ssh-connection"
version = "0.1.0"
edition = "2021"
[dependencies]
ssh-transport = { path = "../ssh-transport" }
tracing = "0.1.40"

5
ssh-connection/README.md Normal file
View file

@ -0,0 +1,5 @@
# ssh-connection
Connection layer for SSH. This crate takes care of channel multiplexing.
Based on [RFC 4254 The Secure Shell (SSH) Connection Protocol](https://datatracker.ietf.org/doc/html/rfc4254).

View file

@ -1,12 +1,11 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::client_error; use ssh_transport::client_error;
use crate::packet::Packet; use ssh_transport::packet::Packet;
use crate::parse::{Parser, Writer}; use ssh_transport::Result;
use crate::Result;
pub(crate) struct ServerChannelsState { pub struct ServerChannelsState {
packets_to_send: VecDeque<Packet>, packets_to_send: VecDeque<Packet>,
channels: Vec<SessionChannel>, channels: Vec<SessionChannel>,
@ -26,13 +25,17 @@ pub struct ChannelUpdate {
pub channel: u32, pub channel: u32,
pub kind: ChannelUpdateKind, pub kind: ChannelUpdateKind,
} }
pub enum ChannelUpdateKind { pub enum ChannelUpdateKind {
ChannelData(Vec<u8>), Create { kind: String, args: Vec<u8> },
Request { kind: String, args: Vec<u8> },
Data { data: Vec<u8> },
ExtendedData { code: u32, data: Vec<u8> },
Eof,
ChannelClosed,
} }
impl ServerChannelsState { impl ServerChannelsState {
pub(crate) fn new() -> Self { pub fn new() -> Self {
ServerChannelsState { ServerChannelsState {
packets_to_send: VecDeque::new(), packets_to_send: VecDeque::new(),
channels: Vec::new(), channels: Vec::new(),
@ -40,14 +43,24 @@ impl ServerChannelsState {
} }
} }
pub(crate) fn on_packet(&mut self, packet_type: u8, mut payload: Parser<'_>) -> Result<()> { pub fn recv_packet(&mut self, packet: Packet) -> Result<()> {
let mut packet = packet.payload_parser();
let packet_type = packet.u8()?;
match packet_type { match packet_type {
Packet::SSH_MSG_GLOBAL_REQUEST => {
let request_name = packet.utf8_string()?;
let want_reply = packet.bool()?;
debug!(?request_name, ?want_reply, "Received global request");
self.packets_to_send
.push_back(Packet::new_msg_request_failure());
}
Packet::SSH_MSG_CHANNEL_OPEN => { Packet::SSH_MSG_CHANNEL_OPEN => {
// <https://datatracker.ietf.org/doc/html/rfc4254#section-5.1> // <https://datatracker.ietf.org/doc/html/rfc4254#section-5.1>
let channel_type = payload.utf8_string()?; let channel_type = packet.utf8_string()?;
let sender_channel = payload.u32()?; let sender_channel = packet.u32()?;
let initial_window_size = payload.u32()?; let initial_window_size = packet.u32()?;
let max_packet_size = payload.u32()?; let max_packet_size = packet.u32()?;
debug!(?channel_type, ?sender_channel, "Opening channel"); debug!(?channel_type, ?sender_channel, "Opening channel");
@ -85,8 +98,8 @@ impl ServerChannelsState {
} }
} }
Packet::SSH_MSG_CHANNEL_DATA => { Packet::SSH_MSG_CHANNEL_DATA => {
let our_channel = payload.u32()?; let our_channel = packet.u32()?;
let data = payload.string()?; let data = packet.string()?;
let channel = self.channel(our_channel)?; let channel = self.channel(our_channel)?;
channel.recv_bytes(data); channel.recv_bytes(data);
@ -108,7 +121,7 @@ impl ServerChannelsState {
} }
Packet::SSH_MSG_CHANNEL_CLOSE => { Packet::SSH_MSG_CHANNEL_CLOSE => {
// <https://datatracker.ietf.org/doc/html/rfc4254#section-5.3> // <https://datatracker.ietf.org/doc/html/rfc4254#section-5.3>
let our_channel = payload.u32()?; let our_channel = packet.u32()?;
let channel = self.channel(our_channel)?; let channel = self.channel(our_channel)?;
if !channel.we_closed { if !channel.we_closed {
let close = Packet::new_msg_channel_close(channel.peer_channel); let close = Packet::new_msg_channel_close(channel.peer_channel);
@ -120,9 +133,9 @@ impl ServerChannelsState {
debug!("Channel has been closed"); debug!("Channel has been closed");
} }
Packet::SSH_MSG_CHANNEL_REQUEST => { Packet::SSH_MSG_CHANNEL_REQUEST => {
let our_channel = payload.u32()?; let our_channel = packet.u32()?;
let request_type = payload.utf8_string()?; let request_type = packet.utf8_string()?;
let want_reply = payload.bool()?; let want_reply = packet.bool()?;
debug!(?our_channel, ?request_type, "Got channel request"); debug!(?our_channel, ?request_type, "Got channel request");
@ -131,12 +144,12 @@ impl ServerChannelsState {
match request_type { match request_type {
"pty-req" => { "pty-req" => {
let term = payload.utf8_string()?; let term = packet.utf8_string()?;
let width_chars = payload.u32()?; let width_chars = packet.u32()?;
let height_rows = payload.u32()?; let height_rows = packet.u32()?;
let _width_px = payload.u32()?; let _width_px = packet.u32()?;
let _height_px = payload.u32()?; let _height_px = packet.u32()?;
let _term_modes = payload.string()?; let _term_modes = packet.string()?;
debug!( debug!(
?our_channel, ?our_channel,
@ -186,7 +199,7 @@ impl ServerChannelsState {
Ok(()) Ok(())
} }
pub(crate) fn packets_to_send(&mut self) -> impl Iterator<Item = Packet> + '_ { pub fn packets_to_send(&mut self) -> impl Iterator<Item = Packet> + '_ {
self.packets_to_send.drain(..) self.packets_to_send.drain(..)
} }

9
ssh-protocol/Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "ssh-protocol"
version = "0.1.0"
edition = "2021"
[dependencies]
ssh-connection = { path = "../ssh-connection" }
ssh-transport = { path = "../ssh-transport" }
tracing = "0.1.40"

5
ssh-protocol/README.md Normal file
View file

@ -0,0 +1,5 @@
# ssh-protocol
Combines `ssh-connection` and `ssh-transport` into a higher level interface.
Also implements authentication based on [RFC 4252 The Secure Shell (SSH) Authentication Protocol](https://datatracker.ietf.org/doc/html/rfc4252).

160
ssh-protocol/src/lib.rs Normal file
View file

@ -0,0 +1,160 @@
pub use ssh_transport as transport;
pub use ssh_transport::{Result, SshStatus};
pub struct ServerConnection {
transport: ssh_transport::ServerConnection,
state: ServerConnectionState,
}
enum ServerConnectionState {
Auth(auth::BadAuth),
Open(ssh_connection::ServerChannelsState),
}
impl ServerConnection {
pub fn new(transport: ssh_transport::ServerConnection) -> Self {
Self {
transport,
state: ServerConnectionState::Auth(auth::BadAuth::new()),
}
}
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
self.transport.recv_bytes(bytes)?;
while let Some(packet) = self.transport.next_plaintext_packet() {
match &mut self.state {
ServerConnectionState::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 = ServerConnectionState::Open(ssh_connection::ServerChannelsState::new());
}
}
ServerConnectionState::Open(con) => {
con.recv_packet(packet)?;
for to_send in con.packets_to_send() {
self.transport.send_plaintext_packet(to_send);
}
}
}
}
Ok(())
}
pub fn next_msg_to_send(&mut self) -> Option<ssh_transport::Msg> {
self.transport.next_msg_to_send()
}
}
/// <https://datatracker.ietf.org/doc/html/rfc4252>
pub mod auth {
use std::collections::VecDeque;
use ssh_transport::{client_error, packet::Packet, parse::NameList, Result};
use tracing::info;
pub struct BadAuth {
has_failed: bool,
packets_to_send: VecDeque<Packet>,
is_authenticated: bool,
}
impl BadAuth {
pub fn new() -> Self {
Self {
has_failed: false,
packets_to_send: VecDeque::new(),
is_authenticated: false,
}
}
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()");
// 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 auth_req = packet.payload_parser();
if auth_req.u8()? != Packet::SSH_MSG_USERAUTH_REQUEST {
return Err(client_error!("did not send SSH_MSG_SERVICE_REQUEST"));
}
let username = auth_req.utf8_string()?;
let service_name = auth_req.utf8_string()?;
let method_name = auth_req.utf8_string()?;
info!(
?username,
?service_name,
?method_name,
"User trying to authenticate"
);
if service_name != "ssh-connection" {
return Err(client_error!(
"client tried to unsupported service: {service_name}"
));
}
match method_name {
"password" => {
let change_password = auth_req.bool()?;
if change_password {
return Err(client_error!("client tried to change password unprompted"));
}
let password = auth_req.utf8_string()?;
info!(?password, "Got password");
// Don't worry queen, your password is correct!
self.queue_packet(Packet::new_msg_userauth_success());
self.is_authenticated = true;
}
"publickey" => {
info!("Got public key");
// Don't worry queen, your key is correct!
self.queue_packet(Packet::new_msg_userauth_success());
self.is_authenticated = true;
}
_ if self.has_failed => {
return Err(client_error!(
"client tried unsupported method twice: {method_name}"
));
}
_ => {
// Initial.
self.queue_packet(Packet::new_msg_userauth_banner(
b"!! this system ONLY allows catgirls to enter !!\r\n\
!! all other attempts WILL be prosecuted to the full extent of the rawr !!\r\n",
b"",
));
self.queue_packet(Packet::new_msg_userauth_failure(
NameList::one("publickey"),
false,
));
// Stay in the same state
}
}
Ok(())
}
pub fn packets_to_send(&mut self) -> impl Iterator<Item = Packet> + '_ {
self.packets_to_send.drain(..)
}
pub fn is_authenticated(&self) -> bool {
self.is_authenticated
}
fn queue_packet(&mut self, packet: Packet) {
self.packets_to_send.push_back(packet);
}
}
}

View file

@ -6,10 +6,6 @@ Based on [RFC 4253 The Secure Shell (SSH) Transport Layer Protocol](https://data
and [RFC 4251 The Secure Shell (SSH) Protocol Architecture](https://datatracker.ietf.org/doc/html/rfc4251) and [RFC 4251 The Secure Shell (SSH) Protocol Architecture](https://datatracker.ietf.org/doc/html/rfc4251)
and [RFC 4250 The Secure Shell (SSH) Protocol Assigned Numbers](https://datatracker.ietf.org/doc/html/rfc4250). and [RFC 4250 The Secure Shell (SSH) Protocol Assigned Numbers](https://datatracker.ietf.org/doc/html/rfc4250).
Also authentication and connection for now.
- [The Secure Shell (SSH) Authentication Protocol](https://datatracker.ietf.org/doc/html/rfc4252)
- [The Secure Shell (SSH) Connection Protocol](https://datatracker.ietf.org/doc/html/rfc4254)
Other relevant RFCs: Other relevant RFCs:
- [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647) - [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647)
- [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656) - [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656)

View file

@ -1,4 +1,3 @@
mod channel;
mod keys; mod keys;
pub mod packet; pub mod packet;
pub mod parse; pub mod parse;
@ -6,7 +5,6 @@ pub mod parse;
use core::str; use core::str;
use std::{collections::VecDeque, mem::take}; use std::{collections::VecDeque, mem::take};
use channel::ServerChannelsState;
use ed25519_dalek::ed25519::signature::Signer; use ed25519_dalek::ed25519::signature::Signer;
use packet::{ use packet::{
DhKeyExchangeInitPacket, DhKeyExchangeInitReplyPacket, KeyExchangeInitPacket, Packet, DhKeyExchangeInitPacket, DhKeyExchangeInitReplyPacket, KeyExchangeInitPacket, Packet,
@ -18,7 +16,6 @@ use sha2::Digest;
use tracing::{debug, info, trace}; use tracing::{debug, info, trace};
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
pub use channel::ChannelUpdate;
pub use packet::Msg; pub use packet::Msg;
#[derive(Debug)] #[derive(Debug)]
@ -52,7 +49,7 @@ pub struct ServerConnection {
packet_transport: PacketTransport, packet_transport: PacketTransport,
rng: Box<dyn SshRng + Send + Sync>, rng: Box<dyn SshRng + Send + Sync>,
channel_updates: VecDeque<ChannelUpdate>, plaintext_packets: VecDeque<Packet>,
} }
enum ServerState { enum ServerState {
@ -72,14 +69,7 @@ enum ServerState {
k: [u8; 32], k: [u8; 32],
}, },
ServiceRequest, ServiceRequest,
// At this point we transfer to <https://datatracker.ietf.org/doc/html/rfc4252> Open,
UserAuthRequest {
/// Whether the client has failed already (by sending the wrong method).
// The second failure results in disconnecting.
has_failed: bool,
},
/// The connection has been opened, all connection-related messages are delegated to the connection handler.
ConnectionOpen(ServerChannelsState),
} }
pub trait SshRng { pub trait SshRng {
@ -121,7 +111,8 @@ impl ServerConnection {
}, },
packet_transport: PacketTransport::new(), packet_transport: PacketTransport::new(),
rng: Box::new(rng), rng: Box::new(rng),
channel_updates: VecDeque::new(),
plaintext_packets: VecDeque::new(),
} }
} }
} }
@ -343,6 +334,7 @@ impl ServerConnection {
self.packet_transport.set_key(h, k); self.packet_transport.set_key(h, k);
} }
ServerState::ServiceRequest => { ServerState::ServiceRequest => {
// TODO: this should probably move out of here? unsure.
if packet.payload.first() != Some(&Packet::SSH_MSG_SERVICE_REQUEST) { if packet.payload.first() != Some(&Packet::SSH_MSG_SERVICE_REQUEST) {
return Err(client_error!("did not send SSH_MSG_SERVICE_REQUEST")); return Err(client_error!("did not send SSH_MSG_SERVICE_REQUEST"));
} }
@ -362,107 +354,10 @@ impl ServerConnection {
writer.finish() writer.finish()
}, },
}); });
self.state = ServerState::UserAuthRequest { has_failed: false }; self.state = ServerState::Open;
} }
ServerState::UserAuthRequest { has_failed } => { ServerState::Open => {
// This is a super simplistic implementation of RFC4252 SSH authentication. self.plaintext_packets.push_back(packet);
// 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 auth_req = packet.payload_parser();
if auth_req.u8()? != Packet::SSH_MSG_USERAUTH_REQUEST {
return Err(client_error!("did not send SSH_MSG_SERVICE_REQUEST"));
}
let username = auth_req.utf8_string()?;
let service_name = auth_req.utf8_string()?;
let method_name = auth_req.utf8_string()?;
info!(
?username,
?service_name,
?method_name,
"User trying to authenticate"
);
if service_name != "ssh-connection" {
return Err(client_error!(
"client tried to unsupported service: {service_name}"
));
}
match method_name {
"password" => {
let change_password = auth_req.bool()?;
if change_password {
return Err(client_error!(
"client tried to change password unprompted"
));
}
let password = auth_req.utf8_string()?;
info!(?password, "Got password");
// Don't worry queen, your password is correct!
self.packet_transport
.queue_packet(Packet::new_msg_userauth_success());
self.state = ServerState::ConnectionOpen(ServerChannelsState::new());
}
"publickey" => {
info!("Got public key");
// Don't worry queen, your key is correct!
self.packet_transport
.queue_packet(Packet::new_msg_userauth_success());
self.state = ServerState::ConnectionOpen(ServerChannelsState::new());
}
_ if *has_failed => {
return Err(client_error!(
"client tried unsupported method twice: {method_name}"
));
}
_ => {
// Initial.
self.packet_transport.queue_packet(Packet::new_msg_userauth_banner(
b"!! this system ONLY allows catgirls to enter !!\r\n\
!! all other attempts WILL be prosecuted to the full extent of the rawr !!\r\n",
b"",
));
self.packet_transport
.queue_packet(Packet::new_msg_userauth_failure(
NameList::one("publickey"),
false,
));
// Stay in the same state
}
}
}
ServerState::ConnectionOpen(con) => {
let mut payload = packet.payload_parser();
let packet_type = payload.u8()?;
match packet_type {
// Connection-related packets
90..128 => {
con.on_packet(packet_type, payload)?;
for packet in con.packets_to_send() {
self.packet_transport.queue_packet(packet);
}
self.channel_updates.extend(con.channel_updates());
}
Packet::SSH_MSG_GLOBAL_REQUEST => {
let request_name = payload.utf8_string()?;
let want_reply = payload.bool()?;
debug!(?request_name, ?want_reply, "Received global request");
self.packet_transport
.queue_packet(Packet::new_msg_request_failure());
}
_ => {
todo!("packet: {packet_type}");
}
}
} }
} }
} }
@ -473,8 +368,12 @@ impl ServerConnection {
self.packet_transport.next_msg_to_send() self.packet_transport.next_msg_to_send()
} }
pub fn next_channel_update(&mut self) -> Option<ChannelUpdate> { pub fn next_plaintext_packet(&mut self) -> Option<Packet> {
self.channel_updates.pop_front() self.plaintext_packets.pop_front()
}
pub fn send_plaintext_packet(&mut self, packet: Packet) {
self.packet_transport.queue_packet(packet);
} }
} }

View file

@ -119,16 +119,16 @@ impl Writer {
pub struct NameList<'a>(&'a str); pub struct NameList<'a>(&'a str);
impl<'a> NameList<'a> { impl<'a> NameList<'a> {
pub(crate) fn one(item: &'a str) -> Self { pub fn one(item: &'a str) -> Self {
if item.contains(',') { if item.contains(',') {
//panic!("tried creating name list with comma in item: {item}"); //panic!("tried creating name list with comma in item: {item}");
} }
Self(item) Self(item)
} }
pub(crate) fn none() -> NameList<'static> { pub fn none() -> NameList<'static> {
NameList("") NameList("")
} }
pub(crate) fn iter(&self) -> std::str::Split<char> { pub fn iter(&self) -> std::str::Split<char> {
self.0.split(',') self.0.split(',')
} }
} }