333 lines
11 KiB
Rust
333 lines
11 KiB
Rust
use crate::config::connection::ClientConnectionOpts;
|
|
use crate::ssh::channel::SshPacket;
|
|
use crate::ssh::message_ids::SshMessageID;
|
|
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
|
use itertools::Itertools;
|
|
use proptest::arbitrary::Arbitrary;
|
|
use proptest::strategy::{BoxedStrategy, Strategy};
|
|
use rand::{CryptoRng, Rng, SeedableRng};
|
|
use std::string::FromUtf8Error;
|
|
use thiserror::Error;
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct SshKeyExchange {
|
|
cookie: [u8; 16],
|
|
keyx_algorithms: Vec<String>,
|
|
server_host_key_algorithms: Vec<String>,
|
|
encryption_algorithms_client_to_server: Vec<String>,
|
|
encryption_algorithms_server_to_client: Vec<String>,
|
|
mac_algorithms_client_to_server: Vec<String>,
|
|
mac_algorithms_server_to_client: Vec<String>,
|
|
compression_algorithms_client_to_server: Vec<String>,
|
|
compression_algorithms_server_to_client: Vec<String>,
|
|
languages_client_to_server: Vec<String>,
|
|
languages_server_to_client: Vec<String>,
|
|
first_kex_packet_follows: bool,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum SshKeyExchangeProcessingError {
|
|
#[error("Message not appropriately tagged as SSH_MSG_KEXINIT")]
|
|
TaggedWrong,
|
|
#[error("Initial key exchange message was too short.")]
|
|
TooShort,
|
|
#[error("Invalid string encoding for name-list (not ASCII)")]
|
|
NotAscii,
|
|
#[error("Invalid conversion (from ASCII to UTF-8??): {0}")]
|
|
NotUtf8(FromUtf8Error),
|
|
#[error("Extraneous data at the end of key exchange message")]
|
|
ExtraneousData,
|
|
#[error("Received invalid reserved word ({0} != 0)")]
|
|
InvalidReservedWord(u32),
|
|
}
|
|
|
|
impl TryFrom<SshPacket> for SshKeyExchange {
|
|
type Error = SshKeyExchangeProcessingError;
|
|
|
|
fn try_from(mut value: SshPacket) -> Result<Self, Self::Error> {
|
|
if SshMessageID::from(value.buffer.get_u8()) != SshMessageID::SSH_MSG_KEXINIT {
|
|
return Err(SshKeyExchangeProcessingError::TaggedWrong);
|
|
}
|
|
|
|
let mut cookie = [0; 16];
|
|
check_length(&mut value.buffer, 16)?;
|
|
value.buffer.copy_to_slice(&mut cookie);
|
|
|
|
let keyx_algorithms = name_list(&mut value.buffer)?;
|
|
let server_host_key_algorithms = name_list(&mut value.buffer)?;
|
|
let encryption_algorithms_client_to_server = name_list(&mut value.buffer)?;
|
|
let encryption_algorithms_server_to_client = name_list(&mut value.buffer)?;
|
|
let mac_algorithms_client_to_server = name_list(&mut value.buffer)?;
|
|
let mac_algorithms_server_to_client = name_list(&mut value.buffer)?;
|
|
let compression_algorithms_client_to_server = name_list(&mut value.buffer)?;
|
|
let compression_algorithms_server_to_client = name_list(&mut value.buffer)?;
|
|
let languages_client_to_server = name_list(&mut value.buffer)?;
|
|
let languages_server_to_client = name_list(&mut value.buffer)?;
|
|
check_length(&mut value.buffer, 5)?;
|
|
let first_kex_packet_follows = value.buffer.get_u8() != 0;
|
|
let reserved = value.buffer.get_u32();
|
|
if reserved != 0 {
|
|
return Err(SshKeyExchangeProcessingError::InvalidReservedWord(reserved));
|
|
}
|
|
|
|
if value.buffer.remaining() > 0 {
|
|
return Err(SshKeyExchangeProcessingError::ExtraneousData);
|
|
}
|
|
|
|
Ok(SshKeyExchange {
|
|
cookie,
|
|
keyx_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,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl From<SshKeyExchange> for SshPacket {
|
|
fn from(value: SshKeyExchange) -> Self {
|
|
let mut buffer = BytesMut::new();
|
|
|
|
let put_options = |buffer: &mut BytesMut, vals: Vec<String>| {
|
|
let mut merged = String::new();
|
|
#[allow(unstable_name_collisions)]
|
|
let comma_sepped = vals.into_iter().intersperse(String::from(","));
|
|
merged.extend(comma_sepped);
|
|
let bytes = merged.as_bytes();
|
|
buffer.put_u32(bytes.len() as u32);
|
|
buffer.put_slice(bytes);
|
|
};
|
|
|
|
buffer.put_u8(SshMessageID::SSH_MSG_KEXINIT.into());
|
|
buffer.put_slice(&value.cookie);
|
|
put_options(&mut buffer, value.keyx_algorithms);
|
|
put_options(&mut buffer, value.server_host_key_algorithms);
|
|
put_options(&mut buffer, value.encryption_algorithms_client_to_server);
|
|
put_options(&mut buffer, value.encryption_algorithms_server_to_client);
|
|
put_options(&mut buffer, value.mac_algorithms_client_to_server);
|
|
put_options(&mut buffer, value.mac_algorithms_server_to_client);
|
|
put_options(&mut buffer, value.compression_algorithms_client_to_server);
|
|
put_options(&mut buffer, value.compression_algorithms_server_to_client);
|
|
put_options(&mut buffer, value.languages_client_to_server);
|
|
put_options(&mut buffer, value.languages_server_to_client);
|
|
buffer.put_u8(value.first_kex_packet_follows as u8);
|
|
buffer.put_u32(0);
|
|
|
|
SshPacket {
|
|
buffer: buffer.freeze(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SshKeyExchange {
|
|
/// Create a new SshKeyExchange message for this client or server based
|
|
/// on the given connection options.
|
|
///
|
|
/// This function takes a random number generator because it needs to
|
|
/// seed the message with a random cookie, but is otherwise deterministic.
|
|
/// It will fail only in the case that the underlying random number
|
|
/// generator fails, and return exactly that error.
|
|
pub fn new<R>(rng: &mut R, value: ClientConnectionOpts) -> Result<Self, rand::Error>
|
|
where
|
|
R: CryptoRng + Rng,
|
|
{
|
|
let mut result = SshKeyExchange {
|
|
cookie: [0; 16],
|
|
keyx_algorithms: value
|
|
.key_exchange_algorithms
|
|
.iter()
|
|
.map(|x| x.to_string())
|
|
.collect(),
|
|
server_host_key_algorithms: value
|
|
.server_host_key_algorithms
|
|
.iter()
|
|
.map(|x| x.to_string())
|
|
.collect(),
|
|
encryption_algorithms_client_to_server: value
|
|
.encryption_algorithms
|
|
.iter()
|
|
.map(|x| x.to_string())
|
|
.collect(),
|
|
encryption_algorithms_server_to_client: value
|
|
.encryption_algorithms
|
|
.iter()
|
|
.map(|x| x.to_string())
|
|
.collect(),
|
|
mac_algorithms_client_to_server: value
|
|
.mac_algorithms
|
|
.iter()
|
|
.map(|x| x.to_string())
|
|
.collect(),
|
|
mac_algorithms_server_to_client: value
|
|
.mac_algorithms
|
|
.iter()
|
|
.map(|x| x.to_string())
|
|
.collect(),
|
|
compression_algorithms_client_to_server: value
|
|
.compression_algorithms
|
|
.iter()
|
|
.map(|x| x.to_string())
|
|
.collect(),
|
|
compression_algorithms_server_to_client: value
|
|
.compression_algorithms
|
|
.iter()
|
|
.map(|x| x.to_string())
|
|
.collect(),
|
|
languages_client_to_server: value.languages.to_vec(),
|
|
languages_server_to_client: value.languages.to_vec(),
|
|
first_kex_packet_follows: value.predict.is_some(),
|
|
};
|
|
|
|
rng.try_fill(&mut result.cookie)?;
|
|
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
impl Arbitrary for SshKeyExchange {
|
|
type Parameters = ();
|
|
type Strategy = BoxedStrategy<Self>;
|
|
|
|
fn arbitrary_with(_: Self::Parameters) -> BoxedStrategy<Self> {
|
|
let client_config = ClientConnectionOpts::arbitrary();
|
|
let seed = <[u8; 32]>::arbitrary();
|
|
|
|
(client_config, seed)
|
|
.prop_map(|(config, seed)| {
|
|
let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed);
|
|
SshKeyExchange::new(&mut rng, config).unwrap()
|
|
})
|
|
.boxed()
|
|
}
|
|
}
|
|
|
|
proptest::proptest! {
|
|
#[test]
|
|
fn valid_kex_messages_parse(kex in SshKeyExchange::arbitrary()) {
|
|
let as_packet: SshPacket = kex.clone().try_into().expect("can generate packet");
|
|
let as_message = as_packet.try_into().expect("can regenerate message");
|
|
assert_eq!(kex, as_message);
|
|
}
|
|
}
|
|
|
|
fn check_length(buffer: &mut Bytes, length: usize) -> Result<(), SshKeyExchangeProcessingError> {
|
|
if buffer.remaining() < length {
|
|
Err(SshKeyExchangeProcessingError::TooShort)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn name_list(buffer: &mut Bytes) -> Result<Vec<String>, SshKeyExchangeProcessingError> {
|
|
check_length(buffer, 4)?;
|
|
let list_length = buffer.get_u32() as usize;
|
|
|
|
if list_length == 0 {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
check_length(buffer, list_length)?;
|
|
let mut raw_bytes = vec![0u8; list_length];
|
|
buffer.copy_to_slice(&mut raw_bytes);
|
|
if !raw_bytes.iter().all(|c| c.is_ascii()) {
|
|
return Err(SshKeyExchangeProcessingError::NotAscii);
|
|
}
|
|
|
|
let mut result = Vec::new();
|
|
for split in raw_bytes.split(|b| char::from(*b) == ',') {
|
|
let str =
|
|
String::from_utf8(split.to_vec()).map_err(SshKeyExchangeProcessingError::NotUtf8)?;
|
|
result.push(str);
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn standard_kex_message() -> SshPacket {
|
|
let seed = [0u8; 32];
|
|
let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed);
|
|
let config = ClientConnectionOpts::default();
|
|
let message = SshKeyExchange::new(&mut rng, config).expect("default settings work");
|
|
message.try_into().expect("default settings serialize")
|
|
}
|
|
|
|
#[test]
|
|
fn can_see_bad_tag() {
|
|
let standard = standard_kex_message();
|
|
let mut buffer = standard.buffer.to_vec();
|
|
buffer[0] += 1;
|
|
let bad = SshPacket {
|
|
buffer: buffer.into(),
|
|
};
|
|
assert!(matches!(
|
|
SshKeyExchange::try_from(bad),
|
|
Err(SshKeyExchangeProcessingError::TaggedWrong)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn checks_for_extraneous_data() {
|
|
let standard = standard_kex_message();
|
|
let mut buffer = standard.buffer.to_vec();
|
|
buffer.push(3);
|
|
let bad = SshPacket {
|
|
buffer: buffer.into(),
|
|
};
|
|
assert!(matches!(
|
|
SshKeyExchange::try_from(bad),
|
|
Err(SshKeyExchangeProcessingError::ExtraneousData)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn checks_for_short_packets() {
|
|
let standard = standard_kex_message();
|
|
let mut buffer = standard.buffer.to_vec();
|
|
let _ = buffer.pop();
|
|
let bad = SshPacket {
|
|
buffer: buffer.into(),
|
|
};
|
|
assert!(matches!(
|
|
SshKeyExchange::try_from(bad),
|
|
Err(SshKeyExchangeProcessingError::TooShort)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn checks_for_invalid_data() {
|
|
let standard = standard_kex_message();
|
|
let mut buffer = standard.buffer.to_vec();
|
|
buffer[22] = 0xc3;
|
|
buffer[23] = 0x28;
|
|
let bad = SshPacket {
|
|
buffer: buffer.into(),
|
|
};
|
|
assert!(matches!(
|
|
SshKeyExchange::try_from(bad),
|
|
Err(SshKeyExchangeProcessingError::NotAscii)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn checks_for_bad_reserved_word() {
|
|
let standard = standard_kex_message();
|
|
let mut buffer = standard.buffer.to_vec();
|
|
let _ = buffer.pop();
|
|
buffer.push(1);
|
|
let bad = SshPacket {
|
|
buffer: buffer.into(),
|
|
};
|
|
assert!(matches!(
|
|
SshKeyExchange::try_from(bad),
|
|
Err(SshKeyExchangeProcessingError::InvalidReservedWord(1))
|
|
));
|
|
}
|