russh 0.61.2

A client and server SSH library.
Documentation
use std::fmt::Debug;

use ssh_encoding::{Decode, Encode};

#[doc(hidden)]
pub trait EncodedExt {
    fn encoded(&self) -> ssh_key::Result<Vec<u8>>;
}

impl<E: Encode> EncodedExt for E {
    fn encoded(&self) -> ssh_key::Result<Vec<u8>> {
        let mut buf = Vec::new();
        self.encode(&mut buf)?;
        Ok(buf)
    }
}

mod limited_string {
    use super::*;
    use std::ops::Deref;

    pub struct LimitedString<const N: usize>(String);

    impl<const N: usize> Deref for LimitedString<N> {
        type Target = String;

        fn deref(&self) -> &Self::Target {
            &self.0
        }
    }

    impl<const N: usize> Decode for LimitedString<N> {
        type Error = ssh_encoding::Error;

        fn decode(reader: &mut impl ssh_encoding::Reader) -> Result<Self, Self::Error> {
            reader.read_prefixed(|reader| {
                let len = reader.remaining_len();
                if len > N {
                    return Err(ssh_encoding::Error::Length);
                }

                // Allocate only after the SSH string length has been bounded.
                let mut buf = vec![0; len];
                reader.read(&mut buf)?;
                let value =
                    String::from_utf8(buf).map_err(|_| ssh_encoding::Error::CharacterEncoding)?;
                reader.ensure_finished()?;

                Ok(Self(value))
            })
        }
    }
}

mod name_list {
    use std::ops::Deref;

    use super::*;
    const MAX_NAME_LIST_ENTRIES: usize = 1024;
    const MAX_NAME_LIST_BYTES: usize = 16 * 1024;

    pub struct NameList(pub Vec<String>);

    impl Debug for NameList {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            self.0.fmt(f)
        }
    }

    impl Deref for NameList {
        type Target = [String];

        fn deref(&self) -> &Self::Target {
            &self.0
        }
    }

    impl NameList {
        pub fn as_encoded_string(&self) -> String {
            self.0.join(",")
        }

        pub fn from_encoded_string(value: &str) -> Result<Self, ssh_encoding::Error> {
            // RFC 4251 §5: a name-list may have zero names (a string of
            // zero length). `"".split(',')` yields one empty element,
            // which the per-name validation below would reject.
            if value.is_empty() {
                return Ok(Self(Vec::new()));
            }
            Ok(Self(value.split(',').try_fold(
                Vec::new(),
                |mut list, name| {
                    if name.is_empty() || !name.is_ascii() {
                        return Err(ssh_encoding::Error::CharacterEncoding);
                    }
                    if list.len() > MAX_NAME_LIST_ENTRIES {
                        Err(ssh_encoding::Error::Length)
                    } else {
                        list.push(name.into());
                        Ok(list)
                    }
                },
            )?))
        }
    }

    impl Encode for NameList {
        fn encoded_len(&self) -> Result<usize, ssh_encoding::Error> {
            self.as_encoded_string().encoded_len()
        }

        fn encode(
            &self,
            writer: &mut impl ssh_encoding::Writer,
        ) -> Result<(), ssh_encoding::Error> {
            self.as_encoded_string().encode(writer)
        }
    }

    impl Decode for NameList {
        fn decode(reader: &mut impl ssh_encoding::Reader) -> Result<Self, ssh_encoding::Error> {
            let s = LimitedString::<MAX_NAME_LIST_BYTES>::decode(reader)?;
            Self::from_encoded_string(&s)
        }

        type Error = ssh_encoding::Error;
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn empty_name_list_is_valid() {
            // RFC 4251 §5 permits a zero-name list. Servers that only
            // offer AEAD ciphers (e.g. hssh) send empty MAC name-lists.
            let nl = NameList::from_encoded_string("").unwrap();
            assert!(nl.0.is_empty());
        }

        #[test]
        fn name_list_round_trip() {
            let nl = NameList::from_encoded_string("a,b,c").unwrap();
            assert_eq!(nl.0, vec!["a", "b", "c"]);
            assert_eq!(nl.as_encoded_string(), "a,b,c");
        }

        #[test]
        fn name_list_rejects_empty_entry() {
            // An empty entry mid-list (",,") is still invalid — only
            // the zero-length whole-list case is allowed.
            assert!(NameList::from_encoded_string("a,,b").is_err());
        }
    }
}

pub use limited_string::LimitedString;
pub use name_list::NameList;

pub(crate) mod macros {
    #[allow(clippy::crate_in_macro_def)]
    macro_rules! map_err {
        ($result:expr) => {
            $result.map_err(|e| crate::Error::from(e))
        };
    }

    pub(crate) use map_err;
}

#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
pub(crate) use macros::map_err;

#[doc(hidden)]
pub fn sign_with_hash_alg(key: &PrivateKeyWithHashAlg, data: &[u8]) -> ssh_key::Result<Vec<u8>> {
    Ok(match key.key_data() {
        #[cfg(feature = "rsa")]
        ssh_key::private::KeypairData::Rsa(rsa_keypair) => {
            let ssh_key::Algorithm::Rsa { hash } = key.algorithm() else {
                unreachable!();
            };
            signature::Signer::try_sign(&(rsa_keypair, hash), data)?.encoded()?
        }
        keypair => signature::Signer::try_sign(keypair, data)?.encoded()?,
    })
}

mod algorithm {
    use ssh_key::{Algorithm, HashAlg};

    pub trait AlgorithmExt {
        fn hash_alg(&self) -> Option<HashAlg>;
        fn with_hash_alg(&self, hash_alg: Option<HashAlg>) -> Self;
        fn new_certificate_ext(algo: &str) -> Result<Self, ssh_key::Error>
        where
            Self: Sized;
    }

    impl AlgorithmExt for Algorithm {
        fn hash_alg(&self) -> Option<HashAlg> {
            match self {
                Algorithm::Rsa { hash } => *hash,
                _ => None,
            }
        }

        fn with_hash_alg(&self, hash_alg: Option<HashAlg>) -> Self {
            match self {
                Algorithm::Rsa { .. } => Algorithm::Rsa { hash: hash_alg },
                x => x.clone(),
            }
        }

        fn new_certificate_ext(algo: &str) -> Result<Self, ssh_key::Error> {
            match algo {
                "rsa-sha2-256-cert-v01@openssh.com" => Ok(Algorithm::Rsa {
                    hash: Some(HashAlg::Sha256),
                }),
                "rsa-sha2-512-cert-v01@openssh.com" => Ok(Algorithm::Rsa {
                    hash: Some(HashAlg::Sha512),
                }),
                x => Algorithm::new_certificate(x),
            }
        }
    }
}

#[doc(hidden)]
pub use algorithm::AlgorithmExt;

use crate::keys::key::PrivateKeyWithHashAlg;