Skip to content
Closed
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 russh/examples/client_exec_interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl client::Handler for Client {

async fn check_server_key(
&mut self,
_server_public_key: &ssh_key::PublicKey,
_server_public_key: &cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down
2 changes: 1 addition & 1 deletion russh/examples/client_exec_simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl client::Handler for Client {

async fn check_server_key(
&mut self,
_server_public_key: &ssh_key::PublicKey,
_server_public_key: &cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down
2 changes: 1 addition & 1 deletion russh/examples/client_open_direct_tcpip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl client::Handler for Client {

async fn check_server_key(
&mut self,
_server_public_key: &ssh_key::PublicKey,
_server_public_key: &cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down
3 changes: 1 addition & 2 deletions russh/examples/sftp_client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::sync::Arc;

use log::{LevelFilter, error, info};
use russh::keys::*;
use russh::*;
use russh_sftp::client::SftpSession;
use russh_sftp::protocol::OpenFlags;
Expand All @@ -14,7 +13,7 @@ impl client::Handler for Client {

async fn check_server_key(
&mut self,
server_public_key: &ssh_key::PublicKey,
server_public_key: &cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
info!("check_server_key: {server_public_key:?}");
Ok(true)
Expand Down
2 changes: 1 addition & 1 deletion russh/src/cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::keys::key::PrivateKeyWithHashAlg;

#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum PublicKeyOrCertificate {
pub enum PublicKeyOrCertificate {
PublicKey {
key: PublicKey,
hash_alg: Option<HashAlg>,
Expand Down
55 changes: 41 additions & 14 deletions russh/src/client/kex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ use std::sync::Arc;
use bytes::Bytes;
use log::{debug, error, warn};
use signature::Verifier;
use ssh_encoding::{Decode, Encode};
use ssh_key::{Mpint, PublicKey, Signature};
use ssh_encoding::{Decode, Encode };
use ssh_key::{Algorithm, Certificate, Mpint, Signature};

use super::IncomingSshPacket;
use crate::cert::PublicKeyOrCertificate;
use crate::client::{Config, NewKeys};
use crate::helpers::AlgorithmExt;
use crate::kex::dh::groups::DhGroup;
use crate::kex::{KexAlgorithm, KexAlgorithmImplementor, KexCause, KexProgress, KEXES};
use crate::keys::key::parse_public_key;
Expand All @@ -37,7 +39,7 @@ enum ClientKexState {
kex: KexAlgorithm,
},
WaitingForNewKeys {
server_host_key: PublicKey,
server_host_key: PublicKeyOrCertificate,
newkeys: NewKeys,
},
}
Expand Down Expand Up @@ -262,20 +264,41 @@ impl ClientKex {
#[allow(clippy::indexing_slicing)] // length checked
let r = &mut &input.buffer[1..];

let server_host_key = Bytes::decode(r)?; // server public key.
let server_host_key = parse_public_key(&server_host_key)?;
debug!(
"received server host key: {:?}",
server_host_key.to_openssh()
);

let mut pubkey_vec = CryptoVec::new();
let server_host_key_bytes = Bytes::decode(r)?;
let algo = String::decode(&mut &server_host_key_bytes[..])?;

// SSH supports two modes for server authentication during the handshake:
// In normal mode, the server sends a raw public key during the SSH handshake:
// host_key = raw public key (e.g. "ssh-rsa <publickey>")
//
// In host certificate mode, the server sends a certificate instead:
// host_key = certificate (e.g. "ssh-rsa-cert-v01@openssh.com <cert_blob>")
//
// The cert blob contains the public key itself, the CA signature, and metadata
// such as principals and validity period. The server's private key remains the
// same — the cert is essentially a CA-endorsed wrapper around the public key.
let server_host_key = if Algorithm::new_certificate(&algo).is_ok() {
let cert = Certificate::from_bytes(&server_host_key_bytes)?;
server_host_key_bytes.as_ref().encode(&mut pubkey_vec)?;
PublicKeyOrCertificate::Certificate(cert)
} else {
let public_key = parse_public_key(&server_host_key_bytes)?;
debug!(
"received server host key: {:?}",
public_key.to_openssh()
);
public_key.to_bytes()?.encode(&mut pubkey_vec)?;
let hash_alg = Algorithm::new(&algo)
.ok()
.and_then(|algorithm| algorithm.hash_alg());
PublicKeyOrCertificate::PublicKey { key: public_key, hash_alg }
};

let server_ephemeral = Bytes::decode(r)?;
self.exchange.server_ephemeral.extend(&server_ephemeral);
kex.compute_shared_secret(&self.exchange.server_ephemeral)?;

let mut pubkey_vec = CryptoVec::new();
server_host_key.to_bytes()?.encode(&mut pubkey_vec)?;

let exchange = &self.exchange;
let hash = HASH_BUFFER.with({
|buffer| {
Expand All @@ -287,8 +310,12 @@ impl ClientKex {

let signature = Bytes::decode(r)?;
let signature = Signature::decode(&mut &signature[..])?;
let public_key = match &server_host_key {
PublicKeyOrCertificate::PublicKey { key, .. } => key.clone(),
PublicKeyOrCertificate::Certificate(cert) => cert.public_key().clone().into(),
};

if let Err(e) = Verifier::verify(&server_host_key, hash.as_ref(), &signature) {
if let Err(e) = Verifier::verify(&public_key, hash.as_ref(), &signature) {
debug!("wrong server sig: {e:?}");
return Err(Error::WrongServerSig);
}
Expand Down
3 changes: 2 additions & 1 deletion russh/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ use crate::channels::{
Channel, ChannelMsg, ChannelReadHalf, ChannelRef, ChannelWriteHalf, WindowSizeRef,
};
use crate::cipher::{self, OpeningKey, clear};
use crate::cert::PublicKeyOrCertificate;
use crate::kex::{KexAlgorithmImplementor, KexCause, KexProgress, SessionKexState};
use crate::keys::PrivateKeyWithHashAlg;
use crate::msg::{is_kex_msg, validate_server_msg_strict_kex};
Expand Down Expand Up @@ -1743,7 +1744,7 @@ pub trait Handler: Sized + Send {
#[allow(unused_variables)]
fn check_server_key(
&mut self,
server_public_key: &ssh_key::PublicKey,
server_public_key: &PublicKeyOrCertificate,
) -> impl Future<Output = Result<bool, Self::Error>> + Send {
async { Ok(false) }
}
Expand Down
2 changes: 1 addition & 1 deletion russh/src/client/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ mod tests {
impl Handler for Client {
type Error = Error;

async fn check_server_key(&mut self, _: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
async fn check_server_key(&mut self, _: &crate::cert::PublicKeyOrCertificate) -> Result<bool, Self::Error> {
Ok(true)
}
}
Expand Down
4 changes: 2 additions & 2 deletions russh/src/kex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ use p521::NistP521;
use sha1::Sha1;
use sha2::{Sha256, Sha384, Sha512};
use ssh_encoding::{Encode, Writer};
use ssh_key::PublicKey;

use crate::cert::PublicKeyOrCertificate;
use crate::cipher::CIPHERS;
use crate::client::GexParams;
use crate::mac::{self, MACS};
Expand Down Expand Up @@ -120,7 +120,7 @@ pub(crate) enum KexProgress<T> {
reset_seqn: bool,
},
Done {
server_host_key: Option<PublicKey>,
server_host_key: Option<PublicKeyOrCertificate>,
newkeys: NewKeys,
},
}
Expand Down
2 changes: 1 addition & 1 deletion russh/src/lib_inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod tests;

mod auth;

mod cert;
pub mod cert;
/// Cipher names
pub mod cipher;
/// Compression algorithm names
Expand Down
47 changes: 40 additions & 7 deletions russh/src/negotiation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use ssh_encoding::{Decode, Encode};
use ssh_key::{Algorithm, EcdsaCurve, HashAlg, PrivateKey};

use crate::cipher::CIPHERS;
use crate::helpers::NameList;
use crate::helpers::{AlgorithmExt, NameList};
use crate::kex::{
EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER, KexCause,
};
Expand Down Expand Up @@ -194,6 +194,15 @@ pub(crate) fn parse_kex_algo_list(list: &str) -> Vec<&str> {
list.split(',').collect()
}

fn host_key_algorithm_names(algo: &Algorithm) -> Vec<String> {
let mut names = vec![algo.to_string()];
let cert_name = algo.to_certificate_type();
if cert_name != algo.as_ref() {
names.push(cert_name);
}
names
}

pub(crate) trait Select {
fn is_server() -> bool;

Expand Down Expand Up @@ -269,11 +278,34 @@ pub(crate) trait Select {
None => pref.key.iter().map(ToOwned::to_owned).collect::<Vec<_>>(),
};

let (key_both_first, key_algorithm) = Self::select(
&possible_host_key_algos[..],
&parse_kex_algo_list(&key_string),
AlgorithmKind::Key,
)?;
let (key_both_first, key_algorithm) = if Self::is_server() {
Self::select(
&possible_host_key_algos[..],
&parse_kex_algo_list(&key_string),
AlgorithmKind::Key,
)?
} else {
// For client-side matching, extend preferred host key names with their
// OpenSSH certificate variants (e.g. "*-cert-v01@openssh.com").
let possible_host_key_algo_names = possible_host_key_algos
.iter()
.flat_map(host_key_algorithm_names)
.collect::<Vec<_>>();

let (key_both_first, key_algorithm_name) = Self::select(
&possible_host_key_algo_names[..],
&parse_kex_algo_list(&key_string),
AlgorithmKind::Key,
)?;

let key_algorithm = if key_algorithm_name.ends_with("-cert-v01@openssh.com") {
Algorithm::new_certificate_ext(&key_algorithm_name)?
} else {
Algorithm::new(&key_algorithm_name)?
};

(key_both_first, key_algorithm)
};

// Cipher

Expand Down Expand Up @@ -470,7 +502,8 @@ pub(crate) fn write_kex(
)
.encode(w)?;
} else {
NameList(prefs.key.iter().map(ToString::to_string).collect()).encode(w)?;
// Support for host cert is added, so post the OpenSSH certificate variants of algorithms to server as well.
NameList(prefs.key.iter().flat_map(host_key_algorithm_names).collect()).encode(w)?;
}

// cipher client to server
Expand Down
12 changes: 6 additions & 6 deletions russh/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ mod compress {

async fn check_server_key(
&mut self,
_server_public_key: &crate::keys::ssh_key::PublicKey,
_server_public_key: &crate::cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
// println!("check_server_key: {:?}", server_public_key);
Ok(true)
Expand Down Expand Up @@ -220,12 +220,12 @@ mod channels {
#[derive(Debug)]
struct Client {}

impl client::Handler for Client {
impl client::Handler for Client {
type Error = crate::Error;

async fn check_server_key(
&mut self,
_server_public_key: &crate::keys::ssh_key::PublicKey,
_server_public_key: &crate::cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down Expand Up @@ -305,7 +305,7 @@ mod channels {

async fn check_server_key(
&mut self,
_server_public_key: &crate::keys::ssh_key::PublicKey,
_server_public_key: &crate::cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down Expand Up @@ -399,7 +399,7 @@ mod channels {

async fn check_server_key(
&mut self,
_server_public_key: &crate::keys::ssh_key::PublicKey,
_server_public_key: &crate::cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down Expand Up @@ -474,7 +474,7 @@ mod channels {

async fn check_server_key(
&mut self,
_server_public_key: &crate::keys::ssh_key::PublicKey,
_server_public_key: &crate::cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down
2 changes: 1 addition & 1 deletion russh/tests/test_backpressure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ struct Client;
impl russh::client::Handler for Client {
type Error = anyhow::Error;

async fn check_server_key(&mut self, _: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
async fn check_server_key(&mut self, _: &russh::cert::PublicKeyOrCertificate) -> Result<bool, Self::Error> {
Ok(true)
}
}
2 changes: 1 addition & 1 deletion russh/tests/test_data_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ struct Client;
impl russh::client::Handler for Client {
type Error = anyhow::Error;

async fn check_server_key(&mut self, _: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
async fn check_server_key(&mut self, _: &russh::cert::PublicKeyOrCertificate) -> Result<bool, Self::Error> {
Ok(true)
}
}
4 changes: 2 additions & 2 deletions russh/tests/test_kex_shared_secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ impl client::Handler for TestClientWithKexCapture {

async fn check_server_key(
&mut self,
_server_public_key: &ssh_key::PublicKey,
_server_public_key: &cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down Expand Up @@ -363,7 +363,7 @@ impl client::Handler for TestClientWithRekeyCapture {

async fn check_server_key(
&mut self,
_server_public_key: &ssh_key::PublicKey,
_server_public_key: &cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down
2 changes: 1 addition & 1 deletion russh/tests/test_mlkem_kex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ impl client::Handler for TestClient {

async fn check_server_key(
&mut self,
_server_public_key: &ssh_key::PublicKey,
_server_public_key: &cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down
2 changes: 1 addition & 1 deletion russh/tests/test_rekey_strict_kex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ impl client::Handler for TestClient {

async fn check_server_key(
&mut self,
_server_public_key: &ssh_key::PublicKey,
_server_public_key: &cert::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
Expand Down