This commit is contained in:
nora 2024-08-22 20:59:32 +02:00
parent 9b49e09983
commit a52b6b37d7
19 changed files with 770 additions and 92 deletions

View file

@ -49,8 +49,12 @@ enum ClientState {
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
ServiceRequest,
Open,
ServiceRequest {
session_identifier: [u8; 32],
},
Open {
session_identifier: [u8; 32],
},
}
impl ClientConnection {
@ -229,6 +233,7 @@ impl ClientConnection {
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,
@ -279,13 +284,15 @@ impl ClientConnection {
false,
);
debug!("Requestin ssh-userauth service");
debug!("Requesting ssh-userauth service");
self.packet_transport
.queue_packet(Packet::new_msg_service_request(b"ssh-userauth"));
self.state = ClientState::ServiceRequest;
self.state = ClientState::ServiceRequest {
session_identifier: *h,
};
}
ClientState::ServiceRequest => {
ClientState::ServiceRequest { session_identifier } => {
let mut accept = packet.payload_parser();
let packet_type = accept.u8()?;
if packet_type != numbers::SSH_MSG_SERVICE_ACCEPT {
@ -297,9 +304,11 @@ impl ClientConnection {
}
debug!("Connection has been opened successfully");
self.state = ClientState::Open;
self.state = ClientState::Open {
session_identifier: *session_identifier,
};
}
ClientState::Open => {
ClientState::Open { .. } => {
self.plaintext_packets.push_back(packet);
}
}
@ -319,8 +328,11 @@ impl ClientConnection {
self.packet_transport.queue_packet(packet);
}
pub fn is_open(&self) -> bool {
matches!(self.state, ClientState::Open)
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>) {

59
ssh-transport/src/key.rs Normal file
View file

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

View file

@ -1,5 +1,6 @@
pub mod client;
mod crypto;
pub mod key;
pub mod numbers;
pub mod packet;
pub mod parse;
@ -20,7 +21,6 @@ pub enum SshStatus {
pub type Result<T, E = SshStatus> = std::result::Result<T, E>;
pub trait SshRng {
fn fill_bytes(&mut self, dest: &mut [u8]);
}

View file

@ -5,7 +5,7 @@ use std::mem;
use tracing::{debug, trace};
use crate::crypto::{EncryptionAlgorithm, Keys, Plaintext, Session};
use crate::crypto::{self, EncryptionAlgorithm, Keys, Plaintext, Session};
use crate::parse::{NameList, Parser, Writer};
use crate::Result;
use crate::{numbers, peer_error};
@ -347,19 +347,38 @@ impl RawPacket {
}
}
struct PacketParser {
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 {
fn new() -> Self {
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],
@ -378,6 +397,11 @@ impl PacketParser {
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) => {
@ -460,7 +484,7 @@ impl ProtocolIdentParser {
// 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, "Peer identifier");
debug!(identification = %peer_ident_string.trim(), "Peer identifier");
Some(peer_ident)
} else {

View file

@ -1,7 +1,24 @@
use core::str;
use std::fmt::Debug;
use std::fmt::{Debug, Display};
use crate::Result;
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]);
@ -11,6 +28,10 @@ impl<'a> Parser<'a> {
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])
@ -24,7 +45,7 @@ impl<'a> Parser<'a> {
pub fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
assert!(N < 100_000);
if self.0.len() < N {
return Err(crate::peer_error!("packet too short"));
return Err(ParseError(format!("packet too short")));
}
let result = self.0[..N].try_into().unwrap();
self.0 = &self.0[N..];
@ -33,10 +54,10 @@ impl<'a> Parser<'a> {
pub fn slice(&mut self, len: usize) -> Result<&'a [u8]> {
if self.0.len() < len {
return Err(crate::peer_error!("packet too short"));
return Err(ParseError(format!("packet too short")));
}
if len > 100_000 {
return Err(crate::peer_error!("bytes too long: {len}"));
return Err(ParseError(format!("bytes too long: {len}")));
}
let result = &self.0[..len];
self.0 = &self.0[len..];
@ -48,7 +69,7 @@ impl<'a> Parser<'a> {
match b {
0 => Ok(false),
1 => Ok(true),
_ => Err(crate::peer_error!("invalid bool: {b}")),
_ => Err(ParseError(format!("invalid bool: {b}"))),
}
}
@ -70,7 +91,7 @@ impl<'a> Parser<'a> {
pub fn utf8_string(&mut self) -> Result<&'a str> {
let s = self.string()?;
let Ok(s) = str::from_utf8(s) else {
return Err(crate::peer_error!("name-list is invalid UTF-8"));
return Err(ParseError(format!("name-list is invalid UTF-8")));
};
Ok(s)
}
@ -165,7 +186,7 @@ impl<'a> NameList<'a> {
impl Debug for NameList<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
Debug::fmt(&self.0, f)
}
}