Skip to content

Commit 3bfd99f

Browse files
gleason-mEugeny
andauthored
ecdh-sha2-nistp{256,384,521} kex support (#282)
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 <inbox@null.page>
1 parent 0fc65ea commit 3bfd99f

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

russh/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ chacha20 = "0.9"
2727
ctr = "0.9"
2828
curve25519-dalek = "4.1.2"
2929
digest = { workspace = true }
30+
elliptic-curve = { version = "0.13", features = ["ecdh"] }
3031
flate2 = { version = "1.0", optional = true }
3132
futures = { workspace = true }
3233
generic-array = "0.14"
@@ -36,8 +37,12 @@ log = { workspace = true }
3637
num-bigint = { version = "0.4", features = ["rand"] }
3738
once_cell = "1.13"
3839
openssl = { workspace = true, optional = true }
40+
p256 = { version = "0.13", features = ["ecdh"] }
41+
p384 = { version = "0.13", features = ["ecdh"] }
42+
p521 = { version = "0.13", features = ["ecdh"] }
3943
poly1305 = "0.8"
4044
rand = { workspace = true }
45+
rand_core = { version = "0.6.4", features = ["getrandom"] }
4146
russh-cryptovec = { version = "0.7.0", path = "../cryptovec" }
4247
russh-keys = { version = "0.44.0-beta.1", path = "../russh-keys" }
4348
sha1 = { workspace = true }

russh/src/kex/ecdh_nistp.rs

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
use byteorder::{BigEndian, ByteOrder};
2+
use elliptic_curve::ecdh::{EphemeralSecret, SharedSecret};
3+
use elliptic_curve::point::PointCompression;
4+
use elliptic_curve::sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint};
5+
use elliptic_curve::{AffinePoint, Curve, CurveArithmetic, FieldBytesSize};
6+
use log::debug;
7+
use p256::NistP256;
8+
use p384::NistP384;
9+
use p521::NistP521;
10+
use russh_cryptovec::CryptoVec;
11+
use russh_keys::encoding::Encoding;
12+
13+
use crate::kex::{compute_keys, KexAlgorithm, KexType};
14+
use crate::mac::{self};
15+
use crate::session::Exchange;
16+
use crate::{cipher, msg};
17+
18+
pub struct EcdhNistP256KexType {}
19+
20+
impl KexType for EcdhNistP256KexType {
21+
fn make(&self) -> Box<dyn KexAlgorithm + Send> {
22+
Box::new(EcdhNistPKex::<NistP256> {
23+
local_secret: None,
24+
shared_secret: None,
25+
}) as Box<dyn KexAlgorithm + Send>
26+
}
27+
}
28+
29+
pub struct EcdhNistP384KexType {}
30+
31+
impl KexType for EcdhNistP384KexType {
32+
fn make(&self) -> Box<dyn KexAlgorithm + Send> {
33+
Box::new(EcdhNistPKex::<NistP384> {
34+
local_secret: None,
35+
shared_secret: None,
36+
}) as Box<dyn KexAlgorithm + Send>
37+
}
38+
}
39+
40+
pub struct EcdhNistP521KexType {}
41+
42+
impl KexType for EcdhNistP521KexType {
43+
fn make(&self) -> Box<dyn KexAlgorithm + Send> {
44+
Box::new(EcdhNistPKex::<NistP521> {
45+
local_secret: None,
46+
shared_secret: None,
47+
}) as Box<dyn KexAlgorithm + Send>
48+
}
49+
}
50+
51+
#[doc(hidden)]
52+
pub struct EcdhNistPKex<C: Curve + CurveArithmetic> {
53+
local_secret: Option<EphemeralSecret<C>>,
54+
shared_secret: Option<SharedSecret<C>>,
55+
}
56+
57+
impl<C: Curve + CurveArithmetic> std::fmt::Debug for EcdhNistPKex<C> {
58+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
59+
write!(
60+
f,
61+
"Algorithm {{ local_secret: [hidden], shared_secret: [hidden] }}",
62+
)
63+
}
64+
}
65+
66+
impl<C: Curve + CurveArithmetic> KexAlgorithm for EcdhNistPKex<C>
67+
where
68+
C: PointCompression,
69+
FieldBytesSize<C>: ModulusSize,
70+
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
71+
{
72+
fn skip_exchange(&self) -> bool {
73+
false
74+
}
75+
76+
#[doc(hidden)]
77+
fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), crate::Error> {
78+
debug!("server_dh");
79+
80+
let client_pubkey = {
81+
if payload.first() != Some(&msg::KEX_ECDH_INIT) {
82+
return Err(crate::Error::Inconsistent);
83+
}
84+
85+
#[allow(clippy::indexing_slicing)] // length checked
86+
let pubkey_len = BigEndian::read_u32(&payload[1..]) as usize;
87+
88+
if payload.len() < 5 + pubkey_len {
89+
return Err(crate::Error::Inconsistent);
90+
}
91+
92+
#[allow(clippy::indexing_slicing)] // length checked
93+
elliptic_curve::PublicKey::<C>::from_sec1_bytes(&payload[5..(5 + pubkey_len)])
94+
.map_err(|_| crate::Error::Inconsistent)?
95+
};
96+
97+
let server_secret =
98+
elliptic_curve::ecdh::EphemeralSecret::<C>::random(&mut rand_core::OsRng);
99+
let server_pubkey = server_secret.public_key();
100+
101+
// fill exchange.
102+
exchange.server_ephemeral.clear();
103+
exchange
104+
.server_ephemeral
105+
.extend(&server_pubkey.to_sec1_bytes());
106+
let shared = server_secret.diffie_hellman(&client_pubkey);
107+
self.shared_secret = Some(shared);
108+
Ok(())
109+
}
110+
111+
#[doc(hidden)]
112+
fn client_dh(
113+
&mut self,
114+
client_ephemeral: &mut CryptoVec,
115+
buf: &mut CryptoVec,
116+
) -> Result<(), crate::Error> {
117+
let client_secret =
118+
elliptic_curve::ecdh::EphemeralSecret::<C>::random(&mut rand_core::OsRng);
119+
let client_pubkey = client_secret.public_key();
120+
121+
// fill exchange.
122+
client_ephemeral.clear();
123+
client_ephemeral.extend(&client_pubkey.to_sec1_bytes());
124+
125+
buf.push(msg::KEX_ECDH_INIT);
126+
buf.extend_ssh_string(&client_pubkey.to_sec1_bytes());
127+
128+
self.local_secret = Some(client_secret);
129+
Ok(())
130+
}
131+
132+
fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), crate::Error> {
133+
let local_secret = self.local_secret.take().ok_or(crate::Error::KexInit)?;
134+
let pubkey = elliptic_curve::PublicKey::<C>::from_sec1_bytes(remote_pubkey_)
135+
.map_err(|_| crate::Error::KexInit)?;
136+
self.shared_secret = Some(local_secret.diffie_hellman(&pubkey));
137+
Ok(())
138+
}
139+
140+
fn compute_exchange_hash(
141+
&self,
142+
key: &CryptoVec,
143+
exchange: &Exchange,
144+
buffer: &mut CryptoVec,
145+
) -> Result<CryptoVec, crate::Error> {
146+
// Computing the exchange hash, see page 7 of RFC 5656.
147+
buffer.clear();
148+
buffer.extend_ssh_string(&exchange.client_id);
149+
buffer.extend_ssh_string(&exchange.server_id);
150+
buffer.extend_ssh_string(&exchange.client_kex_init);
151+
buffer.extend_ssh_string(&exchange.server_kex_init);
152+
153+
buffer.extend(key);
154+
buffer.extend_ssh_string(&exchange.client_ephemeral);
155+
buffer.extend_ssh_string(&exchange.server_ephemeral);
156+
157+
if let Some(ref shared) = self.shared_secret {
158+
buffer.extend_ssh_mpint(shared.raw_secret_bytes());
159+
}
160+
161+
use sha2::Digest;
162+
let mut hasher = sha2::Sha256::new();
163+
hasher.update(&buffer);
164+
165+
let mut res = CryptoVec::new();
166+
res.extend(hasher.finalize().as_slice());
167+
Ok(res)
168+
}
169+
170+
fn compute_keys(
171+
&self,
172+
session_id: &CryptoVec,
173+
exchange_hash: &CryptoVec,
174+
cipher: cipher::Name,
175+
remote_to_local_mac: mac::Name,
176+
local_to_remote_mac: mac::Name,
177+
is_server: bool,
178+
) -> Result<crate::kex::cipher::CipherPair, crate::Error> {
179+
compute_keys::<sha2::Sha256>(
180+
self.shared_secret
181+
.as_ref()
182+
.map(|x| x.raw_secret_bytes() as &[u8]),
183+
session_id,
184+
exchange_hash,
185+
cipher,
186+
remote_to_local_mac,
187+
local_to_remote_mac,
188+
is_server,
189+
)
190+
}
191+
}
192+
193+
#[cfg(test)]
194+
mod tests {
195+
use super::*;
196+
197+
#[test]
198+
fn test_shared_secret() {
199+
let mut party1 = EcdhNistPKex::<NistP256> {
200+
local_secret: Some(EphemeralSecret::<NistP256>::random(&mut rand_core::OsRng)),
201+
shared_secret: None,
202+
};
203+
let p1_pubkey = party1.local_secret.as_ref().unwrap().public_key();
204+
205+
let mut party2 = EcdhNistPKex::<NistP256> {
206+
local_secret: Some(EphemeralSecret::<NistP256>::random(&mut rand_core::OsRng)),
207+
shared_secret: None,
208+
};
209+
let p2_pubkey = party2.local_secret.as_ref().unwrap().public_key();
210+
211+
party1
212+
.compute_shared_secret(&p2_pubkey.to_sec1_bytes())
213+
.unwrap();
214+
215+
party2
216+
.compute_shared_secret(&p1_pubkey.to_sec1_bytes())
217+
.unwrap();
218+
219+
let p1_shared_secret = party1.shared_secret.unwrap();
220+
let p2_shared_secret = party2.shared_secret.unwrap();
221+
222+
assert_eq!(
223+
p1_shared_secret.raw_secret_bytes(),
224+
p2_shared_secret.raw_secret_bytes()
225+
)
226+
}
227+
}

russh/src/kex/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//! This module exports kex algorithm names for use with [Preferred].
1818
mod curve25519;
1919
mod dh;
20+
mod ecdh_nistp;
2021
mod none;
2122
use std::cell::RefCell;
2223
use std::collections::HashMap;
@@ -27,6 +28,7 @@ use dh::{
2728
DhGroup14Sha1KexType, DhGroup14Sha256KexType, DhGroup16Sha512KexType, DhGroup1Sha1KexType,
2829
};
2930
use digest::Digest;
31+
use ecdh_nistp::{EcdhNistP256KexType, EcdhNistP384KexType, EcdhNistP521KexType};
3032
use once_cell::sync::Lazy;
3133
use russh_cryptovec::CryptoVec;
3234
use russh_keys::encoding::Encoding;
@@ -97,6 +99,12 @@ pub const DH_G14_SHA1: Name = Name("diffie-hellman-group14-sha1");
9799
pub const DH_G14_SHA256: Name = Name("diffie-hellman-group14-sha256");
98100
/// `diffie-hellman-group16-sha512`
99101
pub const DH_G16_SHA512: Name = Name("diffie-hellman-group16-sha512");
102+
/// `ecdh-sha2-nistp256`
103+
pub const ECDH_SHA2_NISTP256: Name = Name("ecdh-sha2-nistp256");
104+
/// `ecdh-sha2-nistp384`
105+
pub const ECDH_SHA2_NISTP384: Name = Name("ecdh-sha2-nistp384");
106+
/// `ecdh-sha2-nistp521`
107+
pub const ECDH_SHA2_NISTP521: Name = Name("ecdh-sha2-nistp521");
100108
/// `none`
101109
pub const NONE: Name = Name("none");
102110
/// `ext-info-c`
@@ -113,6 +121,9 @@ const _DH_G1_SHA1: DhGroup1Sha1KexType = DhGroup1Sha1KexType {};
113121
const _DH_G14_SHA1: DhGroup14Sha1KexType = DhGroup14Sha1KexType {};
114122
const _DH_G14_SHA256: DhGroup14Sha256KexType = DhGroup14Sha256KexType {};
115123
const _DH_G16_SHA512: DhGroup16Sha512KexType = DhGroup16Sha512KexType {};
124+
const _ECDH_SHA2_NISTP256: EcdhNistP256KexType = EcdhNistP256KexType {};
125+
const _ECDH_SHA2_NISTP384: EcdhNistP384KexType = EcdhNistP384KexType {};
126+
const _ECDH_SHA2_NISTP521: EcdhNistP521KexType = EcdhNistP521KexType {};
116127
const _NONE: none::NoneKexType = none::NoneKexType {};
117128

118129
pub(crate) static KEXES: Lazy<HashMap<&'static Name, &(dyn KexType + Send + Sync)>> =
@@ -124,6 +135,9 @@ pub(crate) static KEXES: Lazy<HashMap<&'static Name, &(dyn KexType + Send + Sync
124135
h.insert(&DH_G14_SHA256, &_DH_G14_SHA256);
125136
h.insert(&DH_G14_SHA1, &_DH_G14_SHA1);
126137
h.insert(&DH_G1_SHA1, &_DH_G1_SHA1);
138+
h.insert(&ECDH_SHA2_NISTP256, &_ECDH_SHA2_NISTP256);
139+
h.insert(&ECDH_SHA2_NISTP384, &_ECDH_SHA2_NISTP384);
140+
h.insert(&ECDH_SHA2_NISTP521, &_ECDH_SHA2_NISTP521);
127141
h.insert(&NONE, &_NONE);
128142
h
129143
});

0 commit comments

Comments
 (0)