Skip to content

Commit 1aa340a

Browse files
committed
Merge branch 'terrapin'
2 parents 5f93b89 + a355c62 commit 1aa340a

File tree

9 files changed

+186
-15
lines changed

9 files changed

+186
-15
lines changed

russh/src/client/encrypted.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
//
1515
use std::cell::RefCell;
1616
use std::convert::TryInto;
17+
use std::num::Wrapping;
1718

1819
use log::{debug, error, info, trace, warn};
1920
use russh_cryptovec::CryptoVec;
@@ -26,7 +27,8 @@ use crate::negotiation::{Named, Select};
2627
use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage};
2728
use crate::session::{Encrypted, EncryptedState, Kex, KexInit};
2829
use crate::{
29-
auth, msg, negotiation, Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, Sig,
30+
auth, msg, negotiation, strict_kex_violation, Channel, ChannelId, ChannelMsg,
31+
ChannelOpenFailure, ChannelParams, Sig,
3032
};
3133

3234
thread_local! {
@@ -37,6 +39,7 @@ impl Session {
3739
pub(crate) async fn client_read_encrypted<H: Handler>(
3840
mut self,
3941
mut client: H,
42+
seqn: &mut Wrapping<u32>,
4043
buf: &[u8],
4144
) -> Result<(H, Self), H::Error> {
4245
#[allow(clippy::indexing_slicing)] // length checked
@@ -65,6 +68,12 @@ impl Session {
6568
};
6669

6770
if let Some(kexinit) = kexinit {
71+
if let Some(ref algo) = kexinit.algo {
72+
if self.common.strict_kex && !algo.strict_kex {
73+
return Err(strict_kex_violation(msg::KEXINIT, 0).into());
74+
}
75+
}
76+
6877
let dhdone = kexinit.client_parse(
6978
self.common.config.as_ref(),
7079
&mut *self.common.cipher.local_to_remote,
@@ -100,6 +109,7 @@ impl Session {
100109
.local_to_remote
101110
.write(&[msg::NEWKEYS], &mut self.common.write_buffer);
102111
self.flush()?;
112+
self.common.maybe_reset_seqn();
103113
Ok((client, self))
104114
} else {
105115
error!("Wrong packet received");
@@ -125,6 +135,11 @@ impl Session {
125135
self.pending_len = 0;
126136
self.common.newkeys(newkeys);
127137
self.flush()?;
138+
139+
if self.common.strict_kex {
140+
*seqn = Wrapping(0);
141+
}
142+
128143
return Ok((client, self));
129144
}
130145
Some(Kex::Init(k)) => {

russh/src/client/mod.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
7878
use std::cell::RefCell;
7979
use std::collections::HashMap;
80+
use std::num::Wrapping;
8081
use std::pin::Pin;
8182
use std::sync::Arc;
8283

@@ -104,7 +105,8 @@ use crate::session::{CommonSession, EncryptedState, Exchange, Kex, KexDhDone, Ke
104105
use crate::ssh_read::SshRead;
105106
use crate::sshbuffer::{SSHBuffer, SshId};
106107
use crate::{
107-
auth, msg, negotiation, timeout, ChannelId, ChannelOpenFailure, Disconnect, Limits, Sig,
108+
auth, msg, negotiation, strict_kex_violation, timeout, ChannelId, ChannelOpenFailure,
109+
Disconnect, Limits, Sig,
108110
};
109111

110112
mod encrypted;
@@ -128,6 +130,8 @@ pub struct Session {
128130
inbound_channel_receiver: Receiver<Msg>,
129131
}
130132

133+
const STRICT_KEX_MSG_ORDER: &[u8] = &[msg::KEXINIT, msg::KEX_ECDH_REPLY, msg::NEWKEYS];
134+
131135
impl Drop for Session {
132136
fn drop(&mut self) {
133137
debug!("drop session")
@@ -693,6 +697,7 @@ where
693697
wants_reply: false,
694698
disconnected: false,
695699
buffer: CryptoVec::new(),
700+
strict_kex: false,
696701
},
697702
session_receiver,
698703
session_sender,
@@ -784,7 +789,7 @@ impl Session {
784789
self.send_keepalive(true);
785790
}
786791
r = &mut reading => {
787-
let (stream_read, buffer, mut opening_cipher) = match r {
792+
let (stream_read, mut buffer, mut opening_cipher) = match r {
788793
Ok((_, stream_read, buffer, opening_cipher)) => (stream_read, buffer, opening_cipher),
789794
Err(e) => return Err(e.into())
790795
};
@@ -813,8 +818,8 @@ impl Session {
813818
#[allow(clippy::indexing_slicing)] // length checked
814819
if buf[0] == crate::msg::DISCONNECT {
815820
break;
816-
} else if buf[0] > 4 {
817-
let (h, s) = reply(self, handler, &mut encrypted_signal, buf).await?;
821+
} else {
822+
let (h, s) = reply(self, handler, &mut encrypted_signal, &mut buffer.seqn, buf).await?;
818823
handler = h;
819824
self = s;
820825
}
@@ -1176,8 +1181,24 @@ async fn reply<H: Handler>(
11761181
mut session: Session,
11771182
mut handler: H,
11781183
sender: &mut Option<tokio::sync::oneshot::Sender<()>>,
1184+
seqn: &mut Wrapping<u32>,
11791185
buf: &[u8],
11801186
) -> Result<(H, Session), H::Error> {
1187+
if let Some(message_type) = buf.first() {
1188+
if session.common.strict_kex && session.common.encrypted.is_none() {
1189+
let seqno = seqn.0 - 1; // was incremented after read()
1190+
if let Some(expected) = STRICT_KEX_MSG_ORDER.get(seqno as usize) {
1191+
if message_type != expected {
1192+
return Err(strict_kex_violation(*message_type, seqno as usize).into());
1193+
}
1194+
}
1195+
}
1196+
1197+
if [msg::IGNORE, msg::UNIMPLEMENTED, msg::DEBUG].contains(message_type) {
1198+
return Ok((handler, session));
1199+
}
1200+
}
1201+
11811202
match session.common.kex.take() {
11821203
Some(Kex::Init(kexinit)) => {
11831204
if kexinit.algo.is_some()
@@ -1191,6 +1212,11 @@ async fn reply<H: Handler>(
11911212
&mut session.common.write_buffer,
11921213
)?;
11931214

1215+
// seqno has already been incremented after read()
1216+
if done.names.strict_kex && seqn.0 != 1 {
1217+
return Err(strict_kex_violation(msg::KEXINIT, seqn.0 as usize - 1).into());
1218+
}
1219+
11941220
if done.kex.skip_exchange() {
11951221
session.common.encrypted(
11961222
initial_encrypted_state(&session),
@@ -1216,13 +1242,15 @@ async fn reply<H: Handler>(
12161242
// We've sent ECDH_INIT, waiting for ECDH_REPLY
12171243
let (kex, h) = kexdhdone.server_key_check(false, handler, buf).await?;
12181244
handler = h;
1245+
session.common.strict_kex = session.common.strict_kex || kex.names.strict_kex;
12191246
session.common.kex = Some(Kex::Keys(kex));
12201247
session
12211248
.common
12221249
.cipher
12231250
.local_to_remote
12241251
.write(&[msg::NEWKEYS], &mut session.common.write_buffer);
12251252
session.flush()?;
1253+
session.common.maybe_reset_seqn();
12261254
Ok((handler, session))
12271255
} else {
12281256
error!("Wrong packet received");
@@ -1241,13 +1269,16 @@ async fn reply<H: Handler>(
12411269
.common
12421270
.encrypted(initial_encrypted_state(&session), newkeys);
12431271
// Ok, NEWKEYS received, now encrypted.
1272+
if session.common.strict_kex {
1273+
*seqn = Wrapping(0);
1274+
}
12441275
Ok((handler, session))
12451276
}
12461277
Some(kex) => {
12471278
session.common.kex = Some(kex);
12481279
Ok((handler, session))
12491280
}
1250-
None => session.client_read_encrypted(handler, buf).await,
1281+
None => session.client_read_encrypted(handler, seqn, buf).await,
12511282
}
12521283
}
12531284

russh/src/kex/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ pub const NONE: Name = Name("none");
9999
pub const EXTENSION_SUPPORT_AS_CLIENT: Name = Name("ext-info-c");
100100
/// `ext-info-s`
101101
pub const EXTENSION_SUPPORT_AS_SERVER: Name = Name("ext-info-s");
102+
/// `kex-strict-c-v00@openssh.com`
103+
pub const EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT: Name = Name("kex-strict-c-v00@openssh.com");
104+
/// `kex-strict-s-v00@openssh.com`
105+
pub const EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER: Name = Name("kex-strict-s-v00@openssh.com");
102106

103107
const _CURVE25519: Curve25519KexType = Curve25519KexType {};
104108
const _DH_G1_SHA1: DhGroup1Sha1KexType = DhGroup1Sha1KexType {};

russh/src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
9797
use std::fmt::{Debug, Display, Formatter};
9898

99+
use log::debug;
99100
use parsing::ChannelOpenConfirmation;
100101
pub use russh_cryptovec::CryptoVec;
101102
use thiserror::Error;
@@ -285,6 +286,23 @@ pub enum Error {
285286

286287
#[error(transparent)]
287288
Elapsed(#[from] tokio::time::error::Elapsed),
289+
290+
#[error("Violation detected during strict key exchange, message {message_type} at seq no {sequence_number}")]
291+
StrictKeyExchangeViolation {
292+
message_type: u8,
293+
sequence_number: usize,
294+
},
295+
}
296+
297+
pub(crate) fn strict_kex_violation(message_type: u8, sequence_number: usize) -> crate::Error {
298+
debug!(
299+
"strict kex violated at sequence no. {:?}, message type: {:?}",
300+
sequence_number, message_type
301+
);
302+
crate::Error::StrictKeyExchangeViolation {
303+
message_type,
304+
sequence_number,
305+
}
288306
}
289307

290308
#[derive(Debug, Error)]

russh/src/negotiation.rs

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use russh_keys::key::{KeyPair, PublicKey};
2323

2424
use crate::cipher::CIPHERS;
2525
use crate::compression::*;
26+
use crate::kex::{EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER};
2627
use crate::{cipher, kex, mac, msg, Error};
2728

2829
#[derive(Debug)]
@@ -35,6 +36,7 @@ pub struct Names {
3536
pub server_compression: Compression,
3637
pub client_compression: Compression,
3738
pub ignore_guessed: bool,
39+
pub strict_kex: bool,
3840
}
3941

4042
/// Lists of preferred algorithms. This is normally hard-coded into implementations.
@@ -56,6 +58,10 @@ const SAFE_KEX_ORDER: &[kex::Name] = &[
5658
kex::CURVE25519,
5759
kex::CURVE25519_PRE_RFC_8731,
5860
kex::DH_G14_SHA256,
61+
kex::EXTENSION_SUPPORT_AS_CLIENT,
62+
kex::EXTENSION_SUPPORT_AS_SERVER,
63+
kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT,
64+
kex::EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER,
5965
];
6066

6167
const CIPHER_ORDER: &[cipher::Name] = &[
@@ -143,7 +149,9 @@ impl Named for KeyPair {
143149
}
144150
}
145151

146-
pub trait Select {
152+
pub(crate) trait Select {
153+
fn is_server() -> bool;
154+
147155
fn select<S: AsRef<str> + Copy>(a: &[S], b: &[u8]) -> Option<(bool, S)>;
148156

149157
fn read_kex(buffer: &[u8], pref: &Preferred) -> Result<Names, Error> {
@@ -160,6 +168,24 @@ pub trait Select {
160168
return Err(Error::NoCommonKexAlgo);
161169
};
162170

171+
let strict_kex_requested = pref.kex.contains(if Self::is_server() {
172+
&EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER
173+
} else {
174+
&EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT
175+
});
176+
let strict_kex_provided = Self::select(
177+
&[if Self::is_server() {
178+
EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT
179+
} else {
180+
EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER
181+
}],
182+
kex_string,
183+
)
184+
.is_some();
185+
if strict_kex_requested && strict_kex_provided {
186+
debug!("strict kex enabled")
187+
}
188+
163189
let key_string = r.read_string()?;
164190
let (key_both_first, key_algorithm) = if let Some(x) = Self::select(pref.key, key_string) {
165191
x
@@ -238,6 +264,7 @@ pub trait Select {
238264
server_compression,
239265
// Ignore the next packet if (1) it follows and (2) it's not the correct guess.
240266
ignore_guessed: fol && !(kex_both_first && key_both_first),
267+
strict_kex: strict_kex_requested && strict_kex_provided,
241268
})
242269
}
243270
_ => Err(Error::KexInit),
@@ -249,6 +276,10 @@ pub struct Server;
249276
pub struct Client;
250277

251278
impl Select for Server {
279+
fn is_server() -> bool {
280+
true
281+
}
282+
252283
fn select<S: AsRef<str> + Copy>(server_list: &[S], client_list: &[u8]) -> Option<(bool, S)> {
253284
let mut both_first_choice = true;
254285
for c in client_list.split(|&x| x == b',') {
@@ -264,6 +295,10 @@ impl Select for Server {
264295
}
265296

266297
impl Select for Client {
298+
fn is_server() -> bool {
299+
false
300+
}
301+
267302
fn select<S: AsRef<str> + Copy>(client_list: &[S], server_list: &[u8]) -> Option<(bool, S)> {
268303
let mut both_first_choice = true;
269304
for &c in client_list {
@@ -287,11 +322,18 @@ pub fn write_kex(prefs: &Preferred, buf: &mut CryptoVec, as_server: bool) -> Res
287322

288323
buf.extend(&cookie); // cookie
289324
buf.extend_list(prefs.kex.iter().filter(|k| {
290-
**k != if as_server {
291-
crate::kex::EXTENSION_SUPPORT_AS_CLIENT
325+
!(if as_server {
326+
[
327+
crate::kex::EXTENSION_SUPPORT_AS_CLIENT,
328+
crate::kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT,
329+
]
292330
} else {
293-
crate::kex::EXTENSION_SUPPORT_AS_SERVER
294-
}
331+
[
332+
crate::kex::EXTENSION_SUPPORT_AS_SERVER,
333+
crate::kex::EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER,
334+
]
335+
})
336+
.contains(*k)
295337
})); // kex algo
296338

297339
buf.extend_list(prefs.key.iter());

russh/src/server/encrypted.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ impl Session {
3434
pub(crate) async fn server_read_encrypted<H: Handler + Send>(
3535
mut self,
3636
mut handler: H,
37+
seqn: &mut Wrapping<u32>,
3738
buf: &[u8],
3839
) -> Result<(H, Self), H::Error> {
3940
#[allow(clippy::indexing_slicing)] // length checked
@@ -70,6 +71,9 @@ impl Session {
7071
&mut self.common.write_buffer,
7172
)?);
7273
}
74+
if let Some(Kex::Dh(KexDh { ref names, .. })) = enc.rekey {
75+
self.common.strict_kex = self.common.strict_kex || names.strict_kex;
76+
}
7377
self.flush()?;
7478
return Ok((handler, self));
7579
}
@@ -82,6 +86,10 @@ impl Session {
8286
buf,
8387
&mut self.common.write_buffer,
8488
)?);
89+
if let Some(Kex::Keys(_)) = enc.rekey {
90+
// just sent NEWKEYS
91+
self.common.maybe_reset_seqn();
92+
}
8593
self.flush()?;
8694
return Ok((handler, self));
8795
}
@@ -103,11 +111,21 @@ impl Session {
103111
self.pending_reads = pending;
104112
self.pending_len = 0;
105113
self.common.newkeys(newkeys);
114+
if self.common.strict_kex {
115+
*seqn = Wrapping(0);
116+
}
106117
self.flush()?;
107118
return Ok((handler, self));
108119
}
109120
Some(Kex::Init(k)) => {
121+
if let Some(ref algo) = k.algo {
122+
if self.common.strict_kex && !algo.strict_kex {
123+
return Err(strict_kex_violation(msg::KEXINIT, 0).into());
124+
}
125+
}
126+
110127
enc.rekey = Some(Kex::Init(k));
128+
111129
self.pending_len += buf.len() as u32;
112130
if self.pending_len > 2 * self.target_window_size {
113131
return Err(Error::Pending.into());

0 commit comments

Comments
 (0)