mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-15 17:05:05 +01:00
authentication and start of connection
This commit is contained in:
parent
faf2010051
commit
2b2e2ac1f0
6 changed files with 232 additions and 17 deletions
|
|
@ -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)
|
||||
|
|
|
|||
40
ssh-transport/src/channel.rs
Normal file
40
ssh-transport/src/channel.rs
Normal file
|
|
@ -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 => {
|
||||
// <https://datatracker.ietf.org/doc/html/rfc4254#section-5.1>
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
|
||||
let mut cipher = SshChaCha20::new(&self.header_key, &packet_number.to_be_bytes().into());
|
||||
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::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 = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
|
||||
&self.header_key,
|
||||
&packet_number.to_be_bytes().into(),
|
||||
);
|
||||
len_cipher.apply_keystream(&mut bytes[..4]);
|
||||
|
||||
// Advance ChaCha's block counter to 1
|
||||
|
|
|
|||
|
|
@ -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 <https://datatracker.ietf.org/doc/html/rfc4252>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,12 +127,56 @@ pub(crate) struct Packet {
|
|||
pub(crate) payload: Vec<u8>,
|
||||
}
|
||||
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<Self> {
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -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<u8> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue