From 04b196fc10cf29f686c505e63f216d9be7f87423 Mon Sep 17 00:00:00 2001
From: Matt Johnston <matt@ucc.asn.au>
Date: Fri, 7 Apr 2023 21:40:43 +0800
Subject: [PATCH] Add MSG_EXT_INFO handling for rsa2

Could eventually perhaps go behind a rsa feature
---
 async/examples/sunsetc.rs   |  6 ++---
 async/src/cmdline_client.rs |  1 +
 src/cliauth.rs              | 18 ++++++++++---
 src/client.rs               |  2 --
 src/conn.rs                 | 12 ++++++++-
 src/kex.rs                  | 39 +++++++++++++++++++++++----
 src/namelist.rs             |  4 ++-
 src/packets.rs              | 53 +++++++++++++++++++++++++++++++++++--
 src/sshnames.rs             |  3 +++
 9 files changed, 121 insertions(+), 17 deletions(-)

diff --git a/async/examples/sunsetc.rs b/async/examples/sunsetc.rs
index fb0171c..b4a36cc 100644
--- a/async/examples/sunsetc.rs
+++ b/async/examples/sunsetc.rs
@@ -179,9 +179,9 @@ fn setup_log(args: &Args) -> Result<()> {
     .add_filter_allow_str("sunset")
     .add_filter_allow_str("sshclient")
     // not debugging these bits of the stack at present
-    // .add_filter_ignore_str("sunset::traffic")
-    // .add_filter_ignore_str("sunset::runner")
-    // .add_filter_ignore_str("sunset_embassy")
+    .add_filter_ignore_str("sunset::traffic")
+    .add_filter_ignore_str("sunset::runner")
+    .add_filter_ignore_str("sunset_embassy")
     .set_time_offset_to_local().expect("Couldn't get local timezone")
     .build();
 
diff --git a/async/src/cmdline_client.rs b/async/src/cmdline_client.rs
index ecc54d9..a5c08af 100644
--- a/async/src/cmdline_client.rs
+++ b/async/src/cmdline_client.rs
@@ -271,6 +271,7 @@ impl<'a> CmdlineRunner<'a> {
     /// Runs the `CmdlineClient` session. Requests a shell or command, performs
     /// channel IO.
     pub async fn run(&mut self, cli: &'a SSHClient<'a, CmdlineHooks<'a>>) -> Result<()> {
+        // chanio is only set once a channel is opened below
         let chanio = Fuse::terminated();
         pin_mut!(chanio);
 
diff --git a/src/cliauth.rs b/src/cliauth.rs
index 9d453a4..9ac2435 100644
--- a/src/cliauth.rs
+++ b/src/cliauth.rs
@@ -74,12 +74,15 @@ pub(crate) struct CliAuth {
 
     username: ResponseString,
 
-    // Starts as true, set to false if hook.auth_password() returns None.
-    // Not set false if the server rejects auth.
+    /// Starts as true, set to false if hook.auth_password() returns None.
+    /// Not set false if the server rejects auth.
     try_password: bool,
 
-    // Set to false if hook.next_authkey() returns None.
+    /// Set to false if hook.next_authkey() returns None.
     try_pubkey: bool,
+
+    /// Set once we are OKed from MSG_EXT_INFO
+    allow_rsa_sha2: bool,
 }
 
 impl CliAuth {
@@ -90,6 +93,7 @@ impl CliAuth {
             username: ResponseString::new(),
             try_password: true,
             try_pubkey: true,
+            allow_rsa_sha2: false,
         }
     }
 
@@ -308,4 +312,12 @@ impl CliAuth {
         // TODO errors
         Ok(())
     }
+
+    pub fn handle_ext_info(&mut self, p: &packets::ExtInfo<'_>) {
+        if let Some(ref algs) = p.server_sig_algs {
+            // OK unwrap: is a remote namelist
+            self.allow_rsa_sha2 = algs.has_algo(SSH_NAME_RSA_SHA256).unwrap();
+            trace!("setting allow_rsa_sha2 = {}", self.allow_rsa_sha2);
+        }
+    }
 }
diff --git a/src/client.rs b/src/client.rs
index 4aaf7ac..d9f02f8 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -24,8 +24,6 @@ impl Client {
         Client { auth: CliAuth::new() }
     }
 
-    // pub fn check_hostkey(hostkey: )
-
     pub(crate) fn auth_success(
         &mut self,
         parse_ctx: &mut ParseContext,
diff --git a/src/conn.rs b/src/conn.rs
index a2502a3..e42c82f 100644
--- a/src/conn.rs
+++ b/src/conn.rs
@@ -263,6 +263,12 @@ impl<C: CliBehaviour, S: ServBehaviour> Conn<C, S> {
             Packet::NewKeys(_) => {
                 self.kex.handle_newkeys(&mut self.sess_id, s)?;
             }
+            Packet::ExtInfo(p) => {
+                if let ClientServer::Client(cli) = &mut self.cliserv {
+                    cli.auth.handle_ext_info(&p);
+                }
+                // could potentially pass it to other handlers too
+            }
             Packet::ServiceRequest(p) => {
                 if let ClientServer::Server(serv) = &mut self.cliserv {
                     serv.service_request(&p, s)?;
@@ -282,7 +288,11 @@ impl<C: CliBehaviour, S: ServBehaviour> Conn<C, S> {
                 warn!("Received SSH unimplemented message");
             }
             Packet::DebugPacket(p) => {
-                warn!("SSH debug message from remote host: '{:?}'", p.message);
+                let level = match p.always_display {
+                    true => log::Level::Info,
+                    false => log::Level::Debug,
+                };
+                log!(level, "SSH debug message from remote host: {}", p.message);
             }
             Packet::Disconnect(p) => {
                 // We ignore p.reason.
diff --git a/src/kex.rs b/src/kex.rs
index b79b6ce..7ee639b 100644
--- a/src/kex.rs
+++ b/src/kex.rs
@@ -34,6 +34,10 @@ use pretty_hex::PrettyHex;
 const fixed_options_kex: &[&'static str] =
     &[SSH_NAME_CURVE25519, SSH_NAME_CURVE25519_LIBSSH];
 
+/// Options that can't be negotiated
+const marker_only_kexs: &[&'static str] =
+    &[SSH_NAME_EXT_INFO_C, SSH_NAME_EXT_INFO_S, SSH_NAME_KEXGUESS2];
+
 const fixed_options_hostsig: &[&'static str] = &[
     SSH_NAME_ED25519,
     #[cfg(rsa)]
@@ -56,10 +60,20 @@ pub(crate) struct AlgoConfig {
 impl AlgoConfig {
     /// Creates the standard algorithm configuration
     /// TODO: ext-info-s and ext-info-c
-    pub fn new(_is_client: bool) -> Self {
-        AlgoConfig {
+    pub fn new(is_client: bool) -> Self {
+        // OK unwrap: static arrays are < MAX_LOCAL_NAMES
+        let mut kexs: LocalNames = fixed_options_kex.try_into().unwrap();
+
+        // Only clients are interested in ext-info
+        // TODO perhaps it could go behind cfg(rsa)?
+        if is_client {
             // OK unwrap: static arrays are < MAX_LOCAL_NAMES
-            kexs: fixed_options_kex.try_into().unwrap(),
+            kexs.0.push(SSH_NAME_EXT_INFO_C).unwrap();
+
+        }
+
+        AlgoConfig {
+            kexs,
             hostsig: fixed_options_hostsig.try_into().unwrap(),
             ciphers: fixed_options_cipher.try_into().unwrap(),
             macs: fixed_options_mac.try_into().unwrap(),
@@ -201,6 +215,9 @@ pub(crate) struct Algos {
     // avoid having to keep passing it separately, though this
     // is global state.
     pub is_client: bool,
+
+    // whether the remote side supports ext-info
+    pub ext_info: bool,
 }
 
 impl fmt::Display for Algos {
@@ -367,10 +384,13 @@ impl Kex {
             .kex
             .first_match(is_client, &conf.kexs)?
             .ok_or(Error::AlgoNoMatch { algo: "kex" })?;
-        if kex_method == SSH_NAME_KEXGUESS2 {
-            trace!("kexguess2 was negotiated, returning AlgoNoMatch");
+
+        // Certain kex method names aren't actual algorithms, just markers.
+        // If they are negotiated it means no valid method matched
+        if marker_only_kexs.contains(&kex_method) {
             return Err(Error::AlgoNoMatch { algo: "kex" });
         }
+
         let kex = SharedSecret::from_name(kex_method)?;
         let goodguess_kex = if kexguess2 {
             p.kex.first() == kex_method
@@ -378,6 +398,14 @@ impl Kex {
             p.kex.first() == conf.kexs.first()
         };
 
+        // we only send MSG_EXT_INFO to a client, don't look
+        // for SSH_NAME_EXT_INFO_S
+        let ext_info = match is_client {
+            true => false,
+            // OK unwrap: p.kex is a remote list
+            false => p.kex.has_algo(SSH_NAME_EXT_INFO_C).unwrap(),
+        };
+
         let hostsig_method = p
             .hostsig
             .first_match(is_client, &conf.hostsig)?
@@ -445,6 +473,7 @@ impl Kex {
             integ_dec,
             discard_next,
             is_client,
+            ext_info,
         })
     }
 }
diff --git a/src/namelist.rs b/src/namelist.rs
index 49460c4..318eeda 100644
--- a/src/namelist.rs
+++ b/src/namelist.rs
@@ -72,7 +72,6 @@ impl SSHEncode for &LocalNames {
     }
 }
 
-// for tests
 impl<'a> TryFrom<&'a str> for StringNames<'a> {
     type Error = ();
     fn try_from(s: &'a str) -> Result<Self, Self::Error> {
@@ -86,6 +85,7 @@ impl<'a> TryFrom<&'a str> for NameList<'a> {
     }
 }
 
+// for tests
 impl TryFrom<&[&'static str]> for LocalNames {
     type Error = ();
     fn try_from(s: &[&'static str]) -> Result<Self, ()> {
@@ -123,6 +123,8 @@ impl NameList<'_> {
     }
 
     /// Returns whether the `algo` is contained in this list
+    ///
+    /// Fails iff given a Local variant
     pub fn has_algo(&self, algo: &str) -> Result<bool> {
         match self {
             NameList::String(s) => Ok(s.has_algo(algo)),
diff --git a/src/packets.rs b/src/packets.rs
index c4f58b2..b027891 100644
--- a/src/packets.rs
+++ b/src/packets.rs
@@ -97,6 +97,55 @@ pub struct ServiceAccept<'a> {
     pub name: &'a str,
 }
 
+/// MSG_EXT_INFO
+///
+/// `ExtInfo` differs from most packet structs, it only tracks known extension types
+/// rather than an unknown-sized list.
+#[derive(Debug)]
+pub struct ExtInfo<'a> {
+    // Wire format is
+    // byte       SSH_MSG_EXT_INFO (value 7)
+    // uint32     nr-extensions
+    // repeat the following 2 fields "nr-extensions" times:
+    //   string   extension-name
+    //   string   extension-value (binary)
+
+    pub server_sig_algs: Option<NameList<'a>>,
+}
+
+impl<'de: 'a, 'a> SSHDecode<'de> for ExtInfo<'a> {
+    fn dec<S>(s: &mut S) -> WireResult<Self> where S: SSHSource<'de> {
+        let mut server_sig_algs = None;
+        let num = u32::dec(s)?;
+        for _ in 0..num {
+            let ext: &str = SSHDecode::dec(s)?;
+            match ext {
+                SSH_EXT_SERVER_SIG_ALGS => {
+                    server_sig_algs = Some(SSHDecode::dec(s)?);
+                },
+                _ => {
+                    // skip over
+                    let _: BinString = SSHDecode::dec(s)?;
+                },
+            }
+        }
+        Ok(Self {
+            server_sig_algs
+        })
+    }
+}
+
+impl SSHEncode for ExtInfo<'_> {
+    fn enc<S>(&self, s: &mut S) -> WireResult<()> where S: SSHSink {
+        if let Some(ref algs) = self.server_sig_algs {
+            1u32.enc(s)?;
+            SSH_EXT_SERVER_SIG_ALGS.enc(s)?;
+            algs.enc(s)?;
+        }
+        Ok(())
+    }
+}
+
 #[derive(Debug, SSHEncode, SSHDecode)]
 pub struct UserauthRequest<'a> {
     pub username: TextString<'a>,
@@ -436,7 +485,7 @@ pub enum RequestSuccess {
 }
 
 impl<'de> SSHDecode<'de> for RequestSuccess {
-    fn dec<S>(s: &mut S) -> WireResult<Self> where S: SSHSource<'de> {
+    fn dec<S>(_s: &mut S) -> WireResult<Self> where S: SSHSource<'de> {
         // if s.ctx().last_req_port {
         //     Ok(Self::TcpPort(TcpPort::dec(s)?))
         // } else {
@@ -852,7 +901,7 @@ messagetypes![
 (4, DebugPacket, DebugPacket<'a>, SSH_MSG_DEBUG, All),
 (5, ServiceRequest, ServiceRequest<'a>, SSH_MSG_SERVICE_REQUEST, Auth),
 (6, ServiceAccept, ServiceAccept<'a>, SSH_MSG_SERVICE_ACCEPT, Auth),
-// 7        SSH_MSG_EXT_INFO       RFC 8308
+(7, ExtInfo, ExtInfo<'a>, SSH_MSG_EXT_INFO, All),
 // 8        SSH_MSG_NEWCOMPRESS    RFC 8308
 (20, KexInit, KexInit<'a>, SSH_MSG_KEXINIT, All),
 (21, NewKeys, NewKeys, SSH_MSG_NEWKEYS, Kex),
diff --git a/src/sshnames.rs b/src/sshnames.rs
index 1114a66..57ec5bf 100644
--- a/src/sshnames.rs
+++ b/src/sshnames.rs
@@ -59,6 +59,9 @@ pub const SSH_AUTHMETHOD_INTERACTIVE: &str = "keyboard-interactive";
 /// [RFC4254](https://tools.ietf.org/html/rfc4254)
 pub const SSH_EXTENDED_DATA_STDERR: u32 = 1;
 
+/// [RFC8308](https://tools.ietf.org/html/rfc8308) Extension Negotiation
+pub const SSH_EXT_SERVER_SIG_ALGS: &str = "server-sig-algs";
+
 /// [RFC4254](https://tools.ietf.org/html/rfc4254)
 #[allow(non_camel_case_types)]
 #[derive(Debug)]
-- 
GitLab