Skip to content

Commit efb9a13

Browse files
wi-adamEugeny
andauthored
feat(auth): add certificate-based authentication via SSH agent (#632)
Co-authored-by: Eugene <inbox@null.page>
1 parent 359d708 commit efb9a13

7 files changed

Lines changed: 1249 additions & 60 deletions

File tree

russh/src/auth.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ use ssh_key::{Certificate, HashAlg, PrivateKey};
2222
use thiserror::Error;
2323
use tokio::io::{AsyncRead, AsyncWrite};
2424

25-
use crate::CryptoVec;
2625
use crate::helpers::NameList;
2726
use crate::keys::PrivateKeyWithHashAlg;
27+
use crate::keys::agent::AgentIdentity;
2828

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

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

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

182182
#[allow(clippy::manual_async_fn)]
183-
fn auth_publickey_sign(
183+
fn auth_sign(
184184
&mut self,
185-
key: &ssh_key::PublicKey,
185+
key: &AgentIdentity,
186186
hash_alg: Option<HashAlg>,
187-
to_sign: CryptoVec,
188-
) -> impl Future<Output = Result<CryptoVec, Self::Error>> {
187+
to_sign: Vec<u8>,
188+
) -> impl Future<Output = Result<Vec<u8>, Self::Error>> {
189189
async move {
190190
self.sign_request(key, hash_alg, to_sign)
191191
.await
@@ -212,6 +212,12 @@ pub enum Method {
212212
key: ssh_key::PublicKey,
213213
hash_alg: Option<HashAlg>,
214214
},
215+
/// Certificate-based authentication using an external signer (e.g., SSH agent).
216+
/// The certificate is sent to the server, but signing is delegated to the signer.
217+
FutureCertificate {
218+
cert: Certificate,
219+
hash_alg: Option<HashAlg>,
220+
},
215221
KeyboardInteractive {
216222
submethods: String,
217223
},

russh/src/client/encrypted.rs

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ use super::IncomingSshPacket;
2525
use crate::auth::AuthRequest;
2626
use crate::cert::PublicKeyOrCertificate;
2727
use crate::client::{Handler, Msg, Prompt, Reply, Session};
28-
use crate::helpers::{sign_with_hash_alg, AlgorithmExt, EncodedExt, NameList};
28+
use crate::helpers::{AlgorithmExt, EncodedExt, NameList, sign_with_hash_alg};
2929
use crate::keys::key::parse_public_key;
3030
use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage};
3131
use crate::session::{Encrypted, EncryptedState, GlobalRequestResponse};
3232
use crate::{
33-
auth, map_err, msg, Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, CryptoVec, Error,
34-
MethodSet, Sig,
33+
Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, Error, MethodSet, Sig, auth,
34+
map_err, msg,
3535
};
3636

3737
impl Session {
@@ -120,7 +120,9 @@ impl Session {
120120
let remaining_methods: MethodSet =
121121
(&map_err!(NameList::decode(&mut r))?).into();
122122
let partial_success = map_err!(u8::decode(&mut r))? != 0;
123-
debug!("remaining methods {remaining_methods:?}, partial success {partial_success:?}");
123+
debug!(
124+
"remaining methods {remaining_methods:?}, partial success {partial_success:?}"
125+
);
124126
auth_request.methods = remaining_methods.clone();
125127

126128
let no_more_methods = auth_request.methods.is_empty();
@@ -185,7 +187,7 @@ impl Session {
185187
let responses = loop {
186188
match self.receiver.recv().await {
187189
Some(Msg::AuthInfoResponse { responses }) => {
188-
break responses
190+
break responses;
189191
}
190192
None => return Err(crate::Error::RecvError.into()),
191193
_ => {}
@@ -227,19 +229,13 @@ impl Session {
227229
)?;
228230
let len = self.common.buffer.len();
229231
let buf = std::mem::take(&mut self.common.buffer);
230-
// Convert Vec<u8>→CryptoVec at the Signer
231-
// trait boundary (public API uses CryptoVec).
232-
let mut cv = CryptoVec::new();
233-
cv.extend(&buf);
234232

235233
self.sender
236-
.send(Reply::SignRequest { key, data: cv })
234+
.send(Reply::SignRequest { key, data: buf })
237235
.map_err(|_| crate::Error::SendError)?;
238236
self.common.buffer = loop {
239237
match self.receiver.recv().await {
240-
Some(Msg::Signed { data }) => {
241-
break data[..].to_vec()
242-
}
238+
Some(Msg::Signed { data }) => break data[..].to_vec(),
243239
None => return Err(crate::Error::RecvError.into()),
244240
_ => {}
245241
}
@@ -252,6 +248,39 @@ impl Session {
252248
})
253249
}
254250
}
251+
Some(auth::Method::FutureCertificate { cert, hash_alg }) => {
252+
debug!("certificate");
253+
self.common.buffer.clear();
254+
let i = enc.client_make_to_sign(
255+
&self.common.auth_user,
256+
&PublicKeyOrCertificate::Certificate(cert.clone()),
257+
&mut self.common.buffer,
258+
)?;
259+
let len = self.common.buffer.len();
260+
let buf = std::mem::take(&mut self.common.buffer);
261+
262+
self.sender
263+
.send(Reply::SignRequestCert {
264+
cert,
265+
hash_alg,
266+
data: buf,
267+
})
268+
.map_err(|_| crate::Error::SendError)?;
269+
self.common.buffer = loop {
270+
match self.receiver.recv().await {
271+
Some(Msg::Signed { data }) => break data,
272+
None => return Err(crate::Error::RecvError.into()),
273+
_ => {}
274+
}
275+
};
276+
if self.common.buffer.len() != len {
277+
// The buffer was modified.
278+
push_packet!(enc.write, {
279+
#[allow(clippy::indexing_slicing)] // length checked
280+
enc.write.extend(&self.common.buffer[i..]);
281+
})
282+
}
283+
}
255284
_ => {}
256285
}
257286
}
@@ -413,11 +442,7 @@ impl Session {
413442
}
414443

415444
if let Some(chan) = self.channels.get(&channel_num) {
416-
let _ = chan
417-
.send(ChannelMsg::Data {
418-
data: data.clone(),
419-
})
420-
.await;
445+
let _ = chan.send(ChannelMsg::Data { data: data.clone() }).await;
421446
}
422447

423448
client.data(channel_num, &data, self).await
@@ -867,9 +892,7 @@ impl Session {
867892
}
868893
EncryptedState::InitCompression | EncryptedState::Authenticated => false,
869894
};
870-
debug!(
871-
"write_auth_request_if_needed: is_waiting = {is_waiting:?}"
872-
);
895+
debug!("write_auth_request_if_needed: is_waiting = {is_waiting:?}");
873896
if is_waiting {
874897
enc.write_auth_request(user, &meth)?;
875898
let auth_request = AuthRequest::new(&meth);
@@ -946,6 +969,18 @@ impl Encrypted {
946969
key.to_bytes()?.as_slice().encode(&mut self.write)?;
947970
true
948971
}
972+
auth::Method::FutureCertificate { ref cert, .. } => {
973+
user.as_bytes().encode(&mut self.write)?;
974+
"ssh-connection".encode(&mut self.write)?;
975+
"publickey".encode(&mut self.write)?;
976+
self.write.push(0); // This is a probe
977+
978+
cert.algorithm()
979+
.to_certificate_type()
980+
.encode(&mut self.write)?;
981+
cert.to_bytes()?.as_slice().encode(&mut self.write)?;
982+
true
983+
}
949984
auth::Method::KeyboardInteractive { ref submethods } => {
950985
debug!("Keyboard interactive");
951986
user.as_bytes().encode(&mut self.write)?;

russh/src/client/mod.rs

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ use crate::session::{CommonSession, EncryptedState, GlobalRequestResponse, NewKe
6969
use crate::ssh_read::SshRead;
7070
use crate::sshbuffer::{IncomingSshPacket, PacketWriter, SSHBuffer, SshId};
7171
use crate::{
72-
ChannelId, ChannelOpenFailure, CryptoVec, Disconnect, Error, Limits, MethodSet, Sig, auth,
73-
map_err, msg, negotiation,
72+
ChannelId, ChannelOpenFailure, Disconnect, Error, Limits, MethodSet, Sig, auth, map_err, msg,
73+
negotiation,
7474
};
7575

7676
mod encrypted;
@@ -118,7 +118,12 @@ enum Reply {
118118
ChannelOpenFailure,
119119
SignRequest {
120120
key: ssh_key::PublicKey,
121-
data: CryptoVec,
121+
data: Vec<u8>,
122+
},
123+
SignRequestCert {
124+
cert: Certificate,
125+
hash_alg: Option<HashAlg>,
126+
data: Vec<u8>,
122127
},
123128
AuthInfoRequest {
124129
name: String,
@@ -138,7 +143,7 @@ pub enum Msg {
138143
responses: Vec<String>,
139144
},
140145
Signed {
141-
data: CryptoVec,
146+
data: Vec<u8>,
142147
},
143148
ChannelOpenSession {
144149
channel_ref: ChannelRef,
@@ -481,7 +486,69 @@ impl<H: Handler> Handle<H> {
481486
});
482487
}
483488
Some(Reply::SignRequest { key, data }) => {
484-
let data = signer.auth_publickey_sign(&key, hash_alg, data).await;
489+
let data = signer.auth_sign(&key.into(), hash_alg, data).await;
490+
let data = match data {
491+
Ok(data) => data,
492+
Err(e) => return Err(e),
493+
};
494+
if self.sender.send(Msg::Signed { data }).await.is_err() {
495+
return Err((crate::SendError {}).into());
496+
}
497+
}
498+
None => {
499+
return Ok(AuthResult::Failure {
500+
remaining_methods: MethodSet::empty(),
501+
partial_success: false,
502+
});
503+
}
504+
_ => {}
505+
}
506+
}
507+
}
508+
509+
/// Authenticate using a certificate with a custom signer that implements the
510+
/// [`Signer`][auth::Signer] trait. This is for certificate-based authentication
511+
/// where the signing is delegated to an external signer (e.g., SSH agent).
512+
///
513+
/// For RSA certificates, you can specify the hash algorithm to use.
514+
pub async fn authenticate_certificate_with<U: Into<String>, S: auth::Signer>(
515+
&mut self,
516+
user: U,
517+
cert: Certificate,
518+
hash_alg: Option<HashAlg>,
519+
signer: &mut S,
520+
) -> Result<AuthResult, S::Error> {
521+
let user = user.into();
522+
if self
523+
.sender
524+
.send(Msg::Authenticate {
525+
user,
526+
method: auth::Method::FutureCertificate { cert, hash_alg },
527+
})
528+
.await
529+
.is_err()
530+
{
531+
return Err((crate::SendError {}).into());
532+
}
533+
loop {
534+
let reply = self.receiver.recv().await;
535+
match reply {
536+
Some(Reply::AuthSuccess) => return Ok(AuthResult::Success),
537+
Some(Reply::AuthFailure {
538+
proceed_with_methods: remaining_methods,
539+
partial_success,
540+
}) => {
541+
return Ok(AuthResult::Failure {
542+
remaining_methods,
543+
partial_success,
544+
});
545+
}
546+
Some(Reply::SignRequestCert {
547+
cert,
548+
hash_alg,
549+
data,
550+
}) => {
551+
let data = signer.auth_sign(&cert.into(), hash_alg, data).await;
485552
let data = match data {
486553
Ok(data) => data,
487554
Err(e) => return Err(e),
@@ -816,12 +883,14 @@ impl<H: Handler> Handle<H> {
816883
///
817884
/// This is useful for server-initiated channels; for channels created by
818885
/// the client, prefer to use the Channel returned from the `open_*` methods.
819-
pub async fn data(&self, id: ChannelId, data: impl Into<bytes::Bytes>) -> Result<(), bytes::Bytes> {
886+
pub async fn data(
887+
&self,
888+
id: ChannelId,
889+
data: impl Into<bytes::Bytes>,
890+
) -> Result<(), bytes::Bytes> {
820891
let data = data.into();
821892
self.sender
822-
.send(Msg::Channel(id, ChannelMsg::Data {
823-
data: data.clone(),
824-
}))
893+
.send(Msg::Channel(id, ChannelMsg::Data { data: data.clone() }))
825894
.await
826895
.map_err(|e| match e.0 {
827896
Msg::Channel(_, ChannelMsg::Data { data, .. }) => data,

0 commit comments

Comments
 (0)