aboutsummaryrefslogtreecommitdiffhomepage
path: root/oxish
diff options
context:
space:
mode:
Diffstat (limited to 'oxish')
-rw-r--r--oxish/src/authentication.rs378
-rw-r--r--oxish/src/lib.rs11
-rw-r--r--oxish/src/server.rs2
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(