CVE-2023-43660: Improper Verification of Cryptographic Signature (CWE-347)
value: Warpgate is a smart SSH, HTTPS and MySQL bastion host for Linux that doesn't need special client apps. The SSH key verification for a user can be bypassed by sending an SSH key offer without a signature. This allows bypassing authentication under following conditions: 1. The attacker knows the username and a valid target name 2. The attacked knows the user's public key and 3. Only SSH public key authentication is required for the user account. This issue has been addressed in version 0.8.1. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product does not verify, or incorrectly verifies, the cryptographic signature for data.
CVSS v3 base score: 8.1
Published: 2023-09-27T22:15Z
Repository: https://github.com/warp-tech/warpgate
Language: Rust
Files changed: Cargo.lock, warpgate-protocol-ssh/Cargo.toml, warpgate-protocol-ssh/src/server/russh_handler.rs, warpgate-protocol-ssh/src/server/session.rs
Commit date: 2023-09-27 22:08:49 +0200
Commit message: fixed GHSA-3cjp-w4cp-m9c8 - interpreting SSH public key offers as a successful authentication

Diff: diff --git a/Cargo.lock b/Cargo.lock
index 0b658e5..2f1611f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1471,13 +1471,13 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
 [[package]]
 name = "form_urlencoded"
-version = "1.1.0"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
 dependencies = [
  "percent-encoding",
 ]
 
 [[package]]
@@ -2055,13 +2055,13 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
 [[package]]
 name = "idna"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
 dependencies = [
  "unicode-bidi",
  "unicode-normalization",
 ]
 
@@ -2976,13 +2976,13 @@ dependencies = [
  "base64ct",
 ]
 
 [[package]]
 name = "percent-encoding"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
 
 [[package]]
 name = "phf"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3623,13 +3623,13 @@ dependencies = [
  "zeroize",
 ]
 
 [[package]]
 name = "russh"
-version = "0.38.0"
+version = "0.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae0efcc0f4cd6c062c07e572ce4b806e3967fa029fcbfcc0aa98fb5910a37925"
+checksum = "7878311587d0353a854d5be954fbe68bdf6e77873933b484d1e45db12bb2f8cf"
 dependencies = [
  "aes",
  "aes-gcm",
  "async-trait",
  "bitflags 2.3.3",
@@ -5160,13 +5160,13 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 
 [[package]]
 name = "url"
-version = "2.3.1"
+version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
 dependencies = [
  "form_urlencoded",
  "idna",
  "percent-encoding",
  "serde",
@@ -5254,11 +5254,11 @@ dependencies = [
  "try-lock",
 ]
 
 [[package]]
 name = "warpgate"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "ansi_term",
  "anyhow",
  "async-trait",
  "atty",
@@ -5290,11 +5290,11 @@ dependencies = [
  "warpgate-protocol-ssh",
 ]
 
 [[package]]
 name = "warpgate-admin"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "async-trait",
  "bytes",
  "chrono",
@@ -5319,11 +5319,11 @@ dependencies = [
  "warpgate-protocol-ssh",
 ]
 
 [[package]]
 name = "warpgate-common"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "argon2",
  "async-trait",
  "bytes",
@@ -5355,11 +5355,11 @@ dependencies = [
  "webpki",
 ]
 
 [[package]]
 name = "warpgate-core"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "argon2",
  "async-trait",
  "bytes",
@@ -5395,11 +5395,11 @@ dependencies = [
  "webpki",
 ]
 
 [[package]]
 name = "warpgate-database-protocols"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "bitflags 1.3.2",
  "bytes",
  "futures-core",
  "futures-util",
@@ -5408,11 +5408,11 @@ dependencies = [
  "tokio",
 ]
 
 [[package]]
 name = "warpgate-db-entities"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "chrono",
  "poem-openapi",
  "sea-orm",
  "serde",
@@ -5421,11 +5421,11 @@ dependencies = [
  "warpgate-common",
 ]
 
 [[package]]
 name = "warpgate-db-migrations"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "async-std",
  "chrono",
  "sea-orm",
  "sea-orm-migration",
@@ -5433,11 +5433,11 @@ dependencies = [
  "uuid",
 ]
 
 [[package]]
 name = "warpgate-protocol-http"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "async-trait",
  "chrono",
  "cookie",
@@ -5454,10 +5454,11 @@ dependencies = [
  "serde",
  "serde_json",
  "tokio",
  "tokio-tungstenite 0.17.2",
  "tracing",
+ "url",
  "uuid",
  "warpgate-admin",
  "warpgate-common",
  "warpgate-core",
  "warpgate-db-entities",
@@ -5465,11 +5466,11 @@ dependencies = [
  "warpgate-web",
 ]
 
 [[package]]
 name = "warpgate-protocol-mysql"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "async-trait",
  "bytes",
  "mysql_common",
@@ -5492,11 +5493,11 @@ dependencies = [
  "webpki",
 ]
 
 [[package]]
 name = "warpgate-protocol-ssh"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "ansi_term",
  "anyhow",
  "async-trait",
  "bimap",
@@ -5517,11 +5518,11 @@ dependencies = [
  "zeroize",
 ]
 
 [[package]]
 name = "warpgate-sso"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "bytes",
  "data-encoding",
  "jsonwebtoken",
  "once_cell",
@@ -5533,11 +5534,11 @@ dependencies = [
  "tracing",
 ]
 
 [[package]]
 name = "warpgate-web"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "rust-embed",
  "serde",
  "serde_json",
  "thiserror",
diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml
index cb2bc7a..c3c9d3e 100644
--- a/warpgate-protocol-ssh/Cargo.toml
+++ b/warpgate-protocol-ssh/Cargo.toml
@@ -10,11 +10,11 @@ anyhow = { version = "1.0", features = ["std"] }
 async-trait = "0.1"
 bimap = "0.6"
 bytes = "1.3"
 dialoguer = "0.10"
 futures = "0.3"
-russh = { version = "0.38.0", features = ["vendored-openssl"] }
+russh = { version = "0.39.0", features = ["vendored-openssl"] }
 # russh = { version = "0.35.0-beta.6", features = ["vendored-openssl"], path = "../../russh/russh"}
 russh-keys = { version = "0.38.0", features = ["vendored-openssl"] }
 # russh-keys = { version = "0.23.0-beta.1", features = ["vendored-openssl"], path = "../../russh/russh-keys" }
 sea-orm = { version = "0.12.2", features = [
     "runtime-tokio-native-tls",
diff --git a/warpgate-protocol-ssh/src/server/russh_handler.rs b/warpgate-protocol-ssh/src/server/russh_handler.rs
index a3f3ccb..7dc0eb7 100644
--- a/warpgate-protocol-ssh/src/server/russh_handler.rs
+++ b/warpgate-protocol-ssh/src/server/russh_handler.rs
@@ -27,10 +27,11 @@ pub enum ServerHandlerEvent {
     ChannelOpenSession(ServerChannelId, oneshot::Sender<bool>),
     SubsystemRequest(ServerChannelId, String, oneshot::Sender<bool>),
     PtyRequest(ServerChannelId, PtyRequest, oneshot::Sender<()>),
     ShellRequest(ServerChannelId, oneshot::Sender<bool>),
     AuthPublicKey(Secret<String>, PublicKey, oneshot::Sender<Auth>),
+    AuthPublicKeyOffer(Secret<String>, PublicKey, oneshot::Sender<bool>),
     AuthPassword(Secret<String>, Secret<String>, oneshot::Sender<Auth>),
     AuthKeyboardInteractive(
         Secret<String>,
         Option<Secret<String>>,
         oneshot::Sender<Auth>,
@@ -176,10 +177,37 @@ impl russh::server::Handler for ServerHandler {
         }
 
         Ok((self, session))
     }
 
+    async fn auth_publickey_offered(
+        self,
+        user: &str,
+        key: &russh_keys::key::PublicKey,
+    ) -> Result<(Self, Auth), Self::Error> {
+        let user = Secret::new(user.to_string());
+        let (tx, rx) = oneshot::channel();
+
+        self.send_event(ServerHandlerEvent::AuthPublicKeyOffer(
+            user,
+            key.clone(),
+            tx,
+        ))?;
+
+        let result = rx.await.unwrap_or(false);
+        Ok((
+            self,
+            if result {
+                Auth::Accept
+            } else {
+                Auth::Reject {
+                    proceed_with_methods: None,
+                }
+            },
+        ))
+    }
+
     async fn auth_publickey(
         self,
         user: &str,
         key: &russh_keys::key::PublicKey,
     ) -> Result<(Self, Auth), Self::Error> {
diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs
index cff6b9a..5158537 100644
--- a/warpgate-protocol-ssh/src/server/session.rs
+++ b/warpgate-protocol-ssh/src/server/session.rs
@@ -464,10 +464,14 @@ impl ServerSession {
 
             ServerHandlerEvent::AuthPublicKey(username, key, reply) => {
                 let _ = reply.send(self._auth_publickey(username, key).await);
             }
 
+            ServerHandlerEvent::AuthPublicKeyOffer(username, key, reply) => {
+                let _ = reply.send(self._auth_publickey_offer(username, key).await);
+            }
+
             ServerHandlerEvent::AuthPassword(username, password, reply) => {
                 let _ = reply.send(self._auth_password(username, password).await);
             }
 
             ServerHandlerEvent::AuthKeyboardInteractive(username, response, reply) => {
@@ -1147,23 +1151,11 @@ impl ServerSession {
         self.send_command_and_wait(RCCommand::CancelTCPIPForward(address, port))
             .await
             .map_err(anyhow::Error::from)
     }
 
-    async fn _auth_publickey(
-        &mut self,
-        ssh_username: Secret<String>,
-        key: PublicKey,
-    ) -> russh::server::Auth {
-        let selector: AuthSelector = ssh_username.expose_secret().into();
-
-        info!(
-            "Public key auth as {:?} with key {}",
-            selector,
-            key.public_key_base64()
-        );
-
+    fn _get_public_keys_from_of(&self, key: PublicKey) -> Vec<PublicKey> {
         let mut keys = vec![key.clone()];
         // Try all supported hash algorithms
         if let PublicKey::RSA { key, hash } = &key {
             for h in [
                 SignatureHash::SHA1,
@@ -1176,10 +1168,52 @@ impl ServerSession {
                         hash: h,
                     });
                 }
             }
         }
+        keys
+    }
+
+    async fn _auth_publickey_offer(
+        &mut self,
+        ssh_username: Secret<String>,
+        key: PublicKey,
+    ) -> bool {
+        let keys = self._get_public_keys_from_of(key);
+        let selector: AuthSelector = ssh_username.expose_secret().into();
+
+        for key in keys {
+            if let Ok(true) = self
+                .try_validate_public_key_offer(
+                    &selector,
+                    Some(AuthCredential::PublicKey {
+                        kind: key.name().to_string(),
+                        public_key_bytes: Bytes::from(key.public_key_bytes()),
+                    }),
+                )
+                .await
+            {
+                return true;
+            }
+        }
+        false
+    }
+
+    async fn _auth_publickey(
+        &mut self,
+        ssh_username: Secret<String>,
+        key: PublicKey,
+    ) -> russh::server::Auth {
+        let selector: AuthSelector = ssh_username.expose_secret().into();
+
+        info!(
+            "Public key auth as {:?} with key {}",
+            selector,
+            key.public_key_base64()
+        );
+
+        let keys = self._get_public_keys_from_of(key);
 
         let mut result = Ok(AuthResult::Rejected);
         for key in keys {
             result = self
                 .try_auth(
@@ -1359,10 +1393,33 @@ impl ServerSession {
             }
         }
         m
     }
 
+    async fn try_validate_public_key_offer(
+        &mut self,
+        selector: &AuthSelector,
+        credential: Option<AuthCredential>,
+    ) -> Result<bool> {
+        match selector {
+            AuthSelector::User { username, .. } => {
+                let cp = self.services.config_provider.clone();
+
+                if let Some(credential) = credential {
+                    return Ok(cp
+                        .lock()
+                        .await
+                        .validate_credential(username, &credential)
+                        .await?);
+                }
+
+                Ok(false)
+            }
+            _ => Ok(false),
+        }
+    }
+
     async fn try_auth(
         &mut self,
         selector: &AuthSelector,
         credential: Option<AuthCredential>,
     ) -> Result<AuthResult> {

Vulnerable code: version = "1.1.0"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
version = "0.3.0"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
version = "2.2.0"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
version = "0.38.0"
checksum = "ae0efcc0f4cd6c062c07e572ce4b806e3967fa029fcbfcc0aa98fb5910a37925"
version = "2.3.1"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
version = "0.7.4"
russh = { version = "0.38.0", features = ["vendored-openssl"] }
    async fn _auth_publickey(
        &mut self,
        ssh_username: Secret<String>,
        key: PublicKey,
    ) -> russh::server::Auth {
        let selector: AuthSelector = ssh_username.expose_secret().into();

        info!(
            "Public key auth as {:?} with key {}",
            selector,
            key.public_key_base64()
        );

Fixed code: version = "1.2.0"
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
version = "0.4.0"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
version = "2.3.0"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
version = "0.39.0"
checksum = "7878311587d0353a854d5be954fbe68bdf6e77873933b484d1e45db12bb2f8cf"
version = "2.4.1"
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
version = "0.8.0"
version = "0.8.0"
version = "0.8.0"
version = "0.8.0"
version = "0.8.0"
version = "0.8.0"
version = "0.8.0"
version = "0.8.0"
 "url",
version = "0.8.0"
version = "0.8.0"
version = "0.8.0"
version = "0.8.0"
russh = { version = "0.39.0", features = ["vendored-openssl"] }
    AuthPublicKeyOffer(Secret<String>, PublicKey, oneshot::Sender<bool>),
    async fn auth_publickey_offered(
        self,
        user: &str,
        key: &russh_keys::key::PublicKey,
    ) -> Result<(Self, Auth), Self::Error> {
        let user = Secret::new(user.to_string());
        let (tx, rx) = oneshot::channel();

        self.send_event(ServerHandlerEvent::AuthPublicKeyOffer(
            user,
            key.clone(),
            tx,
        ))?;

        let result = rx.await.unwrap_or(false);
        Ok((
            self,
            if result {
                Auth::Accept
            } else {
                Auth::Reject {
                    proceed_with_methods: None,
                }
            },
        ))
    }

            ServerHandlerEvent::AuthPublicKeyOffer(username, key, reply) => {
                let _ = reply.send(self._auth_publickey_offer(username, key).await);
            }

    fn _get_public_keys_from_of(&self, key: PublicKey) -> Vec<PublicKey> {
        keys
    }

    async fn _auth_publickey_offer(
        &mut self,
        ssh_username: Secret<String>,
        key: PublicKey,
    ) -> bool {
        let keys = self._get_public_keys_from_of(key);
        let selector: AuthSelector = ssh_username.expose_secret().into();

        for key in keys {
            if let Ok(true) = self
                .try_validate_public_key_offer(
                    &selector,
                    Some(AuthCredential::PublicKey {
                        kind: key.name().to_string(),
                        public_key_bytes: Bytes::from(key.public_key_bytes()),
                    }),
                )
                .await
            {
                return true;
            }
        }
        false
    }

    async fn _auth_publickey(
        &mut self,
        ssh_username: Secret<String>,
        key: PublicKey,
    ) -> russh::server::Auth {
        let selector: AuthSelector = ssh_username.expose_secret().into();

        info!(
            "Public key auth as {:?} with key {}",
            selector,
            key.public_key_base64()
        );

        let keys = self._get_public_keys_from_of(key);
    async fn try_validate_public_key_offer(
        &mut self,
        selector: &AuthSelector,
        credential: Option<AuthCredential>,
    ) -> Result<bool> {
        match selector {
            AuthSelector::User { username, .. } => {
                let cp = self.services.config_provider.clone();

                if let Some(credential) = credential {
                    return Ok(cp
                        .lock()
                        .await
                        .validate_credential(username, &credential)
                        .await?);
                }

                Ok(false)
            }
            _ => Ok(false),
        }
    }