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

DH GEX support #440

Merged
merged 6 commits into from
Jan 6, 2025
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ signature = "2.2"
ssh-encoding = { version = "0.2", features = [
"bytes",
] }
ssh-key = { version = "=0.6.8+upstream-0.6.7", features = [
ssh-key = { version = "=0.6.8", features = [
"ed25519",
"rsa",
"rsa-sha1",
Expand Down
5 changes: 5 additions & 0 deletions russh/examples/client_exec_simple.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
///
/// Run this example with:
/// cargo run --example client_exec_simple -- -k <private key path> <host> <command>
Expand Down Expand Up @@ -84,6 +85,10 @@ impl Session {
let key_pair = load_secret_key(key_path, None)?;
let config = client::Config {
inactivity_timeout: Some(Duration::from_secs(5)),
preferred: Preferred {
kex: Cow::Owned(vec![russh::kex::DH_GEX_SHA256]),
..Default::default()
},
..<_>::default()
};

Expand Down
2 changes: 1 addition & 1 deletion russh/examples/echoserver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async fn main() {
russh_keys::PrivateKey::random(&mut OsRng, russh_keys::Algorithm::Ed25519).unwrap(),
],
preferred: Preferred {
// key: Cow::Borrowed(&[CERT_ECDSA_SHA2_P256]),
// kex: std::borrow::Cow::Owned(vec![russh::kex::DH_GEX_SHA256]),
..Preferred::default()
},
..Default::default()
Expand Down
107 changes: 88 additions & 19 deletions russh/src/client/kex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ use std::fmt::{Debug, Formatter};
use std::sync::Arc;

use bytes::Bytes;
use log::{debug, error};
use log::{debug, error, warn};
use russh_cryptovec::CryptoVec;
use russh_keys::key::parse_public_key;
use signature::Verifier;
use ssh_encoding::{Decode, Encode};
use ssh_key::{PublicKey, Signature};
use ssh_key::{Mpint, PublicKey, Signature};

use super::IncomingSshPacket;
use crate::client::{Config, NewKeys};
use crate::kex::{Kex, KexAlgorithm, KexAlgorithmImplementor, KexCause, KexProgress, KEXES};
use crate::kex::dh::groups::DhGroup;
use crate::kex::{KexAlgorithm, KexAlgorithmImplementor, KexCause, KexProgress, KEXES};
use crate::negotiation::{Names, Select};
use crate::session::Exchange;
use crate::sshbuffer::PacketWriter;
Expand All @@ -27,7 +28,11 @@ thread_local! {
#[allow(clippy::large_enum_variant)]
enum ClientKexState {
Created,
WaitingForKexReply {
WaitingForGexReply {
names: Names,
kex: KexAlgorithm,
},
WaitingForDhReply {
// both KexInit and DH init sent
names: Names,
kex: KexAlgorithm,
Expand All @@ -53,8 +58,11 @@ impl Debug for ClientKex {
ClientKexState::Created => {
s.field("state", &"created");
}
ClientKexState::WaitingForKexReply { .. } => {
s.field("state", &"waiting for a reply");
ClientKexState::WaitingForGexReply { .. } => {
s.field("state", &"waiting for GEX response");
}
ClientKexState::WaitingForDhReply { .. } => {
s.field("state", &"waiting for DH response");
}
ClientKexState::WaitingForNewKeys { .. } => {
s.field("state", &"waiting for NEWKEYS");
Expand All @@ -79,17 +87,15 @@ impl ClientKex {
state: ClientKexState::Created,
}
}
}

impl Kex for ClientKex {
fn kexinit(&mut self, output: &mut PacketWriter) -> Result<(), Error> {
pub fn kexinit(&mut self, output: &mut PacketWriter) -> Result<(), Error> {
self.exchange.client_kex_init =
negotiation::write_kex(&self.config.preferred, output, None)?;

Ok(())
}

fn step(
pub fn step(
mut self,
input: Option<&mut IncomingSshPacket>,
output: &mut PacketWriter,
Expand Down Expand Up @@ -126,11 +132,6 @@ impl Kex for ClientKex {

let mut kex = KEXES.get(&names.kex).ok_or(Error::UnknownAlgo)?.make();

output.packet(|w| {
kex.client_dh(&mut self.exchange.client_ephemeral, w)?;
Ok(())
})?;

if kex.skip_exchange() {
// Non-standard no-kex exchange
let newkeys = compute_keys(
Expand All @@ -152,14 +153,77 @@ impl Kex for ClientKex {
});
}

self.state = ClientKexState::WaitingForKexReply { names, kex };
if kex.is_dh_gex() {
output.packet(|w| {
kex.client_dh_gex_init(&self.config.gex, w)?;
Ok(())
})?;

self.state = ClientKexState::WaitingForGexReply { names, kex };
} else {
output.packet(|w| {
kex.client_dh(&mut self.exchange.client_ephemeral, w)?;
Ok(())
})?;

self.state = ClientKexState::WaitingForDhReply { names, kex };
}

Ok(KexProgress::NeedsReply {
kex: self,
reset_seqn: false,
})
}
ClientKexState::WaitingForGexReply { names, mut kex } => {
let Some(input) = input else {
return Err(Error::KexInit);
};

if input.buffer.first() != Some(&msg::KEX_DH_GEX_GROUP) {
error!(
"Unexpected kex message at this stage: {:?}",
input.buffer.first()
);
return Err(Error::KexInit);
}

#[allow(clippy::indexing_slicing)] // length checked
let mut r = &input.buffer[1..];

let prime = Mpint::decode(&mut r)?;
let gen = Mpint::decode(&mut r)?;
debug!("received gex group: prime={}, gen={}", prime, gen);

let group = DhGroup {
prime: prime.as_bytes().to_vec().into(),
generator: gen.as_bytes().to_vec().into(),
};

if group.bit_size() < self.config.gex.min_group_size
|| group.bit_size() > self.config.gex.max_group_size
{
warn!(
"DH prime size ({} bits) not within requested range",
group.bit_size()
);
return Err(Error::KexInit);
}

let exchange = &mut self.exchange;
exchange.gex = Some((self.config.gex.clone(), group.clone()));
kex.dh_gex_set_group(group)?;
output.packet(|w| {
kex.client_dh(&mut exchange.client_ephemeral, w)?;
Ok(())
})?;
self.state = ClientKexState::WaitingForDhReply { names, kex };

Ok(KexProgress::NeedsReply {
kex: self,
reset_seqn: false,
})
}
ClientKexState::WaitingForKexReply { mut names, mut kex } => {
ClientKexState::WaitingForDhReply { mut names, mut kex } => {
// At this point, we've sent ECDH_INTI and
// are waiting for the ECDH_REPLY from the server.

Expand All @@ -171,14 +235,19 @@ impl Kex for ClientKex {
// Ignore the next packet if (1) it follows and (2) it's not the correct guess.
debug!("ignoring guessed kex");
names.ignore_guessed = false;
self.state = ClientKexState::WaitingForKexReply { names, kex };
self.state = ClientKexState::WaitingForDhReply { names, kex };
return Ok(KexProgress::NeedsReply {
kex: self,
reset_seqn: false,
});
}

if input.buffer.first() != Some(&msg::KEX_ECDH_REPLY) {
if input.buffer.first()
!= Some(match kex.is_dh_gex() {
true => &msg::KEX_DH_GEX_REPLY,
false => &msg::KEX_ECDH_REPLY,
})
{
error!(
"Unexpected kex message at this stage: {:?}",
input.buffer.first()
Expand Down
85 changes: 76 additions & 9 deletions russh/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ use tokio::sync::oneshot;

use crate::channels::{Channel, ChannelMsg, ChannelRef, WindowSizeRef};
use crate::cipher::{self, clear, OpeningKey};
use crate::kex::{Kex, KexCause, KexProgress, SessionKexState};
use crate::msg::{is_kex_msg, STRICT_KEX_MSG_ORDER};
use crate::kex::{KexCause, KexProgress, SessionKexState};
use crate::msg::{is_kex_msg, validate_server_msg_strict_kex};
use crate::session::{CommonSession, EncryptedState, GlobalRequestResponse, NewKeys};
use crate::ssh_read::SshRead;
use crate::sshbuffer::{IncomingSshPacket, PacketWriter, SSHBuffer, SshId};
use crate::{
auth, msg, negotiation, strict_kex_violation, ChannelId, ChannelOpenFailure, CryptoVec,
Disconnect, Error, Limits, Sig,
auth, msg, negotiation, ChannelId, ChannelOpenFailure, CryptoVec, Disconnect, Error, Limits,
Sig,
};

mod encrypted;
Expand Down Expand Up @@ -1273,11 +1273,7 @@ async fn reply<H: Handler>(
);
if session.common.strict_kex && session.common.encrypted.is_none() {
let seqno = pkt.seqn.0 - 1; // was incremented after read()
if let Some(expected) = STRICT_KEX_MSG_ORDER.get(seqno as usize) {
if message_type != expected {
return Err(strict_kex_violation(*message_type, seqno as usize).into());
}
}
validate_server_msg_strict_kex(*message_type, seqno as usize)?;
}

if [msg::IGNORE, msg::UNIMPLEMENTED, msg::DEBUG].contains(message_type) {
Expand Down Expand Up @@ -1375,6 +1371,74 @@ fn initial_encrypted_state(session: &Session) -> EncryptedState {
}
}

/// Parameters for dynamic group Diffie-Hellman key exchanges.
#[derive(Debug, Clone)]
pub struct GexParams {
/// Minimum DH group size (in bits)
min_group_size: usize,
/// Preferred DH group size (in bits)
preferred_group_size: usize,
/// Maximum DH group size (in bits)
max_group_size: usize,
}

impl GexParams {
pub fn new(
min_group_size: usize,
preferred_group_size: usize,
max_group_size: usize,
) -> Result<Self, Error> {
let this = Self {
min_group_size,
preferred_group_size,
max_group_size,
};
this.validate()?;
Ok(this)
}

pub(crate) fn validate(&self) -> Result<(), Error> {
if self.min_group_size < 2048 {
return Err(Error::InvalidConfig(
"min_group_size must be at least 2048 bits".into(),
));
}
if self.preferred_group_size < self.min_group_size {
return Err(Error::InvalidConfig(
"preferred_group_size must be at least as large as min_group_size".into(),
));
}
if self.max_group_size < self.preferred_group_size {
return Err(Error::InvalidConfig(
"max_group_size must be at least as large as preferred_group_size".into(),
));
}
Ok(())
}

pub fn min_group_size(&self) -> usize {
self.min_group_size
}

pub fn preferred_group_size(&self) -> usize {
self.preferred_group_size
}

pub fn max_group_size(&self) -> usize {
self.max_group_size
}
}

impl Default for GexParams {
fn default() -> GexParams {
GexParams {
min_group_size: 3072,
preferred_group_size: 8192,
max_group_size: 8192,
}
}
}

/// The configuration of clients.
#[derive(Debug)]
pub struct Config {
Expand All @@ -1398,6 +1462,8 @@ pub struct Config {
pub keepalive_max: usize,
/// Whether to expect and wait for an authentication call.
pub anonymous: bool,
/// DH dynamic group exchange parameters.
pub gex: GexParams,
}

impl Default for Config {
Expand All @@ -1417,6 +1483,7 @@ impl Default for Config {
keepalive_interval: None,
keepalive_max: 3,
anonymous: false,
gex: Default::default(),
}
}
}
Expand Down
Loading
Loading