diff --git a/russh-keys/src/key.rs b/russh-keys/src/key.rs index 91edf0d2..d8ca809d 100644 --- a/russh-keys/src/key.rs +++ b/russh-keys/src/key.rs @@ -56,6 +56,16 @@ pub const NONE: Name = Name("none"); pub const SSH_RSA: Name = Name("ssh-rsa"); +pub static ALL_KEY_TYPES: &[&Name] = &[ + &NONE, + &SSH_RSA, + &RSA_SHA2_256, + &RSA_SHA2_512, + &ECDSA_SHA2_NISTP256, + &ECDSA_SHA2_NISTP384, + &ECDSA_SHA2_NISTP521, +]; + impl Name { /// Base name of the private key file for a key name. pub fn identity_file(&self) -> &'static str { @@ -69,6 +79,17 @@ impl Name { } } +impl TryFrom<&str> for Name { + type Error = (); + fn try_from(s: &str) -> Result { + ALL_KEY_TYPES + .iter() + .find(|x| x.0 == s) + .map(|x| **x) + .ok_or(()) + } +} + #[doc(hidden)] pub trait Verify { fn verify_client_auth(&self, buffer: &[u8], sig: &[u8]) -> bool; diff --git a/russh/src/cipher/mod.rs b/russh/src/cipher/mod.rs index 24b3bbfc..fbb627f7 100644 --- a/russh/src/cipher/mod.rs +++ b/russh/src/cipher/mod.rs @@ -14,7 +14,9 @@ //! //! This module exports cipher names for use with [Preferred]. +use std::borrow::Borrow; use std::collections::HashMap; +use std::convert::TryFrom; use std::fmt::Debug; use std::marker::PhantomData; use std::num::Wrapping; @@ -97,6 +99,19 @@ static _AES_192_CBC: SshBlockCipher> = SshBlockCipher(Phantom static _AES_256_CBC: SshBlockCipher> = SshBlockCipher(PhantomData); static _CHACHA20_POLY1305: SshChacha20Poly1305Cipher = SshChacha20Poly1305Cipher {}; +pub static ALL_CIPHERS: &[&Name] = &[ + &CLEAR, + &NONE, + &AES_128_CTR, + &AES_192_CTR, + &AES_256_CTR, + &AES_256_GCM, + &AES_128_CBC, + &AES_192_CBC, + &AES_256_CBC, + &CHACHA20_POLY1305, +]; + pub(crate) static CIPHERS: Lazy> = Lazy::new(|| { let mut h: HashMap<&'static Name, &(dyn Cipher + Send + Sync)> = HashMap::new(); @@ -110,6 +125,7 @@ pub(crate) static CIPHERS: Lazy for Name { } } +impl Borrow for &Name { + fn borrow(&self) -> &str { + self.0 + } +} + +impl TryFrom<&str> for Name { + type Error = (); + fn try_from(s: &str) -> Result { + CIPHERS.keys().find(|x| x.0 == s).map(|x| **x).ok_or(()) + } +} + pub(crate) struct CipherPair { pub local_to_remote: Box, pub remote_to_local: Box, diff --git a/russh/src/compression.rs b/russh/src/compression.rs index 6a63c2f0..6d739bf6 100644 --- a/russh/src/compression.rs +++ b/russh/src/compression.rs @@ -1,3 +1,5 @@ +use std::convert::TryFrom; + #[derive(Debug, Clone)] pub enum Compression { None, @@ -19,10 +21,43 @@ pub enum Decompress { Zlib(flate2::Decompress), } +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] +pub struct Name(&'static str); +impl AsRef for Name { + fn as_ref(&self) -> &str { + self.0 + } +} + +impl TryFrom<&str> for Name { + type Error = (); + fn try_from(s: &str) -> Result { + ALL_COMPRESSION_ALGORITHMS + .iter() + .find(|x| x.0 == s) + .map(|x| **x) + .ok_or(()) + } +} + +pub const NONE: Name = Name("none"); +#[cfg(feature = "flate2")] +pub const ZLIB: Name = Name("zlib"); +#[cfg(feature = "flate2")] +pub const ZLIB_LEGACY: Name = Name("zlib@openssh.com"); + +pub const ALL_COMPRESSION_ALGORITHMS: &[&Name] = &[ + &NONE, + #[cfg(feature = "flate2")] + &ZLIB, + #[cfg(feature = "flate2")] + &ZLIB_LEGACY, +]; + #[cfg(feature = "flate2")] impl Compression { - pub fn from_string(s: &str) -> Self { - if s == "zlib" || s == "zlib@openssh.com" { + pub fn new(name: &Name) -> Self { + if name == &ZLIB || name == &ZLIB_LEGACY { Compression::Zlib } else { Compression::None @@ -56,7 +91,7 @@ impl Compression { #[cfg(not(feature = "flate2"))] impl Compression { - pub fn from_string(_: &str) -> Self { + pub fn new(_name: &Name) -> Self { Compression::None } diff --git a/russh/src/kex/mod.rs b/russh/src/kex/mod.rs index 4ea89016..3777a01f 100644 --- a/russh/src/kex/mod.rs +++ b/russh/src/kex/mod.rs @@ -21,6 +21,7 @@ mod ecdh_nistp; mod none; use std::cell::RefCell; use std::collections::HashMap; +use std::convert::TryFrom; use std::fmt::Debug; use curve25519::Curve25519KexType; @@ -87,6 +88,13 @@ impl AsRef for Name { } } +impl TryFrom<&str> for Name { + type Error = (); + fn try_from(s: &str) -> Result { + KEXES.keys().find(|x| x.0 == s).map(|x| **x).ok_or(()) + } +} + /// `curve25519-sha256` pub const CURVE25519: Name = Name("curve25519-sha256"); /// `curve25519-sha256@libssh.org` @@ -126,6 +134,19 @@ const _ECDH_SHA2_NISTP384: EcdhNistP384KexType = EcdhNistP384KexType {}; const _ECDH_SHA2_NISTP521: EcdhNistP521KexType = EcdhNistP521KexType {}; const _NONE: none::NoneKexType = none::NoneKexType {}; +pub const ALL_KEX_ALGORITHMS: &[&Name] = &[ + &CURVE25519, + &CURVE25519_PRE_RFC_8731, + &DH_G1_SHA1, + &DH_G14_SHA1, + &DH_G14_SHA256, + &DH_G16_SHA512, + &ECDH_SHA2_NISTP256, + &ECDH_SHA2_NISTP384, + &ECDH_SHA2_NISTP521, + &NONE, +]; + pub(crate) static KEXES: Lazy> = Lazy::new(|| { let mut h: HashMap<&'static Name, &(dyn KexType + Send + Sync)> = HashMap::new(); @@ -139,6 +160,7 @@ pub(crate) static KEXES: Lazy for Name { } } +impl TryFrom<&str> for Name { + type Error = (); + fn try_from(s: &str) -> Result { + MACS.keys().find(|x| x.0 == s).map(|x| **x).ok_or(()) + } +} + /// `none` pub const NONE: Name = Name("none"); /// `hmac-sha1` @@ -81,6 +89,16 @@ static _HMAC_SHA256_ETM: CryptoEtmMacAlgorithm, U32> = static _HMAC_SHA512_ETM: CryptoEtmMacAlgorithm, U64> = CryptoEtmMacAlgorithm(PhantomData, PhantomData); +pub const ALL_MAC_ALGORITHMS: &[&Name] = &[ + &NONE, + &HMAC_SHA1, + &HMAC_SHA256, + &HMAC_SHA512, + &HMAC_SHA1_ETM, + &HMAC_SHA256_ETM, + &HMAC_SHA512_ETM, +]; + pub(crate) static MACS: Lazy> = Lazy::new(|| { let mut h: HashMap<&'static Name, &(dyn MacAlgorithm + Send + Sync)> = HashMap::new(); @@ -91,5 +109,6 @@ pub(crate) static MACS: Lazy, /// Preferred host & public key algorithms. - pub key: &'static [key::Name], + pub key: Cow<'static, [key::Name]>, /// Preferred symmetric ciphers. - pub cipher: &'static [cipher::Name], + pub cipher: Cow<'static, [cipher::Name]>, /// Preferred MAC algorithms. - pub mac: &'static [mac::Name], + pub mac: Cow<'static, [mac::Name]>, /// Preferred compression algorithms. - pub compression: &'static [&'static str], + pub compression: Cow<'static, [compression::Name]>, } impl Preferred { pub(crate) fn possible_host_key_algos_for_keys( &self, available_host_keys: &[KeyPair], - ) -> Vec<&'static key::Name> { + ) -> Vec { self.key .iter() .filter(|n| available_host_keys.iter().any(|k| k.name() == n.0)) + .copied() .collect::>() } } @@ -95,27 +97,35 @@ const HMAC_ORDER: &[mac::Name] = &[ mac::HMAC_SHA1, ]; +const COMPRESSION_ORDER: &[compression::Name] = &[ + compression::NONE, + #[cfg(feature = "flate2")] + compression::ZLIB, + #[cfg(feature = "flate2")] + compression::ZLIB_LEGACY, +]; + impl Preferred { pub const DEFAULT: Preferred = Preferred { - kex: SAFE_KEX_ORDER, - key: &[ + kex: Cow::Borrowed(SAFE_KEX_ORDER), + key: Cow::Borrowed(&[ key::ED25519, key::ECDSA_SHA2_NISTP256, key::ECDSA_SHA2_NISTP521, key::RSA_SHA2_256, key::RSA_SHA2_512, - ], - cipher: CIPHER_ORDER, - mac: HMAC_ORDER, - compression: &["none", "zlib", "zlib@openssh.com"], + ]), + cipher: Cow::Borrowed(CIPHER_ORDER), + mac: Cow::Borrowed(HMAC_ORDER), + compression: Cow::Borrowed(COMPRESSION_ORDER), }; pub const COMPRESSED: Preferred = Preferred { - kex: SAFE_KEX_ORDER, + kex: Cow::Borrowed(SAFE_KEX_ORDER), key: Preferred::DEFAULT.key, - cipher: CIPHER_ORDER, - mac: HMAC_ORDER, - compression: &["zlib", "zlib@openssh.com", "none"], + cipher: Cow::Borrowed(CIPHER_ORDER), + mac: Cow::Borrowed(HMAC_ORDER), + compression: Cow::Borrowed(COMPRESSION_ORDER), }; } @@ -162,7 +172,7 @@ impl Named for KeyPair { pub(crate) trait Select { fn is_server() -> bool; - fn select + Copy>(a: &[S], b: &[u8]) -> Option<(bool, S)>; + fn select + Clone>(a: &[S], b: &[u8]) -> Option<(bool, S)>; /// `available_host_keys`, if present, is used to limit the host key algorithms to the ones we have keys for. fn read_kex( @@ -175,7 +185,7 @@ pub(crate) trait Select { // Key exchange let kex_string = r.read_string()?; - let (kex_both_first, kex_algorithm) = Self::select(pref.kex, kex_string).ok_or_else(|| + let (kex_both_first, kex_algorithm) = Self::select(&pref.kex, kex_string).ok_or_else(|| { debug!( "Could not find common kex algorithm, other side only supports {:?}, we only support {:?}", @@ -210,7 +220,7 @@ pub(crate) trait Select { let key_string: &[u8] = r.read_string()?; let possible_host_key_algos = match available_host_keys { Some(available_host_keys) => pref.possible_host_key_algos_for_keys(available_host_keys), - None => pref.key.iter().collect::>(), + None => pref.key.iter().map(ToOwned::to_owned).collect::>(), }; let (key_both_first, key_algorithm) = @@ -227,7 +237,7 @@ pub(crate) trait Select { let cipher_string = r.read_string()?; let (_cipher_both_first, cipher) = - Self::select(pref.cipher, cipher_string).ok_or_else(|| { + Self::select(&pref.cipher, cipher_string).ok_or_else(|| { debug!( "Could not find common cipher, other side only supports {:?}, we only support {:?}", from_utf8(cipher_string), @@ -242,14 +252,14 @@ pub(crate) trait Select { let need_mac = CIPHERS.get(&cipher).map(|x| x.needs_mac()).unwrap_or(false); - let client_mac = if let Some((_, m)) = Self::select(pref.mac, r.read_string()?) { + let client_mac = if let Some((_, m)) = Self::select(&pref.mac, r.read_string()?) { m } else if need_mac { return Err(Error::NoCommonMac); } else { mac::NONE }; - let server_mac = if let Some((_, m)) = Self::select(pref.mac, r.read_string()?) { + let server_mac = if let Some((_, m)) = Self::select(&pref.mac, r.read_string()?) { m } else if need_mac { return Err(Error::NoCommonMac); @@ -262,16 +272,16 @@ pub(crate) trait Select { debug!("kex {}", line!()); // client-to-server compression. let client_compression = - if let Some((_, c)) = Self::select(pref.compression, r.read_string()?) { - Compression::from_string(c) + if let Some((_, c)) = Self::select(&pref.compression, r.read_string()?) { + compression::Compression::new(&c) } else { return Err(Error::NoCommonCompression); }; debug!("kex {}", line!()); // server-to-client compression. let server_compression = - if let Some((_, c)) = Self::select(pref.compression, r.read_string()?) { - Compression::from_string(c) + if let Some((_, c)) = Self::select(&pref.compression, r.read_string()?) { + compression::Compression::new(&c) } else { return Err(Error::NoCommonCompression); }; @@ -282,7 +292,7 @@ pub(crate) trait Select { let follows = r.read_byte()? != 0; Ok(Names { kex: kex_algorithm, - key: *key_algorithm, + key: key_algorithm, cipher, client_mac, server_mac, @@ -303,12 +313,12 @@ impl Select for Server { true } - fn select + Copy>(server_list: &[S], client_list: &[u8]) -> Option<(bool, S)> { + fn select + Clone>(server_list: &[S], client_list: &[u8]) -> Option<(bool, S)> { let mut both_first_choice = true; for c in client_list.split(|&x| x == b',') { - for &s in server_list { + for s in server_list { if c == s.as_ref().as_bytes() { - return Some((both_first_choice, s)); + return Some((both_first_choice, s.clone())); } both_first_choice = false } @@ -322,12 +332,12 @@ impl Select for Client { false } - fn select + Copy>(client_list: &[S], server_list: &[u8]) -> Option<(bool, S)> { + fn select + Clone>(client_list: &[S], server_list: &[u8]) -> Option<(bool, S)> { let mut both_first_choice = true; - for &c in client_list { + for c in client_list { for s in server_list.split(|&x| x == b',') { if s == c.as_ref().as_bytes() { - return Some((both_first_choice, c)); + return Some((both_first_choice, c.clone())); } both_first_choice = false }