Skip to content

Commit 609b0f2

Browse files
authored
fix(kex): separate GEX peer request validation from client config (#684)
1 parent 1a55f50 commit 609b0f2

4 files changed

Lines changed: 159 additions & 13 deletions

File tree

russh/src/client/mod.rs

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,22 +1685,42 @@ impl GexParams {
16851685
preferred_group_size: usize,
16861686
max_group_size: usize,
16871687
) -> Result<Self, Error> {
1688-
let this = Self {
1688+
Self::for_client_config(min_group_size, preferred_group_size, max_group_size)
1689+
}
1690+
1691+
pub fn for_client_config(
1692+
min_group_size: usize,
1693+
preferred_group_size: usize,
1694+
max_group_size: usize,
1695+
) -> Result<Self, Error> {
1696+
Self::build(
16891697
min_group_size,
16901698
preferred_group_size,
16911699
max_group_size,
1692-
};
1693-
this.validate()?;
1694-
Ok(this)
1695-
}
1696-
1697-
pub(crate) fn validate(&self) -> Result<(), Error> {
1698-
if self.min_group_size < 2048 {
1699-
return Err(Error::InvalidConfig(format!(
1700-
"min_group_size must be at least 2048 bits. We got {} bits",
1701-
self.min_group_size
1702-
)));
1700+
ValidationKind::ClientConfig,
1701+
)
1702+
}
1703+
1704+
fn validate(&self, kind: ValidationKind) -> Result<(), Error> {
1705+
match kind {
1706+
ValidationKind::ClientConfig => {
1707+
if self.min_group_size < 2048 {
1708+
return Err(Error::InvalidConfig(format!(
1709+
"min_group_size must be at least 2048 bits. We got {} bits",
1710+
self.min_group_size
1711+
)));
1712+
}
1713+
}
1714+
ValidationKind::PeerRequest => {
1715+
if self.max_group_size < 2048 {
1716+
return Err(Error::InvalidConfig(format!(
1717+
"max_group_size must be at least 2048 bits. We got {} bits",
1718+
self.max_group_size
1719+
)));
1720+
}
1721+
}
17031722
}
1723+
17041724
if self.preferred_group_size < self.min_group_size {
17051725
return Err(Error::InvalidConfig(format!(
17061726
"preferred_group_size must be at least as large as min_group_size. We have preferred_group_size = {} < min_group_size = {}",
@@ -1713,9 +1733,38 @@ impl GexParams {
17131733
self.max_group_size, self.preferred_group_size
17141734
)));
17151735
}
1736+
17161737
Ok(())
17171738
}
17181739

1740+
pub(crate) fn from_peer_request(
1741+
min_group_size: usize,
1742+
preferred_group_size: usize,
1743+
max_group_size: usize,
1744+
) -> Result<Self, Error> {
1745+
Self::build(
1746+
min_group_size,
1747+
preferred_group_size,
1748+
max_group_size,
1749+
ValidationKind::PeerRequest,
1750+
)
1751+
}
1752+
1753+
fn build(
1754+
min_group_size: usize,
1755+
preferred_group_size: usize,
1756+
max_group_size: usize,
1757+
kind: ValidationKind,
1758+
) -> Result<Self, Error> {
1759+
let this = Self {
1760+
min_group_size,
1761+
preferred_group_size,
1762+
max_group_size,
1763+
};
1764+
this.validate(kind)?;
1765+
Ok(this)
1766+
}
1767+
17191768
pub fn min_group_size(&self) -> usize {
17201769
self.min_group_size
17211770
}
@@ -1729,6 +1778,12 @@ impl GexParams {
17291778
}
17301779
}
17311780

1781+
#[derive(Clone, Copy, Eq, PartialEq)]
1782+
enum ValidationKind {
1783+
ClientConfig,
1784+
PeerRequest,
1785+
}
1786+
17321787
impl Default for GexParams {
17331788
fn default() -> GexParams {
17341789
GexParams {

russh/src/kex/dh/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ impl Decode for GexParams {
347347
let min_group_size = u32::decode(reader)? as usize;
348348
let preferred_group_size = u32::decode(reader)? as usize;
349349
let max_group_size = u32::decode(reader)? as usize;
350-
GexParams::new(min_group_size, preferred_group_size, max_group_size)
350+
GexParams::from_peer_request(min_group_size, preferred_group_size, max_group_size)
351351
}
352352

353353
type Error = Error;

russh/src/tests.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,26 @@ mod channels {
643643
}
644644
}
645645

646+
mod gex {
647+
use super::*;
648+
649+
#[test]
650+
fn peer_request_accepts_rfc4419_minimum_when_server_can_choose_stronger_group() {
651+
let params = client::GexParams::from_peer_request(1024, 4097, 8192).unwrap();
652+
653+
assert_eq!(params.min_group_size(), 1024);
654+
assert_eq!(params.preferred_group_size(), 4097);
655+
assert_eq!(params.max_group_size(), 8192);
656+
}
657+
658+
#[test]
659+
fn local_client_config_still_rejects_minimum_below_2048() {
660+
let error = client::GexParams::for_client_config(1024, 4097, 8192).unwrap_err();
661+
662+
assert!(matches!(error, Error::InvalidConfig(_)));
663+
}
664+
}
665+
646666
mod server_kex_junk {
647667
use std::sync::Arc;
648668

russh/tests/test_kex_shared_secret.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,77 @@ async fn test_kex_done_with_ecdh_nistp256() {
187187
.unwrap();
188188
}
189189

190+
#[tokio::test]
191+
async fn test_kex_done_with_dh_gex_sha256_and_rfc4419_minimum() {
192+
let _ = env_logger::try_init();
193+
194+
let client_key = PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap();
195+
196+
let mut server_config = server::Config::default();
197+
server_config.inactivity_timeout = None;
198+
server_config.auth_rejection_time = std::time::Duration::from_secs(3);
199+
server_config
200+
.keys
201+
.push(PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap());
202+
server_config.preferred = {
203+
let mut p = Preferred::default();
204+
p.kex = Cow::Borrowed(&[kex::DH_GEX_SHA256]);
205+
p
206+
};
207+
let server_config = Arc::new(server_config);
208+
209+
let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
210+
let addr = socket.local_addr().unwrap();
211+
212+
tokio::spawn(async move {
213+
let (socket, _) = socket.accept().await.unwrap();
214+
server::run_stream(server_config, socket, TestServer {})
215+
.await
216+
.unwrap();
217+
});
218+
219+
let captured_secret: Arc<Mutex<Option<Vec<u8>>>> = Arc::new(Mutex::new(None));
220+
let captured_names: Arc<Mutex<Option<Names>>> = Arc::new(Mutex::new(None));
221+
222+
let mut client_config = client::Config::default();
223+
client_config.preferred = {
224+
let mut p = Preferred::default();
225+
p.kex = Cow::Borrowed(&[kex::DH_GEX_SHA256]);
226+
p
227+
};
228+
client_config.gex = client::GexParams::for_client_config(2048, 4097, 8192).unwrap();
229+
let client_config = Arc::new(client_config);
230+
231+
let client = TestClientWithKexCapture {
232+
shared_secret: captured_secret.clone(),
233+
negotiated_cipher: captured_names.clone(),
234+
};
235+
236+
let mut session = client::connect(client_config, addr, client).await.unwrap();
237+
238+
let authenticated = session
239+
.authenticate_publickey(
240+
std::env::var("USER").unwrap_or("user".to_owned()),
241+
PrivateKeyWithHashAlg::new(Arc::new(client_key), None),
242+
)
243+
.await
244+
.unwrap()
245+
.success();
246+
assert!(authenticated);
247+
248+
let secret = captured_secret.lock().unwrap();
249+
assert!(secret.is_some(), "Shared secret should be captured");
250+
assert!(!secret.as_ref().unwrap().is_empty());
251+
252+
let kex_alg = captured_names.lock().unwrap();
253+
assert_eq!(kex_alg.as_ref().unwrap().kex, kex::DH_GEX_SHA256);
254+
255+
session
256+
.disconnect(Disconnect::ByApplication, "", "")
257+
.await
258+
.unwrap();
259+
}
260+
190261
/// Test that kex_done is called on rekey
191262
#[tokio::test]
192263
async fn test_kex_done_on_rekey() {

0 commit comments

Comments
 (0)