-
Notifications
You must be signed in to change notification settings - Fork 1
Closed
Labels
priority:highHigh priority issueHigh priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request
Description
Summary
Implement public key authentication verification for bssh-server. This allows users to authenticate using their SSH key pairs, matching OpenSSH behavior.
Parent Epic
- Implement bssh-server with SFTP/SCP support #123 - bssh-server 추가 구현
- Depends on: Implement basic SSH server handler with russh #125 (basic SSH server handler)
Implementation Details
1. Create Auth Provider Infrastructure
// src/server/auth/mod.rs
pub mod password;
pub mod publickey;
pub mod provider;
pub use provider::{AuthProvider, AuthResult};// src/server/auth/provider.rs
use async_trait::async_trait;
use russh::keys::ssh_key::PublicKey;
/// Result of an authentication attempt
#[derive(Debug, Clone)]
pub enum AuthResult {
/// Authentication successful
Accept,
/// Authentication failed
Reject,
/// Partial success (multi-factor auth)
Partial { remaining_methods: Vec<String> },
}
/// User information returned after successful auth
#[derive(Debug, Clone)]
pub struct UserInfo {
pub username: String,
pub home_dir: PathBuf,
pub shell: PathBuf,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub env: HashMap<String, String>,
}
/// Trait for authentication providers (extensible for future methods)
#[async_trait]
pub trait AuthProvider: Send + Sync {
/// Verify public key authentication
async fn verify_publickey(
&self,
username: &str,
key: &PublicKey,
) -> Result<AuthResult>;
/// Verify password authentication
async fn verify_password(
&self,
username: &str,
password: &str,
) -> Result<AuthResult>;
/// Get user information after successful authentication
async fn get_user_info(&self, username: &str) -> Result<Option<UserInfo>>;
/// Check if a user exists
async fn user_exists(&self, username: &str) -> Result<bool>;
}2. Implement Public Key Verifier
// src/server/auth/publickey.rs
use russh::keys::ssh_key::PublicKey;
use std::path::{Path, PathBuf};
/// Configuration for public key authentication
pub struct PublicKeyAuthConfig {
/// Directory containing authorized_keys files
/// Structure: {dir}/{username}/authorized_keys
pub authorized_keys_dir: PathBuf,
/// Alternative: single file path template with {user} placeholder
pub authorized_keys_pattern: Option<String>,
}
/// Public key authentication verifier
pub struct PublicKeyVerifier {
config: PublicKeyAuthConfig,
}
impl PublicKeyVerifier {
pub fn new(config: PublicKeyAuthConfig) -> Self {
Self { config }
}
/// Verify if the given public key is authorized for the user
pub async fn verify(&self, username: &str, key: &PublicKey) -> Result<bool> {
// Validate username to prevent path traversal
let username = crate::shared::validation::validate_username(username)?;
// Load authorized keys for user
let authorized_keys = self.load_authorized_keys(&username).await?;
// Check if key matches any authorized key
for authorized_key in authorized_keys {
if self.keys_match(key, &authorized_key) {
tracing::info!("Public key authentication successful for {}", username);
return Ok(true);
}
}
tracing::debug!("No matching authorized key found for {}", username);
Ok(false)
}
/// Load authorized keys for a user
async fn load_authorized_keys(&self, username: &str) -> Result<Vec<AuthorizedKey>> {
let path = self.get_authorized_keys_path(username);
if !path.exists() {
tracing::debug!("No authorized_keys file for user {}: {:?}", username, path);
return Ok(Vec::new());
}
let content = tokio::fs::read_to_string(&path).await
.context("Failed to read authorized_keys file")?;
self.parse_authorized_keys(&content)
}
/// Get path to authorized_keys file for user
fn get_authorized_keys_path(&self, username: &str) -> PathBuf {
if let Some(pattern) = &self.config.authorized_keys_pattern {
PathBuf::from(pattern.replace("{user}", username))
} else {
self.config.authorized_keys_dir
.join(username)
.join("authorized_keys")
}
}
/// Parse authorized_keys file content
fn parse_authorized_keys(&self, content: &str) -> Result<Vec<AuthorizedKey>> {
let mut keys = Vec::new();
for line in content.lines() {
let line = line.trim();
// Skip empty lines and comments
if line.is_empty() || line.starts_with('#') {
continue;
}
// Parse authorized_keys format:
// [options] key-type base64-key [comment]
match self.parse_authorized_key_line(line) {
Ok(key) => keys.push(key),
Err(e) => {
tracing::warn!("Failed to parse authorized_keys line: {}", e);
continue;
}
}
}
Ok(keys)
}
/// Parse a single authorized_keys line
fn parse_authorized_key_line(&self, line: &str) -> Result<AuthorizedKey> {
// Use russh-keys to parse the public key
let public_key = russh::keys::parse_public_key_base64(line)
.or_else(|_| {
// Try parsing as full line (with key type prefix)
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let key_data = format!("{} {}", parts[0], parts[1]);
russh::keys::parse_public_key_base64(&parts[1])
} else {
Err(russh::keys::Error::CouldNotReadKey)
}
})?;
Ok(AuthorizedKey {
key: public_key,
comment: None,
options: AuthKeyOptions::default(),
})
}
/// Check if two public keys match
fn keys_match(&self, key1: &PublicKey, key2: &AuthorizedKey) -> bool {
// Compare key fingerprints
key1 == &key2.key
}
}
#[derive(Debug)]
pub struct AuthorizedKey {
pub key: PublicKey,
pub comment: Option<String>,
pub options: AuthKeyOptions,
}
#[derive(Debug, Default)]
pub struct AuthKeyOptions {
pub command: Option<String>,
pub environment: Vec<String>,
pub from: Vec<String>,
pub no_pty: bool,
pub no_port_forwarding: bool,
}3. Integrate with SSH Handler
// Update src/server/handler.rs
impl Handler for SshHandler {
async fn auth_publickey(
&mut self,
user: &str,
public_key: &russh::keys::ssh_key::PublicKey,
) -> Result<Auth, Self::Error> {
// Rate limit check
if let Some(addr) = &self.peer_addr {
if self.config.security.rate_limiter.is_rate_limited(&addr.to_string()).await {
tracing::warn!("Rate limited auth attempt from {}", addr);
return Ok(Auth::Reject { proceed_with_methods: None });
}
}
// Verify public key
match self.config.auth_provider.verify_publickey(user, public_key).await? {
AuthResult::Accept => {
self.user = Some(user.to_string());
tracing::info!("Public key auth accepted for user: {}", user);
Ok(Auth::Accept)
}
AuthResult::Reject => {
tracing::debug!("Public key auth rejected for user: {}", user);
Ok(Auth::Reject {
proceed_with_methods: Some(MethodSet::PASSWORD),
})
}
AuthResult::Partial { remaining_methods } => {
// For future multi-factor auth support
Ok(Auth::Partial {
name: "".into(),
instructions: "".into(),
prompts: vec![],
})
}
}
}
}4. Configuration Support
# Server config example
auth:
methods:
- publickey
- password
publickey:
authorized_keys_dir: /etc/bssh/authorized_keys/
# OR
authorized_keys_pattern: "/home/{user}/.ssh/authorized_keys"Reference Code
Reuse validation from existing client code:
src/security/validation.rs:validate_username()src/ssh/auth.rs- Timing attack mitigation patterns
Files to Create/Modify
| File | Action |
|---|---|
src/server/auth/mod.rs |
Create - Auth module exports |
src/server/auth/provider.rs |
Create - AuthProvider trait |
src/server/auth/publickey.rs |
Create - Public key verifier |
src/server/handler.rs |
Modify - Integrate auth_publickey |
src/server/config/types.rs |
Modify - Add auth config |
Testing Requirements
- Unit test: Parse authorized_keys file formats
- Unit test: Key comparison logic
- Unit test: Path traversal prevention in username
- Integration test: Authenticate with OpenSSH client using key
# Generate test key
ssh-keygen -t ed25519 -f /tmp/test_key -N ""
# Add to authorized_keys
mkdir -p /etc/bssh/authorized_keys/testuser
cat /tmp/test_key.pub > /etc/bssh/authorized_keys/testuser/authorized_keys
# Test authentication
ssh -i /tmp/test_key -p 2222 testuser@localhostAcceptance Criteria
-
AuthProvidertrait defined with extensible interface -
PublicKeyVerifierimplementation complete - authorized_keys file parsing (standard OpenSSH format)
- Key comparison using russh-keys
- Username validation to prevent path traversal
- Timing attack mitigation (constant-time comparison where possible)
- Rate limiting integration
- Configuration support for authorized_keys location
- Logging for auth attempts (success/failure)
- Tests passing
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
priority:highHigh priority issueHigh priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request