aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--oxish-proto/Cargo.toml1
-rw-r--r--oxish-proto/src/auth.rs163
-rw-r--r--oxish/src/authentication.rs163
-rw-r--r--oxish/src/lib.rs2
-rw-r--r--oxish/src/tests.rs3
6 files changed, 166 insertions, 167 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3f3b428..ba1b52e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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,
};