VYPR
Moderate severityNVD Advisory· Published Mar 16, 2023· Updated Feb 25, 2025

russh may use insecure Diffie-Hellman keys

CVE-2023-28113

Description

russh is a Rust SSH client and server library. Starting in version 0.34.0 and prior to versions 0.36.2 and 0.37.1, Diffie-Hellman key validation is insufficient, which can lead to insecure shared secrets and therefore breaks confidentiality. Connections between a russh client and server or those of a russh peer with some other misbehaving peer are most likely to be problematic. These may vulnerable to eavesdropping. Most other implementations reject such keys, so this is mainly an interoperability issue in such a case. This issue is fixed in versions 0.36.2 and 0.37.1

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Insufficient Diffie-Hellman key validation in russh prior to versions 0.36.2 and 0.37.1 allows for insecure shared secrets, leading to potential eavesdropping.

CVE-2023-28113 is a vulnerability in the russh Rust SSH library affecting versions starting from 0.34.0 up to (but not including) 0.36.2 and 0.37.1. The root cause is insufficient validation of Diffie-Hellman (DH) key exchange parameters, specifically in the handling of group parameters during the key exchange process [1]. The flaw exists in the DH key validation logic, which does not properly check that received public keys are within the correct subgroup, allowing for the possibility of an insecure shared secret being computed [2].

To exploit this vulnerability, an attacker would need to be in a position to intercept and manipulate network traffic between a russh client and server, or between two russh peers. The attack is most likely to succeed when a russh implementation communicates with another peer that is also misbehaving or non-conformant, as most other SSH implementations reject such invalid keys, making this primarily an interoperability issue in those cases [1][2]. No authentication is required beyond the normal SSH handshake, as the vulnerability occurs early in the connection establishment process.

The impact is a breach of confidentiality: a successful attacker could eavesdrop on the SSH session by deriving the same shared secret as the legitimate peers, allowing decryption of all subsequent encrypted communication. This effectively breaks the secrecy guarantees of the SSH protocol for affected connections [1].

Mitigation is straightforward: users should update russh to version 0.36.2 or 0.37.1, where the DH key validation has been corrected [3][4]. The fix addresses the insufficient checks and ensures proper validation of DH public keys before computing the shared secret, aligning with standard security practices [2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
russhcrates.io
< 0.36.20.36.2
russhcrates.io
>= 0.37.0, < 0.37.10.37.1

Affected products

2

Patches

2
45d2d82930bf

GHSA-cqvm-j2r2-hwpg validate DH key range

https://github.com/warp-tech/russhEugene PankovMar 15, 2023via ghsa
2 files changed · +59 15
  • russh/src/kex/dh/groups.rs+24 4 modified
    @@ -1,5 +1,3 @@
    -use std::ops::Shl;
    -
     use hex_literal::hex;
     use num_bigint::{BigUint, RandBigInt};
     use rand;
    @@ -69,9 +67,17 @@ impl DH {
             }
         }
     
    -    pub fn generate_private_key(&mut self) -> BigUint {
    +    pub fn generate_private_key(&mut self, is_server: bool) -> BigUint {
    +        let q = (&self.prime_num - &BigUint::from(1u8)) / &BigUint::from(2u8);
             let mut rng = rand::thread_rng();
    -        self.private_key = rng.gen_biguint((self.exp_size * 8) - 2u64).shl(1);
    +        self.private_key = rng.gen_biguint_range(
    +            &if is_server {
    +                1u8.into()
    +            } else {
    +                2u8.into()
    +            },
    +            &q,
    +        );
             self.private_key.clone()
         }
     
    @@ -85,7 +91,21 @@ impl DH {
             self.shared_secret.clone()
         }
     
    +    pub fn validate_shared_secret(&self, shared_secret: &BigUint) -> bool {
    +        let one = BigUint::from(1u8);
    +        let prime_minus_one = &self.prime_num - &one;
    +
    +        shared_secret > &one && shared_secret < &prime_minus_one
    +    }
    +
         pub fn decode_public_key(buffer: &[u8]) -> BigUint {
             BigUint::from_bytes_be(buffer)
         }
    +
    +    pub fn validate_public_key(&self, public_key: &BigUint) -> bool {
    +        let one = BigUint::from(1u8);
    +        let prime_minus_one = &self.prime_num - &one;
    +
    +        public_key > &one && public_key < &prime_minus_one
    +    }
     }
    
  • russh/src/kex/dh/mod.rs+35 11 modified
    @@ -2,9 +2,9 @@ mod groups;
     use std::marker::PhantomData;
     
     use byteorder::{BigEndian, ByteOrder};
    -use log::debug;
     use digest::Digest;
     use groups::DH;
    +use log::debug;
     use num_bigint::BigUint;
     use russh_cryptovec::CryptoVec;
     use russh_keys::encoding::Encoding;
    @@ -105,16 +105,27 @@ impl<D: Digest> KexAlgorithm for DhGroupKex<D> {
     
             debug!("client_pubkey: {:?}", client_pubkey);
     
    -        self.dh.generate_private_key();
    -        let server_pubkey = biguint_to_mpint(&self.dh.generate_public_key());
    +        self.dh.generate_private_key(true);
    +        let server_pubkey = &self.dh.generate_public_key();
    +        if !self.dh.validate_public_key(server_pubkey) {
    +            return Err(crate::Error::Inconsistent);
    +        }
    +
    +        let encoded_server_pubkey = biguint_to_mpint(server_pubkey);
     
             // fill exchange.
             exchange.server_ephemeral.clear();
    -        exchange.server_ephemeral.extend(&server_pubkey);
    +        exchange.server_ephemeral.extend(&encoded_server_pubkey);
     
    -        let shared = self
    -            .dh
    -            .compute_shared_secret(DH::decode_public_key(client_pubkey));
    +        let decoded_client_pubkey = DH::decode_public_key(client_pubkey);
    +        if !self.dh.validate_public_key(&decoded_client_pubkey) {
    +            return Err(crate::Error::Inconsistent);
    +        }
    +
    +        let shared = self.dh.compute_shared_secret(decoded_client_pubkey);
    +        if !self.dh.validate_shared_secret(&shared) {
    +            return Err(crate::Error::Inconsistent);
    +        }
             self.shared_secret = Some(biguint_to_mpint(&shared));
             Ok(())
         }
    @@ -125,22 +136,35 @@ impl<D: Digest> KexAlgorithm for DhGroupKex<D> {
             client_ephemeral: &mut CryptoVec,
             buf: &mut CryptoVec,
         ) -> Result<(), crate::Error> {
    -        self.dh.generate_private_key();
    -        let client_pubkey = biguint_to_mpint(&self.dh.generate_public_key());
    +        self.dh.generate_private_key(false);
    +        let client_pubkey = &self.dh.generate_public_key();
    +
    +        if !self.dh.validate_public_key(client_pubkey) {
    +            return Err(crate::Error::Inconsistent);
    +        }
     
             // fill exchange.
    +        let encoded_pubkey = biguint_to_mpint(client_pubkey);
             client_ephemeral.clear();
    -        client_ephemeral.extend(&client_pubkey);
    +        client_ephemeral.extend(&encoded_pubkey);
     
             buf.push(msg::KEX_ECDH_INIT);
    -        buf.extend_ssh_string(&client_pubkey);
    +        buf.extend_ssh_string(&encoded_pubkey);
     
             Ok(())
         }
     
         fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), crate::Error> {
             let remote_pubkey = DH::decode_public_key(remote_pubkey_);
    +
    +        if !self.dh.validate_public_key(&remote_pubkey) {
    +            return Err(crate::Error::Inconsistent);
    +        }
    +
             let shared = self.dh.compute_shared_secret(remote_pubkey);
    +        if !self.dh.validate_shared_secret(&shared) {
    +            return Err(crate::Error::Inconsistent);
    +        }
             self.shared_secret = Some(biguint_to_mpint(&shared));
             Ok(())
         }
    
d831a3716d37

GHSA-cqvm-j2r2-hwpg validate DH key range

https://github.com/warp-tech/russhEugene PankovMar 15, 2023via ghsa
2 files changed · +59 14
  • russh/src/kex/dh/groups.rs+24 4 modified
    @@ -1,5 +1,3 @@
    -use std::ops::Shl;
    -
     use hex_literal::hex;
     use num_bigint::{BigUint, RandBigInt};
     use rand;
    @@ -69,9 +67,17 @@ impl DH {
             }
         }
     
    -    pub fn generate_private_key(&mut self) -> BigUint {
    +    pub fn generate_private_key(&mut self, is_server: bool) -> BigUint {
    +        let q = (&self.prime_num - &BigUint::from(1u8)) / &BigUint::from(2u8);
             let mut rng = rand::thread_rng();
    -        self.private_key = rng.gen_biguint((self.exp_size * 8) - 2u64).shl(1);
    +        self.private_key = rng.gen_biguint_range(
    +            &if is_server {
    +                1u8.into()
    +            } else {
    +                2u8.into()
    +            },
    +            &q,
    +        );
             self.private_key.clone()
         }
     
    @@ -85,7 +91,21 @@ impl DH {
             self.shared_secret.clone()
         }
     
    +    pub fn validate_shared_secret(&self, shared_secret: &BigUint) -> bool {
    +        let one = BigUint::from(1u8);
    +        let prime_minus_one = &self.prime_num - &one;
    +
    +        shared_secret > &one && shared_secret < &prime_minus_one
    +    }
    +
         pub fn decode_public_key(buffer: &[u8]) -> BigUint {
             BigUint::from_bytes_be(buffer)
         }
    +
    +    pub fn validate_public_key(&self, public_key: &BigUint) -> bool {
    +        let one = BigUint::from(1u8);
    +        let prime_minus_one = &self.prime_num - &one;
    +
    +        public_key > &one && public_key < &prime_minus_one
    +    }
     }
    
  • russh/src/kex/dh/mod.rs+35 10 modified
    @@ -4,6 +4,7 @@ use std::marker::PhantomData;
     use byteorder::{BigEndian, ByteOrder};
     use digest::Digest;
     use groups::DH;
    +use log::debug;
     use num_bigint::BigUint;
     use russh_cryptovec::CryptoVec;
     use russh_keys::encoding::Encoding;
    @@ -104,16 +105,27 @@ impl<D: Digest> KexAlgorithm for DhGroupKex<D> {
     
             debug!("client_pubkey: {:?}", client_pubkey);
     
    -        self.dh.generate_private_key();
    -        let server_pubkey = biguint_to_mpint(&self.dh.generate_public_key());
    +        self.dh.generate_private_key(true);
    +        let server_pubkey = &self.dh.generate_public_key();
    +        if !self.dh.validate_public_key(server_pubkey) {
    +            return Err(crate::Error::Inconsistent);
    +        }
    +
    +        let encoded_server_pubkey = biguint_to_mpint(server_pubkey);
     
             // fill exchange.
             exchange.server_ephemeral.clear();
    -        exchange.server_ephemeral.extend(&server_pubkey);
    +        exchange.server_ephemeral.extend(&encoded_server_pubkey);
    +
    +        let decoded_client_pubkey = DH::decode_public_key(client_pubkey);
    +        if !self.dh.validate_public_key(&decoded_client_pubkey) {
    +            return Err(crate::Error::Inconsistent);
    +        }
     
    -        let shared = self
    -            .dh
    -            .compute_shared_secret(DH::decode_public_key(client_pubkey));
    +        let shared = self.dh.compute_shared_secret(decoded_client_pubkey);
    +        if !self.dh.validate_shared_secret(&shared) {
    +            return Err(crate::Error::Inconsistent);
    +        }
             self.shared_secret = Some(biguint_to_mpint(&shared));
             Ok(())
         }
    @@ -124,22 +136,35 @@ impl<D: Digest> KexAlgorithm for DhGroupKex<D> {
             client_ephemeral: &mut CryptoVec,
             buf: &mut CryptoVec,
         ) -> Result<(), crate::Error> {
    -        self.dh.generate_private_key();
    -        let client_pubkey = biguint_to_mpint(&self.dh.generate_public_key());
    +        self.dh.generate_private_key(false);
    +        let client_pubkey = &self.dh.generate_public_key();
    +
    +        if !self.dh.validate_public_key(client_pubkey) {
    +            return Err(crate::Error::Inconsistent);
    +        }
     
             // fill exchange.
    +        let encoded_pubkey = biguint_to_mpint(client_pubkey);
             client_ephemeral.clear();
    -        client_ephemeral.extend(&client_pubkey);
    +        client_ephemeral.extend(&encoded_pubkey);
     
             buf.push(msg::KEX_ECDH_INIT);
    -        buf.extend_ssh_string(&client_pubkey);
    +        buf.extend_ssh_string(&encoded_pubkey);
     
             Ok(())
         }
     
         fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), crate::Error> {
             let remote_pubkey = DH::decode_public_key(remote_pubkey_);
    +
    +        if !self.dh.validate_public_key(&remote_pubkey) {
    +            return Err(crate::Error::Inconsistent);
    +        }
    +
             let shared = self.dh.compute_shared_secret(remote_pubkey);
    +        if !self.dh.validate_shared_secret(&shared) {
    +            return Err(crate::Error::Inconsistent);
    +        }
             self.shared_secret = Some(biguint_to_mpint(&shared));
             Ok(())
         }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.