authentication and start of connection

This commit is contained in:
nora 2024-08-11 18:14:04 +02:00
parent faf2010051
commit 2b2e2ac1f0
6 changed files with 232 additions and 17 deletions

View 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(())
}
}

View file

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

View file

@ -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}");
}
}
}
}
}

View file

@ -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)]

View file

@ -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)
}