Skip to content

Commit

Permalink
DH GEX support (#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugeny authored Jan 6, 2025
1 parent e0bc545 commit c9baadf
Show file tree
Hide file tree
Showing 15 changed files with 665 additions and 200 deletions.
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

0 comments on commit c9baadf

Please sign in to comment.