aboutsummaryrefslogtreecommitdiffhomepage
path: root/oxish/src/session
diff options
context:
space:
mode:
Diffstat (limited to 'oxish/src/session')
-rw-r--r--oxish/src/session/connections.rs14
-rw-r--r--oxish/src/session/mod.rs140
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,
+ }
+ }
+}