mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
move
This commit is contained in:
parent
7ac2ef4194
commit
de8f5dde21
34 changed files with 10 additions and 15 deletions
10
lib/ssh-agent-client/Cargo.toml
Normal file
10
lib/ssh-agent-client/Cargo.toml
Normal 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
|
||||
4
lib/ssh-agent-client/README.md
Normal file
4
lib/ssh-agent-client/README.md
Normal 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.
|
||||
366
lib/ssh-agent-client/src/lib.rs
Normal file
366
lib/ssh-agent-client/src/lib.rs
Normal 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>",
|
||||
}
|
||||
}
|
||||
}
|
||||
11
lib/ssh-connection/Cargo.toml
Normal file
11
lib/ssh-connection/Cargo.toml
Normal 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"] }
|
||||
5
lib/ssh-connection/README.md
Normal file
5
lib/ssh-connection/README.md
Normal 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).
|
||||
732
lib/ssh-connection/src/lib.rs
Normal file
732
lib/ssh-connection/src/lib.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
10
lib/ssh-protocol/Cargo.toml
Normal file
10
lib/ssh-protocol/Cargo.toml
Normal 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
|
||||
|
||||
5
lib/ssh-protocol/README.md
Normal file
5
lib/ssh-protocol/README.md
Normal 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
420
lib/ssh-protocol/src/lib.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
24
lib/ssh-transport/Cargo.toml
Normal file
24
lib/ssh-transport/Cargo.toml
Normal 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"
|
||||
15
lib/ssh-transport/README.md
Normal file
15
lib/ssh-transport/README.md
Normal 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)
|
||||
372
lib/ssh-transport/src/client.rs
Normal file
372
lib/ssh-transport/src/client.rs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
523
lib/ssh-transport/src/crypto.rs
Normal file
523
lib/ssh-transport/src/crypto.rs
Normal 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()
|
||||
}
|
||||
253
lib/ssh-transport/src/crypto/encrypt.rs
Normal file
253
lib/ssh-transport/src/crypto/encrypt.rs
Normal 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!()
|
||||
}
|
||||
}
|
||||
59
lib/ssh-transport/src/key.rs
Normal file
59
lib/ssh-transport/src/key.rs
Normal 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)
|
||||
}
|
||||
53
lib/ssh-transport/src/lib.rs
Normal file
53
lib/ssh-transport/src/lib.rs
Normal 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)*))
|
||||
};
|
||||
}
|
||||
143
lib/ssh-transport/src/numbers.rs
Normal file
143
lib/ssh-transport/src/numbers.rs
Normal 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;
|
||||
540
lib/ssh-transport/src/packet.rs
Normal file
540
lib/ssh-transport/src/packet.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
138
lib/ssh-transport/src/packet/ctors.rs
Normal file
138
lib/ssh-transport/src/packet/ctors.rs
Normal 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);
|
||||
}
|
||||
194
lib/ssh-transport/src/parse.rs
Normal file
194
lib/ssh-transport/src/parse.rs
Normal 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]);
|
||||
445
lib/ssh-transport/src/server.rs
Normal file
445
lib/ssh-transport/src/server.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue