ssh_parser/
ssh.rs

1//! # SSH parser
2//!
3//! This module contains parsing functions for the SSH 2.0 protocol. It is also
4//! compatible with obsolete version negotiation.
5
6use nom::bytes::streaming::{is_not, tag, take, take_until};
7use nom::character::streaming::{crlf, line_ending, not_line_ending};
8use nom::combinator::{complete, map, map_parser, map_res, opt};
9use nom::error::{make_error, Error, ErrorKind};
10use nom::multi::{length_data, many_till, separated_list1};
11use nom::number::streaming::{be_u32, be_u8};
12use nom::sequence::{delimited, pair, terminated};
13use nom::{Err, IResult};
14use rusticata_macros::newtype_enum;
15use std::str;
16
17/// SSH Protocol Version Exchange
18///
19/// Defined in [RFC4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2).
20///
21/// Unparsed proto and software fields must contain US-ASCII printable
22/// characters only (without space and minus sign). There is no constraint on
23/// the comment field except it must not contain the null byte.
24#[derive(Debug, PartialEq)]
25pub struct SshVersion<'a> {
26    pub proto: &'a [u8],
27    pub software: &'a [u8],
28    pub comments: Option<&'a [u8]>,
29}
30
31// Version exchange terminates with CRLF for SSH 2.0 or LF for compatibility
32// with older versions.
33fn parse_version(i: &[u8]) -> IResult<&[u8], SshVersion> {
34    let (i, proto) = take_until("-")(i)?;
35    let (i, _) = tag("-")(i)?;
36    let (i, software) = is_not(" \r\n")(i)?;
37    let (i, comments) = opt(|d| {
38        let (d, _) = tag(" ")(d)?;
39        let (d, comments) = not_line_ending(d)?;
40        Ok((d, comments))
41    })(i)?;
42    let version = SshVersion {
43        proto,
44        software,
45        comments,
46    };
47    Ok((i, version))
48}
49
50/// Parse the SSH identification phase.
51///
52/// In version 2.0, the SSH server is allowed to send an arbitrary number of
53/// UTF-8 lines before the final identification line containing the server
54/// version. This function allocates a vector to store these line slices in
55/// addition of the advertised version of the SSH implementation.
56pub fn parse_ssh_identification(i: &[u8]) -> IResult<&[u8], (Vec<&[u8]>, SshVersion)> {
57    many_till(
58        terminated(take_until("\r\n"), crlf),
59        delimited(tag("SSH-"), parse_version, line_ending),
60    )(i)
61}
62
63#[inline]
64fn parse_string(i: &[u8]) -> IResult<&[u8], &[u8]> {
65    length_data(be_u32)(i)
66}
67
68// US-ASCII printable characters without comma
69#[inline]
70fn is_us_ascii(c: u8) -> bool {
71    (0x20..=0x7e).contains(&c) && c != 0x2c
72}
73
74#[inline]
75fn parse_name(s: &[u8]) -> IResult<&[u8], &[u8]> {
76    use nom::bytes::complete::take_while1;
77    take_while1(is_us_ascii)(s)
78}
79
80fn parse_name_list<'a>(i: &'a [u8]) -> IResult<&'a [u8], Vec<&str>> {
81    use nom::bytes::complete::tag;
82    match separated_list1(tag(","), map_res(complete(parse_name), str::from_utf8))(i) {
83        Ok((rem, res)) => Ok((rem, res)),
84        Err(_) => Err(Err::Error(make_error(i, ErrorKind::SeparatedList))),
85    }
86}
87
88/// SSH Algorithm Negotiation
89///
90/// Defined in [RFC4253 section 7.1](https://tools.ietf.org/html/rfc4253#section-7.1).
91///
92/// This packet contains all information necessary to prepare the key exchange.
93/// The algorithms are UTF-8 strings in name lists. The order is significant
94/// with most preferred algorithms first. Parsing of lists is done only when
95/// the field are accessed though accessors (note that lists can
96/// be successfully extracted at the packet level but accessing them later can
97/// fail with a UTF-8 conversion error).
98#[derive(Debug, PartialEq)]
99pub struct SshPacketKeyExchange<'a> {
100    pub cookie: &'a [u8],
101    pub kex_algs: &'a [u8],
102    pub server_host_key_algs: &'a [u8],
103    pub encr_algs_client_to_server: &'a [u8],
104    pub encr_algs_server_to_client: &'a [u8],
105    pub mac_algs_client_to_server: &'a [u8],
106    pub mac_algs_server_to_client: &'a [u8],
107    pub comp_algs_client_to_server: &'a [u8],
108    pub comp_algs_server_to_client: &'a [u8],
109    pub langs_client_to_server: &'a [u8],
110    pub langs_server_to_client: &'a [u8],
111    pub first_kex_packet_follows: bool,
112}
113
114fn parse_packet_key_exchange(i: &[u8]) -> IResult<&[u8], SshPacket> {
115    let (i, cookie) = take(16usize)(i)?;
116    let (i, kex_algs) = parse_string(i)?;
117    let (i, server_host_key_algs) = parse_string(i)?;
118    let (i, encr_algs_client_to_server) = parse_string(i)?;
119    let (i, encr_algs_server_to_client) = parse_string(i)?;
120    let (i, mac_algs_client_to_server) = parse_string(i)?;
121    let (i, mac_algs_server_to_client) = parse_string(i)?;
122    let (i, comp_algs_client_to_server) = parse_string(i)?;
123    let (i, comp_algs_server_to_client) = parse_string(i)?;
124    let (i, langs_client_to_server) = parse_string(i)?;
125    let (i, langs_server_to_client) = parse_string(i)?;
126    let (i, first_kex_packet_follows) = be_u8(i)?;
127    let (i, _) = be_u32(i)?;
128    let packet = SshPacketKeyExchange {
129        cookie,
130        kex_algs,
131        server_host_key_algs,
132        encr_algs_client_to_server,
133        encr_algs_server_to_client,
134        mac_algs_client_to_server,
135        mac_algs_server_to_client,
136        comp_algs_client_to_server,
137        comp_algs_server_to_client,
138        langs_client_to_server,
139        langs_server_to_client,
140        first_kex_packet_follows: first_kex_packet_follows > 0,
141    };
142    Ok((i, SshPacket::KeyExchange(packet)))
143}
144
145impl<'a> SshPacketKeyExchange<'a> {
146    pub fn get_kex_algs(&self) -> Result<Vec<&str>, nom::Err<Error<&[u8]>>> {
147        parse_name_list(self.kex_algs).map(|x| x.1)
148    }
149
150    pub fn get_server_host_key_algs(&self) -> Result<Vec<&str>, nom::Err<Error<&[u8]>>> {
151        parse_name_list(self.server_host_key_algs).map(|x| x.1)
152    }
153
154    pub fn get_encr_algs_client_to_server(&self) -> Result<Vec<&str>, nom::Err<Error<&[u8]>>> {
155        parse_name_list(self.encr_algs_client_to_server).map(|x| x.1)
156    }
157
158    pub fn get_encr_algs_server_to_client(&self) -> Result<Vec<&str>, nom::Err<Error<&'a [u8]>>> {
159        parse_name_list(self.encr_algs_server_to_client).map(|x| x.1)
160    }
161
162    pub fn get_mac_algs_client_to_server(&self) -> Result<Vec<&str>, nom::Err<Error<&'a [u8]>>> {
163        parse_name_list(self.mac_algs_client_to_server).map(|x| x.1)
164    }
165
166    pub fn get_mac_algs_server_to_client(&self) -> Result<Vec<&str>, nom::Err<Error<&'a [u8]>>> {
167        parse_name_list(self.mac_algs_server_to_client).map(|x| x.1)
168    }
169
170    pub fn get_comp_algs_client_to_server(&self) -> Result<Vec<&str>, nom::Err<Error<&'a [u8]>>> {
171        parse_name_list(self.comp_algs_client_to_server).map(|x| x.1)
172    }
173
174    pub fn get_comp_algs_server_to_client(&self) -> Result<Vec<&str>, nom::Err<Error<&'a [u8]>>> {
175        parse_name_list(self.comp_algs_server_to_client).map(|x| x.1)
176    }
177
178    pub fn get_langs_client_to_server(&self) -> Result<Vec<&str>, nom::Err<Error<&'a [u8]>>> {
179        parse_name_list(self.langs_client_to_server).map(|x| x.1)
180    }
181
182    pub fn get_langs_server_to_client(&self) -> Result<Vec<&str>, nom::Err<Error<&'a [u8]>>> {
183        parse_name_list(self.langs_server_to_client).map(|x| x.1)
184    }
185}
186
187/// SSH Key Exchange Client Packet
188///
189/// 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).
190///
191/// The single field e is left unparsed because its representation depends on
192/// the negotiated key exchange algorithm:
193///
194/// - with a diffie hellman exchange on multiplicative group of integers modulo
195///   p, such as defined in [RFC4253](https://tools.ietf.org/html/rfc4253), the
196///   field is a multiple precision integer (defined in [RFC4251 section 5](https://tools.ietf.org/html/rfc4251#section-5)).
197/// - with a DH on elliptic curves, such as defined in [RFC6239](https://tools.ietf.org/html/rfc6239), the field is an octet string.
198///
199/// TODO: add accessors for the different representations
200#[derive(Debug, PartialEq)]
201pub struct SshPacketDhInit<'a> {
202    pub e: &'a [u8],
203}
204
205fn parse_packet_dh_init(i: &[u8]) -> IResult<&[u8], SshPacket> {
206    map(parse_string, |e| {
207        SshPacket::DiffieHellmanInit(SshPacketDhInit { e })
208    })(i)
209}
210
211/// SSH Key Exchange Server Packet
212///
213/// 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).
214///
215/// Like the client packet, the fields depend on the algorithm negotiated during
216/// the previous packet exchange.
217#[derive(Debug, PartialEq)]
218pub struct SshPacketDhReply<'a> {
219    pub pubkey_and_cert: &'a [u8],
220    pub f: &'a [u8],
221    pub signature: &'a [u8],
222}
223
224fn parse_packet_dh_reply(i: &[u8]) -> IResult<&[u8], SshPacket> {
225    let (i, pubkey_and_cert) = parse_string(i)?;
226    let (i, f) = parse_string(i)?;
227    let (i, signature) = parse_string(i)?;
228    let reply = SshPacketDhReply {
229        pubkey_and_cert,
230        f,
231        signature,
232    };
233    Ok((i, SshPacket::DiffieHellmanReply(reply)))
234}
235
236impl<'a> SshPacketDhReply<'a> {
237    /// Parse the ECDSA server signature.
238    ///
239    /// Defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2).
240    #[allow(clippy::type_complexity)]
241    pub fn get_ecdsa_signature(&self) -> Result<(&str, Vec<u8>), nom::Err<Error<&[u8]>>> {
242        let (i, identifier) = map_res(parse_string, str::from_utf8)(self.signature)?;
243        let (_, blob) = map_parser(parse_string, pair(parse_string, parse_string))(i)?;
244
245        let mut rs = Vec::new();
246
247        rs.extend_from_slice(blob.0);
248        rs.extend_from_slice(blob.1);
249
250        Ok((identifier, rs))
251    }
252}
253
254/// SSH Disconnection Message
255///
256/// Defined in [RFC4253 Section 11.1](https://tools.ietf.org/html/rfc4253#section-11.1).
257#[derive(Debug, PartialEq)]
258pub struct SshPacketDisconnect<'a> {
259    pub reason_code: u32,
260    pub description: &'a [u8],
261    pub lang: &'a [u8],
262}
263
264/// SSH Disconnection Message Reason Code
265///
266/// Defined in [IANA SSH Protocol Parameters](http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-3).
267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
268pub struct SshDisconnectReason(pub u32);
269
270newtype_enum! {
271impl display SshDisconnectReason {
272    HostNotAllowedToConnect = 1,
273    ProtocolError = 2,
274    KeyExchangeFailed = 3,
275    Reserved = 4,
276    MacError = 5,
277    CompressionError = 6,
278    ServiceNotAvailable = 7,
279    ProtocolVersionNotSupported = 8,
280    HostKeyNotVerifiable = 9,
281    ConnectionLost = 10,
282    ByApplication = 11,
283    TooManyConnections = 12,
284    AuthCancelledByUser = 13,
285    NoMoreAuthMethodsAvailable = 14,
286    IllegalUserName = 15,
287}
288}
289
290fn parse_packet_disconnect(i: &[u8]) -> IResult<&[u8], SshPacket> {
291    let (i, reason_code) = be_u32(i)?;
292    let (i, description) = parse_string(i)?;
293    let (i, lang) = parse_string(i)?;
294    let packet = SshPacketDisconnect {
295        reason_code,
296        description,
297        lang,
298    };
299    Ok((i, SshPacket::Disconnect(packet)))
300}
301
302impl<'a> SshPacketDisconnect<'a> {
303    /// Parse Disconnection Description
304    pub fn get_description(&self) -> Result<&str, str::Utf8Error> {
305        str::from_utf8(self.description)
306    }
307
308    /// Parse Disconnection Reason Code
309    pub fn get_reason(&self) -> SshDisconnectReason {
310        SshDisconnectReason(self.reason_code)
311    }
312}
313
314/// SSH Debug Message
315///
316/// Defined in [RFC4253 Section 11.3](https://tools.ietf.org/html/rfc4253#section-11.3).
317#[derive(Debug, PartialEq)]
318pub struct SshPacketDebug<'a> {
319    pub always_display: bool,
320    pub message: &'a [u8],
321    pub lang: &'a [u8],
322}
323
324fn parse_packet_debug(i: &[u8]) -> IResult<&[u8], SshPacket> {
325    let (i, display) = be_u8(i)?;
326    let (i, message) = parse_string(i)?;
327    let (i, lang) = parse_string(i)?;
328    let packet = SshPacketDebug {
329        always_display: display > 0,
330        message,
331        lang,
332    };
333    Ok((i, SshPacket::Debug(packet)))
334}
335
336impl<'a> SshPacketDebug<'a> {
337    /// Parse Debug Message
338    pub fn get_message(&self) -> Result<&str, str::Utf8Error> {
339        str::from_utf8(self.message)
340    }
341}
342
343/// SSH Packet Enumeration
344#[derive(Debug, PartialEq)]
345pub enum SshPacket<'a> {
346    Disconnect(SshPacketDisconnect<'a>),
347    Ignore(&'a [u8]),
348    Unimplemented(u32),
349    Debug(SshPacketDebug<'a>),
350    ServiceRequest(&'a [u8]),
351    ServiceAccept(&'a [u8]),
352    KeyExchange(SshPacketKeyExchange<'a>),
353    NewKeys,
354    DiffieHellmanInit(SshPacketDhInit<'a>),
355    DiffieHellmanReply(SshPacketDhReply<'a>),
356}
357
358/// Parse a plaintext SSH packet with its padding.
359///
360/// Packet structure is defined in [RFC4253 Section 6](https://tools.ietf.org/html/rfc4253#section-6) and
361/// message codes are defined in [RFC4253 Section 12](https://tools.ietf.org/html/rfc4253#section-12).
362pub fn parse_ssh_packet(i: &[u8]) -> IResult<&[u8], (SshPacket, &[u8])> {
363    let (i, packet_length) = be_u32(i)?;
364    let (i, padding_length) = be_u8(i)?;
365    if padding_length as u32 + 1 > packet_length {
366        return Err(Err::Error(make_error(i, ErrorKind::LengthValue)));
367    }
368    let (i, payload) = map_parser(take(packet_length - padding_length as u32 - 1), |d| {
369        let (d, msg_type) = be_u8(d)?;
370        match msg_type {
371            1 => parse_packet_disconnect(d),
372            2 => map(parse_string, SshPacket::Ignore)(d),
373            3 => map(be_u32, SshPacket::Unimplemented)(d),
374            4 => parse_packet_debug(d),
375            5 => map(parse_string, SshPacket::ServiceRequest)(d),
376            6 => map(parse_string, SshPacket::ServiceAccept)(d),
377            20 => parse_packet_key_exchange(d),
378            21 => Ok((d, SshPacket::NewKeys)),
379            30 => parse_packet_dh_init(d),
380            31 => parse_packet_dh_reply(d),
381            _ => Err(Err::Error(make_error(d, ErrorKind::Switch))),
382        }
383    })(i)?;
384    let (i, padding) = take(padding_length)(i)?;
385    Ok((i, (payload, padding)))
386}
387
388#[cfg(test)]
389mod tests {
390
391    use super::*;
392    use nom::Err;
393
394    #[test]
395    fn test_name() {
396        let res = parse_name(b"ssh-rsa");
397        let expected = Ok((&b""[..], &b"ssh-rsa"[..]));
398        assert_eq!(res, expected);
399    }
400
401    #[test]
402    fn test_empty_name_list() {
403        let res = parse_name_list(b"");
404        let expected = Err(Err::Error(make_error(&b""[..], ErrorKind::SeparatedList)));
405        assert_eq!(res, expected);
406    }
407
408    #[test]
409    fn test_one_name_list() {
410        let res = parse_name_list(b"ssh-rsa");
411        let expected = Ok((&b""[..], vec!["ssh-rsa"]));
412        assert_eq!(res, expected);
413    }
414
415    #[test]
416    fn test_two_names_list() {
417        let res = parse_name_list(b"ssh-rsa,ssh-ecdsa");
418        let expected = Ok((&b""[..], vec!["ssh-rsa", "ssh-ecdsa"]));
419        assert_eq!(res, expected);
420    }
421}