Skip to content

Commit

Permalink
expose the "remaining methods" from auth failure responses
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugeny authored Jan 6, 2025
1 parent 9ab87ef commit 4c7b27a
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 94 deletions.
23 changes: 22 additions & 1 deletion russh-keys/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use ssh_encoding::Encode;
use std::fmt::Debug;

use ssh_encoding::{Decode, Encode};
use ssh_key::private::KeypairData;
use ssh_key::Algorithm;

Expand All @@ -17,10 +19,20 @@ impl<E: Encode> EncodedExt for E {

pub struct NameList(pub Vec<String>);

impl Debug for NameList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}

impl NameList {
pub fn as_encoded_string(&self) -> String {
self.0.join(",")
}

pub fn from_encoded_string(value: &str) -> Self {
Self(value.split(',').map(|x| x.to_string()).collect())
}
}

impl Encode for NameList {
Expand All @@ -33,6 +45,15 @@ impl Encode for NameList {
}
}

impl Decode for NameList {
fn decode(reader: &mut impl ssh_encoding::Reader) -> Result<Self, ssh_encoding::Error> {
let s = String::decode(reader)?;
Ok(Self::from_encoded_string(&s))
}

type Error = ssh_encoding::Error;
}

#[macro_export]
#[doc(hidden)]
#[allow(clippy::crate_in_macro_def)]
Expand Down
4 changes: 2 additions & 2 deletions russh/examples/client_exec_interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,15 @@ impl Session {
.authenticate_publickey(user, PrivateKeyWithHashAlg::new(Arc::new(key_pair), None)?)
.await?;

if !auth_res {
if !auth_res.success() {
anyhow::bail!("Authentication (with publickey) failed");
}
} else {
let auth_res = session
.authenticate_openssh_cert(user, Arc::new(key_pair), openssh_cert.unwrap())
.await?;

if !auth_res {
if !auth_res.success() {
anyhow::bail!("Authentication (with publickey+cert) failed");
}
}
Expand Down
4 changes: 2 additions & 2 deletions russh/examples/client_exec_simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ impl Session {
.authenticate_publickey(user, PrivateKeyWithHashAlg::new(Arc::new(key_pair), None)?)
.await?;

if !auth_res {
anyhow::bail!("Authentication failed");
if !auth_res.success() {
anyhow::bail!("Authentication failed: {auth_res:?}");
}

Ok(Self { session })
Expand Down
1 change: 1 addition & 0 deletions russh/examples/sftp_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ async fn main() {
.authenticate_password("root", "password")
.await
.unwrap()
.success()
{
let channel = session.channel_open_session().await.unwrap();
channel.request_subsystem(true, "sftp").await.unwrap();
Expand Down
159 changes: 102 additions & 57 deletions russh/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
// limitations under the License.
//

use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;

use async_trait::async_trait;
use bitflags::bitflags;
use russh_keys::helpers::NameList;
use russh_keys::key::PrivateKeyWithHashAlg;
use ssh_key::{Certificate, PrivateKey};
Expand All @@ -25,24 +26,106 @@ use tokio::io::{AsyncRead, AsyncWrite};

use crate::CryptoVec;

bitflags! {
/// Set of authentication methods, represented by bit flags.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MethodSet: u32 {
/// The SSH `none` method (no authentication).
const NONE = 1;
/// The SSH `password` method (plaintext passwords).
const PASSWORD = 2;
/// The SSH `publickey` method (sign a challenge sent by the
/// server).
const PUBLICKEY = 4;
/// The SSH `hostbased` method (certain hostnames are allowed
/// by the server).
const HOSTBASED = 8;
/// The SSH `keyboard-interactive` method (answer to a
/// challenge, where the "challenge" can be a password prompt,
/// a bytestring to sign with a smartcard, or something else).
const KEYBOARD_INTERACTIVE = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethodKind {
None,
Password,
PublicKey,
HostBased,
KeyboardInteractive,
}

impl From<&MethodKind> for &'static str {
fn from(value: &MethodKind) -> Self {
match value {
MethodKind::None => "none",
MethodKind::Password => "password",
MethodKind::PublicKey => "publickey",
MethodKind::HostBased => "hostbased",
MethodKind::KeyboardInteractive => "keyboard-interactive",
}
}
}

impl FromStr for MethodKind {
fn from_str(b: &str) -> Result<MethodKind, Self::Err> {
match b {
"none" => Ok(MethodKind::None),
"password" => Ok(MethodKind::Password),
"publickey" => Ok(MethodKind::PublicKey),
"hostbased" => Ok(MethodKind::HostBased),
"keyboard-interactive" => Ok(MethodKind::KeyboardInteractive),
_ => Err(()),
}
}

type Err = ();
}

impl From<&MethodKind> for String {
fn from(value: &MethodKind) -> Self {
<&str>::from(value).to_string()
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MethodSet(Vec<MethodKind>);

impl Deref for MethodSet {
type Target = [MethodKind];

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl From<&MethodSet> for NameList {
fn from(value: &MethodSet) -> Self {
Self(value.iter().map(|x| x.into()).collect())
}
}

impl From<&NameList> for MethodSet {
fn from(value: &NameList) -> Self {
Self(
value
.0
.iter()
.filter_map(|x| MethodKind::from_str(x).ok())
.collect(),
)
}
}

impl MethodSet {
pub fn empty() -> Self {
Self(Vec::new())
}

pub fn all() -> Self {
Self(vec![
MethodKind::None,
MethodKind::Password,
MethodKind::PublicKey,
MethodKind::HostBased,
MethodKind::KeyboardInteractive,
])
}

pub fn remove(&mut self, method: MethodKind) {
self.0.retain(|x| *x != method);
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthResult {
Success,
Failure { remaining_methods: MethodSet },
}

impl AuthResult {
pub fn success(&self) -> bool {
matches!(self, AuthResult::Success)
}
}

Expand Down Expand Up @@ -102,44 +185,6 @@ pub enum Method {
// Hostbased,
}

impl From<MethodSet> for &'static str {
fn from(value: MethodSet) -> Self {
match value {
MethodSet::NONE => "none",
MethodSet::PASSWORD => "password",
MethodSet::PUBLICKEY => "publickey",
MethodSet::HOSTBASED => "hostbased",
MethodSet::KEYBOARD_INTERACTIVE => "keyboard-interactive",
_ => "",
}
}
}

impl From<MethodSet> for String {
fn from(value: MethodSet) -> Self {
<&str>::from(value).to_string()
}
}

impl From<MethodSet> for NameList {
fn from(value: MethodSet) -> Self {
Self(value.into_iter().map(|x| x.into()).collect())
}
}

impl MethodSet {
pub(crate) fn from_str(b: &str) -> Option<MethodSet> {
match b {
"none" => Some(MethodSet::NONE),
"password" => Some(MethodSet::PASSWORD),
"publickey" => Some(MethodSet::PUBLICKEY),
"hostbased" => Some(MethodSet::HOSTBASED),
"keyboard-interactive" => Some(MethodSet::KEYBOARD_INTERACTIVE),
_ => None,
}
}
}

#[doc(hidden)]
#[derive(Debug)]
pub struct AuthRequest {
Expand Down
20 changes: 10 additions & 10 deletions russh/src/client/encrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::ops::Deref;

use bytes::Bytes;
use log::{debug, error, info, trace, warn};
use russh_keys::helpers::{map_err, sign_with_hash_alg, AlgorithmExt, EncodedExt};
use russh_keys::helpers::{map_err, sign_with_hash_alg, AlgorithmExt, EncodedExt, NameList};
use ssh_encoding::{Decode, Encode};

use super::IncomingSshPacket;
Expand All @@ -28,7 +28,8 @@ use crate::keys::key::parse_public_key;
use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage};
use crate::session::{Encrypted, EncryptedState, GlobalRequestResponse};
use crate::{
auth, msg, Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, CryptoVec, Sig,
auth, msg, Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, CryptoVec,
MethodSet, Sig,
};

thread_local! {
Expand Down Expand Up @@ -134,18 +135,17 @@ impl Session {
Some((&msg::USERAUTH_FAILURE, mut r)) => {
debug!("userauth_failure");

let remaining_methods = map_err!(String::decode(&mut r))?;
let remaining_methods: MethodSet =
(&map_err!(NameList::decode(&mut r))?).into();
debug!("remaining methods {remaining_methods:?}",);
auth_request.methods = auth::MethodSet::empty();
for method in remaining_methods.split(',') {
if let Some(m) = auth::MethodSet::from_str(method) {
auth_request.methods |= m
}
}
auth_request.methods = remaining_methods.clone();

let no_more_methods = auth_request.methods.is_empty();
self.common.auth_method = None;
self.sender
.send(Reply::AuthFailure)
.send(Reply::AuthFailure {
proceed_with_methods: remaining_methods,
})
.map_err(|_| crate::Error::SendError)?;

// If no other authentication method is allowed by the server, give up.
Expand Down
Loading

0 comments on commit 4c7b27a

Please sign in to comment.