Skip to content

Commit 2989bbe

Browse files
committed
fix: strict kex sequence number check should only apply to initial exchange
The strict key exchange protocol (draft-ietf-sshm-strict-kex) enforces that the first message after the version exchange must be SSH_MSG_KEXINIT with sequence number 1. However, this requirement only applies to the *initial* key exchange, not to subsequent rekeying operations. During rekeying, sequence numbers continue from their current value and are only reset *after* the SSH_MSG_NEWKEYS message, not before the KEXINIT. The server-side kex code was incorrectly enforcing seqno == 1 for *all* strict kex exchanges, including rekeys. This caused connection failures when clients attempted to rekey after 60 minutes or when rekey limits were reached. The client-side code was already correctly checking `!self.cause.is_rekey()` to skip sequence number validation during rekeying. This fix applies the same logic to the server side. Fixes: warp-tech/warpgate#1523
1 parent ed78d80 commit 2989bbe

3 files changed

Lines changed: 171 additions & 5 deletions

File tree

russh/src/client/kex.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,7 @@ impl ClientKex {
127127
debug!("negotiated algorithms: {names:?}");
128128

129129
// seqno has already been incremented after read()
130-
if (names.strict_kex() || self.cause.is_strict_rekey())
131-
&& !self.cause.is_rekey()
132-
&& input.seqn.0 != 1
133-
{
130+
if names.strict_kex() && !self.cause.is_rekey() && input.seqn.0 != 1 {
134131
return Err(strict_kex_violation(
135132
msg::KEXINIT,
136133
input.seqn.0 as usize - 1,

russh/src/server/kex.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ impl ServerKex {
120120
debug!("negotiated: {names:?}");
121121

122122
// seqno has already been incremented after read()
123-
if names.strict_kex() && input.seqn.0 != 1 {
123+
if names.strict_kex() && !self.cause.is_rekey() && input.seqn.0 != 1 {
124124
return Err(strict_kex_violation(
125125
msg::KEXINIT,
126126
input.seqn.0 as usize - 1,
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2+
3+
//! Test that strict key exchange is works during initial kex and rekey
4+
//! kex. This test ensures that strict_kex sequence number checking is
5+
//! only applied to the initial key exchange, not to rekey operations.
6+
7+
use std::borrow::Cow;
8+
use std::sync::Arc;
9+
10+
use russh::keys::PrivateKeyWithHashAlg;
11+
use russh::*;
12+
use ssh_key::PrivateKey;
13+
14+
#[tokio::test]
15+
async fn test_rekey_with_strict_kex() {
16+
let _ = env_logger::try_init();
17+
18+
// Generate keys
19+
let client_key = PrivateKey::random(&mut rand_core::OsRng, ssh_key::Algorithm::Ed25519)
20+
.unwrap();
21+
22+
// Server config with strict kex enabled
23+
let mut server_config = server::Config::default();
24+
server_config.inactivity_timeout = None;
25+
server_config.auth_rejection_time = std::time::Duration::from_secs(3);
26+
server_config
27+
.keys
28+
.push(PrivateKey::random(&mut rand_core::OsRng, ssh_key::Algorithm::Ed25519).unwrap());
29+
30+
// Enable strict kex by including the strict kex extension
31+
server_config.preferred = {
32+
let mut p = Preferred::default();
33+
// Include the strict kex extension marker for server
34+
p.kex = Cow::Borrowed(&[
35+
kex::CURVE25519,
36+
kex::EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER,
37+
]);
38+
p
39+
};
40+
41+
let server_config = Arc::new(server_config);
42+
43+
// Setup server
44+
let socket = tokio::net::TcpListener::bind("127.0.0.1:0")
45+
.await
46+
.unwrap();
47+
let addr = socket.local_addr().unwrap();
48+
49+
tokio::spawn(async move {
50+
let (socket, _) = socket.accept().await.unwrap();
51+
server::run_stream(server_config, socket, TestServer {})
52+
.await
53+
.unwrap();
54+
});
55+
56+
// Client config with strict kex enabled
57+
let mut client_config = client::Config::default();
58+
client_config.preferred = {
59+
let mut p = Preferred::default();
60+
// Include the strict kex extension marker for client
61+
p.kex = Cow::Borrowed(&[
62+
kex::CURVE25519,
63+
kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT,
64+
]);
65+
p
66+
};
67+
let client_config = Arc::new(client_config);
68+
69+
// Connect and authenticate
70+
let mut session = client::connect(client_config, addr, TestClient {})
71+
.await
72+
.unwrap();
73+
74+
let authenticated = session
75+
.authenticate_publickey(
76+
std::env::var("USER").unwrap_or("user".to_owned()),
77+
PrivateKeyWithHashAlg::new(Arc::new(client_key), None),
78+
)
79+
.await
80+
.unwrap()
81+
.success();
82+
assert!(authenticated);
83+
84+
// Open a channel and send some data
85+
let mut channel = session.channel_open_session().await.unwrap();
86+
channel.data(&b"before rekey"[..]).await.unwrap();
87+
88+
// Wait for response
89+
let msg = channel.wait().await.unwrap();
90+
match msg {
91+
ChannelMsg::Data { data } => {
92+
assert_eq!(&*data, b"before rekey");
93+
}
94+
msg => panic!("Unexpected message before rekey: {:?}", msg),
95+
}
96+
97+
session.rekey_soon().await.unwrap();
98+
99+
// Give rekey time to complete
100+
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
101+
102+
// Send data after rekey to ensure connection still works
103+
// If the rekey failed due to strict_kex violation, this would fail
104+
channel.data(&b"after rekey"[..]).await.unwrap();
105+
106+
let msg = channel.wait().await.unwrap();
107+
match msg {
108+
ChannelMsg::Data { data } => {
109+
assert_eq!(&*data, b"after rekey");
110+
}
111+
msg => panic!("Unexpected message after rekey: {:?}", msg),
112+
}
113+
114+
// Close the channel
115+
channel.eof().await.unwrap();
116+
session
117+
.disconnect(Disconnect::ByApplication, "", "")
118+
.await
119+
.unwrap();
120+
}
121+
122+
#[derive(Clone)]
123+
struct TestServer {}
124+
125+
// Insecure server that accepts any public key and echos back data it receives; ONLY FOR TESTS
126+
impl server::Handler for TestServer {
127+
type Error = russh::Error;
128+
129+
async fn auth_publickey(
130+
&mut self,
131+
_user: &str,
132+
_public_key: &ssh_key::PublicKey,
133+
) -> Result<server::Auth, Self::Error> {
134+
Ok(server::Auth::Accept)
135+
}
136+
137+
async fn channel_open_session(
138+
&mut self,
139+
_channel: Channel<server::Msg>,
140+
_session: &mut server::Session,
141+
) -> Result<bool, Self::Error> {
142+
Ok(true)
143+
}
144+
145+
async fn data(
146+
&mut self,
147+
channel: ChannelId,
148+
data: &[u8],
149+
session: &mut server::Session,
150+
) -> Result<(), Self::Error> {
151+
// Echo back the data
152+
session.data(channel, CryptoVec::from_slice(data))?;
153+
Ok(())
154+
}
155+
}
156+
157+
struct TestClient {}
158+
159+
// Insecure client that accept any server key; ONLY FOR TEST
160+
impl client::Handler for TestClient {
161+
type Error = russh::Error;
162+
163+
async fn check_server_key(
164+
&mut self,
165+
_server_public_key: &ssh_key::PublicKey,
166+
) -> Result<bool, Self::Error> {
167+
Ok(true)
168+
}
169+
}

0 commit comments

Comments
 (0)