Skip to content

Commit

Permalink
Add support for Diffie-Hellman Request, RequestOld and Group.
Browse files Browse the repository at this point in the history
This commit adds support for the following SSH Handshake packets:
  - SSH Key Exchange Diffie-Hellman Request (`SSH_MSG_KEY_DH_GEX_REQUEST`)
  - SSH Key Exchange Diffie-Hellman Request Old (`SSH_MSG_KEY_DH_GEX_REQUEST_OLD`)
  - SSH Key Exchange Diffie-Hellman Group (`SSH_MSG_KEY_DH_GEX_REQUEST_GROUP`)

These messages are defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5).

This commit also fixes a small bug where some packet numbers were assigned
to the wrong ssh packet (I'm thinking of `SshPacketDhRequest` and `SshPacketDhGroup`.

Some tests have been added to ensure that these new messages are working.
  • Loading branch information
thb-sb committed Mar 21, 2024
1 parent 508e28e commit c9b9c09
Show file tree
Hide file tree
Showing 10 changed files with 848 additions and 92 deletions.
Binary file added assets/dh_group.raw
Binary file not shown.
Binary file modified assets/dh_init.raw
Binary file not shown.
Binary file modified assets/dh_reply.raw
Binary file not shown.
Binary file added assets/dh_request.raw
Binary file not shown.
Binary file added assets/dh_request_old.raw
Binary file not shown.
684 changes: 684 additions & 0 deletions src/kex.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! The code is available on [GitHub](https://github.com/rusticata/ssh-parser)
//! and is part of the [Rusticata](https://github.com/rusticata) project.

mod kex;
#[cfg(feature = "integers")]
pub mod mpint;
#[cfg(feature = "serialize")]
Expand All @@ -12,4 +13,5 @@ mod ssh;
#[cfg(test)]
mod tests;

pub use kex::{SshKEX, SshKEXError};
pub use ssh::*;
46 changes: 43 additions & 3 deletions src/serialize.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::ssh::{
SshPacket, SshPacketDebug, SshPacketDhReply, SshPacketDisconnect, SshPacketKeyExchange,
SshPacket, SshPacketDebug, SshPacketDhGroup, SshPacketDhReply, SshPacketDhRequest,

Check failure on line 2 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unresolved imports `crate::ssh::SshPacketDhGroup`, `crate::ssh::SshPacketDhReply`, `crate::ssh::SshPacketDhRequest`, `crate::ssh::SshPacketDhRequestOld`

Check failure on line 2 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unresolved imports `crate::ssh::SshPacketDhGroup`, `crate::ssh::SshPacketDhReply`, `crate::ssh::SshPacketDhRequest`, `crate::ssh::SshPacketDhRequestOld`

Check failure on line 2 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Build documentation

unresolved imports `crate::ssh::SshPacketDhGroup`, `crate::ssh::SshPacketDhReply`, `crate::ssh::SshPacketDhRequest`, `crate::ssh::SshPacketDhRequestOld`

Check failure on line 2 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Build documentation

unresolved imports `crate::ssh::SshPacketDhGroup`, `crate::ssh::SshPacketDhReply`, `crate::ssh::SshPacketDhRequest`, `crate::ssh::SshPacketDhRequestOld`
SshPacketDhRequestOld, SshPacketDisconnect, SshPacketKeyExchange,
};
use cookie_factory::gen::{set_be_u32, set_be_u8};
use cookie_factory::*;
Expand Down Expand Up @@ -31,6 +32,39 @@ fn gen_packet_key_exchange<'a>(
)
}

fn gen_packet_dh_request_old<'a>(
x: (&'a mut [u8], usize),
p: &SshPacketDhRequestOld,
) -> Result<(&'a mut [u8], usize), GenError> {
do_gen!(x, gen_be_u32!(p.n))
}

fn gen_packet_dh_request<'a>(
x: (&'a mut [u8], usize),
p: &SshPacketDhRequest,
) -> Result<(&'a mut [u8], usize), GenError> {
do_gen!(
x,
gen_be_u32!(p.min) >> gen_be_u32!(p.n) >> gen_be_u32!(p.max)
)
}

#[cfg(feature = "integers")]
fn gen_packet_dh_group<'a>(
_: (&'a mut [u8], usize),
_: &SshPacketDhGroup,
) -> Result<(&'a mut [u8], usize), GenError> {
Err(GenError::NotYetImplemented)
}

#[cfg(not(feature = "integers"))]
fn gen_packet_dh_group<'a>(
x: (&'a mut [u8], usize),
p: &SshPacketDhRequest,
) -> Result<(&'a mut [u8], usize), GenError> {
do_gen!(x, gen_string(p.0 .0))
}

fn gen_packet_dh_reply<'a>(
x: (&'a mut [u8], usize),
p: &SshPacketDhReply,
Expand Down Expand Up @@ -73,8 +107,11 @@ fn packet_payload_type(p: &SshPacket) -> u8 {
SshPacket::ServiceAccept(_) => 6,
SshPacket::KeyExchange(_) => 20,
SshPacket::NewKeys => 21,
SshPacket::DiffieHellmanInit(_) => 30,
SshPacket::DiffieHellmanReply(_) => 31,
SshPacket::DiffieHellmanRequestOld(_) => 30,

Check failure on line 110 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

no variant or associated item named `DiffieHellmanRequestOld` found for enum `ssh::SshPacket` in the current scope

Check failure on line 110 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

no variant or associated item named `DiffieHellmanRequestOld` found for enum `SshPacket` in the current scope

Check failure on line 110 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

no variant or associated item named `DiffieHellmanRequestOld` found for enum `ssh::SshPacket` in the current scope

Check failure on line 110 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

no variant or associated item named `DiffieHellmanRequestOld` found for enum `SshPacket` in the current scope
SshPacket::DiffieHellmanRequest(_) => 34,

Check failure on line 111 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

no variant or associated item named `DiffieHellmanRequest` found for enum `ssh::SshPacket` in the current scope

Check failure on line 111 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

no variant or associated item named `DiffieHellmanRequest` found for enum `ssh::SshPacket` in the current scope

Check failure on line 111 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

no variant or associated item named `DiffieHellmanRequest` found for enum `SshPacket` in the current scope
SshPacket::DiffieHellmanGroup(_) => 31,

Check failure on line 112 in src/serialize.rs

View workflow job for this annotation

GitHub Actions / Test Suite

no variant or associated item named `DiffieHellmanGroup` found for enum `ssh::SshPacket` in the current scope
SshPacket::DiffieHellmanInit(_) => 32,
SshPacket::DiffieHellmanReply(_) => 33,
}
}

Expand All @@ -91,6 +128,9 @@ 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::DiffieHellmanRequestOld(ref p) => gen_packet_dh_request_old(x, p),
SshPacket::DiffieHellmanRequest(ref p) => gen_packet_dh_request(x, p),
SshPacket::DiffieHellmanGroup(ref p) => gen_packet_dh_group(x, p),
SshPacket::DiffieHellmanInit(ref p) => gen_string(x, p.e),
SshPacket::DiffieHellmanReply(ref p) => gen_packet_dh_reply(x, p),
}
Expand Down
146 changes: 57 additions & 89 deletions src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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<u8>), nom::Err<Error<&[u8]>>> {
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).
Expand Down Expand Up @@ -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> {
Expand All @@ -356,38 +294,68 @@ 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.
///
/// 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 | 31 | 32 | 33 | 34 => Ok((

Check failure on line 317 in src/ssh.rs

View workflow job for this annotation

GitHub Actions / Clippy

this OR pattern can be rewritten using a range

Check failure on line 317 in src/ssh.rs

View workflow job for this annotation

GitHub Actions / Clippy

this OR pattern can be rewritten using a range
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)]
Expand Down
62 changes: 62 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ 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");
static CLIENT_DH_REQUEST_OLD: &[u8] = include_bytes!("../assets/dh_request_old.raw");
static CLIENT_DH_REQUEST: &[u8] = include_bytes!("../assets/dh_request.raw");
static SERVER_DH_GROUP: &[u8] = include_bytes!("../assets/dh_group.raw");

#[test]
fn test_identification() {
Expand Down Expand Up @@ -148,3 +151,62 @@ fn test_invalid_packet0() {
let res = parse_ssh_packet(data);
assert_eq!(res, expected);
}

#[test]
fn test_client_dh_request_old() {
let dh = SshPacket::DiffieHellmanRequestOld(SshPacketDhRequestOld { n: 32 });

Check failure on line 157 in src/tests.rs

View workflow job for this annotation

GitHub Actions / Test Suite

cannot find struct, variant or union type `SshPacketDhRequestOld` in this scope

Check failure on line 157 in src/tests.rs

View workflow job for this annotation

GitHub Actions / Test Suite

cannot find struct, variant or union type `SshPacketDhRequestOld` in this scope
let padding: &[u8] = &[0, 0, 0, 0, 0, 0];
let expected = Ok((b"" as &[u8], (dh, padding)));
let res = parse_ssh_packet(CLIENT_DH_REQUEST_OLD);
assert_eq!(res, expected);
}

#[test]
fn test_client_dh_request() {
let dh = SshPacket::DiffieHellmanRequest(SshPacketDhRequest {

Check failure on line 166 in src/tests.rs

View workflow job for this annotation

GitHub Actions / Test Suite

cannot find struct, variant or union type `SshPacketDhRequest` in this scope

Check failure on line 166 in src/tests.rs

View workflow job for this annotation

GitHub Actions / Test Suite

cannot find struct, variant or union type `SshPacketDhRequest` in this scope
min: 1024,
n: 1024,
max: 8192,
});
let padding: &[u8] = &[0, 0, 0, 0, 0, 0];
let expected = Ok((b"" as &[u8], (dh, padding)));
let res = parse_ssh_packet(CLIENT_DH_REQUEST);
assert_eq!(res, expected);
}

#[cfg(feature = "integers")]
#[test]
fn test_client_dh_group() {
let dh = SshPacket::DiffieHellmanGroup(SshPacketDhGroup{

Check failure on line 180 in src/tests.rs

View workflow job for this annotation

GitHub Actions / Test Suite

cannot find struct, variant or union type `SshPacketDhGroup` in this scope

Check failure on line 180 in src/tests.rs

View workflow job for this annotation

GitHub Actions / Test Suite

cannot find struct, variant or union type `SshPacketDhGroup` in this scope
p: num_bigint::BigInt::parse_bytes(b"6527644299243943579076507224262728911150677305418581973667217865196749836235559184775254381053139623552506095587086844726109894961002400265609413036492266847472997680811211873057994074347888987967972902356547830289318725768689809706026068767341615260952354322321304769100709507115988158671110920995313178910335979385662734594", 10).unwrap().into(),
g: num_bigint::BigInt::from(0).into(),
});
let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0];
let expected = Ok((b"" as &[u8], (dh, padding)));
let res = parse_ssh_packet(SERVER_DH_GROUP);
assert_eq!(res, expected);
}

#[cfg(not(feature = "integers"))]
#[test]
fn test_client_dh_group() {
let dh = SshPacket::DiffieHellmanGroup(SshPacketDhGroup(
[
0x00, 0x00, 0x00, 0x81, 0x00, 0xde, 0x49, 0xfc, 0x90, 0x69, 0x99, 0x4c, 0x37, 0x9d,
0x2b, 0x65, 0x63, 0xef, 0xd3, 0x7e, 0xfa, 0xe6, 0x78, 0x5e, 0xeb, 0x1d, 0xd0, 0xa1,
0x2b, 0x09, 0x0a, 0xac, 0x27, 0x2b, 0x22, 0xdf, 0x8c, 0x64, 0xa4, 0xa2, 0xab, 0x7b,
0x99, 0xce, 0x0b, 0x77, 0xa9, 0xa5, 0x2e, 0x08, 0x33, 0xd5, 0x2d, 0x53, 0xb2, 0x58,
0xce, 0xdf, 0xfd, 0x17, 0x5d, 0xc8, 0xa3, 0x76, 0x6a, 0x9b, 0x98, 0x07, 0x36, 0x26,
0x46, 0xdc, 0x92, 0x15, 0x62, 0x8c, 0x3f, 0x4a, 0xf0, 0xe0, 0x8d, 0x00, 0xab, 0x60,
0xa3, 0xb9, 0xe5, 0x5b, 0xae, 0x47, 0xe8, 0x26, 0x51, 0xda, 0x0c, 0x15, 0xa2, 0x73,
0x55, 0xdd, 0xb0, 0x63, 0x65, 0xca, 0xe1, 0xdd, 0xde, 0x4c, 0x0c, 0x97, 0xdc, 0x99,
0x42, 0xfd, 0x65, 0xe9, 0x86, 0x7f, 0xa5, 0x0e, 0x72, 0xe1, 0xc7, 0x85, 0x41, 0x1e,
0xdd, 0x28, 0xde, 0x27, 0x4b, 0xb3, 0x23, 0x00, 0x00, 0x00, 0x01, 0x02,
]
.as_slice(),
));
let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0];
let expected = Ok((b"" as &[u8], (dh, padding)));
let res = parse_ssh_packet(SERVER_DH_GROUP);
assert_eq!(res, expected);
}

0 comments on commit c9b9c09

Please sign in to comment.