diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000000000000000000000000000000000000..c29d3fb422a8c19db59f47748d06851e70c5ec51 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,69 @@ +This documents various design decisions, also to be read as _lessons learned_. +At the time you read this they may, or may not, still be relevant. +Apologies if I seem overly critical of any projects - I have huge respect for +all of the dependencies I've tried. + +## Error type + +Snafu seems to do the job for an error type. + +Initially the `Error` type had two 35 byte `UnknownName`s in one of the variants. +Removing that reduced the arm thumb binary size by 16kB! The reason appears to be that +when the error variant of `Result` is larger than the `Ok` item, it can't be used +to construct a struct in-place (it has to write to temporary larger stack space, +then copy the item out). That matters a lot for the `sshwire` deserialisation +that recursively creates lots of structs. + +`.trap()` is there as an alternative to `.unwrap()` - if a server handles multiple connections +you don't want them all going away panicking if there's some un-thought-of edge case. +Each call to `.trap()` seems on the order of perhaps 100 bytes larger than a plain `panic!()` - +perhaps there should be a feature to just panic. In debug builds it panics too so it's quick +to get a backtrace. + +## Serialisation/deserialisation + +Previously the code used serde with a custom `Serializer`/`Deserializer`. That worked +fine for serializing and `derive(Deserialize)`, but deserializing enums was a pain. +For types like `packets::ChannelRequest` +the deserializer had to stash the channel request type string from the parent struct somewhere, +then use that later when deserialising the `ChannelReqType` enum. Other response packets like number 60 `Userauth60` +mean completely different things depending on the previous request, but that state is hard to pass through +the deserializer. + +[`serde_state`](https://docs.rs/serde_state/latest/serde_state/) solves some of it, but was still a bit awkward, +and didn't work well with manually written `Deserialize` implementations. + +serde is fairly flexible design, +but for our purposes we can use something much simpler - the packet format is not self describing +(in most parts), we just take bytes off the wire exactly as we expect them. + +Eventually I gave up fighting and wrote a new `sshwire_derive` with +[virtue](https://github.com/bincode-org/virtue) crate. That is about 10kB smaller +(arm thumb) and easier to customise as required. For example `UnknownName` has a simple attribute to indicate +that's the variant that should store an unmatched string. + +It was easier than I expected - the virtue syntax seems a bit verbose but feels more intuitive than +`syn` or `quote` crates which are the alternative. I think this is because it's still written in +normal Rust, not a new macro language to learn. + +## Ring vs RustCrypto + +Initially the code was written using `ring`, mainly because it already had +[`chacha20_poly1305_openssh`](https://docs.rs/ring/latest/ring/aead/chacha20_poly1305_openssh/index.html). +That worked great until I tried to build for a `thumbv7em` platform. Some of the code wouldn't build +(ARM assembly issues?, possibly fixable), +but the bigger problem is there's no way to insert a custom random number generator. + +Instead switching to RustCrypto crates worked fairly easily, though perhaps a bit messier having +to deal with `GenericArray` and `DynamicDigest` and types like that. At the time of writing `curve25519-dalek` etc +are a bit behind on dependencies which is messy, but I assume that will sort itself out. + +## `Behaviour` + +At some points in packet handling some custom behaviour from the application is required. For example +"is this hostkey valid?", "is this user's password correct?". Those need an immediate response, +so `.await` fits well. + +The problem is that `async_trait` requires `Box`, won't work on `no_std`. The `Behaviour` struct has a `cfg` to switch +between async and non-async traits, hiding that from the main code. Eventually `async fn` [should work OK](https://github.com/rust-lang/rust/issues/91611) in static traits on `no_std`, and then it can be unified. + diff --git a/smol/examples/con1.rs b/smol/examples/con1.rs index 9eb2fe9fb28062f281a1d08cca0cb0b22467806e..74f3d30cb1275a2bb705010af69117ba13b4a2f9 100644 --- a/smol/examples/con1.rs +++ b/smol/examples/con1.rs @@ -71,7 +71,7 @@ fn main() -> Result<()> { // time crate won't read TZ if we're threaded, in case someone // tries to mutate shared state with setenv. // https://github.com/rust-lang/rust/issues/90308 etc - // So we can't use async main. + // logging uses the timezone, so we can't use async main. setup_log(&args); tokio::runtime::Builder::new_multi_thread() @@ -104,7 +104,7 @@ fn setup_log(args: &Args) { .add_filter_allow_str("con1") // not debugging these bits of the stack at present // .add_filter_ignore_str("door_sshproto::traffic") - .add_filter_ignore_str("door_sshproto::runner") + // .add_filter_ignore_str("door_sshproto::runner") .add_filter_ignore_str("door_smol::async_door") .set_time_offset_to_local().expect("Couldn't get local timezone") .build(); diff --git a/smol/src/async_client.rs b/smol/src/async_client.rs index ec699d455249e478a2e2b772cd42b41178760ee2..ec2dc9538125c9f36c3997db5a0f5a8345527afc 100644 --- a/smol/src/async_client.rs +++ b/smol/src/async_client.rs @@ -34,6 +34,8 @@ impl SimpleClient { pub fn add_authkey(&mut self, k: SignKey) { self.authkeys.push_back(k) } + + fn write_stdout } #[async_trait(?Send)] diff --git a/sshproto/src/encrypt.rs b/sshproto/src/encrypt.rs index 8700bd507e9aa699fbdce575867f0ec34a075848..cfb018417d46438d7749f33971e470e216fd5398 100644 --- a/sshproto/src/encrypt.rs +++ b/sshproto/src/encrypt.rs @@ -453,7 +453,7 @@ impl fmt::Display for Cipher { impl Cipher { /// Creates a cipher key by algorithm name. Must be passed a known name. - pub fn from_name(name: &str) -> Result<Self, Error> { + pub fn from_name(name: &'static str) -> Result<Self, Error> { match name { SSH_NAME_CHAPOLY => Ok(Cipher::ChaPoly), SSH_NAME_AES256_CTR => Ok(Cipher::Aes256Ctr), @@ -571,7 +571,7 @@ pub(crate) enum Integ { impl Integ { /// Matches a MAC name. Should not be called for AEAD ciphers, instead use [`EncKey::integ`] etc - pub fn from_name(name: &str) -> Result<Self, Error> { + pub fn from_name(name: &'static str) -> Result<Self, Error> { match name { SSH_NAME_HMAC_SHA256 => Ok(Integ::HmacSha256), _ => Err(Error::bug()), diff --git a/sshproto/src/error.rs b/sshproto/src/error.rs index 78d2760b607e18950fa4f37966cba75f916c709f..3a7b8b8b30ed700fc5b66c812bcb0418f379c0cb 100644 --- a/sshproto/src/error.rs +++ b/sshproto/src/error.rs @@ -11,44 +11,9 @@ use heapless::String; use crate::behaviour::BhError; -// RFC4251 defines a maximum of 64, but 35 is probably enough to identify -// a problem. -#[derive(Debug)] -pub struct UnknownName(pub String<35>); - - impl fmt::Display for UnknownName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From<&str> for UnknownName { - - /// Indicates truncation - fn from(from: &str) -> Self { - let mut s = String::new(); - // +10 to avoid wasteful iteration on untrusted input - let need = from.escape_default().take(s.capacity()+10).count(); - let used = if need > s.capacity() { - s.capacity() - 4 - } else { - need - }; - for e in from.escape_default().take(used) { - s.push(e).unwrap() - } - - if need > used { - s.push_str(" ...").unwrap() - } - UnknownName(s) - } -} - - - // TODO: can we make Snafu not require Debug? // TODO: maybe split this into a list of public vs private errors? + #[non_exhaustive] #[derive(Snafu, Debug)] pub enum Error { @@ -74,8 +39,8 @@ pub enum Error { /// Key exchange incorrect BadKex, - #[snafu(display("Signature \"{sig}\" doesn't match key type \"{key}\""))] - SignatureMismatch { key: UnknownName, sig: UnknownName }, + // Signature doesn't match key type + SignatureMismatch, /// Error in received SSH protocol SSHProtoError, @@ -111,8 +76,8 @@ pub enum Error { /// An unknown SSH name is provided, for a key type, signature type, /// channel name etc. - #[snafu(display("Unknown {kind} method {name}"))] - UnknownMethod { kind: &'static str, name: UnknownName }, + #[snafu(display("Unknown {kind} method"))] + UnknownMethod { kind: &'static str}, /// Implementation behaviour error #[snafu(display("Failure from application: {msg}"))] @@ -263,17 +228,6 @@ pub(crate) mod tests { use crate::packets::Unknown; use proptest::prelude::*; - proptest! { - #[test] - fn unknown_name_from_pt(s: std::string::String) { - let u: UnknownName = s.as_str().into(); - let cap = u.0.capacity(); - if s.escape_default().count() > cap { - assert_eq!(&u.0[cap-4..], " ..."); - } - } - - } } diff --git a/sshproto/src/kex.rs b/sshproto/src/kex.rs index 0d19ac1088cfcb537028d12c0ade44abd90ed302..c934b6c7317738ed302bb62ec2b24b187ee033cb 100644 --- a/sshproto/src/kex.rs +++ b/sshproto/src/kex.rs @@ -383,7 +383,7 @@ impl fmt::Display for SharedSecret { } impl SharedSecret { - pub fn from_name(name: &str) -> Result<Self> { + pub fn from_name(name: &'static str) -> Result<Self> { match name { SSH_NAME_CURVE25519 | SSH_NAME_CURVE25519_LIBSSH => { Ok(SharedSecret::KexCurve25519(KexCurve25519::new()?)) diff --git a/sshproto/src/namelist.rs b/sshproto/src/namelist.rs index 79bb17af5d39a3f1802257b04852ef894c8ed3e6..318017283b9a983d5d396b36a6f12f914b8ec512 100644 --- a/sshproto/src/namelist.rs +++ b/sshproto/src/namelist.rs @@ -93,7 +93,7 @@ impl<'a> NameList<'a> { /// Must only be called on [`StringNames`], will fail if called with self as [`LocalNames`]. pub fn first_match( &self, is_client: bool, our_options: &LocalNames, - ) -> Result<Option<&str>> { + ) -> Result<Option<&'static str>> { match self { NameList::String(s) => Ok(if is_client { s.first_options_match(our_options) @@ -126,11 +126,11 @@ impl<'a> NameList<'a> { impl<'a> StringNames<'a> { /// Returns the first name in this namelist that matches one of the provided options - fn first_string_match(&self, options: &LocalNames) -> Option<&str> { + fn first_string_match(&self, options: &LocalNames) -> Option<&'static str> { for n in self.0.split(',') { for o in options.0.iter() { if n == *o { - return Some(n); + return Some(*o); } } } @@ -138,11 +138,11 @@ impl<'a> StringNames<'a> { } /// Returns the first of "options" that is in this namelist - fn first_options_match(&self, options: &LocalNames) -> Option<&str> { + fn first_options_match(&self, options: &LocalNames) -> Option<&'static str> { for o in options.0.iter() { for n in self.0.split(',') { if n == *o { - return Some(n); + return Some(*o); } } } diff --git a/sshproto/src/packets.rs b/sshproto/src/packets.rs index cbdd41a4f46b333d4036c0baad0c3b27099afad4..2d4341a3694be7693c4f387617276c180bd8dd8a 100644 --- a/sshproto/src/packets.rs +++ b/sshproto/src/packets.rs @@ -303,8 +303,10 @@ impl<'a> Signature<'a> { match pubkey { PubKey::Ed25519(_) => Ok(SSH_NAME_ED25519), PubKey::RSA(_) => Ok(SSH_NAME_RSA_SHA256), - PubKey::Unknown(u) => Err(Error::UnknownMethod {kind: "key", - name: u.0.into() }) + PubKey::Unknown(u) => { + warn!("Unknown key type \"{}\"", u); + Err(Error::UnknownMethod {kind: "key"}) + } } } @@ -313,8 +315,8 @@ impl<'a> Signature<'a> { Signature::Ed25519(_) => Ok(SigType::Ed25519), Signature::RSA256(_) => Ok(SigType::RSA256), Signature::Unknown(u) => { - Err(Error::UnknownMethod {kind: "signature", - name: u.0.into() }) + warn!("Unknown signature type \"{}\"", u); + Err(Error::UnknownMethod {kind: "signature" }) } } } @@ -547,6 +549,13 @@ pub struct DirectTcpip<'a> { #[derive(Debug, Clone, PartialEq)] pub struct Unknown<'a>(pub &'a str); +impl core::fmt::Display for Unknown<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.escape_default().fmt(f) + } + +} + /// State to be passed to decoding. /// Use this so the parser can select the correct enum variant to decode. #[derive(Default, Clone, Debug)] diff --git a/sshproto/src/sign.rs b/sshproto/src/sign.rs index fce77ee5e66c0aa5c8e37ec7d06fd941a37e60a3..7c30fcfeaa5f757e97cd3e053f7a6850dc5eab30 100644 --- a/sshproto/src/sign.rs +++ b/sshproto/src/sign.rs @@ -28,7 +28,7 @@ pub enum SigType { impl SigType { /// Must be a valid name - pub fn from_name(name: &str) -> Result<Self> { + pub fn from_name(name: &'static str) -> Result<Self> { match name { SSH_NAME_ED25519 => Ok(SigType::Ed25519), SSH_NAME_RSA_SHA256 => Ok(SigType::RSA256), @@ -85,10 +85,11 @@ impl SigType { } _ => { - Err(Error::SignatureMismatch { - key: pubkey.algorithm_name().into(), - sig: sig.algorithm_name().into(), - }) + warn!("Signature \"{}\" doesn't match key type \"{}\"", + sig.algorithm_name(), + pubkey.algorithm_name(), + ); + Err(Error::SignatureMismatch) } } }