This commit is contained in:
nora 2024-08-22 21:21:26 +02:00
parent 7ac2ef4194
commit de8f5dde21
34 changed files with 10 additions and 15 deletions

View file

@ -0,0 +1,10 @@
[package]
name = "ssh-agent-client"
version = "0.1.0"
edition = "2021"
[dependencies]
eyre = "0.6.12"
ssh-transport = { path = "../ssh-transport" }
tokio = { version = "1.39.3", features = ["net"] }
tracing.workspace = true

View file

@ -0,0 +1,4 @@
# ssh-agent-client
Client for the SSH agent protocol specified in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent
and https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.agent.

View file

@ -0,0 +1,366 @@
use eyre::{bail, eyre, Context};
use ssh_transport::{
packet::PacketParser,
parse::{Parser, Writer},
SshStatus,
};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tracing::{debug, trace};
/// A message to send to the byte stream.
pub enum Request {
AddIdentity {
key_type: String,
key_contents: Vec<u8>,
key_comment: String,
},
RemoveAllIdentities,
ListIdentities,
Sign {
key_blob: Vec<u8>,
data: Vec<u8>,
flags: u32,
},
Lock {
passphrase: String,
},
Unlock {
passphrase: String,
},
Extension(ExtensionRequest),
}
pub enum ExtensionRequest {
Query,
}
impl Request {
pub fn to_bytes(&self) -> Vec<u8> {
let mut p = Writer::new();
match self {
Self::AddIdentity {
key_type,
key_contents,
key_comment,
} => {
p.u8(numbers::SSH_AGENTC_ADD_IDENTITY);
p.string(key_type.as_bytes());
p.write(&key_contents);
p.string(key_comment.as_bytes());
}
Self::RemoveAllIdentities => p.u8(numbers::SSH_AGENTC_REMOVE_ALL_IDENTITIES),
Self::ListIdentities => p.u8(numbers::SSH_AGENTC_REQUEST_IDENTITIES),
Self::Sign {
key_blob,
data,
flags,
} => {
p.u8(numbers::SSH_AGENTC_SIGN_REQUEST);
p.string(&key_blob);
p.string(&data);
p.u32(*flags);
}
Self::Lock { passphrase } => {
p.u8(numbers::SSH_AGENTC_LOCK);
p.string(passphrase.as_bytes());
}
Self::Unlock { passphrase } => {
p.u8(numbers::SSH_AGENTC_UNLOCK);
p.string(passphrase.as_bytes());
}
Self::Extension(ext) => {
p.u8(numbers::SSH_AGENTC_EXTENSION);
match ext {
ExtensionRequest::Query => {
p.string(b"query");
}
}
}
}
let mut buf = p.finish();
let len = u32::try_from(buf.len()).unwrap();
buf.splice(0..0, len.to_be_bytes());
buf
}
}
/// A server response for an agent message.
#[derive(Debug)]
pub enum ServerResponse {
/// SSH_AGENT_SUCCESS
Success,
/// SSH_AGENT_FAILURE
Failure,
IdentitiesAnswer {
identities: Vec<IdentityAnswer>,
},
/// SSH_AGENT_SIGN_RESPONSE
SignResponse {
signature: Vec<u8>,
},
Extension(ExtensionResponse),
}
#[derive(Debug)]
pub enum ExtensionResponse {
Query { types: Vec<String> },
}
/// A single identity in SSH_AGENT_IDENTITIES_ANSWER.
#[derive(Debug)]
pub struct IdentityAnswer {
pub key_blob: Vec<u8>,
pub comment: String,
}
impl ServerResponse {
pub fn parse(bytes: &[u8]) -> eyre::Result<Self> {
let bytes = &bytes[4..];
let mut p = Parser::new(bytes);
let msg_type = p.u8()?;
trace!(%msg_type, msg_type_str = %numbers::server_response_type_to_string(msg_type), "Received message");
let resp = match msg_type {
numbers::SSH_AGENT_FAILURE => Self::Failure,
numbers::SSH_AGENT_SUCCESS => Self::Success,
numbers::SSH_AGENT_IDENTITIES_ANSWER => {
let nkeys = p.u32()?;
let mut identities = Vec::new();
for _ in 0..nkeys {
let key_blob = p.string()?;
let comment = p.utf8_string()?;
identities.push(IdentityAnswer {
key_blob: key_blob.to_owned(),
comment: comment.to_owned(),
});
}
Self::IdentitiesAnswer { identities }
}
numbers::SSH_AGENT_SIGN_RESPONSE => {
let signature = p.string()?;
Self::SignResponse {
signature: signature.to_owned(),
}
}
numbers::SSH_AGENT_EXTENSION_RESPONSE => {
let ext_type = p.utf8_string()?;
trace!(?ext_type, "Received extension response");
match ext_type {
"query" => {
let mut types = Vec::new();
while p.has_data() {
let name = p.utf8_string()?;
types.push(name.to_owned());
}
Self::Extension(ExtensionResponse::Query { types })
}
_ => bail!("unknown extension response type: {ext_type}"),
}
}
_ => bail!(
"unknown server message: {msg_type} ({})",
numbers::server_response_type_to_string(msg_type)
),
};
Ok(resp)
}
}
pub struct AgentConnection {
packets: PacketParser,
}
impl AgentConnection {
pub fn new() -> Self {
Self {
packets: PacketParser::new(),
}
}
pub fn recv_bytes<'a>(
&'a mut self,
mut bytes: &'a [u8],
) -> impl Iterator<Item = eyre::Result<ServerResponse>> + 'a {
std::iter::from_fn(move || -> Option<eyre::Result<ServerResponse>> {
if bytes.len() == 0 {
return None;
}
match self.packets.recv_plaintext_bytes(bytes) {
Err(err) => Some(Err(match err {
SshStatus::PeerError(err) => eyre!(err),
SshStatus::Disconnect => unreachable!(),
})),
Ok(None) => None,
Ok(Some((consumed, data))) => {
bytes = &bytes[consumed..];
self.packets = PacketParser::new();
Some(ServerResponse::parse(&data))
}
}
})
}
}
pub struct SocketAgentConnection {
conn: AgentConnection,
uds: tokio::net::UnixStream,
}
impl SocketAgentConnection {
pub async fn from_env() -> eyre::Result<Self> {
let sock = std::env::var("SSH_AUTH_SOCK").wrap_err("$SSH_AUTH_SOCK not found")?;
debug!(%sock, "Connecting to SSH agent");
let socket = tokio::net::UnixSocket::new_stream()
.wrap_err("creating unix stream socket")?
.connect(&sock)
.await
.wrap_err_with(|| format!("connecting to Unix stream socket on {sock}"))?;
Ok(Self {
conn: AgentConnection::new(),
uds: socket,
})
}
pub async fn add_identitity(
&mut self,
key_type: &str,
key_contents: &[u8],
key_comment: &str,
) -> eyre::Result<()> {
self.send(Request::AddIdentity {
key_type: key_type.to_owned(),
key_contents: key_contents.to_owned(),
key_comment: key_comment.to_owned(),
})
.await?;
self.generic_response().await
}
pub async fn remove_all_identities(&mut self) -> eyre::Result<()> {
self.send(Request::RemoveAllIdentities).await?;
self.generic_response().await
}
pub async fn list_identities(&mut self) -> eyre::Result<Vec<IdentityAnswer>> {
self.send(Request::ListIdentities).await?;
let resp = self.get_response().await?;
match resp {
ServerResponse::IdentitiesAnswer { identities } => Ok(identities),
_ => bail!("unexpected response: {resp:?}"),
}
}
pub async fn sign(
&mut self,
key_blob: &[u8],
data: &[u8],
flags: u32,
) -> eyre::Result<Vec<u8>> {
self.send(Request::Sign {
key_blob: key_blob.to_owned(),
data: data.to_owned(),
flags,
})
.await?;
let resp = self.get_response().await?;
match resp {
ServerResponse::SignResponse { signature } => Ok(signature),
_ => bail!("unexpected response: {resp:?}"),
}
}
pub async fn lock(&mut self, passphrase: &str) -> eyre::Result<()> {
self.send(Request::Lock {
passphrase: passphrase.to_owned(),
})
.await?;
self.generic_response().await
}
pub async fn unlock(&mut self, passphrase: &str) -> eyre::Result<()> {
self.send(Request::Unlock {
passphrase: passphrase.to_owned(),
})
.await?;
self.generic_response().await
}
pub async fn extension_query(&mut self) -> eyre::Result<Vec<String>> {
self.send(Request::Extension(ExtensionRequest::Query))
.await?;
let resp = self.get_response().await?;
match resp {
ServerResponse::Extension(ExtensionResponse::Query { types }) => Ok(types),
_ => bail!("unexpected response: {resp:?}"),
}
}
async fn generic_response(&mut self) -> eyre::Result<()> {
let resp = self.get_response().await?;
match resp {
ServerResponse::Success => Ok(()),
ServerResponse::Failure => bail!("agent operation failed"),
_ => bail!("unexpected response: {resp:?}"),
}
}
async fn send(&mut self, msg: Request) -> eyre::Result<()> {
self.uds.write_all(&msg.to_bytes()).await?;
Ok(())
}
async fn get_response(&mut self) -> eyre::Result<ServerResponse> {
let mut buf = [0_u8; 1024];
loop {
let read = self.uds.read(&mut buf).await?;
let bytes = &buf[..read];
// In practice, the server won't send more than one packet.
if let Some(resp) = self.conn.recv_bytes(bytes).next() {
return resp;
}
}
}
}
pub mod numbers {
pub const SSH_AGENTC_REQUEST_IDENTITIES: u8 = 11;
pub const SSH_AGENTC_SIGN_REQUEST: u8 = 13;
pub const SSH_AGENTC_ADD_IDENTITY: u8 = 17;
pub const SSH_AGENTC_REMOVE_IDENTITY: u8 = 18;
pub const SSH_AGENTC_REMOVE_ALL_IDENTITIES: u8 = 19;
pub const SSH_AGENTC_ADD_SMARTCARD_KEY: u8 = 20;
pub const SSH_AGENTC_REMOVE_SMARTCARD_KEY: u8 = 21;
pub const SSH_AGENTC_LOCK: u8 = 22;
pub const SSH_AGENTC_UNLOCK: u8 = 23;
pub const SSH_AGENTC_ADD_ID_CONSTRAINED: u8 = 25;
pub const SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED: u8 = 26;
pub const SSH_AGENTC_EXTENSION: u8 = 27;
pub const SSH_AGENT_FAILURE: u8 = 5;
pub const SSH_AGENT_SUCCESS: u8 = 6;
pub const SSH_AGENT_IDENTITIES_ANSWER: u8 = 12;
pub const SSH_AGENT_SIGN_RESPONSE: u8 = 14;
pub const SSH_AGENT_EXTENSION_FAILURE: u8 = 28;
pub const SSH_AGENT_EXTENSION_RESPONSE: u8 = 29;
pub fn server_response_type_to_string(response_type: u8) -> &'static str {
match response_type {
SSH_AGENT_FAILURE => "SSH_AGENT_FAILURE",
SSH_AGENT_SUCCESS => "SSH_AGENT_SUCCESS",
SSH_AGENT_IDENTITIES_ANSWER => "SSH_AGENT_IDENTITIES_ANSWER",
SSH_AGENT_SIGN_RESPONSE => "SSH_AGENT_SIGN_RESPONSE",
SSH_AGENT_EXTENSION_FAILURE => "SSH_AGENT_EXTENSION_FAILURE",
SSH_AGENT_EXTENSION_RESPONSE => "SSH_AGENT_EXTENSION_RESPONSE",
_ => "<unknown>",
}
}
}

View file

@ -0,0 +1,11 @@
[package]
name = "ssh-connection"
version = "0.1.0"
edition = "2021"
[dependencies]
ssh-transport = { path = "../ssh-transport" }
tracing.workspace = true
[dev-dependencies]
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

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

@ -0,0 +1,732 @@
use std::cmp;
use std::collections::{HashMap, VecDeque};
use tracing::{debug, info, trace, warn};
use ssh_transport::packet::Packet;
use ssh_transport::Result;
use ssh_transport::{numbers, peer_error};
/// A channel number (on our side).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChannelNumber(pub u32);
impl std::fmt::Display for ChannelNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
pub struct ChannelsState {
packets_to_send: VecDeque<Packet>,
channel_updates: VecDeque<ChannelUpdate>,
channels: HashMap<ChannelNumber, Channel>,
next_channel_id: ChannelNumber,
is_server: bool,
}
struct Channel {
/// Whether our side has closed this channel.
we_closed: bool,
/// The channel number for the other side.
peer_channel: u32,
/// The current max window size of our peer, controls how many bytes we can still send.
peer_window_size: u32,
/// The max packet size of the peer.
// We need to split our packets if the user requests more.
peer_max_packet_size: u32,
/// For validation only.
our_window_size: u32,
/// For validation only.
our_max_packet_size: u32,
/// By how much we want to increase the window when it gets small.
our_window_size_increase_step: u32,
/// Queued data that we want to send, but have not been able to because of the window limits.
/// Whenever we get more window space, we will send this data.
queued_data: Vec<u8>,
}
/// An update from a channel.
/// The receiver-equivalent of [`ChannelOperation`].
#[derive(Debug)]
pub struct ChannelUpdate {
pub number: ChannelNumber,
pub kind: ChannelUpdateKind,
}
#[derive(Debug)]
pub enum ChannelUpdateKind {
Open(ChannelOpen),
Request(ChannelRequest),
Data { data: Vec<u8> },
ExtendedData { code: u32, data: Vec<u8> },
Eof,
Closed,
}
#[derive(Debug)]
pub enum ChannelOpen {
Session,
}
#[derive(Debug)]
pub enum ChannelRequest {
PtyReq {
want_reply: bool,
term: String,
width_chars: u32,
height_rows: u32,
width_px: u32,
height_px: u32,
term_modes: Vec<u8>,
},
Shell {
want_reply: bool,
},
Exec {
want_reply: bool,
command: Vec<u8>,
},
Env {
want_reply: bool,
name: String,
value: Vec<u8>,
},
ExitStatus {
status: u32,
},
}
impl ChannelNumber {
#[must_use]
pub fn construct_op(self, kind: ChannelOperationKind) -> ChannelOperation {
ChannelOperation { number: self, kind }
}
}
/// An operation to do on a channel.
/// The sender-equivalent of [`ChannelUpdate`].
pub struct ChannelOperation {
pub number: ChannelNumber,
pub kind: ChannelOperationKind,
}
pub enum ChannelOperationKind {
Success,
Failure,
Data(Vec<u8>),
Request(ChannelRequest),
Eof,
Close,
}
impl ChannelsState {
pub fn new(is_server: bool) -> Self {
ChannelsState {
packets_to_send: VecDeque::new(),
channels: HashMap::new(),
channel_updates: VecDeque::new(),
next_channel_id: ChannelNumber(0),
is_server,
}
}
pub fn recv_packet(&mut self, packet: Packet) -> Result<()> {
// TODO: window
let mut packet = packet.payload_parser();
let packet_type = packet.u8()?;
match packet_type {
numbers::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());
}
numbers::SSH_MSG_CHANNEL_OPEN => {
// <https://datatracker.ietf.org/doc/html/rfc4254#section-5.1>
let channel_type = packet.utf8_string()?;
let sender_channel = packet.u32()?;
let initial_window_size = packet.u32()?;
let max_packet_size = packet.u32()?;
debug!(%channel_type, %sender_channel, "Opening channel");
let update_message = match channel_type {
"session" => ChannelOpen::Session,
_ => {
self.packets_to_send
.push_back(Packet::new_msg_channel_open_failure(
sender_channel,
numbers::SSH_OPEN_UNKNOWN_CHANNEL_TYPE,
b"unknown channel type",
b"",
));
return Ok(());
}
};
let our_number = self.next_channel_id;
self.next_channel_id =
ChannelNumber(self.next_channel_id.0.checked_add(1).ok_or_else(|| {
peer_error!("created too many channels, overflowed the counter")
})?);
self.packets_to_send
.push_back(Packet::new_msg_channel_open_confirmation(
sender_channel,
our_number.0,
initial_window_size,
max_packet_size,
));
self.channels.insert(
our_number,
Channel {
we_closed: false,
peer_channel: sender_channel,
peer_max_packet_size: max_packet_size,
peer_window_size: initial_window_size,
our_max_packet_size: max_packet_size,
our_window_size: initial_window_size,
our_window_size_increase_step: initial_window_size,
queued_data: Vec::new(),
},
);
self.channel_updates.push_back(ChannelUpdate {
number: our_number,
kind: ChannelUpdateKind::Open(update_message),
});
debug!(%channel_type, %our_number, "Successfully opened channel");
}
numbers::SSH_MSG_CHANNEL_WINDOW_ADJUST => {
let our_channel = packet.u32()?;
let our_channel = self.validate_channel(our_channel)?;
let bytes_to_add = packet.u32()?;
let channel = self.channel(our_channel)?;
channel.peer_window_size = channel
.peer_window_size
.checked_add(bytes_to_add)
.ok_or_else(|| peer_error!("window size larger than 2^32"))?;
if !channel.queued_data.is_empty() {
let limit =
cmp::min(channel.queued_data.len(), channel.peer_window_size as usize);
let data_to_send = channel.queued_data.splice(..limit, []).collect::<Vec<_>>();
self.send_data(our_channel, &data_to_send);
}
}
numbers::SSH_MSG_CHANNEL_DATA => {
let our_channel = packet.u32()?;
let our_channel = self.validate_channel(our_channel)?;
let data = packet.string()?;
let channel = self.channel(our_channel)?;
channel.our_window_size = channel
.our_window_size
.checked_sub(data.len() as u32)
.ok_or_else(|| {
peer_error!(
"sent more data than the window allows: {} while the window is {}",
data.len(),
channel.our_window_size
)
})?;
if channel.our_max_packet_size < (data.len() as u32) {
return Err(peer_error!(
"data bigger than allowed packet size: {} while the max packet size is {}",
data.len(),
channel.our_max_packet_size
));
}
trace!(channel = %our_channel, window = %channel.our_window_size, "Remaining window on our side");
// We probably want to make this user-controllable in the future.
if channel.our_window_size < 1000 {
let peer = channel.peer_channel;
let bytes_to_add = channel.our_window_size_increase_step;
channel.our_window_size += bytes_to_add;
self.packets_to_send
.push_back(Packet::new_msg_channel_window_adjust(peer, bytes_to_add))
}
self.channel_updates.push_back(ChannelUpdate {
number: our_channel,
kind: ChannelUpdateKind::Data {
data: data.to_owned(),
},
});
}
numbers::SSH_MSG_CHANNEL_EOF => {
// <https://datatracker.ietf.org/doc/html/rfc4254#section-5.3>
let our_channel = packet.u32()?;
let our_channel = self.validate_channel(our_channel)?;
self.channel_updates.push_back(ChannelUpdate {
number: our_channel,
kind: ChannelUpdateKind::Eof,
});
}
numbers::SSH_MSG_CHANNEL_CLOSE => {
// <https://datatracker.ietf.org/doc/html/rfc4254#section-5.3>
let our_channel = packet.u32()?;
let our_channel = self.validate_channel(our_channel)?;
let channel = self.channel(our_channel)?;
if !channel.we_closed {
info!("closeing here");
let close = Packet::new_msg_channel_close(channel.peer_channel);
self.packets_to_send.push_back(close);
}
self.channels.remove(&our_channel);
self.channel_updates.push_back(ChannelUpdate {
number: our_channel,
kind: ChannelUpdateKind::Closed,
});
debug!("Channel has been closed");
}
numbers::SSH_MSG_CHANNEL_REQUEST => {
let our_channel = packet.u32()?;
let our_channel = self.validate_channel(our_channel)?;
let request_type = packet.utf8_string()?;
let want_reply = packet.bool()?;
debug!(channel = %our_channel, %request_type, "Got channel request");
let channel = self.channel(our_channel)?;
let peer_channel = channel.peer_channel;
let channel_request = match request_type {
"pty-req" => {
if !self.is_server {
return Err(peer_error!("server tried to open pty"));
}
let term = packet.utf8_string()?;
let width_chars = packet.u32()?;
let height_rows = packet.u32()?;
let width_px = packet.u32()?;
let height_px = packet.u32()?;
let term_modes = packet.string()?;
debug!(
channel = %our_channel,
%term,
%width_chars,
%height_rows,
"Trying to open a terminal"
);
ChannelRequest::PtyReq {
want_reply,
term: term.to_owned(),
width_chars,
height_rows,
width_px,
height_px,
term_modes: term_modes.to_owned(),
}
}
"shell" => {
if !self.is_server {
return Err(peer_error!("server tried to open shell"));
}
info!(channel = %our_channel, "Opening shell");
ChannelRequest::Shell { want_reply }
}
"exec" => {
if !self.is_server {
return Err(peer_error!("server tried to execute command"));
}
let command = packet.string()?;
info!(channel = %our_channel, command = %String::from_utf8_lossy(command), "Executing command");
ChannelRequest::Exec {
want_reply,
command: command.to_owned(),
}
}
"env" => {
if !self.is_server {
return Err(peer_error!("server tried to set environment var"));
}
let name = packet.utf8_string()?;
let value = packet.string()?;
info!(channel = %our_channel, %name, value = %String::from_utf8_lossy(value), "Setting environment variable");
ChannelRequest::Env {
want_reply,
name: name.to_owned(),
value: value.to_owned(),
}
}
"signal" => {
if !self.is_server {
return Err(peer_error!("server tried to send signal"));
}
debug!(channel = %our_channel, "Received signal");
// Ignore signals, something we can do.
return Ok(());
}
_ => {
warn!(%request_type, channel = %our_channel, "Unknown channel request");
self.send_channel_failure(peer_channel);
return Ok(());
}
};
self.channel_updates.push_back(ChannelUpdate {
number: our_channel,
kind: ChannelUpdateKind::Request(channel_request),
})
}
_ => {
todo!(
"unsupported packet: {} ({packet_type})",
numbers::packet_type_to_string(packet_type)
);
}
}
Ok(())
}
pub fn packets_to_send(&mut self) -> impl Iterator<Item = Packet> + '_ {
self.packets_to_send.drain(..)
}
pub fn next_channel_update(&mut self) -> Option<ChannelUpdate> {
self.channel_updates.pop_front()
}
/// Executes an operation on the channel.
/// If the channel has already been closed, the operation is dropped.
pub fn do_operation(&mut self, op: ChannelOperation) {
op.trace();
let Ok(channel) = self.channel(op.number) else {
debug!(number = %op.number, "Dropping operation as channel does not exist, probably because it has been closed");
return;
};
let peer = channel.peer_channel;
if channel.we_closed {
debug!(number = %op.number, "Dropping operation as channel has been closed already");
return;
}
match op.kind {
ChannelOperationKind::Success => self.send_channel_success(peer),
ChannelOperationKind::Failure => self.send_channel_failure(peer),
ChannelOperationKind::Data(data) => {
self.send_data(op.number, &data);
}
ChannelOperationKind::Request(req) => {
let packet = match req {
ChannelRequest::PtyReq { .. } => todo!("pty-req"),
ChannelRequest::Shell { .. } => todo!("shell"),
ChannelRequest::Exec { .. } => todo!("exec"),
ChannelRequest::Env { .. } => todo!("env"),
ChannelRequest::ExitStatus { status } => {
Packet::new_msg_channel_request_exit_status(
peer,
b"exit-status",
false,
status,
)
}
};
self.packets_to_send.push_back(packet);
}
ChannelOperationKind::Eof => {
self.packets_to_send
.push_back(Packet::new_msg_channel_eof(peer));
}
ChannelOperationKind::Close => {
// <https://datatracker.ietf.org/doc/html/rfc4254#section-5.3>
self.packets_to_send
.push_back(Packet::new_msg_channel_close(peer));
let channel = self.channel(op.number).unwrap();
channel.we_closed = true;
}
}
}
fn send_data(&mut self, channel_number: ChannelNumber, data: &[u8]) {
let channel = self.channel(channel_number).unwrap();
let mut chunks = data.chunks(channel.peer_max_packet_size as usize);
while let Some(data) = chunks.next() {
let channel = self.channel(channel_number).unwrap();
let remaining_window_space_after =
channel.peer_window_size.checked_sub(data.len() as u32);
match remaining_window_space_after {
None => {
let rest = channel.peer_window_size;
let (to_send, to_keep) = data.split_at(rest as usize);
if !to_send.is_empty() {
// Send everything we can, which empties the window.
channel.peer_window_size -= rest;
assert_eq!(channel.peer_window_size, 0);
self.send_data_packet(channel_number, to_send);
}
// It's over, we have exhausted all window space.
// Queue the rest of the bytes.
let channel = self.channel(channel_number).unwrap();
channel.queued_data.extend_from_slice(to_keep);
for data in chunks {
channel.queued_data.extend_from_slice(data);
}
debug!(channel = %channel_number, queue_len = %channel.queued_data.len(), "Exhausted window space, queueing the rest of the data");
return;
}
Some(space) => channel.peer_window_size = space,
}
trace!(channel = %channel_number, window = %channel.peer_window_size, "Remaining window on their side");
self.send_data_packet(channel_number, data);
}
}
/// Send a single data packet.
/// The caller needs to ensure the windowing and packet size requirements are upheld.
fn send_data_packet(&mut self, channel_number: ChannelNumber, data: &[u8]) {
assert!(!data.is_empty(), "Trying to send empty data packet");
trace!(%channel_number, amount = %data.len(), "Sending channel data");
let channel = self.channel(channel_number).unwrap();
let peer = channel.peer_channel;
assert!(channel.peer_max_packet_size >= data.len() as u32);
self.packets_to_send
.push_back(Packet::new_msg_channel_data(peer, data));
}
fn send_channel_success(&mut self, recipient_channel: u32) {
self.packets_to_send
.push_back(Packet::new_msg_channel_success(recipient_channel));
}
fn send_channel_failure(&mut self, recipient_channel: u32) {
self.packets_to_send
.push_back(Packet::new_msg_channel_failure(recipient_channel));
}
fn validate_channel(&self, number: u32) -> Result<ChannelNumber> {
if !self.channels.contains_key(&ChannelNumber(number)) {
return Err(peer_error!("unknown channel: {number}"));
}
Ok(ChannelNumber(number))
}
fn channel(&mut self, number: ChannelNumber) -> Result<&mut Channel> {
self.channels
.get_mut(&number)
.ok_or_else(|| peer_error!("unknown channel: {number:?}"))
}
}
impl ChannelOperation {
/// Logs the attempted operation.
fn trace(&self) {
let kind = match &self.kind {
ChannelOperationKind::Success => "success",
ChannelOperationKind::Failure => "failure",
ChannelOperationKind::Data(_) => "data",
ChannelOperationKind::Request(req) => match req {
ChannelRequest::PtyReq { .. } => "pty-req",
ChannelRequest::Shell { .. } => "shell",
ChannelRequest::Exec { .. } => "exec",
ChannelRequest::Env { .. } => "env",
ChannelRequest::ExitStatus { .. } => "exit-status",
},
ChannelOperationKind::Eof => "eof",
ChannelOperationKind::Close => "close",
};
trace!(number = %self.number, %kind, "Attempt channel operation")
}
}
#[cfg(test)]
mod tests {
use ssh_transport::{numbers, packet::Packet};
use crate::{ChannelNumber, ChannelOperation, ChannelOperationKind, ChannelsState};
/// If a test fails, add this to the test to get logs.
#[allow(dead_code)]
fn init_test_log() {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
}
#[track_caller]
fn assert_response_types(state: &mut ChannelsState, types: &[u8]) {
let response = state
.packets_to_send()
.map(|p| numbers::packet_type_to_string(p.packet_type()))
.collect::<Vec<_>>();
let expected = types
.iter()
.map(|p| numbers::packet_type_to_string(*p))
.collect::<Vec<_>>();
assert_eq!(expected, response);
}
fn open_session_channel(state: &mut ChannelsState) {
state
.recv_packet(Packet::new_msg_channel_open_session(
b"session", 0, 2048, 1024,
))
.unwrap();
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_OPEN_CONFIRMATION]);
}
#[test]
fn interactive_pty() {
let state = &mut ChannelsState::new(true);
open_session_channel(state);
state
.recv_packet(Packet::new_msg_channel_request_pty_req(
0, b"pty-req", true, b"xterm", 80, 24, 0, 0, b"",
))
.unwrap();
state.do_operation(ChannelNumber(0).construct_op(ChannelOperationKind::Success));
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_SUCCESS]);
state
.recv_packet(Packet::new_msg_channel_request_shell(0, b"shell", true))
.unwrap();
state.do_operation(ChannelNumber(0).construct_op(ChannelOperationKind::Success));
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_SUCCESS]);
state
.recv_packet(Packet::new_msg_channel_data(0, b"hello, world"))
.unwrap();
assert_response_types(state, &[]);
state.recv_packet(Packet::new_msg_channel_eof(0)).unwrap();
assert_response_types(state, &[]);
state.recv_packet(Packet::new_msg_channel_close(0)).unwrap();
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_CLOSE]);
}
#[test]
fn only_single_close_for_double_close_operation() {
let state = &mut ChannelsState::new(true);
open_session_channel(state);
state.do_operation(ChannelOperation {
number: ChannelNumber(0),
kind: ChannelOperationKind::Close,
});
state.do_operation(ChannelOperation {
number: ChannelNumber(0),
kind: ChannelOperationKind::Close,
});
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_CLOSE]);
}
#[test]
fn ignore_operation_after_close() {
let mut state = &mut ChannelsState::new(true);
open_session_channel(state);
state.recv_packet(Packet::new_msg_channel_close(0)).unwrap();
assert_response_types(&mut state, &[numbers::SSH_MSG_CHANNEL_CLOSE]);
state.do_operation(ChannelOperation {
number: ChannelNumber(0),
kind: ChannelOperationKind::Data(vec![0]),
});
assert_response_types(state, &[]);
}
#[test]
fn respect_peer_windowing() {
let state = &mut ChannelsState::new(true);
state
.recv_packet(Packet::new_msg_channel_open_session(b"session", 0, 10, 50))
.unwrap();
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_OPEN_CONFIRMATION]);
// Send 100 bytes.
state.do_operation(
ChannelNumber(0)
.construct_op(ChannelOperationKind::Data((0_u8..200).collect::<Vec<_>>())),
);
// 0..10
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_DATA]);
state
.recv_packet(Packet::new_msg_channel_window_adjust(0, 90))
.unwrap();
// 10..60, 60..100
assert_response_types(
state,
&[numbers::SSH_MSG_CHANNEL_DATA, numbers::SSH_MSG_CHANNEL_DATA],
);
state
.recv_packet(Packet::new_msg_channel_window_adjust(0, 100))
.unwrap();
// 100..150, 150..20
assert_response_types(
state,
&[numbers::SSH_MSG_CHANNEL_DATA, numbers::SSH_MSG_CHANNEL_DATA],
);
state
.recv_packet(Packet::new_msg_channel_window_adjust(0, 100))
.unwrap();
assert_response_types(state, &[]);
}
#[test]
fn send_windowing_adjustments() {
let state = &mut ChannelsState::new(true);
state
.recv_packet(Packet::new_msg_channel_open_session(
b"session", 0, 2000, 2000,
))
.unwrap();
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_OPEN_CONFIRMATION]);
state
.recv_packet(Packet::new_msg_channel_data(0, &vec![0; 2000]))
.unwrap();
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_WINDOW_ADJUST]);
// We currently hardcode <1000 for when to send window size adjustments.
state
.recv_packet(Packet::new_msg_channel_data(0, &vec![0; 1000]))
.unwrap();
assert_response_types(state, &[]);
state
.recv_packet(Packet::new_msg_channel_data(0, &vec![0; 1]))
.unwrap();
assert_response_types(state, &[numbers::SSH_MSG_CHANNEL_WINDOW_ADJUST]);
}
}

View file

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

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).

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

@ -0,0 +1,420 @@
use std::mem;
pub use ssh_connection as connection;
use ssh_connection::ChannelOperation;
pub use ssh_connection::{ChannelUpdate, ChannelUpdateKind};
pub use ssh_transport as transport;
pub use ssh_transport::{Result, SshStatus};
use tracing::debug;
pub struct ServerConnection {
transport: ssh_transport::server::ServerConnection,
state: ServerConnectionState,
}
enum ServerConnectionState {
Auth(auth::BadAuth),
Open(ssh_connection::ChannelsState),
}
impl ServerConnection {
pub fn new(transport: ssh_transport::server::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::ChannelsState::new(true));
}
}
ServerConnectionState::Open(con) => {
con.recv_packet(packet)?;
}
}
self.progress();
}
Ok(())
}
pub fn next_msg_to_send(&mut self) -> Option<ssh_transport::Msg> {
self.transport.next_msg_to_send()
}
pub fn next_channel_update(&mut self) -> Option<ssh_connection::ChannelUpdate> {
match &mut self.state {
ServerConnectionState::Auth(_) => None,
ServerConnectionState::Open(con) => con.next_channel_update(),
}
}
pub fn do_operation(&mut self, op: ChannelOperation) {
match &mut self.state {
ServerConnectionState::Auth(_) => panic!("tried to get connection during auth"),
ServerConnectionState::Open(con) => {
con.do_operation(op);
self.progress();
}
}
}
pub fn progress(&mut self) {
match &mut self.state {
ServerConnectionState::Auth(auth) => {
for to_send in auth.packets_to_send() {
self.transport.send_plaintext_packet(to_send);
}
}
ServerConnectionState::Open(con) => {
for to_send in con.packets_to_send() {
self.transport.send_plaintext_packet(to_send);
}
}
}
}
}
pub struct ClientConnection {
transport: ssh_transport::client::ClientConnection,
state: ClientConnectionState,
}
enum ClientConnectionState {
Setup(Option<auth::ClientAuth>),
Auth(auth::ClientAuth),
Open(ssh_connection::ChannelsState),
}
impl ClientConnection {
pub fn new(transport: ssh_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);
for to_send in auth.packets_to_send() {
self.transport.send_plaintext_packet(to_send);
}
debug!("Connection has been opened");
self.state = ClientConnectionState::Auth(auth);
}
}
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(ssh_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 is_open(&self) -> bool {
matches!(self.state, ClientConnectionState::Open(_))
}
pub fn next_msg_to_send(&mut self) -> Option<ssh_transport::Msg> {
self.transport.next_msg_to_send()
}
pub fn next_channel_update(&mut self) -> Option<ssh_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::VecDeque;
use ssh_transport::{numbers, packet::Packet, parse::NameList, peer_error, 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()? != numbers::SSH_MSG_USERAUTH_REQUEST {
return Err(peer_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()?;
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" => {
let change_password = auth_req.bool()?;
if change_password {
return Err(peer_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(peer_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\
!! THIS SYTEM WILL LOG AND STORE YOUR CLEARTEXT PASSWORD !!\r\n\
!! DO NOT ENTER PASSWORDS YOU DON'T WANT STOLEN !!\r\n",
b"",
));
self.queue_packet(Packet::new_msg_userauth_failure(
NameList::one("password"),
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);
}
}
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 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") {
self.user_requests.push_back(ClientUserRequest::Password);
} else if authentications.iter().any(|item| item == "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(())
}
}
}

View file

@ -0,0 +1,24 @@
[package]
name = "ssh-transport"
version = "0.1.0"
edition = "2021"
[dependencies]
aes = "0.8.4"
aes-gcm = "0.10.3"
chacha20 = "0.9.1"
crypto-bigint = "0.5.5"
ctr = "0.9.2"
ed25519-dalek = "2.1.1"
p256 = { version = "0.13.2", features = ["ecdh", "ecdsa"] }
poly1305 = "0.8.0"
rand_core = "0.6.4"
sha2 = "0.10.8"
subtle = "2.6.1"
x25519-dalek = "2.0.1"
tracing.workspace = true
base64 = "0.22.1"
[dev-dependencies]
hex-literal = "0.4.1"

View file

@ -0,0 +1,15 @@
# ssh-transport
Transport layer of SSH.
Based on [RFC 4253 The Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc4253)
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).
Other relevant RFCs:
- [RFC 4344 The Secure Shell (SSH) Transport Layer Encryption Modes](https://datatracker.ietf.org/doc/html/rfc4344)
- [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 6668 SHA-2 Data Integrity Verification for the Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc6668)
- [RFC 8709 Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) Protocol](https://datatracker.ietf.org/doc/html/rfc8709)
- [RFC 8731 Secure Shell (SSH) Key Exchange Method Using Curve25519 and Curve448](https://datatracker.ietf.org/doc/html/rfc8731)

View file

@ -0,0 +1,372 @@
use std::{collections::VecDeque, mem};
use tracing::{debug, info, trace};
use crate::{
crypto::{
self, AlgorithmName, EncodedSshSignature, EncryptionAlgorithm, HostKeySigningAlgorithm,
KeyExchangeSecret, SupportedAlgorithms,
},
numbers,
packet::{Packet, PacketTransport, ProtocolIdentParser},
parse::{NameList, Parser, Writer},
peer_error, Msg, Result, SshRng, SshStatus,
};
pub struct ClientConnection {
state: ClientState,
packet_transport: PacketTransport,
rng: Box<dyn SshRng + Send + Sync>,
plaintext_packets: VecDeque<Packet>,
pub abort_for_dos: bool,
}
enum ClientState {
ProtoExchange {
client_ident: Vec<u8>,
ident_parser: ProtocolIdentParser,
},
KexInit {
client_ident: Vec<u8>,
server_ident: Vec<u8>,
client_kexinit: Vec<u8>,
},
DhKeyInit {
client_ident: Vec<u8>,
server_ident: Vec<u8>,
kex_secret: Option<KeyExchangeSecret>,
server_hostkey_algorithm: HostKeySigningAlgorithm,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
client_kexinit: Vec<u8>,
server_kexinit: Vec<u8>,
},
NewKeys {
h: [u8; 32],
k: Vec<u8>,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
ServiceRequest {
session_identifier: [u8; 32],
},
Open {
session_identifier: [u8; 32],
},
}
impl ClientConnection {
pub fn new(rng: impl SshRng + Send + Sync + 'static) -> Self {
let client_ident = b"SSH-2.0-FakeSSH\r\n".to_vec();
let mut packet_transport = PacketTransport::new();
packet_transport.queue_send_protocol_info(client_ident.clone());
Self {
state: ClientState::ProtoExchange {
ident_parser: ProtocolIdentParser::new(),
client_ident,
},
packet_transport,
rng: Box::new(rng),
plaintext_packets: VecDeque::new(),
abort_for_dos: false,
}
}
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
if let ClientState::ProtoExchange {
ident_parser,
client_ident,
} = &mut self.state
{
ident_parser.recv_bytes(bytes);
if let Some(server_ident) = ident_parser.get_peer_ident() {
let client_ident = mem::take(client_ident);
// This moves to the next state.
self.send_kexinit(client_ident, server_ident);
return Ok(());
}
return Ok(());
}
self.packet_transport.recv_bytes(bytes)?;
while let Some(packet) = self.packet_transport.recv_next_packet() {
let packet_type = packet.payload.first().unwrap_or(&0xFF);
let packet_type_string = numbers::packet_type_to_string(*packet_type);
trace!(%packet_type, %packet_type_string, packet_len = %packet.payload.len(), "Received packet");
// Handle some packets ignoring the state.
match packet.payload.first().copied() {
Some(numbers::SSH_MSG_DISCONNECT) => {
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.1>
let mut disconnect = Parser::new(&packet.payload[1..]);
let reason = disconnect.u32()?;
let description = disconnect.utf8_string()?;
let _language_tag = disconnect.utf8_string()?;
let reason_string =
numbers::disconnect_reason_to_string(reason).unwrap_or("<unknown>");
info!(%reason, %reason_string, %description, "Server disconnecting");
return Err(SshStatus::Disconnect);
}
_ => {}
}
match &mut self.state {
ClientState::ProtoExchange { .. } => unreachable!("handled above"),
ClientState::KexInit {
client_ident,
server_ident,
client_kexinit,
} => {
let mut kexinit = packet.payload_parser();
let packet_type = kexinit.u8()?;
if packet_type != numbers::SSH_MSG_KEXINIT {
return Err(peer_error!(
"expected SSH_MSG_KEXINIT, found {}",
numbers::packet_type_to_string(packet_type)
));
}
let sup_algs = SupportedAlgorithms::secure();
let _cookie = kexinit.array::<16>()?;
let kex_algorithm = kexinit.name_list()?;
let kex_algorithm = sup_algs.key_exchange.find(kex_algorithm.0)?;
debug!(name = %kex_algorithm.name(), "Using KEX algorithm");
let server_hostkey_algorithm = kexinit.name_list()?;
let server_hostkey_algorithm =
sup_algs.hostkey.find(server_hostkey_algorithm.0)?;
debug!(name = %server_hostkey_algorithm.name(), "Using host key algorithm");
let encryption_algorithms_client_to_server = kexinit.name_list()?;
let encryption_client_to_server = sup_algs
.encryption_to_peer
.find(encryption_algorithms_client_to_server.0)?;
debug!(name = %encryption_client_to_server.name(), "Using encryption algorithm C->S");
let encryption_algorithms_server_to_client = kexinit.name_list()?;
let encryption_server_to_client = sup_algs
.encryption_from_peer
.find(encryption_algorithms_server_to_client.0)?;
debug!(name = %encryption_server_to_client.name(), "Using encryption algorithm S->C");
let mac_algorithms_client_to_server = kexinit.name_list()?;
let _mac_client_to_server = sup_algs
.mac_to_peer
.find(mac_algorithms_client_to_server.0)?;
let mac_algorithms_server_to_client = kexinit.name_list()?;
let _mac_server_to_client = sup_algs
.mac_from_peer
.find(mac_algorithms_server_to_client.0)?;
let compression_algorithms_client_to_server = kexinit.name_list()?;
let _compression_client_to_server = sup_algs
.compression_to_peer
.find(compression_algorithms_client_to_server.0)?;
let compression_algorithms_server_to_client = kexinit.name_list()?;
let _compression_server_to_client = sup_algs
.compression_from_peer
.find(compression_algorithms_server_to_client.0)?;
let _languages_client_to_server = kexinit.name_list()?;
let _languages_server_to_client = kexinit.name_list()?;
let first_kex_packet_follows = kexinit.bool()?;
if first_kex_packet_follows {
return Err(peer_error!("does not support guessed kex init packages"));
}
let kex_secret = (kex_algorithm.generate_secret)(&mut *self.rng);
self.packet_transport
.queue_packet(Packet::new_msg_kex_ecdh_init(&kex_secret.pubkey));
self.state = ClientState::DhKeyInit {
client_ident: mem::take(client_ident),
server_ident: mem::take(server_ident),
kex_secret: Some(kex_secret),
server_hostkey_algorithm,
encryption_client_to_server,
encryption_server_to_client,
client_kexinit: mem::take(client_kexinit),
server_kexinit: packet.payload,
};
}
ClientState::DhKeyInit {
client_ident,
server_ident,
kex_secret,
server_hostkey_algorithm,
encryption_client_to_server,
encryption_server_to_client,
client_kexinit,
server_kexinit,
} => {
let mut dh = packet.payload_parser();
let packet_type = dh.u8()?;
if packet_type != numbers::SSH_MSG_KEX_ECDH_REPLY {
return Err(peer_error!(
"expected SSH_MSG_KEX_ECDH_REPLY, found {}",
numbers::packet_type_to_string(packet_type)
));
}
if self.abort_for_dos {
return Err(peer_error!("early abort"));
}
let server_hostkey = dh.string()?;
let server_ephermal_key = dh.string()?;
let signature = dh.string()?;
let kex_secret = mem::take(kex_secret).unwrap();
let shared_secret = (kex_secret.exchange)(server_ephermal_key)?;
// The exchange hash serves as the session identifier.
let hash = crypto::key_exchange_hash(
client_ident,
server_ident,
client_kexinit,
server_kexinit,
server_hostkey,
&kex_secret.pubkey,
server_ephermal_key,
&shared_secret,
);
(server_hostkey_algorithm.verify)(
server_hostkey,
&hash,
&EncodedSshSignature(signature.to_vec()),
)?;
// eprintln!("client_public_key: {:x?}", kex_secret.pubkey);
// eprintln!("server_public_key: {:x?}", server_ephermal_key);
// eprintln!("shared_secret: {:x?}", shared_secret);
// eprintln!("hash: {:x?}", hash);
self.packet_transport.queue_packet(Packet {
payload: vec![numbers::SSH_MSG_NEWKEYS],
});
self.state = ClientState::NewKeys {
h: hash,
k: shared_secret,
encryption_client_to_server: *encryption_client_to_server,
encryption_server_to_client: *encryption_server_to_client,
};
}
ClientState::NewKeys {
h,
k,
encryption_client_to_server,
encryption_server_to_client,
} => {
if packet.payload != [numbers::SSH_MSG_NEWKEYS] {
return Err(peer_error!("did not send SSH_MSG_NEWKEYS"));
}
self.packet_transport.set_key(
*h,
k,
*encryption_client_to_server,
*encryption_server_to_client,
false,
);
debug!("Requesting ssh-userauth service");
self.packet_transport
.queue_packet(Packet::new_msg_service_request(b"ssh-userauth"));
self.state = ClientState::ServiceRequest {
session_identifier: *h,
};
}
ClientState::ServiceRequest { session_identifier } => {
let mut accept = packet.payload_parser();
let packet_type = accept.u8()?;
if packet_type != numbers::SSH_MSG_SERVICE_ACCEPT {
return Err(peer_error!("did not accept service"));
}
let service = accept.utf8_string()?;
if service != "ssh-userauth" {
return Err(peer_error!("server accepted the wrong service: {service}"));
}
debug!("Connection has been opened successfully");
self.state = ClientState::Open {
session_identifier: *session_identifier,
};
}
ClientState::Open { .. } => {
self.plaintext_packets.push_back(packet);
}
}
}
Ok(())
}
pub fn next_msg_to_send(&mut self) -> Option<Msg> {
self.packet_transport.next_msg_to_send()
}
pub fn next_plaintext_packet(&mut self) -> Option<Packet> {
self.plaintext_packets.pop_front()
}
pub fn send_plaintext_packet(&mut self, packet: Packet) {
self.packet_transport.queue_packet(packet);
}
pub fn is_open(&self) -> Option<[u8; 32]> {
match self.state {
ClientState::Open { session_identifier } => Some(session_identifier),
_ => None,
}
}
fn send_kexinit(&mut self, client_ident: Vec<u8>, server_ident: Vec<u8>) {
let mut cookie = [0; 16];
self.rng.fill_bytes(&mut cookie);
let mut kexinit = Writer::new();
kexinit.u8(numbers::SSH_MSG_KEXINIT);
kexinit.array(cookie);
kexinit.name_list(NameList::multi("curve25519-sha256,ecdh-sha2-nistp256")); // kex_algorithms
kexinit.name_list(NameList::multi("ssh-ed25519,ecdsa-sha2-nistp256")); // server_host_key_algorithms
kexinit.name_list(NameList::multi(
"chacha20-poly1305@openssh.com,aes256-gcm@openssh.com",
)); // encryption_algorithms_client_to_server
kexinit.name_list(NameList::multi(
"chacha20-poly1305@openssh.com,aes256-gcm@openssh.com",
)); // encryption_algorithms_server_to_client
kexinit.name_list(NameList::one("hmac-sha2-256")); // mac_algorithms_client_to_server
kexinit.name_list(NameList::one("hmac-sha2-256")); // mac_algorithms_server_to_client
kexinit.name_list(NameList::one("none")); // compression_algorithms_client_to_server
kexinit.name_list(NameList::one("none")); // compression_algorithms_server_to_client
kexinit.name_list(NameList::none()); // languages_client_to_server
kexinit.name_list(NameList::none()); // languages_server_to_client
kexinit.bool(false); // first_kex_packet_follows
kexinit.u32(0); // reserved
let kexinit = kexinit.finish();
self.packet_transport.queue_packet(Packet {
payload: kexinit.clone(),
});
self.state = ClientState::KexInit {
client_ident,
server_ident,
client_kexinit: kexinit,
};
}
}

View file

@ -0,0 +1,523 @@
pub mod encrypt;
use p256::ecdsa::signature::Signer;
use sha2::Digest;
use crate::{
packet::{EncryptedPacket, MsgKind, Packet, RawPacket},
parse::{self, Parser, Writer},
peer_error, Msg, Result, SshRng,
};
pub trait AlgorithmName {
fn name(&self) -> &'static str;
}
// Dummy algorithm.
impl AlgorithmName for &'static str {
fn name(&self) -> &'static str {
self
}
}
#[derive(Clone, Copy)]
pub struct KexAlgorithm {
name: &'static str,
/// Generate an ephemeral key for the exchange.
pub generate_secret: fn(random: &mut (dyn SshRng + Send + Sync)) -> KeyExchangeSecret,
}
impl AlgorithmName for KexAlgorithm {
fn name(&self) -> &'static str {
self.name
}
}
pub struct KeyExchangeSecret {
/// Q_x
pub pubkey: Vec<u8>,
/// Does the exchange, returning the shared secret K.
pub exchange: Box<dyn FnOnce(&[u8]) -> Result<Vec<u8>> + Send + Sync>,
}
/// <https://datatracker.ietf.org/doc/html/rfc8731>
pub const KEX_CURVE_25519_SHA256: KexAlgorithm = KexAlgorithm {
name: "curve25519-sha256",
generate_secret: |rng| {
let secret = x25519_dalek::EphemeralSecret::random_from_rng(crate::SshRngRandAdapter(rng));
let my_public_key = x25519_dalek::PublicKey::from(&secret);
KeyExchangeSecret {
pubkey: my_public_key.as_bytes().to_vec(),
exchange: Box::new(move |peer_public_key| {
let Ok(peer_public_key) = <[u8; 32]>::try_from(peer_public_key) else {
return Err(crate::peer_error!(
"invalid x25519 public key length, should be 32, was: {}",
peer_public_key.len()
));
};
let peer_public_key = x25519_dalek::PublicKey::from(peer_public_key);
let shared_secret = secret.diffie_hellman(&peer_public_key); // K
Ok(shared_secret.as_bytes().to_vec())
}),
}
},
};
/// <https://datatracker.ietf.org/doc/html/rfc5656>
pub const KEX_ECDH_SHA2_NISTP256: KexAlgorithm = KexAlgorithm {
name: "ecdh-sha2-nistp256",
generate_secret: |rng| {
let secret = p256::ecdh::EphemeralSecret::random(&mut crate::SshRngRandAdapter(rng));
let my_public_key = p256::EncodedPoint::from(secret.public_key());
KeyExchangeSecret {
pubkey: my_public_key.as_bytes().to_vec(),
exchange: Box::new(move |peer_public_key| {
let peer_public_key =
p256::PublicKey::from_sec1_bytes(peer_public_key).map_err(|_| {
crate::peer_error!(
"invalid p256 public key length: {}",
peer_public_key.len()
)
})?;
let shared_secret = secret.diffie_hellman(&peer_public_key); // K
Ok(shared_secret.raw_secret_bytes().to_vec())
}),
}
},
};
#[derive(Clone, Copy)]
pub struct EncryptionAlgorithm {
name: &'static str,
iv_size: usize,
key_size: usize,
decrypt_len: fn(state: &mut [u8], bytes: &mut [u8], packet_number: u64),
decrypt_packet: fn(state: &mut [u8], bytes: RawPacket, packet_number: u64) -> Result<Packet>,
encrypt_packet: fn(state: &mut [u8], packet: Packet, packet_number: u64) -> EncryptedPacket,
}
impl AlgorithmName for EncryptionAlgorithm {
fn name(&self) -> &'static str {
self.name
}
}
pub struct EncodedSshPublicHostKey(pub Vec<u8>);
pub struct EncodedSshSignature(pub Vec<u8>);
pub struct HostKeySigningAlgorithm {
name: &'static str,
hostkey_private: Vec<u8>,
public_key: fn(private_key: &[u8]) -> EncodedSshPublicHostKey,
sign: fn(private_key: &[u8], data: &[u8]) -> EncodedSshSignature,
pub verify:
fn(public_key: &[u8], message: &[u8], signature: &EncodedSshSignature) -> Result<()>,
}
impl AlgorithmName for HostKeySigningAlgorithm {
fn name(&self) -> &'static str {
self.name
}
}
impl HostKeySigningAlgorithm {
pub fn sign(&self, data: &[u8]) -> EncodedSshSignature {
(self.sign)(&self.hostkey_private, data)
}
pub fn public_key(&self) -> EncodedSshPublicHostKey {
(self.public_key)(&self.hostkey_private)
}
}
pub fn hostkey_ed25519(hostkey_private: Vec<u8>) -> HostKeySigningAlgorithm {
HostKeySigningAlgorithm {
name: "ssh-ed25519",
hostkey_private,
public_key: |key| {
let key = ed25519_dalek::SigningKey::from_bytes(key.try_into().unwrap());
let public_key = key.verifying_key();
// <https://datatracker.ietf.org/doc/html/rfc8709#section-4>
let mut data = Writer::new();
data.string(b"ssh-ed25519");
data.string(public_key.as_bytes());
EncodedSshPublicHostKey(data.finish())
},
sign: |key, data| {
let key = ed25519_dalek::SigningKey::from_bytes(key.try_into().unwrap());
let signature = key.sign(data);
// <https://datatracker.ietf.org/doc/html/rfc8709#section-6>
let mut data = Writer::new();
data.string(b"ssh-ed25519");
data.string(&signature.to_bytes());
EncodedSshSignature(data.finish())
},
verify: |public_key, message, signature| {
// Parse out public key
let mut public_key = Parser::new(public_key);
let public_key_alg = public_key.string()?;
if public_key_alg != b"ssh-ed25519" {
return Err(peer_error!("incorrect algorithm public host key"));
}
let public_key = public_key.string()?;
let Ok(public_key) = public_key.try_into() else {
return Err(peer_error!("incorrect length for public host key"));
};
let public_key = ed25519_dalek::VerifyingKey::from_bytes(public_key)
.map_err(|err| peer_error!("incorrect public host key: {err}"))?;
// Parse out signature
let mut signature = Parser::new(&signature.0);
let alg = signature.string()?;
if alg != b"ssh-ed25519" {
return Err(peer_error!("incorrect algorithm for signature"));
}
let signature = signature.string()?;
let Ok(signature) = signature.try_into() else {
return Err(peer_error!("incorrect length for signature"));
};
let signature = ed25519_dalek::Signature::from_bytes(signature);
// Verify
public_key
.verify_strict(message, &signature)
.map_err(|err| peer_error!("incorrect signature: {err}"))
},
}
}
pub fn hostkey_ecdsa_sha2_p256(hostkey_private: Vec<u8>) -> HostKeySigningAlgorithm {
HostKeySigningAlgorithm {
name: "ecdsa-sha2-nistp256",
hostkey_private,
public_key: |key| {
let key = p256::ecdsa::SigningKey::from_slice(key).unwrap();
let public_key = key.verifying_key();
let mut data = Writer::new();
// <https://datatracker.ietf.org/doc/html/rfc5656#section-3.1>
data.string(b"ecdsa-sha2-nistp256");
data.string(b"nistp256");
// > point compression MAY be used.
// But OpenSSH does not appear to support that, so let's NOT use it.
data.string(public_key.to_encoded_point(false).as_bytes());
EncodedSshPublicHostKey(data.finish())
},
sign: |key, data| {
let key = p256::ecdsa::SigningKey::from_slice(key).unwrap();
let signature: p256::ecdsa::Signature = key.sign(data);
let (r, s) = signature.split_scalars();
// <https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2>
let mut data = Writer::new();
data.string(b"ecdsa-sha2-nistp256");
let mut signature_blob = Writer::new();
signature_blob.mpint(p256::U256::from(r.as_ref()));
signature_blob.mpint(p256::U256::from(s.as_ref()));
data.string(&signature_blob.finish());
EncodedSshSignature(data.finish())
},
verify: |_public_key, _message, _signature| todo!("ecdsa p256 verification"),
}
}
pub struct AlgorithmNegotiation<T> {
pub supported: Vec<T>,
}
impl<T: AlgorithmName> AlgorithmNegotiation<T> {
pub fn find(mut self, peer_supports: &str) -> Result<T> {
for client_alg in peer_supports.split(',') {
if let Some(alg) = self
.supported
.iter()
.position(|alg| alg.name() == client_alg)
{
return Ok(self.supported.remove(alg));
}
}
Err(peer_error!(
"peer does not support any matching algorithm: peer supports: {peer_supports:?}"
))
}
}
pub struct SupportedAlgorithms {
pub key_exchange: AlgorithmNegotiation<KexAlgorithm>,
pub hostkey: AlgorithmNegotiation<HostKeySigningAlgorithm>,
pub encryption_to_peer: AlgorithmNegotiation<EncryptionAlgorithm>,
pub encryption_from_peer: AlgorithmNegotiation<EncryptionAlgorithm>,
pub mac_to_peer: AlgorithmNegotiation<&'static str>,
pub mac_from_peer: AlgorithmNegotiation<&'static str>,
pub compression_to_peer: AlgorithmNegotiation<&'static str>,
pub compression_from_peer: AlgorithmNegotiation<&'static str>,
}
impl SupportedAlgorithms {
/// A secure default using elliptic curves and AEAD.
pub fn secure() -> Self {
Self {
key_exchange: AlgorithmNegotiation {
supported: vec![KEX_CURVE_25519_SHA256, KEX_ECDH_SHA2_NISTP256],
},
hostkey: AlgorithmNegotiation {
supported: vec![
hostkey_ed25519(crate::server::ED25519_PRIVKEY_BYTES.to_vec()),
hostkey_ecdsa_sha2_p256(crate::server::ECDSA_P256_PRIVKEY_BYTES.to_vec()),
],
},
encryption_to_peer: AlgorithmNegotiation {
supported: vec![encrypt::CHACHA20POLY1305, encrypt::AES256_GCM],
},
encryption_from_peer: AlgorithmNegotiation {
supported: vec![encrypt::CHACHA20POLY1305, encrypt::AES256_GCM],
},
mac_to_peer: AlgorithmNegotiation {
supported: vec!["hmac-sha2-256", "hmac-sha2-256-etm@openssh.com"],
},
mac_from_peer: AlgorithmNegotiation {
supported: vec!["hmac-sha2-256", "hmac-sha2-256-etm@openssh.com"],
},
compression_to_peer: AlgorithmNegotiation {
supported: vec!["none"],
},
compression_from_peer: AlgorithmNegotiation {
supported: vec!["none"],
},
}
}
}
pub(crate) struct Session {
session_id: [u8; 32],
from_peer: Tunnel,
to_peer: Tunnel,
}
struct Tunnel {
/// `key || IV`
state: Vec<u8>,
algorithm: EncryptionAlgorithm,
}
pub(crate) trait Keys: Send + Sync + 'static {
fn decrypt_len(&mut self, bytes: &mut [u8; 4], packet_number: u64);
fn decrypt_packet(&mut self, raw_packet: RawPacket, packet_number: u64) -> Result<Packet>;
fn encrypt_packet_to_msg(&mut self, packet: Packet, packet_number: u64) -> Msg;
fn additional_mac_len(&self) -> usize;
fn rekey(
&mut self,
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
is_server: bool,
) -> Result<(), ()>;
}
pub(crate) struct Plaintext;
impl Keys for Plaintext {
fn decrypt_len(&mut self, _: &mut [u8; 4], _: u64) {}
fn decrypt_packet(&mut self, raw: RawPacket, _: u64) -> Result<Packet> {
Packet::from_full(raw.rest())
}
fn encrypt_packet_to_msg(&mut self, packet: Packet, _: u64) -> Msg {
Msg(MsgKind::PlaintextPacket(packet))
}
fn additional_mac_len(&self) -> usize {
0
}
fn rekey(
&mut self,
_: [u8; 32],
_: &[u8],
_: EncryptionAlgorithm,
_: EncryptionAlgorithm,
_: bool,
) -> Result<(), ()> {
Err(())
}
}
impl Session {
pub(crate) fn new(
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
is_server: bool,
) -> Self {
Self::from_keys(
h,
h,
k,
encryption_client_to_server,
encryption_server_to_client,
is_server,
)
}
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
fn from_keys(
session_id: [u8; 32],
h: [u8; 32],
k: &[u8],
alg_c2s: EncryptionAlgorithm,
alg_s2c: EncryptionAlgorithm,
is_server: bool,
) -> Self {
let c2s = Tunnel {
algorithm: alg_c2s,
state: {
let mut state = derive_key(k, h, "C", session_id, alg_c2s.key_size);
let iv = derive_key(k, h, "A", session_id, alg_c2s.iv_size);
state.extend_from_slice(&iv);
state
},
};
let s2c = Tunnel {
algorithm: alg_s2c,
state: {
let mut state = derive_key(k, h, "D", session_id, alg_s2c.key_size);
state.extend_from_slice(&derive_key(k, h, "B", session_id, alg_s2c.iv_size));
state
},
};
let (from_peer, to_peer) = if is_server { (c2s, s2c) } else { (s2c, c2s) };
Self {
session_id,
from_peer,
to_peer,
// integrity_key_client_to_server: derive("E").into(),
// integrity_key_server_to_client: derive("F").into(),
}
}
}
impl Keys for Session {
fn decrypt_len(&mut self, bytes: &mut [u8; 4], packet_number: u64) {
(self.from_peer.algorithm.decrypt_len)(&mut self.from_peer.state, bytes, packet_number);
}
fn decrypt_packet(&mut self, bytes: RawPacket, packet_number: u64) -> Result<Packet> {
(self.from_peer.algorithm.decrypt_packet)(&mut self.from_peer.state, bytes, packet_number)
}
fn encrypt_packet_to_msg(&mut self, packet: Packet, packet_number: u64) -> Msg {
let packet =
(self.to_peer.algorithm.encrypt_packet)(&mut self.to_peer.state, packet, packet_number);
Msg(MsgKind::EncryptedPacket(packet))
}
fn additional_mac_len(&self) -> usize {
poly1305::BLOCK_SIZE
}
fn rekey(
&mut self,
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
is_server: bool,
) -> Result<(), ()> {
*self = Self::from_keys(
self.session_id,
h,
k,
encryption_client_to_server,
encryption_server_to_client,
is_server,
);
Ok(())
}
}
/// Derive a key from the shared secret K and exchange hash H.
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
fn derive_key(
k: &[u8],
h: [u8; 32],
letter: &str,
session_id: [u8; 32],
key_size: usize,
) -> Vec<u8> {
let sha2len = sha2::Sha256::output_size();
let padded_key_size = key_size.next_multiple_of(sha2len);
let mut output = vec![0; padded_key_size];
for i in 0..(padded_key_size / sha2len) {
let mut hash = <sha2::Sha256 as sha2::Digest>::new();
encode_mpint_for_hash(k, |data| hash.update(data));
hash.update(h);
if i == 0 {
hash.update(letter.as_bytes());
hash.update(session_id);
} else {
hash.update(&output[..(i * sha2len)]);
}
output[(i * sha2len)..][..sha2len].copy_from_slice(&hash.finalize())
}
output.truncate(key_size);
output
}
pub(crate) fn encode_mpint_for_hash(key: &[u8], mut add_to_hash: impl FnMut(&[u8])) {
let (key, pad_zero) = parse::fixup_mpint(key);
add_to_hash(&u32::to_be_bytes((key.len() + (pad_zero as usize)) as u32));
if pad_zero {
add_to_hash(&[0]);
}
add_to_hash(key);
}
pub fn key_exchange_hash(
client_ident: &[u8],
server_ident: &[u8],
client_kexinit: &[u8],
server_kexinit: &[u8],
server_hostkey: &[u8],
eph_client_public_key: &[u8],
eph_server_public_key: &[u8],
shared_secret: &[u8],
) -> [u8; 32] {
let mut hash = sha2::Sha256::new();
let add_hash = |hash: &mut sha2::Sha256, bytes: &[u8]| {
hash.update(bytes);
};
let hash_string = |hash: &mut sha2::Sha256, bytes: &[u8]| {
add_hash(hash, &u32::to_be_bytes(bytes.len() as u32));
add_hash(hash, bytes);
};
let hash_mpint = |hash: &mut sha2::Sha256, bytes: &[u8]| {
encode_mpint_for_hash(bytes, |data| add_hash(hash, data));
};
// Strip the \r\n
hash_string(&mut hash, &client_ident[..(client_ident.len() - 2)]); // V_C
hash_string(&mut hash, &server_ident[..(server_ident.len() - 2)]); // V_S
hash_string(&mut hash, client_kexinit); // I_C
hash_string(&mut hash, server_kexinit); // I_S
hash_string(&mut hash, server_hostkey); // K_S
// For normal DH as in RFC4253, e and f are mpints.
// But for ECDH as defined in RFC5656, Q_C and Q_S are strings.
// <https://datatracker.ietf.org/doc/html/rfc5656#section-4>
hash_string(&mut hash, eph_client_public_key); // Q_C
hash_string(&mut hash, eph_server_public_key); // Q_S
hash_mpint(&mut hash, shared_secret); // K
let hash = hash.finalize();
hash.into()
}

View file

@ -0,0 +1,253 @@
use crate::Result;
use aes_gcm::{aead::AeadMutInPlace, KeyInit};
use chacha20::cipher::{StreamCipher, StreamCipherSeek};
use subtle::ConstantTimeEq;
use crate::packet::{EncryptedPacket, Packet, RawPacket};
use super::EncryptionAlgorithm;
pub const CHACHA20POLY1305: EncryptionAlgorithm = EncryptionAlgorithm {
name: "chacha20-poly1305@openssh.com",
iv_size: 0,
key_size: 64, // 32 for header, 32 for main
decrypt_len: |state, bytes, packet_number| {
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
alg.decrypt_len(bytes, packet_number)
},
decrypt_packet: |state, bytes, packet_number| {
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
alg.decrypt_packet(bytes, packet_number)
},
encrypt_packet: |state, packet, packet_number| {
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
alg.encrypt_packet(packet, packet_number)
},
};
pub const AES256_GCM: EncryptionAlgorithm = EncryptionAlgorithm {
name: "aes256-gcm@openssh.com",
iv_size: 12,
key_size: 32,
decrypt_len: |state, bytes, packet_number| {
let mut alg = Aes256GcmOpenSsh::from_state(state);
alg.decrypt_len(bytes, packet_number)
},
decrypt_packet: |state, bytes, packet_number| {
let mut alg = Aes256GcmOpenSsh::from_state(state);
alg.decrypt_packet(bytes, packet_number)
},
encrypt_packet: |state, packet, packet_number| {
let mut alg = Aes256GcmOpenSsh::from_state(state);
alg.encrypt_packet(packet, packet_number)
},
};
/// RFC 4344 AES128 in counter mode.
/// <https://datatracker.ietf.org/doc/html/rfc4344#section-4>
pub const ENC_AES128_CTR: EncryptionAlgorithm = EncryptionAlgorithm {
name: "aes128-ctr",
iv_size: 12,
key_size: 32,
decrypt_len: |state, bytes, packet_number| {
let mut alg = Aes128Ctr::from_state(state);
alg.decrypt_len(bytes, packet_number)
},
decrypt_packet: |state, bytes, packet_number| {
let mut state = Aes128Ctr::from_state(state);
state.decrypt_packet(bytes, packet_number)
},
encrypt_packet: |state, packet, packet_number| {
let mut state = Aes128Ctr::from_state(state);
state.encrypt_packet(packet, packet_number)
},
};
/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version.
type SshChaCha20 = chacha20::ChaCha20Legacy;
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
struct ChaCha20Poly1305OpenSsh {
header_key: chacha20::Key,
main_key: chacha20::Key,
}
impl ChaCha20Poly1305OpenSsh {
fn from_state(keys: &[u8]) -> Self {
assert_eq!(keys.len(), 64);
Self {
main_key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
header_key: <[u8; 32]>::try_from(&keys[32..]).unwrap().into(),
}
}
fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) {
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
&self.header_key,
&packet_number.to_be_bytes().into(),
);
cipher.apply_keystream(bytes);
}
fn decrypt_packet(&self, mut bytes: RawPacket, packet_number: u64) -> Result<Packet> {
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
&self.main_key,
&packet_number.to_be_bytes().into(),
);
let tag_offset = bytes.full_packet().len() - 16;
let authenticated = &bytes.full_packet()[..tag_offset];
let mac = {
let mut poly1305_key = [0; poly1305::KEY_SIZE];
cipher.apply_keystream(&mut poly1305_key);
poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(authenticated)
};
let read_tag = poly1305::Tag::from_slice(&bytes.full_packet()[tag_offset..]);
if !bool::from(mac.ct_eq(read_tag)) {
return Err(crate::peer_error!(
"failed to decrypt: invalid poly1305 MAC"
));
}
// Advance ChaCha's block counter to 1
cipher
.seek(<chacha20::ChaCha20LegacyCore as chacha20::cipher::BlockSizeUser>::block_size());
let encrypted_packet_content = bytes.content_mut();
cipher.apply_keystream(encrypted_packet_content);
Packet::from_full(encrypted_packet_content)
}
fn encrypt_packet(&self, packet: Packet, packet_number: u64) -> EncryptedPacket {
let mut bytes = packet.to_bytes(false, Packet::DEFAULT_BLOCK_SIZE);
// Prepare the main cipher.
let mut main_cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
&self.main_key,
&packet_number.to_be_bytes().into(),
);
// Get the poly1305 key first, but don't use it yet!
// We encrypt-then-mac.
let mut poly1305_key = [0; poly1305::KEY_SIZE];
main_cipher.apply_keystream(&mut poly1305_key);
// As the first act of encryption, encrypt the length.
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
main_cipher
.seek(<chacha20::ChaCha20LegacyCore as chacha20::cipher::BlockSizeUser>::block_size());
// Encrypt the content of the packet, excluding the length and the MAC, which is not pushed yet.
main_cipher.apply_keystream(&mut bytes[4..]);
// Now, MAC the length || content, and push that to the end.
let mac = poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(&bytes);
bytes.extend_from_slice(mac.as_slice());
EncryptedPacket::from_encrypted_full_bytes(bytes)
}
}
/// <https://datatracker.ietf.org/doc/html/rfc5647>
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL#L188C49-L188C64>
struct Aes256GcmOpenSsh<'a> {
key: aes_gcm::Key<aes_gcm::Aes256Gcm>,
nonce: &'a mut [u8; 12],
}
impl<'a> Aes256GcmOpenSsh<'a> {
fn from_state(keys: &'a mut [u8]) -> Self {
assert_eq!(keys.len(), 44);
Self {
key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
nonce: <&mut [u8; 12]>::try_from(&mut keys[32..]).unwrap(),
}
}
fn decrypt_len(&mut self, _: &mut [u8], _: u64) {
// AES-GCM does not encrypt the length.
// <https://datatracker.ietf.org/doc/html/rfc5647#section-7.3>
}
fn decrypt_packet(&mut self, mut bytes: RawPacket, _packet_number: u64) -> Result<Packet> {
let mut cipher = aes_gcm::Aes256Gcm::new(&self.key);
let mut len = [0; 4];
len.copy_from_slice(&bytes.full_packet()[..4]);
let tag_offset = bytes.full_packet().len() - 16;
let mut tag = [0; 16];
tag.copy_from_slice(&bytes.full_packet()[tag_offset..]);
let encrypted_packet_content = bytes.content_mut();
cipher
.decrypt_in_place_detached(
(&*self.nonce).into(),
&len,
encrypted_packet_content,
(&tag).into(),
)
.map_err(|_| crate::peer_error!("failed to decrypt: invalid GCM MAC"))?;
self.inc_nonce();
Packet::from_full(encrypted_packet_content)
}
fn encrypt_packet(&mut self, packet: Packet, _packet_number: u64) -> EncryptedPacket {
let mut bytes = packet.to_bytes(
false,
<aes_gcm::aes::Aes256 as aes_gcm::aes::cipher::BlockSizeUser>::block_size() as u8,
);
let mut cipher = aes_gcm::Aes256Gcm::new(&self.key);
let (aad, plaintext) = bytes.split_at_mut(4);
let tag = cipher
.encrypt_in_place_detached((&*self.nonce).into(), aad, plaintext)
.unwrap();
bytes.extend_from_slice(&tag);
self.inc_nonce();
EncryptedPacket::from_encrypted_full_bytes(bytes)
}
fn inc_nonce(&mut self) {
let mut carry = 1;
for i in (0..self.nonce.len()).rev() {
let n = self.nonce[i] as u16 + carry;
self.nonce[i] = n as u8;
carry = n >> 8;
}
}
}
struct Aes128Ctr {
_key: ctr::Ctr128BE<aes::Aes128>,
}
impl Aes128Ctr {
fn from_state(_keys: &mut [u8]) -> Self {
todo!()
}
fn decrypt_len(&mut self, _: &mut [u8], _: u64) {}
fn decrypt_packet(&mut self, _bytes: RawPacket, _packet_number: u64) -> Result<Packet> {
todo!()
}
fn encrypt_packet(&mut self, _packet: Packet, _packet_number: u64) -> EncryptedPacket {
todo!()
}
}

View file

@ -0,0 +1,59 @@
//! Operations on SSH keys.
use std::fmt::Display;
use base64::Engine;
use crate::parse::{self, ParseError, Parser, Writer};
pub enum SshPubkey {
Ed25519 { public_key: [u8; 32] },
}
impl SshPubkey {
/// Parses an SSH public key from its wire encoding as specified in
/// RFC4253, RFC5656, and RFC8709.
pub fn from_wire_encoding(bytes: &[u8]) -> parse::Result<Self> {
let mut p = Parser::new(bytes);
let alg = p.utf8_string()?;
let k = match alg {
"ssh-ed25519" => {
let len = p.u32()?;
if len != 32 {
return Err(ParseError(format!("incorrect ed25519 len: {len}")));
}
let public_key = p.array::<32>()?;
Self::Ed25519 { public_key }
}
_ => return Err(ParseError(format!("unsupported key type: {alg}"))),
};
Ok(k)
}
pub fn to_wire_encoding(&self) -> Vec<u8> {
let mut p = Writer::new();
match self {
Self::Ed25519 { public_key } => {
p.string(b"ssh-ed25519");
p.string(public_key);
}
}
p.finish()
}
}
impl Display for SshPubkey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ed25519 { .. } => {
let encoded_pubkey = b64encode(&self.to_wire_encoding());
write!(f, "ssh-ed25519 {encoded_pubkey}")
}
}
}
}
fn b64encode(bytes: &[u8]) -> String {
base64::prelude::BASE64_STANDARD_NO_PAD.encode(bytes)
}

View file

@ -0,0 +1,53 @@
pub mod client;
mod crypto;
pub mod key;
pub mod numbers;
pub mod packet;
pub mod parse;
pub mod server;
pub use packet::Msg;
#[derive(Debug)]
pub enum SshStatus {
/// The client has sent a disconnect request, close the connection.
/// This is not an error.
Disconnect,
/// The peer did something wrong.
/// The connection should be closed and a notice may be logged,
/// but this does not require operator intervention.
PeerError(String),
}
pub type Result<T, E = SshStatus> = std::result::Result<T, E>;
pub trait SshRng {
fn fill_bytes(&mut self, dest: &mut [u8]);
}
struct SshRngRandAdapter<'a>(&'a mut dyn SshRng);
impl rand_core::CryptoRng for SshRngRandAdapter<'_> {}
impl rand_core::RngCore for SshRngRandAdapter<'_> {
fn next_u32(&mut self) -> u32 {
self.next_u64() as u32
}
fn next_u64(&mut self) -> u64 {
rand_core::impls::next_u64_via_fill(self)
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> std::result::Result<(), rand_core::Error> {
self.fill_bytes(dest);
Ok(())
}
}
#[macro_export]
macro_rules! peer_error {
($($tt:tt)*) => {
$crate::SshStatus::PeerError(::std::format!($($tt)*))
};
}

View file

@ -0,0 +1,143 @@
//! Constants for SSH.
//! <https://datatracker.ietf.org/doc/html/rfc4250>
// -----
// 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_KEX_ECDH_INIT: u8 = 30; // Same number
pub const SSH_MSG_KEXDH_REPLY: u8 = 31;
pub const SSH_MSG_KEX_ECDH_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 fn packet_type_to_string(packet_type: u8) -> &'static str {
match packet_type {
1 => "SSH_MSG_DISCONNECT",
2 => "SSH_MSG_IGNORE",
3 => "SSH_MSG_UNIMPLEMENTED",
4 => "SSH_MSG_DEBUG",
5 => "SSH_MSG_SERVICE_REQUEST",
6 => "SSH_MSG_SERVICE_ACCEPT",
20 => "SSH_MSG_KEXINIT",
21 => "SSH_MSG_NEWKEYS",
30 => "SSH_MSG_KEX_ECDH_INIT",
31 => "SSH_MSG_KEX_ECDH_REPLY",
50 => "SSH_MSG_USERAUTH_REQUEST",
51 => "SSH_MSG_USERAUTH_FAILURE",
52 => "SSH_MSG_USERAUTH_SUCCESS",
53 => "SSH_MSG_USERAUTH_BANNER",
80 => "SSH_MSG_GLOBAL_REQUEST",
81 => "SSH_MSG_REQUEST_SUCCESS",
82 => "SSH_MSG_REQUEST_FAILURE",
90 => "SSH_MSG_CHANNEL_OPEN",
91 => "SSH_MSG_CHANNEL_OPEN_CONFIRMATION",
92 => "SSH_MSG_CHANNEL_OPEN_FAILURE",
93 => "SSH_MSG_CHANNEL_WINDOW_ADJUST",
94 => "SSH_MSG_CHANNEL_DATA",
95 => "SSH_MSG_CHANNEL_EXTENDED_DATA",
96 => "SSH_MSG_CHANNEL_EOF",
97 => "SSH_MSG_CHANNEL_CLOSE",
98 => "SSH_MSG_CHANNEL_REQUEST",
99 => "SSH_MSG_CHANNEL_SUCCESS",
100 => "SSH_MSG_CHANNEL_FAILURE",
_ => "<unknown>",
}
}
pub const SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT: u32 = 1;
pub const SSH_DISCONNECT_PROTOCOL_ERROR: u32 = 2;
pub const SSH_DISCONNECT_KEY_EXCHANGE_FAILED: u32 = 3;
pub const SSH_DISCONNECT_RESERVED: u32 = 4;
pub const SSH_DISCONNECT_MAC_ERROR: u32 = 5;
pub const SSH_DISCONNECT_COMPRESSION_ERROR: u32 = 6;
pub const SSH_DISCONNECT_SERVICE_NOT_AVAILABLE: u32 = 7;
pub const SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED: u32 = 8;
pub const SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE: u32 = 9;
pub const SSH_DISCONNECT_CONNECTION_LOST: u32 = 10;
pub const SSH_DISCONNECT_BY_APPLICATION: u32 = 11;
pub const SSH_DISCONNECT_TOO_MANY_CONNECTIONS: u32 = 12;
pub const SSH_DISCONNECT_AUTH_CANCELLED_BY_USER: u32 = 13;
pub const SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE: u32 = 14;
pub const SSH_DISCONNECT_ILLEGAL_USER_NAME: u32 = 15;
pub fn disconnect_reason_to_string(reason: u32) -> Option<&'static str> {
Some(match reason {
1 => "SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT",
2 => "SSH_DISCONNECT_PROTOCOL_ERROR",
3 => "SSH_DISCONNECT_KEY_EXCHANGE_FAILED",
4 => "SSH_DISCONNECT_RESERVED",
5 => "SSH_DISCONNECT_MAC_ERROR",
6 => "SSH_DISCONNECT_COMPRESSION_ERROR",
7 => "SSH_DISCONNECT_SERVICE_NOT_AVAILABLE",
8 => "SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED",
9 => "SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE",
10 => "SSH_DISCONNECT_CONNECTION_LOST",
11 => "SSH_DISCONNECT_BY_APPLICATION",
12 => "SSH_DISCONNECT_TOO_MANY_CONNECTIONS",
13 => "SSH_DISCONNECT_AUTH_CANCELLED_BY_USER",
14 => "SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE",
15 => "SSH_DISCONNECT_ILLEGAL_USER_NAME",
_ => return None,
})
}
pub const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: u32 = 1;
pub const SSH_OPEN_CONNECT_FAILED: u32 = 2;
pub const SSH_OPEN_UNKNOWN_CHANNEL_TYPE: u32 = 3;
pub const SSH_OPEN_RESOURCE_SHORTAGE: u32 = 4;
pub fn channel_connection_failure_to_string(reason: u32) -> Option<&'static str> {
Some(match reason {
1 => "SSH_OPEN_ADMINISTRATIVELY_PROHIBITED",
2 => "SSH_OPEN_CONNECT_FAILED",
3 => "SSH_OPEN_UNKNOWN_CHANNEL_TYPE",
4 => "SSH_OPEN_RESOURCE_SHORTAGE",
_ => return None,
})
}
pub const SSH_EXTENDED_DATA_STDERR: u32 = 1;

View file

@ -0,0 +1,540 @@
mod ctors;
use std::collections::VecDeque;
use std::mem;
use tracing::{debug, trace};
use crate::crypto::{self, EncryptionAlgorithm, Keys, Plaintext, Session};
use crate::parse::{NameList, Parser, Writer};
use crate::Result;
use crate::{numbers, peer_error};
/// Frames the byte stream into packets.
pub(crate) struct PacketTransport {
keys: Box<dyn Keys>,
recv_next_packet: PacketParser,
recv_packets: VecDeque<Packet>,
recv_next_seq_nr: u64,
msgs_to_send: VecDeque<Msg>,
send_next_seq_nr: u64,
}
#[derive(Debug)]
pub struct Msg(pub(crate) MsgKind);
#[derive(Debug, PartialEq)]
pub(crate) enum MsgKind {
ServerProtocolInfo(Vec<u8>),
PlaintextPacket(Packet),
EncryptedPacket(EncryptedPacket),
}
impl Msg {
pub fn to_bytes(self) -> Vec<u8> {
match self.0 {
MsgKind::ServerProtocolInfo(v) => v,
MsgKind::PlaintextPacket(v) => v.to_bytes(true, Packet::DEFAULT_BLOCK_SIZE),
MsgKind::EncryptedPacket(v) => v.into_bytes(),
}
}
}
impl PacketTransport {
pub(crate) fn new() -> Self {
PacketTransport {
keys: Box::new(Plaintext),
recv_next_packet: PacketParser::new(),
recv_packets: VecDeque::new(),
recv_next_seq_nr: 0,
msgs_to_send: VecDeque::new(),
send_next_seq_nr: 0,
}
}
pub(crate) fn recv_bytes(&mut self, mut bytes: &[u8]) -> Result<()> {
while let Some(consumed) = self.recv_bytes_step(bytes)? {
bytes = &bytes[consumed..];
if bytes.is_empty() {
break;
}
}
Ok(())
}
fn recv_bytes_step(&mut self, bytes: &[u8]) -> Result<Option<usize>> {
// TODO: This might not work if we buffer two packets where one changes keys in between?
let result =
self.recv_next_packet
.recv_bytes(bytes, &mut *self.keys, self.recv_next_seq_nr)?;
if let Some((consumed, result)) = result {
self.recv_packets.push_back(result);
self.recv_next_seq_nr = self.recv_next_seq_nr.wrapping_add(1);
self.recv_next_packet = PacketParser::new();
return Ok(Some(consumed));
}
Ok(None)
}
pub(crate) fn queue_packet(&mut self, packet: Packet) {
let packet_type = packet.packet_type();
let packet_type_string = numbers::packet_type_to_string(packet_type);
trace!(%packet_type, %packet_type_string, packet_len = %packet.payload.len(), "Sending packet");
let seq_nr = self.send_next_seq_nr;
self.send_next_seq_nr = self.send_next_seq_nr.wrapping_add(1);
let msg = self.keys.encrypt_packet_to_msg(packet, seq_nr);
self.queue_send_msg(msg);
}
pub(crate) fn queue_send_protocol_info(&mut self, identification: Vec<u8>) {
self.queue_send_msg(Msg(MsgKind::ServerProtocolInfo(identification)));
}
pub(crate) fn recv_next_packet(&mut self) -> Option<Packet> {
self.recv_packets.pop_front()
}
// Private: Make sure all sending goes through variant-specific functions here.
fn queue_send_msg(&mut self, msg: Msg) {
self.msgs_to_send.push_back(msg);
}
pub(crate) fn next_msg_to_send(&mut self) -> Option<Msg> {
self.msgs_to_send.pop_front()
}
pub(crate) fn set_key(
&mut self,
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
is_server: bool,
) {
if let Err(()) = self.keys.rekey(
h,
k,
encryption_client_to_server,
encryption_server_to_client,
is_server,
) {
self.keys = Box::new(Session::new(
h,
k,
encryption_client_to_server,
encryption_server_to_client,
is_server,
));
}
}
}
/*
packet teminology used throughout this crate:
length | padding_length | payload | random padding | MAC
-------------------------------------------------------- "full"
----------------------------------------------- "rest"
------- "payload"
----------------------------------------- "content"
-------------------------------------------------- "authenticated"
^^^^^^ encrypted using K1
^^^^ plaintext
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ encrypted using K2
*/
/// A plaintext SSH packet payload.
#[derive(Debug, PartialEq)]
pub struct Packet {
pub payload: Vec<u8>,
}
impl Packet {
pub const DEFAULT_BLOCK_SIZE: u8 = 8;
pub fn packet_type(&self) -> u8 {
self.payload[0]
}
pub(crate) fn from_full(bytes: &[u8]) -> Result<Self> {
let Some(padding_length) = bytes.first() else {
return Err(peer_error!("empty packet"));
};
let Some(payload_len) = (bytes.len() - 1).checked_sub(*padding_length as usize) else {
return Err(peer_error!("packet padding longer than packet"));
};
let payload = &bytes[1..][..payload_len];
// TODO: handle the annoying decryption special case differnt where its +0 instead of +4
// also TODO: this depends on the cipher!
//if (bytes.len() + 4) % 8 != 0 {
// return Err(peer_error!("full packet length must be multiple of 8: {}", bytes.len()));
//}
Ok(Self {
payload: payload.to_vec(),
})
}
pub(crate) fn to_bytes(&self, respect_len_for_padding: bool, block_size: u8) -> Vec<u8> {
assert!(block_size.is_power_of_two());
let let_bytes = if respect_len_for_padding { 4 } else { 0 };
// <https://datatracker.ietf.org/doc/html/rfc4253#section-6>
let min_full_length = self.payload.len() + let_bytes + 1;
// The padding must give a factor of block_size.
let min_padding_len =
(min_full_length.next_multiple_of(block_size as usize) - min_full_length) as u8;
// > There MUST be at least four bytes of padding.
let padding_len = if min_padding_len < 4 {
min_padding_len + block_size
} else {
min_padding_len
};
let packet_len = self.payload.len() + (padding_len as usize) + 1;
let mut new = Vec::new();
new.extend_from_slice(&u32::to_be_bytes(packet_len as u32));
new.extend_from_slice(&[padding_len]);
new.extend_from_slice(&self.payload);
new.extend(std::iter::repeat(0).take(padding_len as usize));
assert!((let_bytes + 1 + self.payload.len() + (padding_len as usize)) % 8 == 0);
new
}
pub fn payload_parser(&self) -> Parser<'_> {
Parser::new(&self.payload)
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct EncryptedPacket {
data: Vec<u8>,
}
impl EncryptedPacket {
pub(crate) fn into_bytes(self) -> Vec<u8> {
self.data
}
pub(crate) fn from_encrypted_full_bytes(data: Vec<u8>) -> Self {
Self { data }
}
}
#[derive(Debug)]
pub(crate) struct KeyExchangeInitPacket<'a> {
pub(crate) cookie: [u8; 16],
pub(crate) kex_algorithms: NameList<'a>,
pub(crate) server_host_key_algorithms: NameList<'a>,
pub(crate) encryption_algorithms_client_to_server: NameList<'a>,
pub(crate) encryption_algorithms_server_to_client: NameList<'a>,
pub(crate) mac_algorithms_client_to_server: NameList<'a>,
pub(crate) mac_algorithms_server_to_client: NameList<'a>,
pub(crate) compression_algorithms_client_to_server: NameList<'a>,
pub(crate) compression_algorithms_server_to_client: NameList<'a>,
pub(crate) languages_client_to_server: NameList<'a>,
pub(crate) languages_server_to_client: NameList<'a>,
pub(crate) first_kex_packet_follows: bool,
}
impl<'a> KeyExchangeInitPacket<'a> {
pub(crate) fn parse(payload: &'a [u8]) -> Result<KeyExchangeInitPacket<'_>> {
let mut c = Parser::new(payload);
let kind = c.u8()?;
if kind != numbers::SSH_MSG_KEXINIT {
return Err(peer_error!("expected SSH_MSG_KEXINIT packet, found {kind}"));
}
let cookie = c.array::<16>()?;
let kex_algorithms = c.name_list()?;
let server_host_key_algorithms = c.name_list()?;
let encryption_algorithms_client_to_server = c.name_list()?;
let encryption_algorithms_server_to_client = c.name_list()?;
let mac_algorithms_client_to_server = c.name_list()?;
let mac_algorithms_server_to_client = c.name_list()?;
let compression_algorithms_client_to_server = c.name_list()?;
let compression_algorithms_server_to_client = c.name_list()?;
let languages_client_to_server = c.name_list()?;
let languages_server_to_client = c.name_list()?;
let first_kex_packet_follows = c.bool()?;
let _ = c.u32()?; // Reserved.
Ok(Self {
cookie,
kex_algorithms,
server_host_key_algorithms,
encryption_algorithms_client_to_server,
encryption_algorithms_server_to_client,
mac_algorithms_client_to_server,
mac_algorithms_server_to_client,
compression_algorithms_client_to_server,
compression_algorithms_server_to_client,
languages_client_to_server,
languages_server_to_client,
first_kex_packet_follows,
})
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut data = Writer::new();
data.u8(numbers::SSH_MSG_KEXINIT);
data.array(self.cookie);
data.name_list(self.kex_algorithms);
data.name_list(self.server_host_key_algorithms);
data.name_list(self.encryption_algorithms_client_to_server);
data.name_list(self.encryption_algorithms_server_to_client);
data.name_list(self.mac_algorithms_client_to_server);
data.name_list(self.mac_algorithms_server_to_client);
data.name_list(self.compression_algorithms_client_to_server);
data.name_list(self.compression_algorithms_server_to_client);
data.name_list(self.languages_client_to_server);
data.name_list(self.languages_server_to_client);
data.u8(self.first_kex_packet_follows as u8);
data.u32(0); // Reserved.
data.finish()
}
}
#[derive(Debug)]
pub(crate) struct KeyExchangeEcDhInitPacket<'a> {
pub(crate) qc: &'a [u8],
}
impl<'a> KeyExchangeEcDhInitPacket<'a> {
pub(crate) fn parse(payload: &'a [u8]) -> Result<KeyExchangeEcDhInitPacket<'_>> {
let mut c = Parser::new(payload);
let kind = c.u8()?;
if kind != numbers::SSH_MSG_KEX_ECDH_INIT {
return Err(peer_error!(
"expected SSH_MSG_KEXDH_INIT packet, found {kind}"
));
}
let qc = c.string()?;
Ok(Self { qc })
}
}
pub(crate) struct RawPacket {
pub mac_len: usize,
pub raw: Vec<u8>,
}
impl RawPacket {
pub(crate) fn rest(&self) -> &[u8] {
&self.raw[4..]
}
pub(crate) fn full_packet(&self) -> &[u8] {
&self.raw
}
pub(crate) fn content_mut(&mut self) -> &mut [u8] {
let mac_start = self.raw.len() - self.mac_len;
&mut self.raw[4..mac_start]
}
}
pub struct PacketParser {
// The length of the packet.
packet_length: Option<usize>,
// The raw data *encrypted*, including the length.
raw_data: Vec<u8>,
done: bool,
}
impl PacketParser {
pub fn new() -> Self {
Self {
packet_length: None,
raw_data: Vec::new(),
done: false,
}
}
/// Parse a raw packet body out of a plaintext stream of bytes.
/// # Returns
/// - `Err()` - if the packet was invalid
/// - `Ok(None)` - if the packet is incomplete and needs more data
/// - `Ok(Some(consumed, all_data))` if a packet has been parsed.
/// `consumed` is the amount of bytes from `bytes` that were actually consumed,
/// `all_data` is the entire packet including the length.
pub fn recv_plaintext_bytes(&mut self, bytes: &[u8]) -> Result<Option<(usize, Vec<u8>)>> {
let Some((consumed, data)) = self.recv_bytes_inner(bytes, &mut crypto::Plaintext, 0)?
else {
return Ok(None);
};
self.done = true;
Ok(Some((consumed, data.raw)))
}
fn recv_bytes(
&mut self,
bytes: &[u8],
decrytor: &mut dyn Keys,
next_seq_nr: u64,
) -> Result<Option<(usize, Packet)>> {
let Some((consumed, data)) = self.recv_bytes_inner(bytes, decrytor, next_seq_nr)? else {
return Ok(None);
};
let packet = decrytor.decrypt_packet(data, next_seq_nr)?;
Ok(Some((consumed, packet)))
}
fn recv_bytes_inner(
&mut self,
mut bytes: &[u8],
keys: &mut dyn Keys,
next_seq_nr: u64,
) -> Result<Option<(usize, RawPacket)>> {
assert!(
!self.done,
"Passed bytes to packet parser even after it was completed"
);
let mut consumed = 0;
let packet_length = match self.packet_length {
Some(packet_length) => {
assert!(self.raw_data.len() >= 4);
packet_length
}
None => {
let remaining_len = std::cmp::min(bytes.len(), 4 - self.raw_data.len());
// Try to read the bytes of the length.
self.raw_data.extend_from_slice(&bytes[..remaining_len]);
if self.raw_data.len() < 4 {
// Not enough data yet :(.
return Ok(None);
}
let mut len_to_decrypt = [0_u8; 4];
len_to_decrypt.copy_from_slice(self.raw_data.as_slice());
keys.decrypt_len(&mut len_to_decrypt, next_seq_nr);
let packet_length = u32::from_be_bytes(len_to_decrypt);
let packet_length: usize = packet_length.try_into().unwrap();
let packet_length = packet_length + keys.additional_mac_len();
self.packet_length = Some(packet_length);
// We have the data.
bytes = &bytes[remaining_len..];
consumed += remaining_len;
packet_length
}
};
// <https://datatracker.ietf.org/doc/html/rfc4253#section-6.1>
// All implementations MUST be able to process packets with an
// uncompressed payload length of 32768 bytes or less and a total packet
// size of 35000 bytes or less (including 'packet_length',
// 'padding_length', 'payload', 'random padding', and 'mac').
// Implementations SHOULD support longer packets, where they might be needed.
if packet_length > 500_000 {
return Err(peer_error!("packet too large (>500_000): {packet_length}"));
}
let remaining_len = std::cmp::min(bytes.len(), packet_length - (self.raw_data.len() - 4));
self.raw_data.extend_from_slice(&bytes[..remaining_len]);
consumed += remaining_len;
if (self.raw_data.len() - 4) == packet_length {
// We have the full data.
Ok(Some((
consumed,
RawPacket {
raw: std::mem::take(&mut self.raw_data),
mac_len: keys.additional_mac_len(),
},
)))
} else {
Ok(None)
}
}
#[cfg(test)]
fn test_recv_bytes(&mut self, bytes: &[u8]) -> Option<(usize, RawPacket)> {
self.recv_bytes_inner(bytes, &mut Plaintext, 0).unwrap()
}
}
pub(crate) struct ProtocolIdentParser(Vec<u8>);
impl ProtocolIdentParser {
pub(crate) fn new() -> Self {
Self(Vec::new())
}
pub(crate) fn recv_bytes(&mut self, bytes: &[u8]) {
self.0.extend_from_slice(bytes);
}
pub(crate) fn get_peer_ident(&mut self) -> Option<Vec<u8>> {
if self.0.windows(2).any(|win| win == b"\r\n") {
// TODO: care that its SSH 2.0 instead of anythin anything else
// The peer will not send any more information than this until we respond, so discord the rest of the bytes.
let peer_ident = mem::take(&mut self.0);
let peer_ident_string = String::from_utf8_lossy(&peer_ident);
debug!(identification = %peer_ident_string.trim(), "Peer identifier");
Some(peer_ident)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use crate::packet::PacketParser;
trait OptionExt {
fn unwrap_none(self);
}
impl<T> OptionExt for Option<T> {
#[track_caller]
fn unwrap_none(self) {
assert!(self.is_none());
}
}
#[test]
fn packet_parser() {
let mut p = PacketParser::new();
p.test_recv_bytes(&2_u32.to_be_bytes()).unwrap_none();
p.test_recv_bytes(&[1]).unwrap_none();
let (consumed, data) = p.test_recv_bytes(&[2]).unwrap();
assert_eq!(consumed, 1);
assert_eq!(data.rest(), &[1, 2]);
}
#[test]
fn packet_parser_split_len() {
let mut p = PacketParser::new();
let len = &2_u32.to_be_bytes();
p.test_recv_bytes(&len[0..2]).unwrap_none();
p.test_recv_bytes(&len[2..4]).unwrap_none();
p.test_recv_bytes(&[1]).unwrap_none();
let (consumed, data) = p.test_recv_bytes(&[2]).unwrap();
assert_eq!(consumed, 1);
assert_eq!(data.rest(), &[1, 2]);
}
#[test]
fn packet_parser_all() {
let mut p = PacketParser::new();
let (consumed, data) = p.test_recv_bytes(&[0, 0, 0, 2, 1, 2]).unwrap();
assert_eq!(consumed, 6);
assert_eq!(data.rest(), &[1, 2]);
}
}

View file

@ -0,0 +1,138 @@
use crate::packet::Packet;
use crate::parse::Writer;
#[allow(non_camel_case_types)]
mod ssh_type_to_rust {
pub(super) use {bool, u32, u8};
pub(super) type string<'a> = &'a [u8];
pub(super) type name_list<'a> = crate::parse::NameList<'a>;
}
macro_rules! ctors {
(
$(
fn $fn_name:ident(
$msg_type:ident;
$(
$name:ident: $ssh_type:ident
),*
$(,)?
);
)*
) => {
impl Packet {
$(
pub fn $fn_name(
$(
$name: ssh_type_to_rust::$ssh_type
),*
) -> Packet {
let mut w = Writer::new();
w.u8($crate::numbers::$msg_type);
$(
w.$ssh_type($name);
)*
Packet {
payload: w.finish(),
}
}
)*
}
};
}
ctors! {
// -----
// Transport layer protocol:
// 1 to 19 Transport layer generic (e.g., disconnect, ignore, debug, etc.)
fn new_msg_service_request(SSH_MSG_SERVICE_REQUEST; service_name: string);
// 20 to 29 Algorithm negotiation
// 30 to 49 Key exchange method specific (numbers can be reused for different authentication methods)
fn new_msg_kex_ecdh_init(SSH_MSG_KEX_ECDH_INIT; client_ephemeral_public_key_qc: string);
fn new_msg_kex_ecdh_reply(SSH_MSG_KEX_ECDH_REPLY;
server_public_host_key_ks: string,
server_ephemeral_public_key_qs: string,
signature: string,
);
// -----
// User authentication protocol:
// 50 to 59 User authentication generic
fn new_msg_userauth_request_none(SSH_MSG_USERAUTH_REQUEST;
username: string,
service_name: string,
method_name_none: string,
);
fn new_msg_userauth_request_password(SSH_MSG_USERAUTH_REQUEST;
username: string,
service_name: string,
method_name_password: string,
false_: bool,
password: string,
);
fn new_msg_userauth_failure(SSH_MSG_USERAUTH_FAILURE;
auth_options: name_list,
partial_success: bool,
);
fn new_msg_userauth_success(SSH_MSG_USERAUTH_SUCCESS;);
fn new_msg_userauth_banner(SSH_MSG_USERAUTH_BANNER; msg: string, language_tag: string);
// 60 to 79 User authentication method specific (numbers can be reused for different authentication methods)
// -----
// Connection protocol:
// 80 to 89 Connection protocol generic
fn new_msg_request_failure(SSH_MSG_REQUEST_FAILURE;);
// 90 to 127 Channel related messages
fn new_msg_channel_open_session(SSH_MSG_CHANNEL_OPEN;
session: string,
sender_channel: u32,
initial_window_size: u32,
maximum_packet_size: u32,
);
fn new_msg_channel_open_confirmation(SSH_MSG_CHANNEL_OPEN_CONFIRMATION;
peer_channel: u32,
sender_channel: u32,
initial_window_size: u32,
max_packet_size: u32,
);
fn new_msg_channel_open_failure(SSH_MSG_CHANNEL_OPEN_FAILURE;
sender_channe: u32,
reason_code: u32,
description: string,
language_tag: string,
);
fn new_msg_channel_window_adjust(SSH_MSG_CHANNEL_WINDOW_ADJUST; recipient_channel: u32, bytes_to_add: u32);
fn new_msg_channel_data(SSH_MSG_CHANNEL_DATA; recipient_channel: u32, data: string);
fn new_msg_channel_eof(SSH_MSG_CHANNEL_EOF; recipient_channel: u32);
fn new_msg_channel_close(SSH_MSG_CHANNEL_CLOSE; recipient_channel: u32);
fn new_msg_channel_request_pty_req(SSH_MSG_CHANNEL_REQUEST;
recipient_channel: u32,
kind_pty_req: string,
want_reply: bool,
term: string,
term_width_char: u32,
term_height_rows: u32,
term_width_px: u32,
term_height_px: u32,
term_modes: string,
);
fn new_msg_channel_request_shell(SSH_MSG_CHANNEL_REQUEST;
recipient_channel: u32,
kind_shell: string,
want_reply: bool,
);
fn new_msg_channel_request_exit_status(SSH_MSG_CHANNEL_REQUEST; recipient_channel: u32, kind_exit_status: string, false_: bool, exit_status: u32);
fn new_msg_channel_success(SSH_MSG_CHANNEL_SUCCESS; recipient_channel: u32);
fn new_msg_channel_failure(SSH_MSG_CHANNEL_FAILURE; recipient_channel: u32);
}

View file

@ -0,0 +1,194 @@
use core::str;
use std::fmt::{Debug, Display};
use crate::SshStatus;
#[derive(Debug)]
pub struct ParseError(pub String);
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ParseError {}
impl From<ParseError> for SshStatus {
fn from(err: ParseError) -> Self {
Self::PeerError(err.0)
}
}
pub type Result<T, E = ParseError> = std::result::Result<T, E>;
/// A simplified `byteorder` clone that emits client errors when the data is too short.
pub struct Parser<'a>(&'a [u8]);
impl<'a> Parser<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self(data)
}
pub fn has_data(&self) -> bool {
!self.0.is_empty()
}
pub fn u8(&mut self) -> Result<u8> {
let arr = self.array::<1>()?;
Ok(arr[0])
}
pub fn u32(&mut self) -> Result<u32> {
let arr = self.array()?;
Ok(u32::from_be_bytes(arr))
}
pub fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
assert!(N < 100_000);
if self.0.len() < N {
return Err(ParseError(format!("packet too short")));
}
let result = self.0[..N].try_into().unwrap();
self.0 = &self.0[N..];
Ok(result)
}
pub fn slice(&mut self, len: usize) -> Result<&'a [u8]> {
if self.0.len() < len {
return Err(ParseError(format!("packet too short")));
}
if len > 100_000 {
return Err(ParseError(format!("bytes too long: {len}")));
}
let result = &self.0[..len];
self.0 = &self.0[len..];
Ok(result)
}
pub fn bool(&mut self) -> Result<bool> {
let b = self.u8()?;
match b {
0 => Ok(false),
1 => Ok(true),
_ => Err(ParseError(format!("invalid bool: {b}"))),
}
}
pub fn name_list(&mut self) -> Result<NameList<'a>> {
let list = self.utf8_string()?;
Ok(NameList(list))
}
pub fn mpint(&mut self) -> Result<MpInt<'a>> {
todo!("do correctly")
}
pub fn string(&mut self) -> Result<&'a [u8]> {
let len = self.u32()?;
let data = self.slice(len.try_into().unwrap())?;
Ok(data)
}
pub fn utf8_string(&mut self) -> Result<&'a str> {
let s = self.string()?;
let Ok(s) = str::from_utf8(s) else {
return Err(ParseError(format!("name-list is invalid UTF-8")));
};
Ok(s)
}
}
/// A simplified `byteorder` clone that emits client errors when the data is too short.
pub struct Writer(Vec<u8>);
impl Writer {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn u8(&mut self, v: u8) {
self.write(&[v]);
}
pub fn u32(&mut self, v: u32) {
self.write(&u32::to_be_bytes(v));
}
pub fn write(&mut self, v: &[u8]) {
self.0.extend_from_slice(v);
}
pub fn array<const N: usize>(&mut self, arr: [u8; N]) {
self.write(&arr);
}
pub fn name_list(&mut self, list: NameList<'_>) {
self.string(list.0.as_bytes());
}
pub fn mpint<const LIMBS: usize>(&mut self, uint: crypto_bigint::Uint<LIMBS>)
where
crypto_bigint::Uint<LIMBS>: crypto_bigint::ArrayEncoding,
{
let bytes = crypto_bigint::ArrayEncoding::to_be_byte_array(&uint);
let (bytes, pad_zero) = fixup_mpint(&bytes);
let len = bytes.len() + (pad_zero as usize);
self.u32(len as u32);
if pad_zero {
self.u8(0);
}
self.write(bytes);
}
pub fn string(&mut self, data: &[u8]) {
self.u32(data.len() as u32);
self.write(data);
}
pub fn bool(&mut self, v: bool) {
self.u8(v as u8);
}
pub fn finish(self) -> Vec<u8> {
self.0
}
}
/// Returns an array of significant bits for the mpint,
/// and whether a leading 0 needs to be added for padding.
pub fn fixup_mpint(mut int_encoded: &[u8]) -> (&[u8], bool) {
while int_encoded[0] == 0 {
int_encoded = &int_encoded[1..];
}
// If the first high bit is set, pad it with a zero.
(int_encoded, (int_encoded[0] & 0b10000000) > 1)
}
#[derive(Clone, Copy)]
pub struct NameList<'a>(pub &'a str);
impl<'a> NameList<'a> {
pub fn one(item: &'a str) -> Self {
if item.contains(',') {
panic!("tried creating name list with comma in item: {item}");
}
Self(item)
}
pub fn multi(items: &'a str) -> Self {
Self(items)
}
pub fn none() -> NameList<'static> {
NameList("")
}
pub fn iter(&self) -> std::str::Split<'a, char> {
self.0.split(',')
}
}
impl Debug for NameList<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.0, f)
}
}
#[derive(Debug, Clone, Copy)]
pub struct MpInt<'a>(pub &'a [u8]);

View file

@ -0,0 +1,445 @@
use std::{collections::VecDeque, mem::take};
use crate::crypto::{
self, AlgorithmName, EncryptionAlgorithm, HostKeySigningAlgorithm, SupportedAlgorithms,
};
use crate::packet::{
KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet, PacketTransport, ProtocolIdentParser,
};
use crate::parse::{NameList, Parser, Writer};
use crate::{numbers, Result};
use crate::{peer_error, Msg, SshRng, SshStatus};
use tracing::{debug, info, trace};
// This is definitely who we are.
pub const SERVER_IDENTIFICATION: &[u8] = b"SSH-2.0-OpenSSH_9.7\r\n";
pub struct ServerConnection {
state: ServerState,
packet_transport: PacketTransport,
rng: Box<dyn SshRng + Send + Sync>,
plaintext_packets: VecDeque<Packet>,
}
enum ServerState {
ProtoExchange {
ident_parser: ProtocolIdentParser,
},
KeyExchangeInit {
client_identification: Vec<u8>,
},
DhKeyInit {
client_identification: Vec<u8>,
client_kexinit: Vec<u8>,
server_kexinit: Vec<u8>,
kex_algorithm: crypto::KexAlgorithm,
server_host_key_algorithm: HostKeySigningAlgorithm,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
NewKeys {
h: [u8; 32],
k: Vec<u8>,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
ServiceRequest,
Open,
}
impl ServerConnection {
pub fn new(rng: impl SshRng + Send + Sync + 'static) -> Self {
Self {
state: ServerState::ProtoExchange {
ident_parser: ProtocolIdentParser::new(),
},
packet_transport: PacketTransport::new(),
rng: Box::new(rng),
plaintext_packets: VecDeque::new(),
}
}
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
if let ServerState::ProtoExchange { ident_parser } = &mut self.state {
ident_parser.recv_bytes(bytes);
if let Some(client_identification) = ident_parser.get_peer_ident() {
self.packet_transport
.queue_send_protocol_info(SERVER_IDENTIFICATION.to_vec());
self.state = ServerState::KeyExchangeInit {
client_identification,
};
}
// This means that we must be called at least twice, which is fine I think.
return Ok(());
}
self.packet_transport.recv_bytes(bytes)?;
while let Some(packet) = self.packet_transport.recv_next_packet() {
let packet_type = packet.payload.first().unwrap_or(&0xFF);
let packet_type_string = numbers::packet_type_to_string(*packet_type);
trace!(%packet_type, %packet_type_string, packet_len = %packet.payload.len(), "Received packet");
// Handle some packets ignoring the state.
match packet.payload.first().copied() {
Some(numbers::SSH_MSG_DISCONNECT) => {
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.1>
let mut disconnect = Parser::new(&packet.payload[1..]);
let reason = disconnect.u32()?;
let description = disconnect.utf8_string()?;
let _language_tag = disconnect.utf8_string()?;
let reason_string =
numbers::disconnect_reason_to_string(reason).unwrap_or("<unknown>");
info!(%reason, %reason_string, %description, "Client disconnecting");
return Err(SshStatus::Disconnect);
}
_ => {}
}
match &mut self.state {
ServerState::ProtoExchange { .. } => unreachable!("handled above"),
ServerState::KeyExchangeInit {
client_identification,
} => {
let kex = KeyExchangeInitPacket::parse(&packet.payload)?;
let sup_algs = SupportedAlgorithms::secure();
let kex_algorithm = sup_algs.key_exchange.find(kex.kex_algorithms.0)?;
debug!(name = %kex_algorithm.name(), "Using KEX algorithm");
let server_host_key_algorithm =
sup_algs.hostkey.find(kex.server_host_key_algorithms.0)?;
debug!(name = %server_host_key_algorithm.name(), "Using host key algorithm");
// TODO: Implement aes128-ctr
let _ = crypto::encrypt::ENC_AES128_CTR;
let encryption_client_to_server = sup_algs
.encryption_from_peer
.find(kex.encryption_algorithms_client_to_server.0)?;
debug!(name = %encryption_client_to_server.name(), "Using encryption algorithm C->S");
let encryption_server_to_client = sup_algs
.encryption_to_peer
.find(kex.encryption_algorithms_server_to_client.0)?;
debug!(name = %encryption_server_to_client.name(), "Using encryption algorithm S->C");
let mac_algorithm_client_to_server = sup_algs
.mac_from_peer
.find(kex.mac_algorithms_client_to_server.0)?;
let mac_algorithm_server_to_client = sup_algs
.mac_to_peer
.find(kex.mac_algorithms_server_to_client.0)?;
let compression_algorithm_client_to_server = sup_algs
.compression_from_peer
.find(kex.compression_algorithms_client_to_server.0)?;
let compression_algorithm_server_to_client = sup_algs
.compression_to_peer
.find(kex.compression_algorithms_server_to_client.0)?;
let _ = kex.languages_client_to_server;
let _ = kex.languages_server_to_client;
if kex.first_kex_packet_follows {
return Err(peer_error!(
"the client wants to send a guessed packet, that's annoying :("
));
}
let mut cookie = [0; 16];
self.rng.fill_bytes(&mut cookie);
let server_kexinit = KeyExchangeInitPacket {
cookie,
kex_algorithms: NameList::one(kex_algorithm.name()),
server_host_key_algorithms: NameList::one(server_host_key_algorithm.name()),
encryption_algorithms_client_to_server: NameList::one(
encryption_client_to_server.name(),
),
encryption_algorithms_server_to_client: NameList::one(
encryption_server_to_client.name(),
),
mac_algorithms_client_to_server: NameList::one(
mac_algorithm_client_to_server,
),
mac_algorithms_server_to_client: NameList::one(
mac_algorithm_server_to_client,
),
compression_algorithms_client_to_server: NameList::one(
compression_algorithm_client_to_server,
),
compression_algorithms_server_to_client: NameList::one(
compression_algorithm_server_to_client,
),
languages_client_to_server: NameList::none(),
languages_server_to_client: NameList::none(),
first_kex_packet_follows: false,
};
let client_identification = take(client_identification);
let server_kexinit_payload = server_kexinit.to_bytes();
self.packet_transport.queue_packet(Packet {
payload: server_kexinit_payload.clone(),
});
self.state = ServerState::DhKeyInit {
client_identification,
client_kexinit: packet.payload,
server_kexinit: server_kexinit_payload,
kex_algorithm,
server_host_key_algorithm,
encryption_client_to_server,
encryption_server_to_client,
};
}
ServerState::DhKeyInit {
client_identification,
client_kexinit,
server_kexinit,
kex_algorithm,
server_host_key_algorithm,
encryption_client_to_server,
encryption_server_to_client,
} => {
let dh = KeyExchangeEcDhInitPacket::parse(&packet.payload)?;
let client_public_key = dh.qc;
let server_secret = (kex_algorithm.generate_secret)(&mut *self.rng);
let server_public_key = server_secret.pubkey;
let shared_secret = (server_secret.exchange)(client_public_key)?;
let pub_hostkey = server_host_key_algorithm.public_key();
let hash = crypto::key_exchange_hash(
client_identification,
SERVER_IDENTIFICATION,
client_kexinit,
server_kexinit,
&pub_hostkey.0,
client_public_key,
&server_public_key,
&shared_secret,
);
let signature = server_host_key_algorithm.sign(&hash);
// eprintln!("client_public_key: {:x?}", client_public_key);
// eprintln!("server_public_key: {:x?}", server_public_key);
// eprintln!("shared_secret: {:x?}", shared_secret);
// eprintln!("hash: {:x?}", hash);
let packet = Packet::new_msg_kex_ecdh_reply(
&pub_hostkey.0,
&server_public_key,
&signature.0,
);
self.packet_transport.queue_packet(packet);
self.state = ServerState::NewKeys {
h: hash,
k: shared_secret,
encryption_client_to_server: *encryption_client_to_server,
encryption_server_to_client: *encryption_server_to_client,
};
}
ServerState::NewKeys {
h,
k,
encryption_client_to_server,
encryption_server_to_client,
} => {
if packet.payload != [numbers::SSH_MSG_NEWKEYS] {
return Err(peer_error!("did not send SSH_MSG_NEWKEYS"));
}
self.packet_transport.queue_packet(Packet {
payload: vec![numbers::SSH_MSG_NEWKEYS],
});
self.packet_transport.set_key(
*h,
k,
*encryption_client_to_server,
*encryption_server_to_client,
true,
);
self.state = ServerState::ServiceRequest {};
}
ServerState::ServiceRequest => {
// TODO: this should probably move out of here? unsure.
if packet.payload.first() != Some(&numbers::SSH_MSG_SERVICE_REQUEST) {
return Err(peer_error!("did not send SSH_MSG_SERVICE_REQUEST"));
}
let mut p = Parser::new(&packet.payload[1..]);
let service = p.utf8_string()?;
debug!(%service, "Client requesting service");
if service != "ssh-userauth" {
return Err(peer_error!("only supports ssh-userauth"));
}
self.packet_transport.queue_packet(Packet {
payload: {
let mut writer = Writer::new();
writer.u8(numbers::SSH_MSG_SERVICE_ACCEPT);
writer.string(service.as_bytes());
writer.finish()
},
});
self.state = ServerState::Open;
}
ServerState::Open => {
self.plaintext_packets.push_back(packet);
}
}
}
Ok(())
}
pub fn next_msg_to_send(&mut self) -> Option<Msg> {
self.packet_transport.next_msg_to_send()
}
pub fn next_plaintext_packet(&mut self) -> Option<Packet> {
self.plaintext_packets.pop_front()
}
pub fn send_plaintext_packet(&mut self, packet: Packet) {
self.packet_transport.queue_packet(packet);
}
}
/// Manually extracted from the key using <https://peterlyons.com/problog/2017/12/openssh-ed25519-private-key-file-format/>, probably wrong
/// ```text
/// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOk5zfpvwNc3MztTTpE90zLI1Ref4AwwRVdSFyJLGbj2 testkey
/// ```
/// ```text
/// -----BEGIN OPENSSH PRIVATE KEY-----
/// b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
/// QyNTUxOQAAACDpOc36b8DXNzM7U06RPdMyyNUXn+AMMEVXUhciSxm49gAAAJDpgLSk6YC0
/// pAAAAAtzc2gtZWQyNTUxOQAAACDpOc36b8DXNzM7U06RPdMyyNUXn+AMMEVXUhciSxm49g
/// AAAECSeskxuEtJrr9L7ZkbpogXC5pKRNVHx1ueMX2h1XUnmek5zfpvwNc3MztTTpE90zLI
/// 1Ref4AwwRVdSFyJLGbj2AAAAB3Rlc3RrZXkBAgMEBQY=
/// -----END OPENSSH PRIVATE KEY-----
/// ```
// todo: remove this lol, lmao
pub(crate) const ED25519_PRIVKEY_BYTES: &[u8; 32] = &[
0x92, 0x7a, 0xc9, 0x31, 0xb8, 0x4b, 0x49, 0xae, 0xbf, 0x4b, 0xed, 0x99, 0x1b, 0xa6, 0x88, 0x17,
0x0b, 0x9a, 0x4a, 0x44, 0xd5, 0x47, 0xc7, 0x5b, 0x9e, 0x31, 0x7d, 0xa1, 0xd5, 0x75, 0x27, 0x99,
];
pub(crate) const ECDSA_P256_PRIVKEY_BYTES: &[u8; 32] = &[
0x89, 0xdd, 0x0c, 0x96, 0x22, 0x85, 0x10, 0xec, 0x3c, 0xa4, 0xa1, 0xb8, 0xac, 0x2a, 0x77, 0xa8,
0xd4, 0x4d, 0xcb, 0x9d, 0x90, 0x25, 0xc6, 0xd8, 0x3a, 0x02, 0x74, 0x4f, 0x9e, 0x44, 0xcd, 0xa3,
];
#[cfg(test)]
mod tests {
use hex_literal::hex;
use crate::{packet::MsgKind, server::ServerConnection, SshRng};
struct NoRng;
impl SshRng for NoRng {
fn fill_bytes(&mut self, _: &mut [u8]) {
unreachable!()
}
}
struct HardcodedRng(Vec<u8>);
impl SshRng for HardcodedRng {
fn fill_bytes(&mut self, dest: &mut [u8]) {
dest.copy_from_slice(&self.0[..dest.len()]);
self.0.splice(0..dest.len(), []);
}
}
#[test]
fn protocol_exchange() {
let mut con = ServerConnection::new(NoRng);
con.recv_bytes(b"SSH-2.0-OpenSSH_9.7\r\n").unwrap();
let msg = con.next_msg_to_send().unwrap();
assert!(matches!(msg.0, MsgKind::ServerProtocolInfo(_)));
}
#[test]
fn protocol_exchange_slow_client() {
let mut con = ServerConnection::new(NoRng);
con.recv_bytes(b"SSH-2.0-").unwrap();
con.recv_bytes(b"OpenSSH_9.7\r\n").unwrap();
let msg = con.next_msg_to_send().unwrap();
assert!(matches!(msg.0, MsgKind::ServerProtocolInfo(_)));
}
#[test]
fn handshake() {
#[rustfmt::skip]
let rng = vec![
0x14, 0xa2, 0x04, 0xa5, 0x4b, 0x2f, 0x5f, 0xa7, 0xff, 0x53, 0x13, 0x67, 0x57, 0x67, 0xbc,
0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75, 0x95, 0x18, 0x4b, 0xd2, 0xcb, 0xd0,
0x64, 0x06, 0x14, 0xa2, 0x04, 0xa5, 0x4b, 0x2f, 0x5f, 0xa7, 0xff, 0x53, 0x13, 0x67, 0x57,
0x67, 0xbc, 0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75, 0x95, 0x18, 0x4b, 0xd2,
0xcb, 0xd0, 0x64, 0x06, 0x67, 0xbc, 0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75,
0x95, 0x18, 0x4b, 0xd2, 0xcb, 0xd0, 0x64, 0x06,
];
struct Part {
client: &'static [u8],
server: &'static [u8],
}
// Extracted from a real OpenSSH client using this server (with hardcoded creds) using Wireshark.
let conversation = [
Part {
client: &hex!("5353482d322e302d4f70656e5353485f392e370d0a"),
server: &hex!("5353482d322e302d4f70656e5353485f392e370d0a"),
},
// KEX Init
Part {
client: &hex!(
"000005fc0714fd3d911937c7294823f93c5ba691f77e00000131736e747275703736317832353531392d736861353132406f70656e7373682e636f6d2c637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c656364682d736861322d6e697374703235362c656364682d736861322d6e697374703338342c656364682d736861322d6e697374703532312c6469666669652d68656c6c6d616e2d67726f75702d65786368616e67652d7368613235362c6469666669652d68656c6c6d616e2d67726f757031362d7368613531322c6469666669652d68656c6c6d616e2d67726f757031382d7368613531322c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6578742d696e666f2d632c6b65782d7374726963742d632d763030406f70656e7373682e636f6d000001cf7373682d656432353531392d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703235362d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703338342d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703532312d636572742d763031406f70656e7373682e636f6d2c736b2d7373682d656432353531392d636572742d763031406f70656e7373682e636f6d2c736b2d65636473612d736861322d6e697374703235362d636572742d763031406f70656e7373682e636f6d2c7273612d736861322d3531322d636572742d763031406f70656e7373682e636f6d2c7273612d736861322d3235362d636572742d763031406f70656e7373682e636f6d2c7373682d656432353531392c65636473612d736861322d6e697374703235362c65636473612d736861322d6e697374703338342c65636473612d736861322d6e697374703532312c736b2d7373682d65643235353139406f70656e7373682e636f6d2c736b2d65636473612d736861322d6e69737470323536406f70656e7373682e636f6d2c7273612d736861322d3531322c7273612d736861322d3235360000006c63686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733139322d6374722c6165733235362d6374722c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d0000006c63686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733139322d6374722c6165733235362d6374722c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d000000d5756d61632d36342d65746d406f70656e7373682e636f6d2c756d61632d3132382d65746d406f70656e7373682e636f6d2c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312d65746d406f70656e7373682e636f6d2c756d61632d3634406f70656e7373682e636f6d2c756d61632d313238406f70656e7373682e636f6d2c686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d73686131000000d5756d61632d36342d65746d406f70656e7373682e636f6d2c756d61632d3132382d65746d406f70656e7373682e636f6d2c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312d65746d406f70656e7373682e636f6d2c756d61632d3634406f70656e7373682e636f6d2c756d61632d313238406f70656e7373682e636f6d2c686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d736861310000001a6e6f6e652c7a6c6962406f70656e7373682e636f6d2c7a6c69620000001a6e6f6e652c7a6c6962406f70656e7373682e636f6d2c7a6c69620000000000000000000000000000000000000000"
),
server: &hex!(
"000000bc051414a204a54b2f5fa7ff5313675767bc5500000011637572766532353531392d7368613235360000000b7373682d656432353531390000001d63686163686132302d706f6c7931333035406f70656e7373682e636f6d0000001d63686163686132302d706f6c7931333035406f70656e7373682e636f6d0000000d686d61632d736861322d3235360000000d686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000000000000000"
),
},
// ECDH KEX Init
Part {
client: &hex!(
"0000002c061e000000204c646d1281abf23264d63db96e05c0223cfead668d9d38c62579b8856e67ae19000000000000"
),
server: &hex!(
"000000bc081f000000330000000b7373682d6564323535313900000020e939cdfa6fc0d737333b534e913dd332c8d5179fe00c3045575217224b19b8f6000000204260e2c5e5383f1a021c9631fa61f60f305b29183fd219d4c8207c664e063410000000530000000b7373682d65643235353139000000406504a045499f26aa4ee17606ea6bd9e3f288838591f25d8604a63f77a52f5b9e909c00d10f386553e585d86ab329bbde0fca5c64b1b1982d7adcac17cf7f06010000000000000000"
),
},
// New Keys
Part {
client: &hex!("0000000c0a1500000000000000000000"),
server: &hex!("0000000c0a1500000000000000000000"),
},
// Service Request (encrypted)
Part {
client: &hex!("09ca4db7baeb24836a1f7d22368055bf4c26981ed86738ac7a5c31d0730ad656f1967853781dff91ee1c4de8"),
server: &hex!("7b444c0d5faf740d350701a054ea469fab1c98e4b669e4872a454163edb42ec5e4fa95c404ab601f016bd259"),
},
];
let mut con = ServerConnection::new(HardcodedRng(rng));
for part in conversation {
con.recv_bytes(&part.client).unwrap();
eprintln!("client: {:x?}", part.client);
let bytes = con.next_msg_to_send().unwrap().to_bytes();
if part.server != bytes {
panic!(
"expected != found\nexpected: {:x?}\nfound: {:x?}",
part.server, bytes
);
}
}
}
}