Skip to content

Commit

Permalink
ecdh-sha2-nistp{256,384,521} kex support (#282)
Browse files Browse the repository at this point in the history
Adds support for ecdh-sha2-nistp{256,384,521} key exchange algorithms
using the
[elliptic-curve](https://docs.rs/elliptic-curve/latest/elliptic_curve/index.html),
[p256](https://docs.rs/p256/latest/p256/index.html),
[p384](https://docs.rs/p384/latest/p384/), and
[p521](https://docs.rs/p521/latest/p521/) crates.

Intentionally avoids adding these to the preferred Kex list as the
security of these curves is considered controversial. Users would need
to explicitly use the kex via config

Resolves #210

---------

Co-authored-by: Eugene <[email protected]>
  • Loading branch information
gleason-m and Eugeny committed May 10, 2024
1 parent 0fc65ea commit 3bfd99f
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 0 deletions.
5 changes: 5 additions & 0 deletions russh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ chacha20 = "0.9"
ctr = "0.9"
curve25519-dalek = "4.1.2"
digest = { workspace = true }
elliptic-curve = { version = "0.13", features = ["ecdh"] }
flate2 = { version = "1.0", optional = true }
futures = { workspace = true }
generic-array = "0.14"
Expand All @@ -36,8 +37,12 @@ log = { workspace = true }
num-bigint = { version = "0.4", features = ["rand"] }
once_cell = "1.13"
openssl = { workspace = true, optional = true }
p256 = { version = "0.13", features = ["ecdh"] }
p384 = { version = "0.13", features = ["ecdh"] }
p521 = { version = "0.13", features = ["ecdh"] }
poly1305 = "0.8"
rand = { workspace = true }
rand_core = { version = "0.6.4", features = ["getrandom"] }
russh-cryptovec = { version = "0.7.0", path = "../cryptovec" }
russh-keys = { version = "0.44.0-beta.1", path = "../russh-keys" }
sha1 = { workspace = true }
Expand Down
227 changes: 227 additions & 0 deletions russh/src/kex/ecdh_nistp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use byteorder::{BigEndian, ByteOrder};
use elliptic_curve::ecdh::{EphemeralSecret, SharedSecret};
use elliptic_curve::point::PointCompression;
use elliptic_curve::sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint};
use elliptic_curve::{AffinePoint, Curve, CurveArithmetic, FieldBytesSize};
use log::debug;
use p256::NistP256;
use p384::NistP384;
use p521::NistP521;
use russh_cryptovec::CryptoVec;
use russh_keys::encoding::Encoding;

use crate::kex::{compute_keys, KexAlgorithm, KexType};
use crate::mac::{self};
use crate::session::Exchange;
use crate::{cipher, msg};

pub struct EcdhNistP256KexType {}

impl KexType for EcdhNistP256KexType {
fn make(&self) -> Box<dyn KexAlgorithm + Send> {
Box::new(EcdhNistPKex::<NistP256> {
local_secret: None,
shared_secret: None,
}) as Box<dyn KexAlgorithm + Send>
}
}

pub struct EcdhNistP384KexType {}

impl KexType for EcdhNistP384KexType {
fn make(&self) -> Box<dyn KexAlgorithm + Send> {
Box::new(EcdhNistPKex::<NistP384> {
local_secret: None,
shared_secret: None,
}) as Box<dyn KexAlgorithm + Send>
}
}

pub struct EcdhNistP521KexType {}

impl KexType for EcdhNistP521KexType {
fn make(&self) -> Box<dyn KexAlgorithm + Send> {
Box::new(EcdhNistPKex::<NistP521> {
local_secret: None,
shared_secret: None,
}) as Box<dyn KexAlgorithm + Send>
}
}

#[doc(hidden)]
pub struct EcdhNistPKex<C: Curve + CurveArithmetic> {
local_secret: Option<EphemeralSecret<C>>,
shared_secret: Option<SharedSecret<C>>,
}

impl<C: Curve + CurveArithmetic> std::fmt::Debug for EcdhNistPKex<C> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Algorithm {{ local_secret: [hidden], shared_secret: [hidden] }}",
)
}
}

impl<C: Curve + CurveArithmetic> KexAlgorithm for EcdhNistPKex<C>
where
C: PointCompression,
FieldBytesSize<C>: ModulusSize,
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
{
fn skip_exchange(&self) -> bool {
false
}

#[doc(hidden)]
fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), crate::Error> {
debug!("server_dh");

let client_pubkey = {
if payload.first() != Some(&msg::KEX_ECDH_INIT) {
return Err(crate::Error::Inconsistent);
}

#[allow(clippy::indexing_slicing)] // length checked
let pubkey_len = BigEndian::read_u32(&payload[1..]) as usize;

if payload.len() < 5 + pubkey_len {
return Err(crate::Error::Inconsistent);
}

#[allow(clippy::indexing_slicing)] // length checked
elliptic_curve::PublicKey::<C>::from_sec1_bytes(&payload[5..(5 + pubkey_len)])
.map_err(|_| crate::Error::Inconsistent)?
};

let server_secret =
elliptic_curve::ecdh::EphemeralSecret::<C>::random(&mut rand_core::OsRng);
let server_pubkey = server_secret.public_key();

// fill exchange.
exchange.server_ephemeral.clear();
exchange
.server_ephemeral
.extend(&server_pubkey.to_sec1_bytes());
let shared = server_secret.diffie_hellman(&client_pubkey);
self.shared_secret = Some(shared);
Ok(())
}

#[doc(hidden)]
fn client_dh(
&mut self,
client_ephemeral: &mut CryptoVec,
buf: &mut CryptoVec,
) -> Result<(), crate::Error> {
let client_secret =
elliptic_curve::ecdh::EphemeralSecret::<C>::random(&mut rand_core::OsRng);
let client_pubkey = client_secret.public_key();

// fill exchange.
client_ephemeral.clear();
client_ephemeral.extend(&client_pubkey.to_sec1_bytes());

buf.push(msg::KEX_ECDH_INIT);
buf.extend_ssh_string(&client_pubkey.to_sec1_bytes());

self.local_secret = Some(client_secret);
Ok(())
}

fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), crate::Error> {
let local_secret = self.local_secret.take().ok_or(crate::Error::KexInit)?;
let pubkey = elliptic_curve::PublicKey::<C>::from_sec1_bytes(remote_pubkey_)
.map_err(|_| crate::Error::KexInit)?;
self.shared_secret = Some(local_secret.diffie_hellman(&pubkey));
Ok(())
}

fn compute_exchange_hash(
&self,
key: &CryptoVec,
exchange: &Exchange,
buffer: &mut CryptoVec,
) -> Result<CryptoVec, crate::Error> {
// Computing the exchange hash, see page 7 of RFC 5656.
buffer.clear();
buffer.extend_ssh_string(&exchange.client_id);
buffer.extend_ssh_string(&exchange.server_id);
buffer.extend_ssh_string(&exchange.client_kex_init);
buffer.extend_ssh_string(&exchange.server_kex_init);

buffer.extend(key);
buffer.extend_ssh_string(&exchange.client_ephemeral);
buffer.extend_ssh_string(&exchange.server_ephemeral);

if let Some(ref shared) = self.shared_secret {
buffer.extend_ssh_mpint(shared.raw_secret_bytes());
}

use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(&buffer);

let mut res = CryptoVec::new();
res.extend(hasher.finalize().as_slice());
Ok(res)
}

fn compute_keys(
&self,
session_id: &CryptoVec,
exchange_hash: &CryptoVec,
cipher: cipher::Name,
remote_to_local_mac: mac::Name,
local_to_remote_mac: mac::Name,
is_server: bool,
) -> Result<crate::kex::cipher::CipherPair, crate::Error> {
compute_keys::<sha2::Sha256>(
self.shared_secret
.as_ref()
.map(|x| x.raw_secret_bytes() as &[u8]),
session_id,
exchange_hash,
cipher,
remote_to_local_mac,
local_to_remote_mac,
is_server,
)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_shared_secret() {
let mut party1 = EcdhNistPKex::<NistP256> {
local_secret: Some(EphemeralSecret::<NistP256>::random(&mut rand_core::OsRng)),
shared_secret: None,
};
let p1_pubkey = party1.local_secret.as_ref().unwrap().public_key();

let mut party2 = EcdhNistPKex::<NistP256> {
local_secret: Some(EphemeralSecret::<NistP256>::random(&mut rand_core::OsRng)),
shared_secret: None,
};
let p2_pubkey = party2.local_secret.as_ref().unwrap().public_key();

party1
.compute_shared_secret(&p2_pubkey.to_sec1_bytes())
.unwrap();

party2
.compute_shared_secret(&p1_pubkey.to_sec1_bytes())
.unwrap();

let p1_shared_secret = party1.shared_secret.unwrap();
let p2_shared_secret = party2.shared_secret.unwrap();

assert_eq!(
p1_shared_secret.raw_secret_bytes(),
p2_shared_secret.raw_secret_bytes()
)
}
}
14 changes: 14 additions & 0 deletions russh/src/kex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! This module exports kex algorithm names for use with [Preferred].
mod curve25519;
mod dh;
mod ecdh_nistp;
mod none;
use std::cell::RefCell;
use std::collections::HashMap;
Expand All @@ -27,6 +28,7 @@ use dh::{
DhGroup14Sha1KexType, DhGroup14Sha256KexType, DhGroup16Sha512KexType, DhGroup1Sha1KexType,
};
use digest::Digest;
use ecdh_nistp::{EcdhNistP256KexType, EcdhNistP384KexType, EcdhNistP521KexType};
use once_cell::sync::Lazy;
use russh_cryptovec::CryptoVec;
use russh_keys::encoding::Encoding;
Expand Down Expand Up @@ -97,6 +99,12 @@ pub const DH_G14_SHA1: Name = Name("diffie-hellman-group14-sha1");
pub const DH_G14_SHA256: Name = Name("diffie-hellman-group14-sha256");
/// `diffie-hellman-group16-sha512`
pub const DH_G16_SHA512: Name = Name("diffie-hellman-group16-sha512");
/// `ecdh-sha2-nistp256`
pub const ECDH_SHA2_NISTP256: Name = Name("ecdh-sha2-nistp256");
/// `ecdh-sha2-nistp384`
pub const ECDH_SHA2_NISTP384: Name = Name("ecdh-sha2-nistp384");
/// `ecdh-sha2-nistp521`
pub const ECDH_SHA2_NISTP521: Name = Name("ecdh-sha2-nistp521");
/// `none`
pub const NONE: Name = Name("none");
/// `ext-info-c`
Expand All @@ -113,6 +121,9 @@ const _DH_G1_SHA1: DhGroup1Sha1KexType = DhGroup1Sha1KexType {};
const _DH_G14_SHA1: DhGroup14Sha1KexType = DhGroup14Sha1KexType {};
const _DH_G14_SHA256: DhGroup14Sha256KexType = DhGroup14Sha256KexType {};
const _DH_G16_SHA512: DhGroup16Sha512KexType = DhGroup16Sha512KexType {};
const _ECDH_SHA2_NISTP256: EcdhNistP256KexType = EcdhNistP256KexType {};
const _ECDH_SHA2_NISTP384: EcdhNistP384KexType = EcdhNistP384KexType {};
const _ECDH_SHA2_NISTP521: EcdhNistP521KexType = EcdhNistP521KexType {};
const _NONE: none::NoneKexType = none::NoneKexType {};

pub(crate) static KEXES: Lazy<HashMap<&'static Name, &(dyn KexType + Send + Sync)>> =
Expand All @@ -124,6 +135,9 @@ pub(crate) static KEXES: Lazy<HashMap<&'static Name, &(dyn KexType + Send + Sync
h.insert(&DH_G14_SHA256, &_DH_G14_SHA256);
h.insert(&DH_G14_SHA1, &_DH_G14_SHA1);
h.insert(&DH_G1_SHA1, &_DH_G1_SHA1);
h.insert(&ECDH_SHA2_NISTP256, &_ECDH_SHA2_NISTP256);
h.insert(&ECDH_SHA2_NISTP384, &_ECDH_SHA2_NISTP384);
h.insert(&ECDH_SHA2_NISTP521, &_ECDH_SHA2_NISTP521);
h.insert(&NONE, &_NONE);
h
});
Expand Down

0 comments on commit 3bfd99f

Please sign in to comment.