diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | oxish-proto/Cargo.toml | 1 | ||||
| -rw-r--r-- | oxish-proto/src/auth.rs | 163 | ||||
| -rw-r--r-- | oxish/src/authentication.rs | 163 | ||||
| -rw-r--r-- | oxish/src/lib.rs | 2 | ||||
| -rw-r--r-- | oxish/src/tests.rs | 3 |
6 files changed, 166 insertions, 167 deletions
@@ -564,6 +564,7 @@ dependencies = [ name = "oxish-proto" version = "0.1.0" dependencies = [ + "data-encoding", "sha1", "sha2", "thiserror", diff --git a/oxish-proto/Cargo.toml b/oxish-proto/Cargo.toml index 40b26c7..000939b 100644 --- a/oxish-proto/Cargo.toml +++ b/oxish-proto/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["ssh", "protocol", "cryptography"] categories = ["network-programming", "cryptography"] [dependencies] +data-encoding = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } zeroize = { workspace = true } diff --git a/oxish-proto/src/auth.rs b/oxish-proto/src/auth.rs index a7ce60c..843dfc0 100644 --- a/oxish-proto/src/auth.rs +++ b/oxish-proto/src/auth.rs @@ -1,13 +1,152 @@ -use core::{ops::Deref, str}; -use std::borrow::Cow; +use core::{fmt, str}; +use std::{borrow::Cow, sync::Arc}; -use tracing::warn; +use tracing::{debug, warn}; use crate::{ Decode, Decoded, Encode, IncomingPacket, MessageType, ProtoError, - named::{MethodName, OutgoingNameList, PublicKeyAlgorithm, ServiceName}, + crypto::{CryptoProvider, VerifyingKey}, + named::{MethodName, Named, OutgoingNameList, PublicKeyAlgorithm, ServiceName}, }; +/// An authorized public key for a user +#[derive(Clone)] +pub struct AuthorizedKey { + algorithm: PublicKeyAlgorithm<'static>, + blob: Vec<u8>, + key: Arc<dyn VerifyingKey>, +} + +impl AuthorizedKey { + /// Build an `AuthorizedKey` from a string in the format used in `authorized_keys` + pub fn from_str(s: &str, provider: &dyn CryptoProvider) -> Option<Self> { + let key = match s.split_once('#') { + Some((contents, _)) => contents, + None => s, + } + .trim(); + + if key.is_empty() { + return None; + } + + let mut parts = key.split_whitespace(); + let Some(alg) = parts.next() else { + debug!("missing algorithm"); + return None; + }; + + // TODO: support options before key type + let algorithm = PublicKeyAlgorithm::typed(alg); + let Some(key_data) = parts.next() else { + debug!("missing key data"); + return None; + }; + + let Ok(blob) = data_encoding::BASE64.decode(key_data.as_bytes()) else { + debug!("invalid base64 key data"); + return None; + }; + + let Ok(Decoded { + value: key_type, + next, + }) = <&[u8]>::decode(&blob) + else { + debug!("failed to decode key blob"); + return None; + }; + + if key_type != algorithm.name().as_bytes() { + debug!(?key_type, ?algorithm, "key type does not match algorithm"); + return None; + } + + let key = match algorithm { + PublicKeyAlgorithm::EcdsaSha2Nistp256 => { + let Ok(Decoded { next, .. }) = <&[u8]>::decode(next) else { + debug!("invalid public key data"); + return None; + }; + + let Ok(Decoded { value, next }) = <&[u8]>::decode(next) else { + debug!("invalid public key data"); + return None; + }; + + if !next.is_empty() { + debug!("trailing data after ECDSA public key"); + return None; + } + + let Ok(key) = provider.verifying_key(value, &algorithm) else { + debug!("failed to build verifying key"); + return None; + }; + + key + } + PublicKeyAlgorithm::Ed25519 => { + let Ok(Decoded { value, next }) = <&[u8]>::decode(next) else { + debug!("invalid public key data"); + return None; + }; + + if !next.is_empty() { + debug!("trailing data after ED25519 public key"); + return None; + } + + let Ok(key) = provider.verifying_key(value, &algorithm) else { + debug!("failed to build verifying key"); + return None; + }; + + key + } + PublicKeyAlgorithm::Unknown(_) => { + debug!(?algorithm, "unsupported public key algorithm"); + return None; + } + }; + + Some(Self { + algorithm: algorithm.to_owned(), + key, + blob, + }) + } + + /// Verify a signature over the given message + pub fn verify( + &self, + message: SignatureInput, + signature: EncodedSignature, + ) -> Result<(), ProtoError> { + self.key + .verify(&message.0, &signature.0) + .map_err(|_| ProtoError::InvalidPacket("invalid signature")) + } + + /// Check whether the given public key matches this authorized key + pub fn matches(&self, public_key: &PublicKey<'_>) -> bool { + self.algorithm == public_key.algorithm && self.blob.as_slice() == public_key.key_blob + } + + /// Get the public key algorithm for this authorized key + pub fn algorithm(&self) -> &PublicKeyAlgorithm<'_> { + &self.algorithm + } +} + +impl fmt::Debug for AuthorizedKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AuthorizedKey") + .field("algorithm", &self.algorithm) + .finish_non_exhaustive() + } +} + /// The `SSH_MSG_USERAUTH_REQUEST` message /// /// Sent by the client to start or continue authentication. @@ -275,14 +414,6 @@ impl<'a> Decode<'a> for Signature<'a> { /// Constructed by [`Signature::encode()`]. pub struct EncodedSignature(Vec<u8>); -impl Deref for EncodedSignature { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - /// The `SSH_MSG_USERAUTH_FAILURE` message /// /// See <https://www.rfc-editor.org/rfc/rfc4252#section-5.1>. @@ -370,14 +501,6 @@ impl<'a> SignatureData<'a> { /// Constructed by [`SignatureData::encode()`]. pub struct SignatureInput(Vec<u8>); -impl Deref for SignatureInput { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - /// The `SSH_MSG_SERVICE_ACCEPT` message /// /// See <https://www.rfc-editor.org/rfc/rfc4253#section-10>. diff --git a/oxish/src/authentication.rs b/oxish/src/authentication.rs index 705d26a..8d60362 100644 --- a/oxish/src/authentication.rs +++ b/oxish/src/authentication.rs @@ -21,18 +21,17 @@ use std::{ }, path::{Path, PathBuf}, str, - sync::Arc, }; use libc::{_SC_GETPW_R_SIZE_MAX, O_DIRECTORY, O_RDONLY, getpwnam_r, getpwuid_r, sysconf}; use proto::{ - Decode, Decoded, Disconnect, DisconnectReason, MessageType, ProtoError, + Disconnect, DisconnectReason, MessageType, ProtoError, auth::{ - Method, ServiceAccept, ServiceRequest, Signature, SignatureData, UserAuthPkOk, + AuthorizedKey, Method, ServiceAccept, ServiceRequest, SignatureData, UserAuthPkOk, UserAuthRequest, }, - crypto::{CryptoError, CryptoProvider, Digest, VerifyingKey}, - named::{Named, PublicKeyAlgorithm, ServiceName}, + crypto::{CryptoError, CryptoProvider, Digest}, + named::{PublicKeyAlgorithm, ServiceName}, }; use tokio::{ io::{AsyncRead, AsyncWrite}, @@ -161,13 +160,10 @@ async fn inner<T: AsyncRead + AsyncWrite + Unpin>( } }; - let authorized_key = user.keys.iter().find(|key| { - key.algorithm == public_key.algorithm && key.blob.as_slice() == public_key.key_blob - }); - + 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()), + (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, @@ -212,10 +208,20 @@ async fn inner<T: AsyncRead + AsyncWrite + Unpin>( 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 authorized_key.verify(message, sig).await { - Ok(()) => { + 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()); }; @@ -616,136 +622,3 @@ fn check_permissions(file: &File, uid: u32, level: &str) -> ControlFlow<()> { false => ControlFlow::Break(()), } } - -/// An authorized public key for a user -#[derive(Clone)] -pub struct AuthorizedKey { - algorithm: PublicKeyAlgorithm<'static>, - blob: Vec<u8>, - key: Arc<dyn VerifyingKey>, -} - -impl AuthorizedKey { - /// Build an `AuthorizedKey` from a string in the format used in `authorized_keys` - pub fn from_str(s: &str, provider: &dyn CryptoProvider) -> Option<Self> { - let key = match s.split_once('#') { - Some((contents, _)) => contents, - None => s, - } - .trim(); - - if key.is_empty() { - return None; - } - - let mut parts = key.split_whitespace(); - let Some(alg) = parts.next() else { - debug!("missing algorithm"); - return None; - }; - - // TODO: support options before key type - let algorithm = PublicKeyAlgorithm::typed(alg); - let Some(key_data) = parts.next() else { - debug!("missing key data"); - return None; - }; - - let Ok(blob) = data_encoding::BASE64.decode(key_data.as_bytes()) else { - debug!("invalid base64 key data"); - return None; - }; - - let Ok(Decoded { - value: key_type, - next, - }) = <&[u8]>::decode(&blob) - else { - debug!("failed to decode key blob"); - return None; - }; - - if key_type != algorithm.name().as_bytes() { - debug!(?key_type, ?algorithm, "key type does not match algorithm"); - return None; - } - - let key = match algorithm { - PublicKeyAlgorithm::EcdsaSha2Nistp256 => { - let Ok(Decoded { next, .. }) = <&[u8]>::decode(next) else { - debug!("invalid public key data"); - return None; - }; - - let Ok(Decoded { value, next }) = <&[u8]>::decode(next) else { - debug!("invalid public key data"); - return None; - }; - - if !next.is_empty() { - debug!("trailing data after ECDSA public key"); - return None; - } - - let Ok(key) = provider.verifying_key(value, &algorithm) else { - debug!("failed to build verifying key"); - return None; - }; - - key - } - PublicKeyAlgorithm::Ed25519 => { - let Ok(Decoded { value, next }) = <&[u8]>::decode(next) else { - debug!("invalid public key data"); - return None; - }; - - if !next.is_empty() { - debug!("trailing data after ED25519 public key"); - return None; - } - - let Ok(key) = provider.verifying_key(value, &algorithm) else { - debug!("failed to build verifying key"); - return None; - }; - - key - } - PublicKeyAlgorithm::Unknown(_) => { - debug!(?algorithm, "unsupported public key algorithm"); - return None; - } - }; - - Some(Self { - algorithm: algorithm.to_owned(), - key, - blob, - }) - } - - async fn verify( - &self, - message: SignatureData<'_>, - signature: Signature<'_>, - ) -> Result<(), ProtoError> { - let encoded = message.encode(); - let signature = signature.encode()?; - let key = self.key.clone(); - spawn_blocking(move || { - key.verify(&encoded, &signature) - .map_err(|_| ProtoError::InvalidPacket("invalid signature")) - }) - .await - .map_err(|_| ProtoError::InvalidPacket("signature verification task failed"))? - } -} - -impl fmt::Debug for AuthorizedKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AuthorizedKey") - .field("algorithm", &self.algorithm) - .finish_non_exhaustive() - } -} diff --git a/oxish/src/lib.rs b/oxish/src/lib.rs index 9de9891..65b3554 100644 --- a/oxish/src/lib.rs +++ b/oxish/src/lib.rs @@ -48,7 +48,7 @@ pub use graviola::DEFAULT_PROVIDER; compile_error!("no crypto providers enabled -- enable at least one to fix this error"); mod authentication; -pub use authentication::{AuthorizedKey, DefaultStore, User, UserStore, Username}; +pub use authentication::{DefaultStore, User, UserStore, Username}; mod session; pub use session::Session; mod server; diff --git a/oxish/src/tests.rs b/oxish/src/tests.rs index 349c19b..4937154 100644 --- a/oxish/src/tests.rs +++ b/oxish/src/tests.rs @@ -3,6 +3,7 @@ use std::{env, fs, panic::resume_unwind, path::PathBuf, process::Stdio, sync::On use proto::{ Decoded, Encode, + auth::AuthorizedKey, crypto::{CryptoProvider, Digest, KeySourceSide}, key_exchange::{HostKeys, Identities, ServerHostKey}, named::{EncryptionAlgorithm, PublicKeyAlgorithm}, @@ -13,7 +14,7 @@ use zeroize::Zeroizing; use crate::{ SessionState, SideState, Username, - authentication::{AuthorizedKey, SingleUser, User}, + authentication::{SingleUser, User}, server::Server, }; |
