diff options
Diffstat (limited to 'oxish/src/session')
| -rw-r--r-- | oxish/src/session/connections.rs | 14 | ||||
| -rw-r--r-- | oxish/src/session/mod.rs | 140 |
2 files changed, 151 insertions, 3 deletions
diff --git a/oxish/src/session/connections.rs b/oxish/src/session/connections.rs index 30013ce..e514a70 100644 --- a/oxish/src/session/connections.rs +++ b/oxish/src/session/connections.rs @@ -69,6 +69,7 @@ impl Channels { &mut self, request: ChannelRequest<'_>, encoder: &mut Encoder<'_>, + banner: Option<&str>, ) -> Result<(), Error> { let Some(channel) = self.channels.get_mut(&request.recipient_channel) else { return Err(ProtoError::InvalidPacket("channel request for unknown channel ID").into()); @@ -121,6 +122,19 @@ impl Channels { encoder.enqueue(&channel.success())?; } + let Some(banner) = banner else { + return Ok(()); + }; + + let Some(window) = channel.send_window.checked_sub(banner.len() as u32) else { + return Ok(()); + }; + + channel.send_window = window; + encoder.enqueue(&ChannelData { + recipient_channel: channel.remote_id, + data: Cow::Borrowed(banner.as_bytes()), + })?; Ok(()) } diff --git a/oxish/src/session/mod.rs b/oxish/src/session/mod.rs index 44b4f95..ae4a45c 100644 --- a/oxish/src/session/mod.rs +++ b/oxish/src/session/mod.rs @@ -1,4 +1,9 @@ -use core::{cmp::Ordering, future, mem::MaybeUninit}; +use core::{ + cmp::Ordering, + future, + mem::MaybeUninit, + str::{self, FromStr}, +}; use std::{ io::{self, IoSliceMut}, os::fd::AsFd, @@ -6,6 +11,7 @@ use std::{ use proto::{ Decoded, Disconnect, Encoder, MessageType, Pretty, ReadState, WriteState, + channels::{ChannelRequest, ChannelRequestType}, crypto::CryptoProvider, key_exchange::{Rekey, SessionHostKey}, }; @@ -30,6 +36,7 @@ pub struct Session<T> { conn: Connection<T>, rekey: Rekey, channels: Channels, + post_quantum_kx: bool, } impl Session<TcpStream> { @@ -120,6 +127,7 @@ impl Session<TcpStream> { addr, host_key, identities, + post_quantum_kx, strict_kx, session_id, read, @@ -152,16 +160,18 @@ impl Session<TcpStream> { }, rekey: Rekey::new(session_id, strict_kx, identities, host_key), channels: Channels::default(), + post_quantum_kx, }) } } impl<T: AsyncRead + AsyncWrite + Unpin> Session<T> { - pub(crate) fn new(conn: Connection<T>, rekey: Rekey) -> Self { + pub(crate) fn new(conn: Connection<T>, rekey: Rekey, post_quantum_kx: bool) -> Self { Self { conn, channels: Channels::default(), rekey, + post_quantum_kx, } } @@ -190,7 +200,9 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Session<T> { // key exchange init (RFC 4253 section 9). MessageType::KeyExchangeInit => { let kx = self.rekey.start(packet, provider)?; + let post_quantum_kx = kx.negotiated.key_exchange.post_quantum_secure(); self.conn.rekey(kx, &self.rekey, provider).await?; + self.post_quantum_kx = post_quantum_kx; continue; } _ => {} @@ -201,7 +213,10 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Session<T> { let mut encoder = Encoder::new(&mut self.conn.write); match channel_message { IncomingChannelMessage::Open(open) => self.channels.open(open, &mut encoder), - IncomingChannelMessage::Request(request) => self.channels.request(request, &mut encoder), + IncomingChannelMessage::Request(request) => { + let banner = banner(&request, self.rekey.client_identity(), self.post_quantum_kx); + self.channels.request(request, &mut encoder, banner.as_deref()) + } IncomingChannelMessage::Data(data) => match self.channels.data(&data, &mut encoder) { Ok(Some((session, data))) => match session.write(data).await { Ok(_) => Ok(()), @@ -232,3 +247,122 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Session<T> { } } } + +fn banner( + request: &ChannelRequest<'_>, + client_identity: &[u8], + post_quantum_kx: bool, +) -> Option<String> { + if post_quantum_kx { + return None; + } + + let width = match &request.r#type { + // A zero dimension means the client left it unspecified (RFC 4254 section 6.2) + ChannelRequestType::PtyReq(pty) => match pty.cols { + 0 => 80, + cols => Ord::max(40, cols as usize), + }, + _ => return None, + }; + + let mut banner = String::with_capacity(PREFIX.len() + NO_PQ_WARNING.len()); + banner.push_str(PREFIX); + let mut left = width.saturating_sub(PREFIX.len()); + for token in NO_PQ_WARNING.split(' ') { + if token.len() + 1 >= left { + banner.push_str("\r\n"); + banner.push_str(PREFIX); + left = width.saturating_sub(PREFIX.len()); + } + + banner.push_str(token); + banner.push(' '); + left = left.saturating_sub(token.len() + 1); + } + + banner.push_str("\r\n"); + let Some(version) = client_identity.strip_prefix(b"SSH-2.0-OpenSSH_") else { + return Some(banner); + }; + + let Ok(version) = str::from_utf8(version) else { + return Some(banner); + }; + + let Some((major, minor)) = version.split_once('.') else { + return Some(banner); + }; + + let minor = match minor.split_once(|c: char| !c.is_ascii_digit()) { + Some((minor, _)) => minor, + None => minor, + }; + + let (Ok(major), Ok(minor)) = (u8::from_str(major), u8::from_str(minor)) else { + return Some(banner); + }; + + if (major, minor) < (9, 9) { + banner.push_str(PREFIX); + banner.push_str(NO_PQ_WARNING_OPENSSH); + banner.push_str("\r\n"); + } + + Some(banner) +} + +const PREFIX: &str = "WARNING: "; +const NO_PQ_WARNING: &str = "the client negotiated a key exchange algorithm that is not post-quantum secure; your session may be decrypted by a cryptographically relevant quantum computer in the future"; +const NO_PQ_WARNING_OPENSSH: &str = + "consider upgrading your client version to OpenSSH 9.9 or newer"; + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + use std::collections::BTreeMap; + + use proto::channels::PtyReq; + + use super::*; + + #[test] + fn banner_at_80_columns() { + let banner = banner(&pty_request(80), b"SSH-2.0-OpenSSH_10.0", false).unwrap(); + assert_eq!( + banner, + "WARNING: the client negotiated a key exchange algorithm that is not \r\n\ + WARNING: post-quantum secure; your session may be decrypted by a \r\n\ + WARNING: cryptographically relevant quantum computer in the future \r\n" + ); + assert!(banner.lines().all(|line| line.len() <= 80)); + } + + #[test] + fn banner_at_80_columns_with_openssh_warning() { + let banner = banner(&pty_request(80), b"SSH-2.0-OpenSSH_9.8p1", false).unwrap(); + assert_eq!( + banner, + "WARNING: the client negotiated a key exchange algorithm that is not \r\n\ + WARNING: post-quantum secure; your session may be decrypted by a \r\n\ + WARNING: cryptographically relevant quantum computer in the future \r\n\ + WARNING: consider upgrading your client version to OpenSSH 9.9 or newer\r\n" + ); + assert!(banner.lines().all(|line| line.len() <= 80)); + } + + fn pty_request(cols: u32) -> ChannelRequest<'static> { + ChannelRequest { + recipient_channel: 0, + r#type: ChannelRequestType::PtyReq(PtyReq { + term: Cow::Borrowed("xterm-256color"), + cols, + rows: 24, + width_px: 0, + height_px: 0, + terminal_modes: BTreeMap::new(), + }), + want_reply: true, + } + } +} |
