diff --git a/assets/dh_init.raw b/assets/dh_init.raw deleted file mode 100644 index 917b770..0000000 Binary files a/assets/dh_init.raw and /dev/null differ diff --git a/assets/dh_reply.raw b/assets/dh_reply.raw deleted file mode 100644 index d2b46ac..0000000 Binary files a/assets/dh_reply.raw and /dev/null differ diff --git a/assets/kex/dh-kex-gex/client_kex_init.raw b/assets/kex/dh-kex-gex/client_kex_init.raw new file mode 100644 index 0000000..51bdc71 Binary files /dev/null and b/assets/kex/dh-kex-gex/client_kex_init.raw differ diff --git a/assets/kex/dh-kex-gex/group.raw b/assets/kex/dh-kex-gex/group.raw new file mode 100644 index 0000000..6eef0bb Binary files /dev/null and b/assets/kex/dh-kex-gex/group.raw differ diff --git a/assets/kex/dh-kex-gex/init.raw b/assets/kex/dh-kex-gex/init.raw new file mode 100644 index 0000000..bf9ced0 Binary files /dev/null and b/assets/kex/dh-kex-gex/init.raw differ diff --git a/assets/kex/dh-kex-gex/reply.raw b/assets/kex/dh-kex-gex/reply.raw new file mode 100644 index 0000000..3692fad Binary files /dev/null and b/assets/kex/dh-kex-gex/reply.raw differ diff --git a/assets/kex/dh-kex-gex/request.raw b/assets/kex/dh-kex-gex/request.raw new file mode 100644 index 0000000..cea9cc0 Binary files /dev/null and b/assets/kex/dh-kex-gex/request.raw differ diff --git a/assets/kex/dh-kex-gex/server_kex_init.raw b/assets/kex/dh-kex-gex/server_kex_init.raw new file mode 100644 index 0000000..1e8448e Binary files /dev/null and b/assets/kex/dh-kex-gex/server_kex_init.raw differ diff --git a/assets/kex/dh/client_kex_init.raw b/assets/kex/dh/client_kex_init.raw new file mode 100644 index 0000000..0c8faf1 Binary files /dev/null and b/assets/kex/dh/client_kex_init.raw differ diff --git a/assets/kex/dh/init.raw b/assets/kex/dh/init.raw new file mode 100644 index 0000000..db52ecf Binary files /dev/null and b/assets/kex/dh/init.raw differ diff --git a/assets/kex/dh/reply.raw b/assets/kex/dh/reply.raw new file mode 100644 index 0000000..37e38cd Binary files /dev/null and b/assets/kex/dh/reply.raw differ diff --git a/assets/kex/dh/server_kex_init.raw b/assets/kex/dh/server_kex_init.raw new file mode 100644 index 0000000..d674938 Binary files /dev/null and b/assets/kex/dh/server_kex_init.raw differ diff --git a/assets/kex/ecdh/client_kex_init.raw b/assets/kex/ecdh/client_kex_init.raw new file mode 100644 index 0000000..359daab Binary files /dev/null and b/assets/kex/ecdh/client_kex_init.raw differ diff --git a/assets/kex/ecdh/init.raw b/assets/kex/ecdh/init.raw new file mode 100644 index 0000000..3727ed9 Binary files /dev/null and b/assets/kex/ecdh/init.raw differ diff --git a/assets/kex/ecdh/reply.raw b/assets/kex/ecdh/reply.raw new file mode 100644 index 0000000..462394c Binary files /dev/null and b/assets/kex/ecdh/reply.raw differ diff --git a/assets/kex/ecdh/server_kex_init.raw b/assets/kex/ecdh/server_kex_init.raw new file mode 100644 index 0000000..828a89e Binary files /dev/null and b/assets/kex/ecdh/server_kex_init.raw differ diff --git a/src/kex.rs b/src/kex.rs new file mode 100644 index 0000000..7321b60 --- /dev/null +++ b/src/kex.rs @@ -0,0 +1,703 @@ +//! # KEX parser +//! +//! This module contains parsing functions for the Key Exchange part of the +//! SSH 2.0 protocol. +//! +//! The supported Key Exchange protocols are the following: +//! +//! - Diffie Hellman Key Exchange, `SSH_MSG_KEXDH_`, defined in RFC4253 section 8. +//! - Elliptic Curve Diffie Hellman Key Exchange, `SSH_MSG_KEXECDH_INIT`, defined +//! in RFC6239 sections 4.1 and 4.2. +//! - Diffie Hellman Group and Key Exchange, `SSH_MSG_KEY_DH_GEX_`, defined in +//! RFC4419 section 5. + +#[cfg(feature = "integers")] +use std::marker::PhantomData; + +use nom::combinator::{all_consuming, map}; +use nom::error::Error; +use nom::number::streaming::be_u32; +use nom::sequence::{pair, tuple}; +use nom::IResult; +#[cfg(feature = "integers")] +use num_bigint::BigInt; + +use super::ssh::parse_string; + +use super::{SshPacketKeyExchange, SshPacketUnparsed}; + +/// Diffie-Hellman Key Exchange Init message code. +/// Defined in [RFC4253 errata 1486](https://www.rfc-editor.org/errata/eid1486). +pub const SSH_MSG_KEXDH_INIT: u8 = 30; + +/// Diffie-Hellman Key Exchange Reply message code. +/// Defined in [RFC4253 errata 1486](https://www.rfc-editor.org/errata/eid1486). +pub const SSH_MSG_KEXDH_REPLY: u8 = 31; + +/// Elliptic Curve Diffie-Hellman Key Exchange Init message code. +/// Defined in [RFC6239 section 4.1](https://datatracker.ietf.org/doc/html/rfc6239#section-4.1). +pub const SSH_MSG_KEXECDH_INIT: u8 = SSH_MSG_KEXDH_INIT; + +/// Elliptic Curve Diffie-Hellman Key Exchange Reply message code. +/// Defined in [RFC6239 section 4.2](https://datatracker.ietf.org/doc/html/rfc6239#section-4.2). +pub const SSH_MSG_KEXECDH_REPLY: u8 = SSH_MSG_KEXDH_REPLY; + +/// Diffie-Hellman Group and Key Exchange Request message code. +/// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +pub const SSH_MSG_KEX_DH_GEX_REQUEST: u8 = 34; + +/// Diffie-Hellman Group and Key Exchange Request Old message code. +/// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +pub const SSH_MSG_KEX_DH_GEX_REQUEST_OLD: u8 = 30; + +/// Diffie-Hellman Group and Key Exchange Group message code. +/// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +pub const SSH_MSG_KEX_DH_GEX_GROUP: u8 = 31; + +/// Diffie-Hellman Group and Key Exchange Init message code. +/// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +pub const SSH_MSG_KEX_DH_GEX_INIT: u8 = 32; + +/// Diffie-Hellman Group and Key Exchange Reply message code. +/// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +pub const SSH_MSG_KEX_DH_GEX_REPLY: u8 = 33; + +#[cfg(feature = "integers")] +fn parse_mpint(i: &[u8]) -> IResult<&[u8], BigInt> { + nom::combinator::map_parser(parse_string, crate::mpint::parse_ssh_mpint)(i) +} + +#[cfg(not(feature = "integers"))] +fn parse_mpint(i: &[u8]) -> IResult<&[u8], &[u8]> { + parse_string(i) +} + +/// SSH Diffie-Hellman Key Exchange Init. +/// +/// The message code is `SSH_MSG_KEXDH_INIT`, defined in [RFC4253 section 8](https://datatracker.ietf.org/doc/html/rfc4253#section-8). +#[derive(Debug, PartialEq)] +pub struct SshPacketDHKEXInit<'a> { + /// The public key. + #[cfg(feature = "integers")] + pub e: BigInt, + + /// The public key. + #[cfg(not(feature = "integers"))] + pub e: &'a [u8], + + #[cfg(feature = "integers")] + phantom: std::marker::PhantomData<&'a [u8]>, +} + +#[cfg(feature = "integers")] +impl From for SshPacketDHKEXInit<'_> { + fn from(e: BigInt) -> Self { + Self { + e, + phantom: PhantomData, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b> From<&'b [u8]> for SshPacketDHKEXInit<'b> +where + 'b: 'a, +{ + fn from(e: &'b [u8]) -> Self { + Self { e } + } +} + +impl<'a> SshPacketDHKEXInit<'a> { + /// Parses a SSH Diffie-Hellman Key Exchange Init. + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(parse_mpint, Self::from)(i) + } +} + +/// SSH Diffie-Hellman Key Exchange Reply. +/// +/// The message code is `SSH_MSG_KEXDH_REPLY`, defined in [RFC4253 section 8](https://datatracker.ietf.org/doc/html/rfc4253#section-8). +#[derive(Debug, PartialEq)] +pub struct SshPacketDHKEXReply<'a> { + /// The server public host key and certificate. + pub pubkey_and_cert: &'a [u8], + + /// The `f` value corresponding to `g^y mod p` where `g` is the group and `y` a random number. + #[cfg(feature = "integers")] + pub f: BigInt, + + /// The `f` value corresponding to `g^y mod p` where `g` is the group and `y` a random number. + #[cfg(not(feature = "integers"))] + pub f: &'a [u8], + + /// The signature. + pub signature: &'a [u8], +} + +#[cfg(feature = "integers")] +impl<'a, 'b, 'c> From<(&'b [u8], BigInt, &'c [u8])> for SshPacketDHKEXReply<'a> +where + 'b: 'a, + 'c: 'a, +{ + fn from((pubkey_and_cert, f, signature): (&'b [u8], BigInt, &'c [u8])) -> Self { + Self { + pubkey_and_cert, + f, + signature, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketDHKEXReply<'a> +where + 'b: 'a, + 'c: 'a, + 'd: 'a, +{ + fn from((pubkey_and_cert, f, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { + Self { + pubkey_and_cert, + f, + signature, + } + } +} + +/// An ECDSA signature. +/// +/// ECDSA signatures are defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2). +#[derive(Debug, PartialEq)] +pub struct ECDSASignature<'a> { + /// Identifier. + pub identifier: &'a str, + + /// Blob. + pub blob: &'a [u8], +} + +impl<'a> SshPacketDHKEXReply<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(tuple((parse_string, parse_mpint, parse_string)), Self::from)(i) + } + + /// Parses the ECDSA signature. + /// + /// ECDSA signatures are Defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2). + pub fn get_ecdsa_signature(&self) -> Result, SshKEXError<'a>> { + let (_, (identifier, blob)) = pair(parse_string, parse_string)(self.signature)?; + + let identifier = std::str::from_utf8(identifier)?; + Ok(ECDSASignature { identifier, blob }) + } +} + +/// The key exchange protocol using Diffie Hellman Key Exchange, defined in RFC4253. +#[derive(Debug, Default, PartialEq)] +pub struct SshKEXDiffieHellman<'a> { + /// The init message, i.e. `SSH_MSG_KEXDH_INIT`. + pub init: Option>, + + /// The reply message, i.e. `SSH_MSG_KEXDH_REPLY`. + pub reply: Option>, +} + +/// SSH Elliptic Curve Diffie-Hellman Key Exchange Init. +/// +/// The message is `SSH_MSG_KEXECDH_INIT`, defined in [RFC6239 section 4.1](https://datatracker.ietf.org/doc/html/rfc6239#section-4.1). +#[derive(Debug, PartialEq)] +pub struct SshPacketECDHKEXInit<'a> { + /// The client's ephemeral contribution to theECDH exchange, encoded as an octet string. + pub q_c: &'a [u8], +} + +impl<'a, 'b> From<&'b [u8]> for SshPacketECDHKEXInit<'a> +where + 'b: 'a, +{ + fn from(q_c: &'b [u8]) -> Self { + Self { q_c } + } +} + +impl<'a> SshPacketECDHKEXInit<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(parse_string, Self::from)(i) + } +} + +/// SSH Elliptic Curve Diffie-Hellman Key Exchange Reply. +/// +/// The message is `SSH_MSG_KEXECDH_REPLY`, defined in [RFC6239 section 4.2](https://datatracker.ietf.org/doc/html/rfc6239#section-4.2). +#[derive(Debug, PartialEq)] +pub struct SshPacketECDHKEXReply<'a> { + /// A string encoding an X.509v3 certificate containing the server's ECDSA public host key. + pub pubkey_and_cert: &'a [u8], + + /// The server's ephemeral contribution to the ECDH exchange, encoded as an octet string. + pub q_s: &'a [u8], + + /// The server's signature of the newly established exchange hash value. + pub signature: &'a [u8], +} + +impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketECDHKEXReply<'a> +where + 'b: 'a, + 'c: 'a, + 'd: 'a, +{ + fn from((pubkey_and_cert, q_s, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { + Self { + pubkey_and_cert, + q_s, + signature, + } + } +} + +impl<'a> SshPacketECDHKEXReply<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map( + tuple((parse_string, parse_string, parse_string)), + Self::from, + )(i) + } +} + +/// The key exchange protocol using Elliptic Curve Diffie Hellman Key Exchange, defined in RFC6239. +#[derive(Debug, Default, PartialEq)] +pub struct SshKEXECDiffieHellman<'a> { + /// The init message, i.e. `SSH_MSG_KEXECDH_INIT`. + pub init: Option>, + + /// The reply message, i.e. `SSH_MSG_KEXECDH_REPLY`. + pub reply: Option>, +} + +/// SSH Diffie-Hellman Group and Key Exchange Request. +/// +/// The message code is `SSH_MSG_KEY_DH_GEX_REQUEST`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXRequest { + /// Minimal size in bits of an acceptable group. + pub min: u32, + + /// Preferred size in bits of the group the server will send. + pub n: u32, + + /// Maximal size in bits of an acceptable group. + pub max: u32, +} + +impl From<(u32, u32, u32)> for SshPacketDhKEXGEXRequest { + fn from((min, n, max): (u32, u32, u32)) -> Self { + Self { min, n, max } + } +} + +impl SshPacketDhKEXGEXRequest { + pub fn parse(i: &[u8]) -> IResult<&[u8], Self> { + map(tuple((be_u32, be_u32, be_u32)), Self::from)(i) + } +} + +/// SSH Diffie-Hellman Group and Key Exchange Request (old). +/// +/// The message code is `SSH_MSG_KEY_DH_GEX_REQUEST_OLD`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXRequestOld { + /// Preferred size in bits of the group the server will send. + pub n: u32, +} + +impl From for SshPacketDhKEXGEXRequestOld { + fn from(n: u32) -> Self { + Self { n } + } +} + +impl SshPacketDhKEXGEXRequestOld { + pub fn parse(i: &[u8]) -> IResult<&[u8], Self> { + map(be_u32, Self::from)(i) + } +} + +/// SSH Diffie-Hellman Group and Key Exchange Group. +/// +/// The message code is `SSH_MSG_KEX_DH_GEX_GROUP`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXGroup<'a> { + /// The safe prime. + #[cfg(feature = "integers")] + pub p: BigInt, + + /// The safe prime. + #[cfg(not(feature = "integers"))] + pub p: &'a [u8], + + /// The generator for the subgroup in the Galois Field GF(p). + #[cfg(feature = "integers")] + pub g: BigInt, + + /// The generator for the subgroup in the Galois Field GF(p). + #[cfg(not(feature = "integers"))] + pub g: &'a [u8], + + #[cfg(feature = "integers")] + phantom: PhantomData<&'a [u8]>, +} + +#[cfg(feature = "integers")] +impl From<(BigInt, BigInt)> for SshPacketDhKEXGEXGroup<'_> { + fn from((p, g): (BigInt, BigInt)) -> Self { + Self { + p, + g, + phantom: PhantomData, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b, 'c> From<(&'b [u8], &'c [u8])> for SshPacketDhKEXGEXGroup<'a> +where + 'b: 'a, + 'c: 'a, +{ + fn from((p, g): (&'b [u8], &'c [u8])) -> Self { + Self { p, g } + } +} + +impl<'a> SshPacketDhKEXGEXGroup<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(pair(parse_mpint, parse_mpint), Self::from)(i) + } +} + +/// SSH Diffie-Hellman Group and Key Exchange Init. +/// +/// The message code is `SSH_MSG_KEX_DH_GEX_INIT`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXInit<'a> { + /// The public key. + #[cfg(feature = "integers")] + pub e: BigInt, + + /// The public key. + #[cfg(not(feature = "integers"))] + pub e: &'a [u8], + + #[cfg(feature = "integers")] + phantom: PhantomData<&'a [u8]>, +} + +#[cfg(feature = "integers")] +impl From for SshPacketDhKEXGEXInit<'_> { + fn from(e: BigInt) -> Self { + Self { + e, + phantom: PhantomData, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b> From<&'b [u8]> for SshPacketDhKEXGEXInit<'a> +where + 'b: 'a, +{ + fn from(e: &'b [u8]) -> Self { + Self { e } + } +} + +/// Parses a SSH Diffie-Hellman Group and Key Exchange init. +impl<'a> SshPacketDhKEXGEXInit<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(parse_mpint, Self::from)(i) + } +} + +/// SSH Diffie-Hellman Group and Key Exchange Reply. +/// +/// The message code is `SSH_MSG_KEX_DH_GEX_REPLY`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXReply<'a> { + /// Server public host key and certificate. + pub pubkey_and_cert: &'a [u8], + + /// f. + #[cfg(feature = "integers")] + pub f: BigInt, + + /// f. + #[cfg(not(feature = "integers"))] + pub f: &'a [u8], + + /// Signature. + pub signature: &'a [u8], +} + +#[cfg(feature = "integers")] +impl<'a, 'b, 'c> From<(&'b [u8], BigInt, &'c [u8])> for SshPacketDhKEXGEXReply<'a> +where + 'b: 'a, + 'c: 'a, +{ + fn from((pubkey_and_cert, f, signature): (&'b [u8], BigInt, &'c [u8])) -> Self { + Self { + pubkey_and_cert, + f, + signature, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketDhKEXGEXReply<'a> +where + 'b: 'a, + 'c: 'a, + 'd: 'a, +{ + fn from((pubkey_and_cert, f, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { + Self { + pubkey_and_cert, + f, + signature, + } + } +} + +impl<'a> SshPacketDhKEXGEXReply<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(tuple((parse_string, parse_mpint, parse_string)), Self::from)(i) + } +} + +/// The key exchange protocol using Diffie Hellman Group and Key, defined in RFC4419. +#[derive(Debug, Default, PartialEq)] +pub struct SshKEXDiffieHellmanKEXGEX<'a> { + /// The request message, i.e. `SSH_MSG_KEY_DH_GEX_REQUEST`. + pub request: Option, + + /// The request message (old variant), i.e. `SSH_MSG_KEY_DH_GEX_REQUEST_OLD`. + pub request_old: Option, + + /// The group message, i.e. `SSH_MSG_KEX_DH_GEX_GROUP`. + pub group: Option>, + + /// The init message, i.e. `SSH_MSG_KEX_DH_GEX_INIT`. + pub init: Option>, + + /// The init message, i.e. `SSH_MSG_KEX_DH_GEX_REPLY`. + pub reply: Option>, +} + +/// An error occurring in the KEX parser. +#[derive(Debug)] +pub enum SshKEXError<'a> { + /// nom error. + Nom(nom::Err>), + + /// Could not negociate a KEX algorithm. + NegociationFailed, + + /// Unknown KEX protocol. + UnknownProtocol, + + /// Duplicated message. + DuplicatedMessage, + + /// Unexpected message. + UnexpectedMessage, + + /// Invalid UTF-8 string. + InvalidUtf8(std::str::Utf8Error), + + /// Other error. + Other(String), +} + +impl std::fmt::Display for SshKEXError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl<'a> From>> for SshKEXError<'a> { + fn from(e: nom::Err>) -> Self { + Self::Nom(e) + } +} + +impl From for SshKEXError<'_> { + fn from(e: String) -> Self { + Self::Other(e) + } +} + +impl From<&str> for SshKEXError<'_> { + fn from(e: &str) -> Self { + Self::Other(e.to_string()) + } +} + +impl From for SshKEXError<'_> { + fn from(e: std::str::Utf8Error) -> Self { + Self::InvalidUtf8(e) + } +} + +impl std::error::Error for SshKEXError<'_> {} + +macro_rules! parse_match_and_assign { + ($variant:ident, $field:ident, $struct:ident, $payload:ident) => { + if $variant.$field.is_some() { + Err(SshKEXError::DuplicatedMessage) + } else { + $variant.$field = Some(all_consuming($struct::parse)($payload)?.1); + Ok(()) + } + }; +} + +/// Negociates the KEX algorithm. +pub fn ssh_kex_negociate_algorithm<'a, 'b, 'c, S1, S2>( + client_kex_algs: impl IntoIterator, + server_kex_algs: impl IntoIterator, +) -> Option<&'a str> +where + 'b: 'a, + 'c: 'a, + S1: AsRef + 'b + ?Sized, + S2: AsRef + 'c + ?Sized, +{ + let server_algs = server_kex_algs + .into_iter() + .map(|s| s.as_ref()) + .collect::>(); + client_kex_algs + .into_iter() + .find(|&item| server_algs.contains(&item.as_ref())) + .map(|s| s.as_ref()) +} + +/// The key exchange protocol. +#[derive(Debug, PartialEq)] +pub enum SshKEX<'a> { + /// Diffie Hellman Key Exchange, defined in RFC4253. + DiffieHellman(SshKEXDiffieHellman<'a>), + + /// Elliptic Curve Diffie Hellman, defined in RFC6239. + ECDiffieHellman(SshKEXECDiffieHellman<'a>), + + /// Diffie Hellman Group and Key, defined in RFC4419. + DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX<'a>), +} + +impl<'a> SshKEX<'a> { + /// Initializes a [`SshKEX`] using the kex algorithms sent during the kex exchange + /// init stage. + /// The returned string is the negociated KEX algorithm. + pub fn init<'b, 'c>( + client_kex_init: &'b SshPacketKeyExchange<'b>, + server_kex_init: &'c SshPacketKeyExchange<'c>, + ) -> Result<(Self, &'a str), SshKEXError<'a>> + where + 'b: 'a, + 'c: 'a, + { + let client_kex_list = client_kex_init.get_kex_algs()?; + let server_kex_list = server_kex_init.get_kex_algs()?; + let negociated_alg = ssh_kex_negociate_algorithm(client_kex_list, server_kex_list) + .ok_or(SshKEXError::NegociationFailed)?; + match negociated_alg { + "diffie-hellman-group1-sha1" + | "diffie-hellman-group14-sha1" + | "diffie-hellman-group14-sha256" + | "diffie-hellman-group16-sha512" + | "diffie-hellman-group18-sha512" => { + Ok(Self::DiffieHellman(SshKEXDiffieHellman::default())) + } + "curve25519-sha256" + | "curve25519-sha256@libssh.org" + | "curve448-sha512" + | "ecdh-sha2-nistp256" + | "ecdh-sha2-nistp384" + | "ecdh-sha2-nistp521" => Ok(Self::ECDiffieHellman(SshKEXECDiffieHellman::default())), + "diffie-hellman-group-exchange-sha1" | "diffie-hellman-group-exchange-sha256" => Ok( + Self::DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX::default()), + ), + _ => Err(SshKEXError::UnknownProtocol), + } + .map(|kex| (kex, negociated_alg)) + } + + /// Parses a new message according to the selected KEX method. + /// If the parsed message is not related to the KEX protocol, SshKEXError::UnexpectedMessage + /// is returned. + pub fn parse_ssh_packet<'c>( + &mut self, + unparsed_ssh_packet: &'c SshPacketUnparsed<'c>, + ) -> Result<(), SshKEXError<'a>> + where + 'c: 'a, + { + let payload = unparsed_ssh_packet.payload; + match self { + Self::DiffieHellman(dh) => match unparsed_ssh_packet.message_code { + SSH_MSG_KEXDH_INIT => { + parse_match_and_assign!(dh, init, SshPacketDHKEXInit, payload) + } + SSH_MSG_KEXDH_REPLY => { + parse_match_and_assign!(dh, reply, SshPacketDHKEXReply, payload) + } + _ => Err(SshKEXError::UnexpectedMessage), + }, + Self::ECDiffieHellman(dh) => match unparsed_ssh_packet.message_code { + SSH_MSG_KEXECDH_INIT => { + parse_match_and_assign!(dh, init, SshPacketECDHKEXInit, payload) + } + SSH_MSG_KEXECDH_REPLY => { + parse_match_and_assign!(dh, reply, SshPacketECDHKEXReply, payload) + } + _ => Err(SshKEXError::UnexpectedMessage), + }, + Self::DiffieHellmanKEXGEX(dh) => match unparsed_ssh_packet.message_code { + SSH_MSG_KEX_DH_GEX_REQUEST => { + parse_match_and_assign!(dh, request, SshPacketDhKEXGEXRequest, payload) + } + SSH_MSG_KEX_DH_GEX_REQUEST_OLD => { + parse_match_and_assign!(dh, request_old, SshPacketDhKEXGEXRequestOld, payload) + } + SSH_MSG_KEX_DH_GEX_GROUP => { + parse_match_and_assign!(dh, group, SshPacketDhKEXGEXGroup, payload) + } + SSH_MSG_KEX_DH_GEX_INIT => { + parse_match_and_assign!(dh, init, SshPacketDhKEXGEXInit, payload) + } + SSH_MSG_KEX_DH_GEX_REPLY => { + parse_match_and_assign!(dh, reply, SshPacketDhKEXGEXReply, payload) + } + _ => Err(SshKEXError::UnexpectedMessage), + }, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f098267..52d5dbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,13 +3,19 @@ //! The code is available on [GitHub](https://github.com/rusticata/ssh-parser) //! and is part of the [Rusticata](https://github.com/rusticata) project. +pub mod kex; #[cfg(feature = "integers")] pub mod mpint; #[cfg(feature = "serialize")] /// SSH packet crafting functions pub mod serialize; mod ssh; -#[cfg(test)] -mod tests; +pub use kex::{ + ssh_kex_negociate_algorithm, ECDSASignature, SshKEX, SshKEXDiffieHellman, + SshKEXDiffieHellmanKEXGEX, SshKEXECDiffieHellman, SshKEXError, SshPacketDHKEXInit, + SshPacketDHKEXReply, SshPacketDhKEXGEXGroup, SshPacketDhKEXGEXInit, SshPacketDhKEXGEXReply, + SshPacketDhKEXGEXRequest, SshPacketDhKEXGEXRequestOld, SshPacketECDHKEXInit, + SshPacketECDHKEXReply, +}; pub use ssh::*; diff --git a/src/serialize.rs b/src/serialize.rs index b7b6455..bd29784 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -1,6 +1,4 @@ -use crate::ssh::{ - SshPacket, SshPacketDebug, SshPacketDhReply, SshPacketDisconnect, SshPacketKeyExchange, -}; +use super::{SshPacket, SshPacketDebug, SshPacketDisconnect, SshPacketKeyExchange}; use cookie_factory::gen::{set_be_u32, set_be_u8}; use cookie_factory::*; use std::iter::repeat; @@ -31,16 +29,6 @@ fn gen_packet_key_exchange<'a>( ) } -fn gen_packet_dh_reply<'a>( - x: (&'a mut [u8], usize), - p: &SshPacketDhReply, -) -> Result<(&'a mut [u8], usize), GenError> { - do_gen!( - x, - gen_string(p.pubkey_and_cert) >> gen_string(p.f) >> gen_string(p.signature) - ) -} - fn gen_packet_disconnect<'a>( x: (&'a mut [u8], usize), p: &SshPacketDisconnect, @@ -73,8 +61,7 @@ fn packet_payload_type(p: &SshPacket) -> u8 { SshPacket::ServiceAccept(_) => 6, SshPacket::KeyExchange(_) => 20, SshPacket::NewKeys => 21, - SshPacket::DiffieHellmanInit(_) => 30, - SshPacket::DiffieHellmanReply(_) => 31, + SshPacket::DiffieHellmanKEX(ref p) => p.0.message_code, } } @@ -91,8 +78,7 @@ fn gen_packet_payload<'a>( SshPacket::ServiceAccept(p) => gen_string(x, p), SshPacket::KeyExchange(ref p) => gen_packet_key_exchange(x, p), SshPacket::NewKeys => Ok(x), - SshPacket::DiffieHellmanInit(ref p) => gen_string(x, p.e), - SshPacket::DiffieHellmanReply(ref p) => gen_packet_dh_reply(x, p), + SshPacket::DiffieHellmanKEX(ref p) => gen_string(x, p.0.payload), } } diff --git a/src/ssh.rs b/src/ssh.rs index 3eae177..2a68020 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -5,11 +5,11 @@ use nom::bytes::streaming::{is_not, tag, take, take_until}; use nom::character::streaming::{crlf, line_ending, not_line_ending}; -use nom::combinator::{complete, map, map_parser, map_res, opt}; +use nom::combinator::{complete, map, map_res, opt}; use nom::error::{make_error, Error, ErrorKind}; use nom::multi::{length_data, many_till, separated_list1}; use nom::number::streaming::{be_u32, be_u8}; -use nom::sequence::{delimited, pair, terminated}; +use nom::sequence::{delimited, terminated}; use nom::{Err, IResult}; use rusticata_macros::newtype_enum; use std::str; @@ -61,7 +61,7 @@ pub fn parse_ssh_identification(i: &[u8]) -> IResult<&[u8], (Vec<&[u8]>, SshVers } #[inline] -fn parse_string(i: &[u8]) -> IResult<&[u8], &[u8]> { +pub(super) fn parse_string(i: &[u8]) -> IResult<&[u8], &[u8]> { length_data(be_u32)(i) } @@ -189,73 +189,6 @@ impl<'a> SshPacketKeyExchange<'a> { } } -/// SSH Key Exchange Client Packet -/// -/// Defined in [RFC4253 section 8](https://tools.ietf.org/html/rfc4253#section-8) and [errata](https://www.rfc-editor.org/errata_search.php?rfc=4253). -/// -/// The single field e is left unparsed because its representation depends on -/// the negotiated key exchange algorithm: -/// -/// - with a diffie hellman exchange on multiplicative group of integers modulo -/// p, such as defined in [RFC4253](https://tools.ietf.org/html/rfc4253), the -/// field is a multiple precision integer (defined in [RFC4251 section 5](https://tools.ietf.org/html/rfc4251#section-5)). -/// - with a DH on elliptic curves, such as defined in [RFC6239](https://tools.ietf.org/html/rfc6239), the field is an octet string. -/// -/// TODO: add accessors for the different representations -#[derive(Debug, PartialEq)] -pub struct SshPacketDhInit<'a> { - pub e: &'a [u8], -} - -fn parse_packet_dh_init(i: &[u8]) -> IResult<&[u8], SshPacket> { - map(parse_string, |e| { - SshPacket::DiffieHellmanInit(SshPacketDhInit { e }) - })(i) -} - -/// SSH Key Exchange Server Packet -/// -/// Defined in [RFC4253 section 8](https://tools.ietf.org/html/rfc4253#section-8) and [errata](https://www.rfc-editor.org/errata_search.php?rfc=4253). -/// -/// Like the client packet, the fields depend on the algorithm negotiated during -/// the previous packet exchange. -#[derive(Debug, PartialEq)] -pub struct SshPacketDhReply<'a> { - pub pubkey_and_cert: &'a [u8], - pub f: &'a [u8], - pub signature: &'a [u8], -} - -fn parse_packet_dh_reply(i: &[u8]) -> IResult<&[u8], SshPacket> { - let (i, pubkey_and_cert) = parse_string(i)?; - let (i, f) = parse_string(i)?; - let (i, signature) = parse_string(i)?; - let reply = SshPacketDhReply { - pubkey_and_cert, - f, - signature, - }; - Ok((i, SshPacket::DiffieHellmanReply(reply))) -} - -impl<'a> SshPacketDhReply<'a> { - /// Parse the ECDSA server signature. - /// - /// Defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2). - #[allow(clippy::type_complexity)] - pub fn get_ecdsa_signature(&self) -> Result<(&str, Vec), nom::Err>> { - let (i, identifier) = map_res(parse_string, str::from_utf8)(self.signature)?; - let (_, blob) = map_parser(parse_string, pair(parse_string, parse_string))(i)?; - - let mut rs = Vec::new(); - - rs.extend_from_slice(blob.0); - rs.extend_from_slice(blob.1); - - Ok((identifier, rs)) - } -} - /// SSH Disconnection Message /// /// Defined in [RFC4253 Section 11.1](https://tools.ietf.org/html/rfc4253#section-11.1). @@ -345,6 +278,11 @@ impl<'a> SshPacketDebug<'a> { } } +/// A SSH message that may belong to the KEX stage. +/// use [`super::SshKEX`] to parse this message. +#[derive(Debug, PartialEq)] +pub struct MaybeDiffieHellmanKEX<'a>(pub SshPacketUnparsed<'a>); + /// SSH Packet Enumeration #[derive(Debug, PartialEq)] pub enum SshPacket<'a> { @@ -356,8 +294,7 @@ pub enum SshPacket<'a> { ServiceAccept(&'a [u8]), KeyExchange(SshPacketKeyExchange<'a>), NewKeys, - DiffieHellmanInit(SshPacketDhInit<'a>), - DiffieHellmanReply(SshPacketDhReply<'a>), + DiffieHellmanKEX(MaybeDiffieHellmanKEX<'a>), } /// Parse a plaintext SSH packet with its padding. @@ -365,29 +302,60 @@ pub enum SshPacket<'a> { /// Packet structure is defined in [RFC4253 Section 6](https://tools.ietf.org/html/rfc4253#section-6) and /// message codes are defined in [RFC4253 Section 12](https://tools.ietf.org/html/rfc4253#section-12). pub fn parse_ssh_packet(i: &[u8]) -> IResult<&[u8], (SshPacket, &[u8])> { + let (i, unparsed_ssh_packet) = parse_ssh_packet_with_message_code(i)?; + let padding = unparsed_ssh_packet.padding; + let d = unparsed_ssh_packet.payload; + let (_, msg) = match unparsed_ssh_packet.message_code { + 1 => parse_packet_disconnect(d), + 2 => map(parse_string, SshPacket::Ignore)(d), + 3 => map(be_u32, SshPacket::Unimplemented)(d), + 4 => parse_packet_debug(d), + 5 => map(parse_string, SshPacket::ServiceRequest)(d), + 6 => map(parse_string, SshPacket::ServiceAccept)(d), + 20 => parse_packet_key_exchange(d), + 21 => Ok((d, SshPacket::NewKeys)), + 30..=34 => Ok(( + i, + SshPacket::DiffieHellmanKEX(MaybeDiffieHellmanKEX(unparsed_ssh_packet)), + )), + _ => Err(Err::Error(make_error(d, ErrorKind::Switch))), + }?; + Ok((i, (msg, padding))) +} + +/// A plaintext SSH packet in raw format, with the message code. +#[derive(Debug, PartialEq)] +pub struct SshPacketUnparsed<'a> { + /// The payload, **without** the message code byte. + pub payload: &'a [u8], + + /// The padding. + pub padding: &'a [u8], + + /// The message code. + pub message_code: u8, +} + +/// Parse a plaintext SSH packet header with its message code. +/// +/// Packet structure is defined in [RFC4253 Section 6](https://tools.ietf.org/html/rfc4253#section-6) and +pub fn parse_ssh_packet_with_message_code(i: &[u8]) -> IResult<&[u8], SshPacketUnparsed> { let (i, packet_length) = be_u32(i)?; let (i, padding_length) = be_u8(i)?; if padding_length as u32 + 1 > packet_length { return Err(Err::Error(make_error(i, ErrorKind::LengthValue))); } - let (i, payload) = map_parser(take(packet_length - padding_length as u32 - 1), |d| { - let (d, msg_type) = be_u8(d)?; - match msg_type { - 1 => parse_packet_disconnect(d), - 2 => map(parse_string, SshPacket::Ignore)(d), - 3 => map(be_u32, SshPacket::Unimplemented)(d), - 4 => parse_packet_debug(d), - 5 => map(parse_string, SshPacket::ServiceRequest)(d), - 6 => map(parse_string, SshPacket::ServiceAccept)(d), - 20 => parse_packet_key_exchange(d), - 21 => Ok((d, SshPacket::NewKeys)), - 30 => parse_packet_dh_init(d), - 31 => parse_packet_dh_reply(d), - _ => Err(Err::Error(make_error(d, ErrorKind::Switch))), - } - })(i)?; + let (i, payload) = take(packet_length - padding_length as u32 - 1)(i)?; + let (payload_without_message_code, message_code) = be_u8(payload)?; let (i, padding) = take(padding_length)(i)?; - Ok((i, (payload, padding))) + Ok(( + i, + SshPacketUnparsed { + payload: payload_without_message_code, + padding, + message_code, + }, + )) } #[cfg(test)] diff --git a/src/tests.rs b/src/tests.rs deleted file mode 100644 index c1e2f8e..0000000 --- a/src/tests.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Public API tests -use nom::error::{make_error, ErrorKind}; -use nom::Err; - -use super::ssh::*; - -static CLIENT_KEY_EXCHANGE: &[u8] = include_bytes!("../assets/client_init.raw"); -static CLIENT_DH_INIT: &[u8] = include_bytes!("../assets/dh_init.raw"); -static SERVER_DH_REPLY: &[u8] = include_bytes!("../assets/dh_reply.raw"); -static SERVER_NEW_KEYS: &[u8] = include_bytes!("../assets/new_keys.raw"); -static SERVER_COMPAT: &[u8] = include_bytes!("../assets/server_compat.raw"); - -#[test] -fn test_identification() { - let empty: Vec<&[u8]> = vec![]; - let version = SshVersion { - proto: b"2.0", - software: b"OpenSSH_7.3", - comments: None, - }; - - let expected = Ok((b"" as &[u8], (empty, version))); - let res = parse_ssh_identification(&CLIENT_KEY_EXCHANGE[..21]); - assert_eq!(res, expected); -} - -#[test] -fn test_compatibility() { - let empty: Vec<&[u8]> = vec![]; - let version = SshVersion { - proto: b"1.99", - software: b"OpenSSH_3.1p1", - comments: None, - }; - - let expected = Ok((b"" as &[u8], (empty, version))); - let res = parse_ssh_identification(&SERVER_COMPAT[..23]); - assert_eq!(res, expected); -} - -#[test] -fn test_version_with_comments() { - let empty: Vec<&[u8]> = vec![]; - let version = SshVersion { - proto: b"2.0", - software: b"OpenSSH_7.3", - comments: Some(b"toto"), - }; - let expected = Ok((b"" as &[u8], (empty, version))); - let res = parse_ssh_identification(b"SSH-2.0-OpenSSH_7.3 toto\r\n"); - assert_eq!(res, expected); -} - -#[test] -fn test_client_key_exchange() { - let cookie = [ - 0xca, 0x98, 0x42, 0x14, 0xd6, 0xa5, 0xa7, 0xfd, 0x6c, 0xe8, 0xd4, 0x7c, 0x0b, 0xc0, 0x96, - 0xcc, - ]; - let key_exchange = SshPacket::KeyExchange(SshPacketKeyExchange { - cookie: &cookie, - kex_algs: b"curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c", - server_host_key_algs: b"ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa", - encr_algs_client_to_server: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", - encr_algs_server_to_client: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", - mac_algs_client_to_server: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", - mac_algs_server_to_client: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", - comp_algs_client_to_server: b"none,zlib@openssh.com,zlib", - comp_algs_server_to_client: b"none,zlib@openssh.com,zlib", - langs_client_to_server: b"", - langs_server_to_client: b"", - first_kex_packet_follows: false, - }); - let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - - let expected = Ok((b"" as &[u8], (key_exchange, padding))); - let res = parse_ssh_packet(&CLIENT_KEY_EXCHANGE[21..]); - assert_eq!(res, expected); -} - -#[test] -fn test_dh_init() { - let e = [ - 0x04, 0xe7, 0x59, 0x2a, 0xe1, 0xb9, 0xb6, 0xbe, 0x7c, 0x81, 0x5f, 0xc8, 0x3d, 0x55, 0x7b, - 0x8f, 0xc7, 0x09, 0x1d, 0x71, 0x6c, 0xed, 0x68, 0x45, 0x6c, 0x31, 0xc7, 0xf3, 0x65, 0x98, - 0xa5, 0x44, 0x7d, 0xa4, 0x28, 0xdd, 0xe7, 0x3a, 0xd9, 0xa1, 0x0e, 0x4b, 0x75, 0x3a, 0xde, - 0x33, 0x99, 0x6e, 0x41, 0x7d, 0xea, 0x88, 0xe9, 0x90, 0xe3, 0x5a, 0x27, 0xf8, 0x38, 0x09, - 0x01, 0x66, 0x46, 0xd4, 0xdc, - ]; - let dh = SshPacket::DiffieHellmanInit(SshPacketDhInit { e: &e }); - let padding: &[u8] = &[0, 0, 0, 0, 0]; - let expected = Ok((b"" as &[u8], (dh, padding))); - let res = parse_ssh_packet(CLIENT_DH_INIT); - assert_eq!(res, expected); -} - -#[test] -fn test_dh_reply() { - let pubkey = [ - 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, - 0x6e, 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, - 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x41, 0x04, 0x55, 0xa1, 0xb5, 0x65, 0xde, - 0xf5, 0x6a, 0xac, 0xcb, 0xa9, 0x60, 0xd1, 0x49, 0xf8, 0x8c, 0x46, 0x42, 0x1c, 0xe2, 0x92, - 0x59, 0xe4, 0x5d, 0x85, 0xdf, 0xb9, 0x27, 0x84, 0xa2, 0x6a, 0x28, 0x83, 0xe8, 0x49, 0xf6, - 0x23, 0x78, 0xc9, 0x60, 0x71, 0x73, 0xc7, 0x78, 0xf5, 0x83, 0x85, 0xdd, 0xcf, 0x74, 0x63, - 0x0e, 0xbd, 0xcf, 0x78, 0x33, 0xeb, 0x5e, 0xfa, 0xfe, 0x2f, 0xd8, 0x1c, 0x65, 0xbc, - ]; - let f = [ - 0x04, 0x99, 0x2c, 0x48, 0xfd, 0xeb, 0x2d, 0x58, 0xdf, 0x37, 0xfd, 0x74, 0xf0, 0x60, 0xe9, - 0x9c, 0x73, 0x40, 0x42, 0x8f, 0x73, 0x28, 0x3f, 0x05, 0x1a, 0x44, 0x6b, 0xdb, 0xb1, 0x87, - 0x4c, 0xe8, 0xe8, 0x96, 0x4a, 0x36, 0x98, 0x6e, 0x5e, 0x91, 0x87, 0xd3, 0x04, 0x86, 0x43, - 0x83, 0x5f, 0x04, 0xdd, 0x6e, 0x27, 0x22, 0x2b, 0x3f, 0xb8, 0x00, 0x82, 0x3f, 0x76, 0x0d, - 0xbd, 0x40, 0xc1, 0xd6, 0x2a, - ]; - let signature = [ - 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, - 0x6e, 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, - 0x20, 0x0b, 0xca, 0x56, 0x33, 0xaf, 0xe5, 0xd6, 0x72, 0xaf, 0x3f, 0x8c, 0x1a, 0x8c, 0x28, - 0x50, 0x6d, 0x3f, 0x5a, 0xa4, 0x55, 0xba, 0x80, 0x4d, 0x98, 0x16, 0x56, 0x9b, 0x6b, 0x1f, - 0x79, 0x21, 0xc8, 0x00, 0x00, 0x00, 0x20, 0x0c, 0xa5, 0x7a, 0xce, 0x69, 0xcf, 0x38, 0x28, - 0xb4, 0xb4, 0xf8, 0xf0, 0x4e, 0xa9, 0x67, 0x8f, 0xd2, 0x62, 0x3c, 0x94, 0x63, 0x6f, 0x5d, - 0x08, 0x25, 0xad, 0xfc, 0x2d, 0x95, 0x25, 0x73, 0xbc, - ]; - let dh = SshPacket::DiffieHellmanReply(SshPacketDhReply { - pubkey_and_cert: &pubkey, - f: &f, - signature: &signature, - }); - let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - let expected = Ok((b"" as &[u8], (dh, padding))); - let res = parse_ssh_packet(SERVER_DH_REPLY); - assert_eq!(res, expected); -} - -#[test] -fn test_new_keys() { - let keys = SshPacket::NewKeys; - let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - let expected = Ok((b"" as &[u8], (keys, padding))); - let res = parse_ssh_packet(SERVER_NEW_KEYS); - assert_eq!(res, expected); -} - -#[test] -fn test_invalid_packet0() { - let data = b"\x00\x00\x00\x00\x00\x00\x00\x00"; - let expected = Err(Err::Error(make_error(&data[5..], ErrorKind::LengthValue))); - let res = parse_ssh_packet(data); - assert_eq!(res, expected); -} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..28e3087 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,75 @@ +// Public API tests +extern crate ssh_parser; + +use ssh_parser::*; + +static CLIENT_KEY_EXCHANGE: &[u8] = include_bytes!("../assets/client_init.raw"); +static SERVER_COMPAT: &[u8] = include_bytes!("../assets/server_compat.raw"); + +#[test] +fn test_identification() { + let empty: Vec<&[u8]> = vec![]; + let version = SshVersion { + proto: b"2.0", + software: b"OpenSSH_7.3", + comments: None, + }; + + let expected = Ok((b"" as &[u8], (empty, version))); + let res = parse_ssh_identification(&CLIENT_KEY_EXCHANGE[..21]); + assert_eq!(res, expected); +} + +#[test] +fn test_compatibility() { + let empty: Vec<&[u8]> = vec![]; + let version = SshVersion { + proto: b"1.99", + software: b"OpenSSH_3.1p1", + comments: None, + }; + + let expected = Ok((b"" as &[u8], (empty, version))); + let res = parse_ssh_identification(&SERVER_COMPAT[..23]); + assert_eq!(res, expected); +} + +#[test] +fn test_version_with_comments() { + let empty: Vec<&[u8]> = vec![]; + let version = SshVersion { + proto: b"2.0", + software: b"OpenSSH_7.3", + comments: Some(b"toto"), + }; + let expected = Ok((b"" as &[u8], (empty, version))); + let res = parse_ssh_identification(b"SSH-2.0-OpenSSH_7.3 toto\r\n"); + assert_eq!(res, expected); +} + +#[test] +fn test_client_key_exchange() { + let cookie = [ + 0xca, 0x98, 0x42, 0x14, 0xd6, 0xa5, 0xa7, 0xfd, 0x6c, 0xe8, 0xd4, 0x7c, 0x0b, 0xc0, 0x96, + 0xcc, + ]; + let key_exchange = SshPacket::KeyExchange(SshPacketKeyExchange { + cookie: &cookie, + kex_algs: b"curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c", + server_host_key_algs: b"ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa", + encr_algs_client_to_server: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", + encr_algs_server_to_client: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", + mac_algs_client_to_server: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", + mac_algs_server_to_client: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", + comp_algs_client_to_server: b"none,zlib@openssh.com,zlib", + comp_algs_server_to_client: b"none,zlib@openssh.com,zlib", + langs_client_to_server: b"", + langs_server_to_client: b"", + first_kex_packet_follows: false, + }); + let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + let expected = Ok((b"" as &[u8], (key_exchange, padding))); + let res = parse_ssh_packet(&CLIENT_KEY_EXCHANGE[21..]); + assert_eq!(res, expected); +} diff --git a/tests/tests_kex.rs b/tests/tests_kex.rs new file mode 100644 index 0000000..43f77e3 --- /dev/null +++ b/tests/tests_kex.rs @@ -0,0 +1,212 @@ +// Public API tests for KEX. +extern crate ssh_parser; + +use ssh_parser::*; + +fn load_client_server_key_exchange_init( + client: &'static [u8], + server: &'static [u8], +) -> (SshPacketKeyExchange<'static>, SshPacketKeyExchange<'static>) { + let client = parse_ssh_packet(client).unwrap().1 .0; + let server = parse_ssh_packet(server).unwrap().1 .0; + assert!(matches!( + (&client, &server), + (SshPacket::KeyExchange(_), SshPacket::KeyExchange(_)) + )); + match (client, server) { + (SshPacket::KeyExchange(client), SshPacket::KeyExchange(server)) => (client, server), + _ => unreachable!(), + } +} + +fn load_kex_packet(packet: &[u8]) -> SshPacketUnparsed<'_> { + let kex_packet = parse_ssh_packet(packet).unwrap().1 .0; + assert!(matches!(&kex_packet, SshPacket::DiffieHellmanKEX(_))); + match kex_packet { + SshPacket::DiffieHellmanKEX(kex) => kex.0, + _ => unreachable!(), + } +} + +mod ecdh { + use super::*; + + static CLIENT_KEY_EXCHANGE_INIT: &[u8] = + include_bytes!("../assets/kex/ecdh/client_kex_init.raw"); + static SERVER_KEY_EXCHANGE_INIT: &[u8] = + include_bytes!("../assets/kex/ecdh/server_kex_init.raw"); + static INIT: &[u8] = include_bytes!("../assets/kex/ecdh/init.raw"); + static REPLY: &[u8] = include_bytes!("../assets/kex/ecdh/reply.raw"); + + #[test] + fn test_kex() { + let (client_kex, server_kex) = load_client_server_key_exchange_init( + CLIENT_KEY_EXCHANGE_INIT, + SERVER_KEY_EXCHANGE_INIT, + ); + let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); + assert_eq!(negociated_alg, "curve25519-sha256"); + assert!(matches!(kex, SshKEX::ECDiffieHellman(_))); + + let init_packet = load_kex_packet(INIT); + assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&init_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let reply_packet = load_kex_packet(REPLY); + assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&reply_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let kex = match kex { + SshKEX::ECDiffieHellman(kex) => kex, + _ => unreachable!(), + }; + + assert!(kex.init.is_some()); + assert!(kex.reply.is_some()); + } +} + +mod dh { + use super::*; + + static CLIENT_KEY_EXCHANGE_INIT: &[u8] = include_bytes!("../assets/kex/dh/client_kex_init.raw"); + static SERVER_KEY_EXCHANGE_INIT: &[u8] = include_bytes!("../assets/kex/dh/server_kex_init.raw"); + static INIT: &[u8] = include_bytes!("../assets/kex/dh/init.raw"); + static REPLY: &[u8] = include_bytes!("../assets/kex/dh/reply.raw"); + + #[test] + fn test_kex() { + let (client_kex, server_kex) = load_client_server_key_exchange_init( + CLIENT_KEY_EXCHANGE_INIT, + SERVER_KEY_EXCHANGE_INIT, + ); + let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); + assert_eq!(negociated_alg, "diffie-hellman-group18-sha512"); + assert!(matches!(kex, SshKEX::DiffieHellman(_))); + + let init_packet = load_kex_packet(INIT); + assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&init_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let reply_packet = load_kex_packet(REPLY); + assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&reply_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let kex = match kex { + SshKEX::DiffieHellman(kex) => kex, + _ => unreachable!(), + }; + + assert!(kex.init.is_some()); + assert!(kex.reply.is_some()); + + let ecdsa_signature = kex.reply.as_ref().unwrap().get_ecdsa_signature().unwrap(); + assert_eq!(ecdsa_signature.identifier, "ssh-ed25519"); + } +} + +mod dh_kex_gex { + use super::*; + + static CLIENT_KEY_EXCHANGE_INIT: &[u8] = + include_bytes!("../assets/kex/dh-kex-gex/client_kex_init.raw"); + static SERVER_KEY_EXCHANGE_INIT: &[u8] = + include_bytes!("../assets/kex/dh-kex-gex/server_kex_init.raw"); + static REQUEST: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/request.raw"); + static GROUP: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/group.raw"); + static INIT: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/init.raw"); + static REPLY: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/reply.raw"); + + #[test] + fn test_kex() { + let (client_kex, server_kex) = load_client_server_key_exchange_init( + CLIENT_KEY_EXCHANGE_INIT, + SERVER_KEY_EXCHANGE_INIT, + ); + let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); + assert_eq!(negociated_alg, "diffie-hellman-group-exchange-sha256"); + assert!(matches!(kex, SshKEX::DiffieHellmanKEXGEX(_))); + + let request_packet = load_kex_packet(REQUEST); + assert!(matches!(kex.parse_ssh_packet(&request_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&request_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let group_packet = load_kex_packet(GROUP); + assert!(matches!(kex.parse_ssh_packet(&group_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&group_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let init_packet = load_kex_packet(INIT); + assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&init_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let reply_packet = load_kex_packet(REPLY); + assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&reply_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let kex = match kex { + SshKEX::DiffieHellmanKEXGEX(kex) => kex, + _ => unreachable!(), + }; + + assert!(kex.request.is_some()); + assert!(kex.group.is_some()); + assert!(kex.init.is_some()); + assert!(kex.reply.is_some()); + } +} + +mod kex_algorithm_negociation { + use super::ssh_kex_negociate_algorithm; + + #[test] + fn test_negociation() { + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["a", "b", "c"]), + Some("a") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["b", "a", "c"]), + Some("a") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["b", "d", "c"]), + Some("b") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["d", "c", "e"]), + Some("c") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["c", "b", "a"]), + Some("a") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["d", "e", "f"]), + None + ); + } +}