Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#259, #245, ref #227 - fixed host key algo selection when Preferred::key and the available host keys don't match #262

Merged
merged 3 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@
"contributions": [
"code"
]
},
{
"login": "T0b1-iOS",
"name": "T0b1-iOS",
"avatar_url": "https://avatars.githubusercontent.com/u/15174814?v=4",
"profile": "https://github.com/T0b1-iOS",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Russh
[![Rust](https://github.com/warp-tech/russh/actions/workflows/rust.yml/badge.svg)](https://github.com/warp-tech/russh/actions/workflows/rust.yml) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

Low-level Tokio SSH2 client and server implementation.
Expand Down Expand Up @@ -109,6 +109,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="http://samlikes.pizza/"><img src="https://avatars.githubusercontent.com/u/226872?v=4?s=100" width="100px;" alt="Samuel Ainsworth"/><br /><sub><b>Samuel Ainsworth</b></sub></a><br /><a href="https://github.com/warp-tech/russh/commits?author=samuela" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Sherlock-Holo"><img src="https://avatars.githubusercontent.com/u/10096425?v=4?s=100" width="100px;" alt="Sherlock Holo"/><br /><sub><b>Sherlock Holo</b></sub></a><br /><a href="https://github.com/warp-tech/russh/commits?author=sherlock-holo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ricott1"><img src="https://avatars.githubusercontent.com/u/16502243?v=4?s=100" width="100px;" alt="Alessandro Ricottone"/><br /><sub><b>Alessandro Ricottone</b></sub></a><br /><a href="https://github.com/warp-tech/russh/commits?author=ricott1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/T0b1-iOS"><img src="https://avatars.githubusercontent.com/u/15174814?v=4?s=100" width="100px;" alt="T0b1-iOS"/><br /><sub><b>T0b1-iOS</b></sub></a><br /><a href="https://github.com/warp-tech/russh/commits?author=T0b1-iOS" title="Code">💻</a></td>
</tr>
</tbody>
</table>
Expand Down
6 changes: 5 additions & 1 deletion russh/src/client/encrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ impl Session {
} else if let Some(exchange) = enc.exchange.take() {
Some(KexInit::received_rekey(
exchange,
negotiation::Client::read_kex(buf, &self.common.config.as_ref().preferred)?,
negotiation::Client::read_kex(
buf,
&self.common.config.as_ref().preferred,
None,
)?,
&enc.session_id,
))
} else {
Expand Down
2 changes: 1 addition & 1 deletion russh/src/client/kex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ impl KexInit {
// read algorithms from packet.
debug!("extending {:?}", &self.exchange.server_kex_init[..]);
self.exchange.server_kex_init.extend(buf);
negotiation::Client::read_kex(buf, &config.preferred)?
negotiation::Client::read_kex(buf, &config.preferred, None)?
};
debug!("algo = {:?}", algo);
debug!("write = {:?}", &write_buffer.buffer[..]);
Expand Down
6 changes: 2 additions & 4 deletions russh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,8 @@
//! messages sent through a `server::Handle` are processed when there
//! is no incoming packet to read.

use std::{
convert::TryFrom,
fmt::{Debug, Display, Formatter},
};
use std::convert::TryFrom;
use std::fmt::{Debug, Display, Formatter};

use log::debug;
use parsing::ChannelOpenConfirmation;
Expand Down
109 changes: 67 additions & 42 deletions russh/src/negotiation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ pub struct Preferred {
pub compression: &'static [&'static str],
}

impl Preferred {
pub(crate) fn possible_host_key_algos_for_keys(
&self,
available_host_keys: &[KeyPair],
) -> Vec<&'static key::Name> {
self.key
.iter()
.filter(|n| available_host_keys.iter().any(|k| k.name() == n.0))
.collect::<Vec<_>>()
}
}

const SAFE_KEX_ORDER: &[kex::Name] = &[
kex::CURVE25519,
kex::CURVE25519_PRE_RFC_8731,
Expand Down Expand Up @@ -158,19 +170,28 @@ pub(crate) trait Select {

fn select<S: AsRef<str> + Copy>(a: &[S], b: &[u8]) -> Option<(bool, S)>;

fn read_kex(buffer: &[u8], pref: &Preferred) -> Result<Names, Error> {
/// `available_host_keys`, if present, is used to limit the host key algorithms to the ones we have keys for.
fn read_kex(
buffer: &[u8],
pref: &Preferred,
available_host_keys: Option<&[KeyPair]>,
) -> Result<Names, Error> {
let mut r = buffer.reader(17);

// Key exchange

let kex_string = r.read_string()?;
let (kex_both_first, kex_algorithm) = if let Some(x) = Self::select(pref.kex, kex_string) {
x
} else {
let (kex_both_first, kex_algorithm) = Self::select(pref.kex, kex_string).ok_or_else(||
{
debug!(
"Could not find common kex algorithm, other side only supports {:?}, we only support {:?}",
from_utf8(kex_string),
pref.kex
);
return Err(Error::NoCommonKexAlgo);
};
Error::NoCommonKexAlgo
})?;

// Strict kex detection

let strict_kex_requested = pref.kex.contains(if Self::is_server() {
&EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER
Expand All @@ -190,35 +211,42 @@ pub(crate) trait Select {
debug!("strict kex enabled")
}

let key_string = r.read_string()?;
let (key_both_first, key_algorithm) = if let Some(x) = Self::select(pref.key, key_string) {
x
} else {
debug!(
"Could not find common key algorithm, other side only supports {:?}, we only support {:?}",
from_utf8(key_string),
pref.key
);
return Err(Error::NoCommonKeyAlgo);
// Host key

let key_string: &[u8] = r.read_string()?;
let possible_host_key_algos = match available_host_keys {
Some(available_host_keys) => pref.possible_host_key_algos_for_keys(available_host_keys),
None => pref.key.iter().collect::<Vec<_>>(),
};

let (key_both_first, key_algorithm) =
Self::select(&possible_host_key_algos[..], key_string).ok_or_else(|| {
debug!(
"Could not find common key algorithm, other side only supports {:?}, we only support {:?}",
from_utf8(key_string),
pref.key
);
Error::NoCommonKeyAlgo
})?;

// Cipher

let cipher_string = r.read_string()?;
let cipher = Self::select(pref.cipher, cipher_string);
if cipher.is_none() {
debug!(
let (_cipher_both_first, cipher) =
Self::select(pref.cipher, cipher_string).ok_or_else(|| {
debug!(
"Could not find common cipher, other side only supports {:?}, we only support {:?}",
from_utf8(cipher_string),
pref.cipher
);
return Err(Error::NoCommonCipher);
}
Error::NoCommonCipher
})?;
r.read_string()?; // cipher server-to-client.
debug!("kex {}", line!());

let need_mac = cipher
.and_then(|x| CIPHERS.get(&x.1))
.map(|x| x.needs_mac())
.unwrap_or(false);
// MAC

let need_mac = CIPHERS.get(&cipher).map(|x| x.needs_mac()).unwrap_or(false);

let client_mac = if let Some((_, m)) = Self::select(pref.mac, r.read_string()?) {
m
Expand All @@ -235,6 +263,8 @@ pub(crate) trait Select {
mac::NONE
};

// Compression

debug!("kex {}", line!());
// client-to-server compression.
let client_compression =
Expand All @@ -256,23 +286,18 @@ pub(crate) trait Select {
r.read_string()?; // languages server-to-client

let follows = r.read_byte()? != 0;
match (cipher, follows) {
(Some((_, cipher)), fol) => {
Ok(Names {
kex: kex_algorithm,
key: key_algorithm,
cipher,
client_mac,
server_mac,
client_compression,
server_compression,
// Ignore the next packet if (1) it follows and (2) it's not the correct guess.
ignore_guessed: fol && !(kex_both_first && key_both_first),
strict_kex: strict_kex_requested && strict_kex_provided,
})
}
_ => Err(Error::KexInit),
}
Ok(Names {
kex: kex_algorithm,
key: *key_algorithm,
cipher,
client_mac,
server_mac,
client_compression,
server_compression,
// Ignore the next packet if (1) it follows and (2) it's not the correct guess.
ignore_guessed: follows && !(kex_both_first && key_both_first),
strict_kex: strict_kex_requested && strict_kex_provided,
})
}
}

Expand Down
6 changes: 5 additions & 1 deletion russh/src/server/encrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ impl Session {
} else if let Some(exchange) = enc.exchange.take() {
let kexinit = KexInit::received_rekey(
exchange,
negotiation::Server::read_kex(buf, &self.common.config.as_ref().preferred)?,
negotiation::Server::read_kex(
buf,
&self.common.config.as_ref().preferred,
Some(&self.common.config.as_ref().keys),
)?,
&enc.session_id,
);
enc.rekey = Some(kexinit.server_parse(
Expand Down
2 changes: 1 addition & 1 deletion russh/src/server/kex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl KexInit {
let algo = {
// read algorithms from packet.
self.exchange.client_kex_init.extend(buf);
super::negotiation::Server::read_kex(buf, &config.preferred)?
super::negotiation::Server::read_kex(buf, &config.preferred, Some(&config.keys))?
};
if !self.sent {
self.server_write(config, cipher, write_buffer)?
Expand Down
Loading