diff options
Diffstat (limited to 'oxish/src')
| -rw-r--r-- | oxish/src/authentication.rs | 378 | ||||
| -rw-r--r-- | oxish/src/lib.rs | 11 | ||||
| -rw-r--r-- | oxish/src/server.rs | 2 |
3 files changed, 221 insertions, 170 deletions
diff --git a/oxish/src/authentication.rs b/oxish/src/authentication.rs index 551f291..3dd9c67 100644 --- a/oxish/src/authentication.rs +++ b/oxish/src/authentication.rs @@ -1,6 +1,7 @@ use core::{ ffi::c_char, fmt, + future::poll_fn, ops::{ControlFlow, Deref}, time::Duration, }; @@ -22,13 +23,13 @@ use std::{ use libc::{_SC_GETPW_R_SIZE_MAX, ERANGE, getpwnam_r, getpwuid_r, sysconf}; use proto::{ - Disconnect, DisconnectReason, MessageType, ProtoError, + Disconnect, DisconnectReason, Encoder, IncomingPacket, MessageType, ProtoError, auth::{ AuthorizedKey, Method, ServiceAccept, ServiceRequest, SignatureData, UserAuthPkOk, UserAuthRequest, }, crypto::{CryptoError, CryptoProvider, Digest}, - named::{PublicKeyAlgorithm, ServiceName}, + named::{MethodName, PublicKeyAlgorithm, ServiceName}, }; use rustix::fs::{Mode, OFlags, openat}; use tokio::{ @@ -38,24 +39,49 @@ use tokio::{ }; use tracing::{debug, error, info, instrument, warn}; -use crate::{Connection, Error, receive}; +use crate::{Connection, Error, receive, send}; -/// Authenticate a user over the given SSH connection #[instrument(name = "authentication", skip(session_id, conn, store, provider), fields(addr = %conn.addr))] pub(crate) async fn authenticate<T: AsyncRead + AsyncWrite + Unpin>( session_id: &Digest, conn: &mut Connection<T>, store: &dyn UserStore, provider: &dyn CryptoProvider, -) -> Result<User, Error> { - let future = inner(session_id, conn, store, provider); - if let Ok(result) = timeout(Duration::from_secs(60), future).await { - if let Err(error) = &result { - let disconnect = match error { +) -> anyhow::Result<User> { + let mut state = AuthenticationState::default(); + let future = async { + loop { + let packet = receive(&mut conn.stream, &mut conn.read).await?; + let mut encoder = Encoder::new(&mut conn.write); + let handled = state + .handle(packet, session_id, &mut encoder, store, provider) + .await; + + let sent = poll_fn(|cx| send(&mut conn.stream, encoder.write, cx)).await; + match (handled, sent) { + (Ok(AuthenticationState::Complete(user)), Ok(())) => return Ok(user), + (Ok(next), Ok(())) => state = next, + (Err(error), _) | (_, Err(error)) => return Err(error), + } + } + }; + + let (error, disconnect) = match timeout(Duration::from_secs(60), future).await { + Ok(Ok(user)) => return Ok(user), + Ok(Err(error)) => { + let disconnect = match &error { Error::Auth(AuthError::TooManyAttempts) => Disconnect { reason_code: DisconnectReason::ByApplication, description: "too many authentication attempts", }, + Error::InvalidState(description) => Disconnect { + reason_code: DisconnectReason::ByApplication, + description, + }, + Error::InvalidUsername => Disconnect { + reason_code: DisconnectReason::IllegalUserName, + description: "invalid username", + }, Error::Proto(ProtoError::ServiceNotAvailable(description)) => Disconnect { reason_code: DisconnectReason::ServiceNotAvailable, description, @@ -66,176 +92,205 @@ pub(crate) async fn authenticate<T: AsyncRead + AsyncWrite + Unpin>( }, }; - conn.send(&disconnect).await?; + (error, disconnect) } - - return result; - } - - error!("authentication timed out"); - let disconnect = Disconnect { - reason_code: DisconnectReason::ByApplication, - description: "authentication timed out", + Err(_) => ( + Error::Io(io::Error::from(io::ErrorKind::TimedOut)), + Disconnect { + reason_code: DisconnectReason::ByApplication, + description: "authentication timed out", + }, + ), }; let _ = timeout(Duration::from_secs(1), conn.send(&disconnect)).await; - Err(Error::Io(io::Error::from(io::ErrorKind::TimedOut))) + Err(error.into()) } -async fn inner<T: AsyncRead + AsyncWrite + Unpin>( - session_id: &Digest, - conn: &mut Connection<T>, - store: &dyn UserStore, - provider: &dyn CryptoProvider, -) -> Result<User, Error> { - let packet = receive(&mut conn.stream, &mut conn.read).await?; - let service_request = ServiceRequest::try_from(packet)?; - if service_request.service_name != ServiceName::UserAuth { - error!( - service_name = ?service_request.service_name, - "unsupported service requested" - ); - - return Err(ProtoError::ServiceNotAvailable( - "only user authentication service is supported", - ) - .into()); - } - - let service_accept = ServiceAccept { - service_name: ServiceName::UserAuth, - }; - conn.send(&service_accept).await?; - - let mut cached_user = None::<CachedUser>; - let mut attempts = 6; - loop { - attempts -= 1; - if attempts == 0 { - return Err(AuthError::TooManyAttempts.into()); - } +#[derive(Default)] +enum AuthenticationState { + #[default] + AwaitServiceRequest, + AwaitAuthRequest { + cached: Option<CachedUser>, + attempts: u8, + }, + Complete(User), +} - let packet = receive(&mut conn.stream, &mut conn.read).await?; - if matches!( - packet.message_type, - MessageType::Ignore | MessageType::Debug - ) { - continue; - } +impl AuthenticationState { + pub(crate) async fn handle( + self, + packet: IncomingPacket<'_>, + session_id: &Digest, + encoder: &mut Encoder<'_>, + store: &dyn UserStore, + provider: &dyn CryptoProvider, + ) -> Result<Self, Error> { + match (self, packet.message_type) { + (state, MessageType::Ignore | MessageType::Debug) => Ok(state), + (_, MessageType::Disconnect) => Err(AuthError::Canceled.into()), + (Self::AwaitServiceRequest, MessageType::ServiceRequest) => { + match ServiceRequest::try_from(packet)?.service_name { + ServiceName::UserAuth => { + encoder.enqueue(&ServiceAccept { + service_name: ServiceName::UserAuth, + })?; + Ok(Self::AwaitAuthRequest { + cached: None, + attempts: 6, + }) + } + service_name => { + error!(?service_name, "unsupported service requested"); + Err(ProtoError::ServiceNotAvailable( + "only user authentication service is supported", + ) + .into()) + } + } + } + ( + Self::AwaitAuthRequest { + mut cached, + mut attempts, + }, + MessageType::UserAuthRequest, + ) => { + attempts -= 1; + if attempts == 0 { + error!("too many authentication attempts"); + return Err(AuthError::TooManyAttempts.into()); + } - let user_auth_request = UserAuthRequest::try_from(packet)?; - debug!(?user_auth_request, "received user auth request"); - if user_auth_request.service_name != ServiceName::Connection { - error!( - service_name = ?user_auth_request.service_name, - "unsupported service requested" - ); - - return Err( - ProtoError::ServiceNotAvailable("only connection service is supported").into(), - ); - } + let user_auth_request = UserAuthRequest::try_from(packet)?; + debug!(?user_auth_request, "received user auth request"); + if user_auth_request.service_name != ServiceName::Connection { + error!( + service_name = ?user_auth_request.service_name, + "unsupported service requested" + ); - let Method::PublicKey(public_key) = user_auth_request.method else { - warn!( - method = ?user_auth_request.method, - "unsupported authentication method requested" - ); - conn.send_auth_failed().await?; - continue; - }; + return Err(ProtoError::ServiceNotAvailable( + "only connection service is supported", + ) + .into()); + } - let user = match &mut cached_user { - Some(user) if &*user.data.name == user_auth_request.user_name => user, - _ => { - let Ok(name) = Username::try_from(user_auth_request.user_name.to_owned()) else { - conn.send_auth_failed().await?; - continue; + let Method::PublicKey(public_key) = user_auth_request.method else { + warn!( + method = ?user_auth_request.method, + "unsupported authentication method requested" + ); + encoder.send_auth_failed(SUPPORTED_METHODS)?; + return Ok(Self::AwaitAuthRequest { cached, attempts }); }; - let Some(user) = store.lookup(name) else { - conn.send_auth_failed().await?; - continue; + let user = match &mut cached { + Some(user) if &*user.data.name == user_auth_request.user_name => user, + _ => { + let Ok(name) = Username::try_from(user_auth_request.user_name.to_owned()) + else { + encoder.send_auth_failed(SUPPORTED_METHODS)?; + return Ok(Self::AwaitAuthRequest { cached, attempts }); + }; + + let Some(user) = store.lookup(name) else { + encoder.send_auth_failed(SUPPORTED_METHODS)?; + return Ok(Self::AwaitAuthRequest { cached, attempts }); + }; + + let keys = store.keys(&user, provider); + cached.insert(CachedUser { data: user, keys }) + } }; - let keys = store.keys(&user, provider); - cached_user.insert(CachedUser { data: user, keys }) - } - }; - - let authorized_key = user.keys.iter().find(|key| key.matches(&public_key)); - let (sig, authorized_key) = match (public_key.signature, authorized_key) { - // Signature, authorized key => verify signature - (Some(sig), Some(key)) if &sig.algorithm == key.algorithm() => (sig, key.clone()), - // Signature, no authorized key => verify signature against fake key - (Some(sig), None) => ( - sig, - match fake_key(&public_key.algorithm, provider) { - Ok(key) => key, - Err(_) => { - warn!(algorithm = ?public_key.algorithm, "unsupported public key algorithm"); - conn.send_auth_failed().await?; - continue; + let authorized_key = user.keys.iter().find(|key| key.matches(&public_key)); + let (sig, authorized_key) = match (public_key.signature, authorized_key) { + // Signature, authorized key => verify signature + (Some(sig), Some(key)) if &sig.algorithm == key.algorithm() => { + (sig, key.clone()) + } + // Signature, no authorized key => verify signature against fake key + (Some(sig), None) => ( + sig, + match fake_key(&public_key.algorithm, provider) { + Ok(key) => key, + Err(_) => { + warn!(algorithm = ?public_key.algorithm, "unsupported public key algorithm"); + encoder.send_auth_failed(SUPPORTED_METHODS)?; + return Ok(Self::AwaitAuthRequest { cached, attempts }); + } + }, + ), + // Signature, authorized key but mismatched algorithms => fail authentication without verifying signature + (Some(_), Some(_)) => { + warn!( + algorithm = ?public_key.algorithm, + "mismatched signature algorithm in authentication request" + ); + encoder.send_auth_failed(SUPPORTED_METHODS)?; + return Ok(Self::AwaitAuthRequest { cached, attempts }); + } + // No signature, authorized key => send pk-ok and wait for signature + (None, Some(_)) => { + let pk_ok = UserAuthPkOk { + algorithm: public_key.algorithm.to_owned(), + key_blob: Cow::Owned(public_key.key_blob.to_vec()), + }; + debug!(ok = ?pk_ok, "sending pk-ok for user"); + encoder.enqueue(&pk_ok)?; + return Ok(Self::AwaitAuthRequest { cached, attempts }); + } + // No signature, no authorized key => fail authentication + (None, None) => { + encoder.send_auth_failed(SUPPORTED_METHODS)?; + return Ok(Self::AwaitAuthRequest { cached, attempts }); } - }, - ), - // Signature, authorized key but mismatched algorithms => fail authentication without verifying signature - (Some(_), Some(_)) => { - warn!( - algorithm = ?public_key.algorithm, - "mismatched signature algorithm in authentication request" - ); - conn.send_auth_failed().await?; - continue; - } - // No signature, authorized key => send pk-ok and wait for signature - (None, Some(_)) => { - let pk_ok = UserAuthPkOk { - algorithm: public_key.algorithm.to_owned(), - key_blob: Cow::Owned(public_key.key_blob.to_vec()), }; - debug!(ok = ?pk_ok, "sending pk-ok for user"); - conn.send(&pk_ok).await?; - continue; - } - // No signature, no authorized key => fail authentication - (None, None) => { - conn.send_auth_failed().await?; - continue; - } - }; - - let message = SignatureData { - session_id: session_id.as_ref(), - user_name: &user.data.name, - service_name: user_auth_request.service_name, - algorithm: public_key.algorithm, - public_key: public_key.key_blob, - } - .encode(); - - let signature = match sig.encode() { - Ok(signature) => signature, - Err(error) => { - debug!(%error, "failed to encode signature"); - conn.send_auth_failed().await?; - continue; - } - }; - match spawn_blocking(move || authorized_key.verify(message, signature)).await { - Ok(Ok(())) => { - let Some(user) = cached_user else { - return Err(ProtoError::Unreachable("must have cached user").into()); + let message = SignatureData { + session_id: session_id.as_ref(), + user_name: &user.data.name, + service_name: user_auth_request.service_name, + algorithm: public_key.algorithm, + public_key: public_key.key_blob, + } + .encode(); + + let signature = match sig.encode() { + Ok(signature) => signature, + Err(error) => { + debug!(%error, "failed to encode signature"); + encoder.send_auth_failed(SUPPORTED_METHODS)?; + return Ok(Self::AwaitAuthRequest { cached, attempts }); + } }; - info!(user = %user.data.name, "authentication successful"); - conn.send(&MessageType::UserAuthSuccess).await?; - break Ok(user.data); + match spawn_blocking(move || authorized_key.verify(message, signature)).await { + Ok(Ok(())) => { + let Some(user) = cached else { + return Err(ProtoError::Unreachable("must have cached user").into()); + }; + + info!(user = %user.data.name, "authentication successful"); + encoder.enqueue(&MessageType::UserAuthSuccess)?; + Ok(Self::Complete(user.data)) + } + _ => { + encoder.send_auth_failed(SUPPORTED_METHODS)?; + Ok(Self::AwaitAuthRequest { cached, attempts }) + } + } } - _ => { - conn.send_auth_failed().await?; - continue; + (_, _) => { + error!( + message_type = ?packet.message_type, + "unexpected packet received during authentication" + ); + Err(Error::InvalidState( + "unexpected packet received during authentication", + )) } } } @@ -674,11 +729,16 @@ fn check_permissions(file: &File, uid: u32, level: &str) -> ControlFlow<()> { /// Errors that can occur during authentication #[derive(Debug, Error)] pub enum AuthError { + /// The client canceled the authentication process + #[error("canceled by the client")] + Canceled, /// Too many authentication attempts for a single connection #[error("too many authentication attempts")] TooManyAttempts, } +const SUPPORTED_METHODS: &[MethodName<'_>] = &[MethodName::PublicKey]; + #[cfg(test)] mod tests { use super::*; diff --git a/oxish/src/lib.rs b/oxish/src/lib.rs index d1c299c..6f36bb6 100644 --- a/oxish/src/lib.rs +++ b/oxish/src/lib.rs @@ -15,7 +15,6 @@ use anyhow::Context as _; use proto::{ Completion, Decode, Decoded, Encode, Identification, IdentificationError, Ignore, IncomingPacket, PROTOCOL, ProtoError, ReadState, WriteState, - auth::UserAuthFailure, crypto::{ CryptoError, CryptoProvider, Digest, HandshakeBuffer, HandshakeHash, KeyLengths, KeySourceSide, @@ -24,7 +23,7 @@ use proto::{ EcdhKeyExchangeInit, HostKeys, Identities, KeyExchange, KeySourceSet, NewKeys, Rekey, ServerHostKey, SessionHostKey, StrictKeyExchange, }, - named::{EncryptionAlgorithm, ExtensionId, MethodName}, + named::{EncryptionAlgorithm, ExtensionId}, }; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -222,14 +221,6 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Connection<T> { Ok((exchange, identities)) } - async fn send_auth_failed(&mut self) -> Result<(), Error> { - self.send(&UserAuthFailure { - can_continue: &[MethodName::PublicKey], - partial_success: false, - }) - .await - } - async fn send(&mut self, payload: &impl Encode) -> Result<(), Error> { self.send_handshake(payload, None).await } diff --git a/oxish/src/server.rs b/oxish/src/server.rs index 2a0a472..7f91705 100644 --- a/oxish/src/server.rs +++ b/oxish/src/server.rs @@ -135,8 +135,8 @@ impl Server { let user = authenticate(&session_id, &mut conn, &*self.store, self.provider) .await .context("authentication failed")?; - drop(authenticating); + if !self.config.spawn { let host_key = SessionHostKey::from_server(host_key, self.provider)?; let session = Session::new( |
