Skip to content

Commit 09f6582

Browse files
Eugenybiao29
andauthored
Support host certificates in client (#752)
Co-authored-by: biao29 <hi@ctx.st>
1 parent 5d1dddc commit 09f6582

22 files changed

Lines changed: 352 additions & 59 deletions

russh/examples/client_exec_interactive.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl client::Handler for Client {
6969

7070
async fn check_server_key(
7171
&mut self,
72-
_server_public_key: &ssh_key::PublicKey,
72+
_server_public_key: &PublicKeyOrCertificate,
7373
) -> Result<bool, Self::Error> {
7474
Ok(true)
7575
}

russh/examples/client_exec_simple.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ impl client::Handler for Client {
6363

6464
async fn check_server_key(
6565
&mut self,
66-
_server_public_key: &ssh_key::PublicKey,
66+
_server_public_key: &PublicKeyOrCertificate,
6767
) -> Result<bool, Self::Error> {
6868
Ok(true)
6969
}

russh/examples/client_open_direct_tcpip.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ impl client::Handler for Client {
6262

6363
async fn check_server_key(
6464
&mut self,
65-
_server_public_key: &ssh_key::PublicKey,
65+
_server_public_key: &PublicKeyOrCertificate,
6666
) -> Result<bool, Self::Error> {
6767
Ok(true)
6868
}

russh/examples/sftp_client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ impl client::Handler for Client {
1414

1515
async fn check_server_key(
1616
&mut self,
17-
server_public_key: &ssh_key::PublicKey,
17+
server_public_key: &PublicKeyOrCertificate,
1818
) -> Result<bool, Self::Error> {
1919
info!("check_server_key: {server_public_key:?}");
2020
Ok(true)

russh/src/cert.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::keys::key::PrivateKeyWithHashAlg;
99

1010
#[derive(Debug)]
1111
#[allow(clippy::large_enum_variant)]
12-
pub(crate) enum PublicKeyOrCertificate {
12+
pub enum PublicKeyOrCertificate {
1313
PublicKey {
1414
key: PublicKey,
1515
hash_alg: Option<HashAlg>,
@@ -26,9 +26,24 @@ impl From<&PrivateKeyWithHashAlg> for PublicKeyOrCertificate {
2626
}
2727
}
2828

29+
impl From<Certificate> for PublicKeyOrCertificate {
30+
fn from(cert: Certificate) -> Self {
31+
PublicKeyOrCertificate::Certificate(cert)
32+
}
33+
}
34+
35+
impl From<PublicKey> for PublicKeyOrCertificate {
36+
fn from(key: PublicKey) -> Self {
37+
PublicKeyOrCertificate::PublicKey {
38+
key,
39+
hash_alg: None,
40+
}
41+
}
42+
}
43+
2944
impl PublicKeyOrCertificate {
3045
#[cfg(not(target_arch = "wasm32"))]
31-
pub fn decode(pubkey_algo: &str, buf: &[u8]) -> Result<Self, ssh_key::Error> {
46+
pub(crate) fn decode(pubkey_algo: &str, buf: &[u8]) -> Result<Self, ssh_key::Error> {
3247
let mut reader = buf;
3348
match Algorithm::new_certificate_ext(pubkey_algo) {
3449
Ok(Algorithm::Other(_)) | Err(ssh_key::Error::Encoding(_)) => {
@@ -43,4 +58,20 @@ impl PublicKeyOrCertificate {
4358
)?)),
4459
}
4560
}
61+
62+
pub fn public_key(&self) -> PublicKey {
63+
match self {
64+
PublicKeyOrCertificate::PublicKey { key, .. } => key.clone(),
65+
PublicKeyOrCertificate::Certificate(cert) => {
66+
PublicKey::new(cert.public_key().clone(), "")
67+
}
68+
}
69+
}
70+
71+
pub fn certificate(&self) -> Option<&Certificate> {
72+
match self {
73+
PublicKeyOrCertificate::PublicKey { .. } => None,
74+
PublicKeyOrCertificate::Certificate(cert) => Some(cert),
75+
}
76+
}
4677
}

russh/src/client/kex.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::sync::Arc;
66
use bytes::Bytes;
77
use log::{debug, error, warn};
88
use ssh_encoding::{Decode, Encode};
9-
use ssh_key::{Mpint, PublicKey, Signature};
9+
use ssh_key::{Certificate, Mpint, PublicKey, Signature};
1010

1111
use super::IncomingSshPacket;
1212
use crate::client::{Config, NewKeys};
@@ -38,6 +38,7 @@ enum ClientKexState {
3838
},
3939
WaitingForNewKeys {
4040
server_host_key: PublicKey,
41+
server_host_certificate: Option<Certificate>,
4142
newkeys: NewKeys,
4243
},
4344
}
@@ -152,6 +153,7 @@ impl ClientKex {
152153
})?;
153154

154155
return Ok(KexProgress::Done {
156+
server_host_certificate: None,
155157
newkeys,
156158
server_host_key: None,
157159
});
@@ -263,11 +265,31 @@ impl ClientKex {
263265
#[allow(clippy::indexing_slicing)] // length checked
264266
let r = &mut &input.buffer[1..];
265267

266-
let server_host_key = Bytes::decode(r)?; // server public key.
267-
let server_host_key = parse_public_key(&server_host_key)?;
268+
// The raw blob is kept as well as the parsed key. It is what
269+
// goes into the exchange hash below: for a certificate the
270+
// parsed form is only the key *inside* it, and re-encoding that
271+
// would hash something the server never sent — a failure that
272+
// looks like a bad signature and is computed entirely locally,
273+
// so there is nothing on the wire to compare against.
274+
let server_host_key_blob = Bytes::decode(r)?;
275+
let server_host_certificate = if names.host_key_is_certificate {
276+
Some(Certificate::from_bytes(&server_host_key_blob)?)
277+
} else {
278+
None
279+
};
280+
let server_host_key = match &server_host_certificate {
281+
// The certificate's own signature is checked by the client
282+
// against its trusted authorities, not here; what the key
283+
// exchange is signed with is the key the certificate
284+
// contains. The two are separate proofs and collapsing them
285+
// would accept a certificate nobody vouched for.
286+
Some(certificate) => PublicKey::new(certificate.public_key().clone(), ""),
287+
None => parse_public_key(&server_host_key_blob)?,
288+
};
268289
debug!(
269-
"received server host key: {:?}",
270-
server_host_key.to_openssh()
290+
"received server host key: {:?} (certificate: {})",
291+
server_host_key.to_openssh(),
292+
server_host_certificate.is_some()
271293
);
272294

273295
let server_ephemeral = Bytes::decode(r)?;
@@ -277,7 +299,7 @@ impl ClientKex {
277299
kex.compute_shared_secret(&self.exchange.server_ephemeral)?;
278300

279301
let mut pubkey_vec = Vec::new();
280-
server_host_key.to_bytes()?.encode(&mut pubkey_vec)?;
302+
server_host_key_blob.encode(&mut pubkey_vec)?;
281303

282304
let exchange = &self.exchange;
283305
let hash = HASH_BUFFER.with({
@@ -318,6 +340,7 @@ impl ClientKex {
318340

319341
self.state = ClientKexState::WaitingForNewKeys {
320342
server_host_key,
343+
server_host_certificate,
321344
newkeys,
322345
};
323346

@@ -328,6 +351,7 @@ impl ClientKex {
328351
}
329352
ClientKexState::WaitingForNewKeys {
330353
server_host_key,
354+
server_host_certificate,
331355
newkeys,
332356
} => {
333357
// At this point the exchange is complete
@@ -349,6 +373,7 @@ impl ClientKex {
349373
ensure_end(&r)?;
350374

351375
Ok(KexProgress::Done {
376+
server_host_certificate,
352377
newkeys,
353378
server_host_key: Some(server_host_key),
354379
})

russh/src/client/mod.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ use tokio::sync::mpsc::{
5858
use tokio::sync::oneshot;
5959

6060
pub use crate::auth::AuthResult;
61+
use crate::cert::PublicKeyOrCertificate;
6162
use crate::channels::{
6263
Channel, ChannelMsg, ChannelReadHalf, ChannelRef, ChannelWriteHalf, WindowSizeRef,
6364
};
@@ -1804,6 +1805,7 @@ async fn reply<H: Handler>(
18041805
}
18051806
KexProgress::Done {
18061807
server_host_key,
1808+
server_host_certificate,
18071809
newkeys,
18081810
} => {
18091811
debug!("kex impl has completed");
@@ -1836,8 +1838,17 @@ async fn reply<H: Handler>(
18361838
session.pending_len = 0;
18371839
} else {
18381840
// This is the initial kex
1839-
if let Some(server_host_key) = &server_host_key {
1840-
let check = handler.check_server_key(server_host_key).await?;
1841+
// A certificate replaces the key check rather than
1842+
// adding to it. The key inside a certificate is not
1843+
// something the client was ever told to trust — asking
1844+
// about it as well would invite an implementation to
1845+
// answer yes to the wrong question.
1846+
if let Some(certificate) = server_host_certificate {
1847+
if !handler.check_server_key(&certificate.into()).await? {
1848+
return Err(crate::Error::UnknownKey.into());
1849+
}
1850+
} else if let Some(server_host_key) = server_host_key {
1851+
let check = handler.check_server_key(&server_host_key.into()).await?;
18411852
if !check {
18421853
return Err(crate::Error::UnknownKey.into());
18431854
}
@@ -1895,7 +1906,7 @@ mod tests {
18951906
impl Handler for TestHandler {
18961907
type Error = crate::Error;
18971908

1898-
async fn check_server_key(&mut self, _: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
1909+
async fn check_server_key(&mut self, _: &PublicKeyOrCertificate) -> Result<bool, Self::Error> {
18991910
Ok(true)
19001911
}
19011912
}
@@ -2299,13 +2310,17 @@ pub trait Handler: Sized + Send {
22992310
async { Ok(()) }
23002311
}
23012312

2302-
/// Called to check the server's public key. This is a very important
2303-
/// step to help prevent man-in-the-middle attacks. The default
2304-
/// implementation rejects all keys.
2313+
/// Called to check the server's public key or certificate.
2314+
/// This is a very important step to help prevent man-in-the-middle attacks.
2315+
/// The default implementation rejects all keys, and you must override it.
2316+
///
2317+
/// The library verifies the key exchange signature before this call,
2318+
/// but it's up to the implementation to decide whether the key or certificate
2319+
/// is trusted.
23052320
#[allow(unused_variables)]
23062321
fn check_server_key(
23072322
&mut self,
2308-
server_public_key: &ssh_key::PublicKey,
2323+
server_public_key: &PublicKeyOrCertificate,
23092324
) -> impl Future<Output = Result<bool, Self::Error>> + Send {
23102325
async { Ok(false) }
23112326
}

russh/src/client/test.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod tests {
77
use ssh_key::PrivateKey;
88
use tokio::net::TcpListener;
99

10+
use crate::cert::PublicKeyOrCertificate;
1011
// Import client types directly since we're in the client module
1112
use crate::Error;
1213
use crate::client::{Config, Handler, connect};
@@ -74,7 +75,10 @@ mod tests {
7475
impl Handler for Client {
7576
type Error = Error;
7677

77-
async fn check_server_key(&mut self, _: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
78+
async fn check_server_key(
79+
&mut self,
80+
_: &PublicKeyOrCertificate,
81+
) -> Result<bool, Self::Error> {
7882
Ok(true)
7983
}
8084
}

russh/src/kex/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use p521::NistP521;
4444
use sha1::Sha1;
4545
use sha2::{Sha256, Sha384, Sha512};
4646
use ssh_encoding::{Encode, Writer};
47-
use ssh_key::PublicKey;
47+
use ssh_key::{Certificate, PublicKey};
4848

4949
use crate::cipher::CIPHERS;
5050
use crate::client::GexParams;
@@ -121,6 +121,14 @@ pub(crate) enum KexProgress<T> {
121121
},
122122
Done {
123123
server_host_key: Option<PublicKey>,
124+
/// The certificate the server presented, when it presented one.
125+
///
126+
/// Carried beside the key rather than replacing it: the key exchange is
127+
/// signed with the key the certificate contains, so both are needed —
128+
/// one to know what signed the handshake, the other to decide whether
129+
/// anyone vouches for it. Collapsing them would leave the second
130+
/// question unasked.
131+
server_host_certificate: Option<Certificate>,
124132
newkeys: NewKeys,
125133
},
126134
}

russh/src/keys/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub mod known_hosts;
8686
#[cfg(not(target_arch = "wasm32"))]
8787
pub use known_hosts::{check_known_hosts, check_known_hosts_path};
8888

89+
pub use crate::cert::PublicKeyOrCertificate;
90+
8991
#[derive(Debug, Error)]
9092
pub enum Error {
9193
/// The key could not be read, for an unknown reason

0 commit comments

Comments
 (0)