Skip to content
Merged
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
24 changes: 15 additions & 9 deletions russh/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ use ssh_key::{Certificate, HashAlg, PrivateKey};
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite};

use crate::CryptoVec;
use crate::helpers::NameList;
use crate::keys::PrivateKeyWithHashAlg;
use crate::keys::agent::AgentIdentity;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethodKind {
Expand Down Expand Up @@ -157,12 +157,12 @@ impl AuthResult {
pub trait Signer: Sized {
type Error: From<crate::SendError>;

fn auth_publickey_sign(
fn auth_sign(
&mut self,
key: &ssh_key::PublicKey,
key: &AgentIdentity,
hash_alg: Option<HashAlg>,
to_sign: CryptoVec,
) -> impl Future<Output = Result<CryptoVec, Self::Error>> + Send;
to_sign: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, Self::Error>> + Send;
}

#[derive(Debug, Error)]
Expand All @@ -180,12 +180,12 @@ impl<R: AsyncRead + AsyncWrite + Unpin + Send + 'static> Signer
type Error = AgentAuthError;

#[allow(clippy::manual_async_fn)]
fn auth_publickey_sign(
fn auth_sign(
&mut self,
key: &ssh_key::PublicKey,
key: &AgentIdentity,
hash_alg: Option<HashAlg>,
to_sign: CryptoVec,
) -> impl Future<Output = Result<CryptoVec, Self::Error>> {
to_sign: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, Self::Error>> {
async move {
self.sign_request(key, hash_alg, to_sign)
.await
Expand All @@ -212,6 +212,12 @@ pub enum Method {
key: ssh_key::PublicKey,
hash_alg: Option<HashAlg>,
},
/// Certificate-based authentication using an external signer (e.g., SSH agent).
/// The certificate is sent to the server, but signing is delegated to the signer.
FutureCertificate {
cert: Certificate,
hash_alg: Option<HashAlg>,
},
KeyboardInteractive {
submethods: String,
},
Expand Down
77 changes: 56 additions & 21 deletions russh/src/client/encrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ use super::IncomingSshPacket;
use crate::auth::AuthRequest;
use crate::cert::PublicKeyOrCertificate;
use crate::client::{Handler, Msg, Prompt, Reply, Session};
use crate::helpers::{sign_with_hash_alg, AlgorithmExt, EncodedExt, NameList};
use crate::helpers::{AlgorithmExt, EncodedExt, NameList, sign_with_hash_alg};
use crate::keys::key::parse_public_key;
use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage};
use crate::session::{Encrypted, EncryptedState, GlobalRequestResponse};
use crate::{
auth, map_err, msg, Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, CryptoVec, Error,
MethodSet, Sig,
Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, Error, MethodSet, Sig, auth,
map_err, msg,
};

impl Session {
Expand Down Expand Up @@ -120,7 +120,9 @@ impl Session {
let remaining_methods: MethodSet =
(&map_err!(NameList::decode(&mut r))?).into();
let partial_success = map_err!(u8::decode(&mut r))? != 0;
debug!("remaining methods {remaining_methods:?}, partial success {partial_success:?}");
debug!(
"remaining methods {remaining_methods:?}, partial success {partial_success:?}"
);
auth_request.methods = remaining_methods.clone();

let no_more_methods = auth_request.methods.is_empty();
Expand Down Expand Up @@ -185,7 +187,7 @@ impl Session {
let responses = loop {
match self.receiver.recv().await {
Some(Msg::AuthInfoResponse { responses }) => {
break responses
break responses;
}
None => return Err(crate::Error::RecvError.into()),
_ => {}
Expand Down Expand Up @@ -227,19 +229,13 @@ impl Session {
)?;
let len = self.common.buffer.len();
let buf = std::mem::take(&mut self.common.buffer);
// Convert Vec<u8>→CryptoVec at the Signer
// trait boundary (public API uses CryptoVec).
let mut cv = CryptoVec::new();
cv.extend(&buf);

self.sender
.send(Reply::SignRequest { key, data: cv })
.send(Reply::SignRequest { key, data: buf })
.map_err(|_| crate::Error::SendError)?;
self.common.buffer = loop {
match self.receiver.recv().await {
Some(Msg::Signed { data }) => {
break data[..].to_vec()
}
Some(Msg::Signed { data }) => break data[..].to_vec(),
None => return Err(crate::Error::RecvError.into()),
_ => {}
}
Expand All @@ -252,6 +248,39 @@ impl Session {
})
}
}
Some(auth::Method::FutureCertificate { cert, hash_alg }) => {
debug!("certificate");
self.common.buffer.clear();
let i = enc.client_make_to_sign(
&self.common.auth_user,
&PublicKeyOrCertificate::Certificate(cert.clone()),
&mut self.common.buffer,
)?;
let len = self.common.buffer.len();
let buf = std::mem::take(&mut self.common.buffer);

self.sender
.send(Reply::SignRequestCert {
cert,
hash_alg,
data: buf,
})
.map_err(|_| crate::Error::SendError)?;
self.common.buffer = loop {
match self.receiver.recv().await {
Some(Msg::Signed { data }) => break data,
None => return Err(crate::Error::RecvError.into()),
_ => {}
}
};
if self.common.buffer.len() != len {
// The buffer was modified.
push_packet!(enc.write, {
#[allow(clippy::indexing_slicing)] // length checked
enc.write.extend(&self.common.buffer[i..]);
})
}
}
_ => {}
}
}
Expand Down Expand Up @@ -413,11 +442,7 @@ impl Session {
}

if let Some(chan) = self.channels.get(&channel_num) {
let _ = chan
.send(ChannelMsg::Data {
data: data.clone(),
})
.await;
let _ = chan.send(ChannelMsg::Data { data: data.clone() }).await;
}

client.data(channel_num, &data, self).await
Expand Down Expand Up @@ -867,9 +892,7 @@ impl Session {
}
EncryptedState::InitCompression | EncryptedState::Authenticated => false,
};
debug!(
"write_auth_request_if_needed: is_waiting = {is_waiting:?}"
);
debug!("write_auth_request_if_needed: is_waiting = {is_waiting:?}");
if is_waiting {
enc.write_auth_request(user, &meth)?;
let auth_request = AuthRequest::new(&meth);
Expand Down Expand Up @@ -946,6 +969,18 @@ impl Encrypted {
key.to_bytes()?.as_slice().encode(&mut self.write)?;
true
}
auth::Method::FutureCertificate { ref cert, .. } => {
user.as_bytes().encode(&mut self.write)?;
"ssh-connection".encode(&mut self.write)?;
"publickey".encode(&mut self.write)?;
self.write.push(0); // This is a probe

cert.algorithm()
.to_certificate_type()
.encode(&mut self.write)?;
cert.to_bytes()?.as_slice().encode(&mut self.write)?;
true
}
auth::Method::KeyboardInteractive { ref submethods } => {
debug!("Keyboard interactive");
user.as_bytes().encode(&mut self.write)?;
Expand Down
87 changes: 78 additions & 9 deletions russh/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ use crate::session::{CommonSession, EncryptedState, GlobalRequestResponse, NewKe
use crate::ssh_read::SshRead;
use crate::sshbuffer::{IncomingSshPacket, PacketWriter, SSHBuffer, SshId};
use crate::{
ChannelId, ChannelOpenFailure, CryptoVec, Disconnect, Error, Limits, MethodSet, Sig, auth,
map_err, msg, negotiation,
ChannelId, ChannelOpenFailure, Disconnect, Error, Limits, MethodSet, Sig, auth, map_err, msg,
negotiation,
};

mod encrypted;
Expand Down Expand Up @@ -118,7 +118,12 @@ enum Reply {
ChannelOpenFailure,
SignRequest {
key: ssh_key::PublicKey,
data: CryptoVec,
data: Vec<u8>,
},
SignRequestCert {
cert: Certificate,
hash_alg: Option<HashAlg>,
data: Vec<u8>,
},
AuthInfoRequest {
name: String,
Expand All @@ -138,7 +143,7 @@ pub enum Msg {
responses: Vec<String>,
},
Signed {
data: CryptoVec,
data: Vec<u8>,
},
ChannelOpenSession {
channel_ref: ChannelRef,
Expand Down Expand Up @@ -481,7 +486,69 @@ impl<H: Handler> Handle<H> {
});
}
Some(Reply::SignRequest { key, data }) => {
let data = signer.auth_publickey_sign(&key, hash_alg, data).await;
let data = signer.auth_sign(&key.into(), hash_alg, data).await;
let data = match data {
Ok(data) => data,
Err(e) => return Err(e),
};
if self.sender.send(Msg::Signed { data }).await.is_err() {
return Err((crate::SendError {}).into());
}
}
None => {
return Ok(AuthResult::Failure {
remaining_methods: MethodSet::empty(),
partial_success: false,
});
}
_ => {}
}
}
}

/// Authenticate using a certificate with a custom signer that implements the
/// [`Signer`][auth::Signer] trait. This is for certificate-based authentication
/// where the signing is delegated to an external signer (e.g., SSH agent).
///
/// For RSA certificates, you can specify the hash algorithm to use.
pub async fn authenticate_certificate_with<U: Into<String>, S: auth::Signer>(
&mut self,
user: U,
cert: Certificate,
hash_alg: Option<HashAlg>,
signer: &mut S,
) -> Result<AuthResult, S::Error> {
let user = user.into();
if self
.sender
.send(Msg::Authenticate {
user,
method: auth::Method::FutureCertificate { cert, hash_alg },
})
.await
.is_err()
{
return Err((crate::SendError {}).into());
}
loop {
let reply = self.receiver.recv().await;
match reply {
Some(Reply::AuthSuccess) => return Ok(AuthResult::Success),
Some(Reply::AuthFailure {
proceed_with_methods: remaining_methods,
partial_success,
}) => {
return Ok(AuthResult::Failure {
remaining_methods,
partial_success,
});
}
Some(Reply::SignRequestCert {
cert,
hash_alg,
data,
}) => {
let data = signer.auth_sign(&cert.into(), hash_alg, data).await;
let data = match data {
Ok(data) => data,
Err(e) => return Err(e),
Expand Down Expand Up @@ -816,12 +883,14 @@ impl<H: Handler> Handle<H> {
///
/// This is useful for server-initiated channels; for channels created by
/// the client, prefer to use the Channel returned from the `open_*` methods.
pub async fn data(&self, id: ChannelId, data: impl Into<bytes::Bytes>) -> Result<(), bytes::Bytes> {
pub async fn data(
&self,
id: ChannelId,
data: impl Into<bytes::Bytes>,
) -> Result<(), bytes::Bytes> {
let data = data.into();
self.sender
.send(Msg::Channel(id, ChannelMsg::Data {
data: data.clone(),
}))
.send(Msg::Channel(id, ChannelMsg::Data { data: data.clone() }))
.await
.map_err(|e| match e.0 {
Msg::Channel(_, ChannelMsg::Data { data, .. }) => data,
Expand Down
Loading
Loading