diff --git a/ssh-transport/README.md b/ssh-transport/README.md index 7d8983f..a321dd0 100644 --- a/ssh-transport/README.md +++ b/ssh-transport/README.md @@ -6,6 +6,10 @@ 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 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: - [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) diff --git a/ssh-transport/src/channel.rs b/ssh-transport/src/channel.rs new file mode 100644 index 0000000..b7da62c --- /dev/null +++ b/ssh-transport/src/channel.rs @@ -0,0 +1,40 @@ +use tracing::debug; + +use crate::packet::Packet; +use crate::parse::Parser; +use crate::Result; +use crate::client_error; + +pub(crate) struct ServerChannelsState {} + +impl ServerChannelsState { + pub(crate) fn new() -> Self { + ServerChannelsState {} + } + + pub(crate) fn on_packet(&mut self, packet_type: u8, mut payload: Parser<'_>) -> Result<()> { + match packet_type { + Packet::SSH_MSG_CHANNEL_OPEN => { + // + let channel_type = payload.utf8_string()?; + let sender_channel = payload.u32()?; + let initial_window_size = payload.u32()?; + let max_packet_size = payload.u32()?; + + debug!(?channel_type, ?sender_channel, "Opening channel"); + + match channel_type { + "session" => { + todo!("open session") + } + _ => todo!("response with SSH_MSG_CHANNEL_OPEN_FAILURE"), + } + } + _ => { + todo!("{packet_type}"); + } + } + + Ok(()) + } +} diff --git a/ssh-transport/src/keys.rs b/ssh-transport/src/keys.rs index 389289a..288a8f9 100644 --- a/ssh-transport/src/keys.rs +++ b/ssh-transport/src/keys.rs @@ -1,4 +1,4 @@ -use chacha20::cipher::{KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek}; +use chacha20::cipher::{KeyInit, StreamCipher, StreamCipherSeek}; use sha2::Digest; use subtle::ConstantTimeEq; @@ -154,7 +154,10 @@ impl SshChaCha20Poly1305 { fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) { // - let mut cipher = SshChaCha20::new(&self.header_key, &packet_number.to_be_bytes().into()); + let mut cipher = ::new( + &self.header_key, + &packet_number.to_be_bytes().into(), + ); cipher.apply_keystream(bytes); } @@ -208,9 +211,10 @@ impl SshChaCha20Poly1305 { main_cipher.apply_keystream(&mut poly1305_key); // As the first act of encryption, encrypt the length. - // THIS PART IS CORRECT!!! - let mut len_cipher = - SshChaCha20::new(&self.header_key, &packet_number.to_be_bytes().into()); + let mut len_cipher = ::new( + &self.header_key, + &packet_number.to_be_bytes().into(), + ); len_cipher.apply_keystream(&mut bytes[..4]); // Advance ChaCha's block counter to 1 diff --git a/ssh-transport/src/lib.rs b/ssh-transport/src/lib.rs index 0cf39b2..fee639f 100644 --- a/ssh-transport/src/lib.rs +++ b/ssh-transport/src/lib.rs @@ -1,3 +1,4 @@ +mod channel; mod keys; mod packet; mod parse; @@ -5,6 +6,7 @@ mod parse; use core::str; use std::mem::take; +use channel::ServerChannelsState; use ed25519_dalek::ed25519::signature::Signer; use packet::{ DhKeyExchangeInitPacket, DhKeyExchangeInitReplyPacket, KeyExchangeInitPacket, Packet, @@ -13,7 +15,7 @@ use packet::{ use parse::{MpInt, NameList, Parser, Writer}; use rand::RngCore; use sha2::Digest; -use tracing::debug; +use tracing::{debug, info, trace}; use x25519_dalek::{EphemeralSecret, PublicKey}; pub use packet::Msg; @@ -26,6 +28,7 @@ pub enum SshError { ClientError(String), /// Something went wrong on the server. /// The connection should be closed and an error should be logged. + // TODO: does this ever happen? ServerError(eyre::Report), } @@ -64,7 +67,13 @@ enum ServerState { }, ServiceRequest, // At this point we transfer to - UserAuthRequest, + 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 { @@ -130,6 +139,7 @@ impl ServerConnection { self.packet_transport.recv_bytes(bytes)?; while let Some(packet) = self.packet_transport.recv_next_packet() { + trace!(packet_type = ?packet.payload.get(0), packet_len = ?packet.payload.len(), "Received packet"); match &mut self.state { ServerState::ProtoExchange { .. } => unreachable!("handled above"), ServerState::KeyExchangeInit { @@ -328,10 +338,115 @@ impl ServerConnection { writer.finish() }, }); - self.state = ServerState::UserAuthRequest; + self.state = ServerState::UserAuthRequest { has_failed: false }; } - ServerState::UserAuthRequest => { - todo!() + ServerState::UserAuthRequest { has_failed } => { + // 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! + let mut success = Writer::new(); + success.u8(Packet::SSH_MSG_USERAUTH_SUCCESS); + self.packet_transport.queue_packet(Packet { + payload: success.finish(), + }); + self.state = ServerState::ConnectionOpen(ServerChannelsState::new()); + } + "publickey" => { + info!("Got public key"); + // Don't worry queen, your key is correct! + let mut success = Writer::new(); + success.u8(Packet::SSH_MSG_USERAUTH_SUCCESS); + self.packet_transport.queue_packet(Packet { + payload: success.finish(), + }); + self.state = ServerState::ConnectionOpen(ServerChannelsState::new()); + } + _ if *has_failed => { + return Err(client_error!( + "client tried unsupported method twice: {method_name}" + )); + } + _ => { + // Initial. + + let mut banner = Writer::new(); + banner.u8(Packet::SSH_MSG_USERAUTH_BANNER); + banner.string(b"this system ONLY allows catgirls to enter.\r\nall other attempts WILL be prosecuted to the full extent of the rawr!!\r\n"); + banner.string(b"en-US"); + self.packet_transport.queue_packet(Packet { + payload: banner.finish(), + }); + + let mut rejection = Writer::new(); + rejection.u8(Packet::SSH_MSG_USERAUTH_FAILURE); + rejection.name_list(NameList::one("publickey")); + rejection.bool(false); + self.packet_transport.queue_packet(Packet { + payload: rejection.finish(), + }); + // 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)?; + } + Packet::SSH_MSG_GLOBAL_REQUEST => { + let request_name = payload.utf8_string()?; + let want_reply = payload.bool()?; + debug!(?request_name, ?want_reply, "Received global request"); + + let mut failure = Writer::new(); + failure.u8(Packet::SSH_MSG_REQUEST_FAILURE); + //self.packet_transport.queue_packet(Packet { + // payload: failure.finish(), + //}); + } + _ => { + todo!("packet: {packet_type}"); + } + } } } } diff --git a/ssh-transport/src/packet.rs b/ssh-transport/src/packet.rs index 8cc1508..fccb610 100644 --- a/ssh-transport/src/packet.rs +++ b/ssh-transport/src/packet.rs @@ -127,12 +127,56 @@ pub(crate) struct Packet { pub(crate) payload: Vec, } impl Packet { - pub(crate) const SSH_MSG_SERVICE_REQUEST: u8 = 5; - pub(crate) const SSH_MSG_SERVICE_ACCEPT: u8 = 6; - pub(crate) const SSH_MSG_KEXINIT: u8 = 20; - pub(crate) const SSH_MSG_NEWKEYS: u8 = 21; - pub(crate) const SSH_MSG_KEXDH_INIT: u8 = 30; - pub(crate) const SSH_MSG_KEXDH_REPLY: u8 = 31; + // ----- + // Transport layer protocol: + + // 1 to 19 Transport layer generic (e.g., disconnect, ignore, debug, etc.) + pub const SSH_MSG_DISCONNECT: u8 = 1; + pub const SSH_MSG_IGNORE: u8 = 2; + pub const SSH_MSG_UNIMPLEMENTED: u8 = 3; + pub const SSH_MSG_DEBUG: u8 = 4; + pub const SSH_MSG_SERVICE_REQUEST: u8 = 5; + pub const SSH_MSG_SERVICE_ACCEPT: u8 = 6; + + // 20 to 29 Algorithm negotiation + pub const SSH_MSG_KEXINIT: u8 = 20; + pub const SSH_MSG_NEWKEYS: u8 = 21; + + // 30 to 49 Key exchange method specific (numbers can be reused for different authentication methods) + pub const SSH_MSG_KEXDH_INIT: u8 = 30; + pub const SSH_MSG_KEXDH_REPLY: u8 = 31; + + // ----- + // User authentication protocol: + + // 50 to 59 User authentication generic + pub const SSH_MSG_USERAUTH_REQUEST: u8 = 50; + pub const SSH_MSG_USERAUTH_FAILURE: u8 = 51; + pub const SSH_MSG_USERAUTH_SUCCESS: u8 = 52; + pub const SSH_MSG_USERAUTH_BANNER: u8 = 53; + + // 60 to 79 User authentication method specific (numbers can be reused for different authentication methods) + + // ----- + // Connection protocol: + + // 80 to 89 Connection protocol generic + pub const SSH_MSG_GLOBAL_REQUEST: u8 = 80; + pub const SSH_MSG_REQUEST_SUCCESS: u8 = 81; + pub const SSH_MSG_REQUEST_FAILURE: u8 = 82; + + // 90 to 127 Channel related messages + pub const SSH_MSG_CHANNEL_OPEN: u8 = 90; + pub const SSH_MSG_CHANNEL_OPEN_CONFIRMATION: u8 = 91; + pub const SSH_MSG_CHANNEL_OPEN_FAILURE: u8 = 92; + pub const SSH_MSG_CHANNEL_WINDOW_ADJUST: u8 = 93; + pub const SSH_MSG_CHANNEL_DATA: u8 = 94; + pub const SSH_MSG_CHANNEL_EXTENDED_DATA: u8 = 95; + pub const SSH_MSG_CHANNEL_EOF: u8 = 96; + pub const SSH_MSG_CHANNEL_CLOSE: u8 = 97; + pub const SSH_MSG_CHANNEL_REQUEST: u8 = 98; + pub const SSH_MSG_CHANNEL_SUCCESS: u8 = 99; + pub const SSH_MSG_CHANNEL_FAILURE: u8 = 100; pub(crate) fn from_raw(bytes: &[u8]) -> Result { let Some(padding_length) = bytes.first() else { @@ -181,6 +225,10 @@ impl Packet { new } + + pub(crate) fn payload_parser(&self) -> Parser<'_> { + Parser::new(&self.payload) + } } #[derive(Debug, PartialEq)] diff --git a/ssh-transport/src/parse.rs b/ssh-transport/src/parse.rs index a962309..e159e33 100644 --- a/ssh-transport/src/parse.rs +++ b/ssh-transport/src/parse.rs @@ -106,6 +106,10 @@ impl Writer { self.write(data); } + pub(crate) fn bool(&mut self, v: bool) { + self.u8(v as u8); + } + pub(crate) fn finish(self) -> Vec { self.0 } @@ -117,7 +121,7 @@ pub struct NameList<'a>(&'a str); impl<'a> NameList<'a> { pub(crate) fn one(item: &'a str) -> Self { 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) }