diff --git a/Cargo.lock b/Cargo.lock index 14434d1..bc0ca13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2888,6 +2888,7 @@ dependencies = [ "filetree", "flate2", "futures-util", + "hex", "hmac 0.12.1", "log", "md5 0.8.0", @@ -2926,6 +2927,7 @@ dependencies = [ "x25519-dalek", "xz2", "zip", + "zstd 0.13.3", ] [[package]] @@ -7067,6 +7069,15 @@ dependencies = [ "zstd-safe 6.0.6", ] +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe 7.2.4", +] + [[package]] name = "zstd-safe" version = "5.0.2+zstd.1.5.2" @@ -7087,6 +7098,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" diff --git a/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key b/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key new file mode 100644 index 0000000..17d9639 --- /dev/null +++ b/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key @@ -0,0 +1,2 @@ +O7.JBK6w +螎Nt& \ No newline at end of file diff --git a/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key.meta b/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key.meta new file mode 100644 index 0000000..c72ec33 --- /dev/null +++ b/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key.meta @@ -0,0 +1,6 @@ +{ + "created_at": 1781989019, + "expires_at": 1813525019, + "fingerprint": "ROdbODpphK5Kg7obS0fqzJyZJDpo5qszDrNvph/DqxQ=", + "key_type": "ed25519" +} \ No newline at end of file diff --git a/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key.pub b/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key.pub new file mode 100644 index 0000000..8c99909 --- /dev/null +++ b/markbase-core/config/ssh_host_keys/ssh_host_ed25519_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/pxhXVCfsQXAOGk6/QBTZf4HPMwfLwqc63Prps4366 markbase_ssh_host_key diff --git a/markbase-core/src/ssh_server/host_key.rs b/markbase-core/src/ssh_server/host_key.rs new file mode 100644 index 0000000..4d8f077 --- /dev/null +++ b/markbase-core/src/ssh_server/host_key.rs @@ -0,0 +1,596 @@ +use anyhow::{anyhow, Result}; +use ed25519_dalek::{Signer, SigningKey}; +use log::{info, warn}; +use rand::rngs::OsRng; +use std::fs; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime}; + +#[derive(Debug, Clone)] +pub struct HostKeyInfo { + pub key_type: HostKeyType, + pub key_path: PathBuf, + pub created_at: SystemTime, + pub expires_at: Option, + pub fingerprint: String, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum HostKeyType { + Ed25519, + Rsa, +} + +pub struct HostKeyManager { + keys_dir: PathBuf, + rotation_interval: Duration, + max_key_age: Duration, +} + +impl HostKeyManager { + pub fn new(keys_dir: PathBuf) -> Self { + Self { + keys_dir, + rotation_interval: Duration::from_secs(30 * 24 * 3600), + max_key_age: Duration::from_secs(365 * 24 * 3600), + } + } + + pub fn with_rotation_interval(mut self, interval: Duration) -> Self { + self.rotation_interval = interval; + self + } + + pub fn with_max_key_age(mut self, age: Duration) -> Self { + self.max_key_age = age; + self + } + + pub fn ensure_keys_dir(&self) -> Result<()> { + if !self.keys_dir.exists() { + fs::create_dir_all(&self.keys_dir)?; + info!("Created host keys directory: {}", self.keys_dir.display()); + } + Ok(()) + } + + pub fn load_or_generate_all(&self) -> Result> { + self.ensure_keys_dir()?; + + let mut keys = Vec::new(); + + keys.push(self.load_or_generate_ed25519()?); + + if self.should_rotate(&keys[0]) { + info!("Rotating expired Ed25519 host key"); + self.rotate_ed25519()?; + keys[0] = self.load_ed25519()?; + } + + Ok(keys) + } + + fn ed25519_key_path(&self) -> PathBuf { + self.keys_dir.join("ssh_host_ed25519_key") + } + + fn ed25519_pub_path(&self) -> PathBuf { + self.keys_dir.join("ssh_host_ed25519_key.pub") + } + + fn ed25519_meta_path(&self) -> PathBuf { + self.keys_dir.join("ssh_host_ed25519_key.meta") + } + + fn rsa_key_path(&self) -> PathBuf { + self.keys_dir.join("ssh_host_rsa_key") + } + + fn rsa_pub_path(&self) -> PathBuf { + self.keys_dir.join("ssh_host_rsa_key.pub") + } + + pub fn load_or_generate_ed25519(&self) -> Result { + let key_path = self.ed25519_key_path(); + + if key_path.exists() { + info!("Loading existing Ed25519 host key from {}", key_path.display()); + self.load_ed25519() + } else { + info!("Generating new Ed25519 host key at {}", key_path.display()); + self.generate_ed25519() + } + } + + pub fn load_ed25519(&self) -> Result { + let key_path = self.ed25519_key_path(); + let pub_path = self.ed25519_pub_path(); + let meta_path = self.ed25519_meta_path(); + + if !key_path.exists() { + return Err(anyhow!("Ed25519 host key not found at {}", key_path.display())); + } + + let key_data = fs::read(&key_path)?; + let signing_key = self.parse_ed25519_private_key(&key_data)?; + + let fingerprint = if pub_path.exists() { + self.calculate_fingerprint_ed25519(&signing_key) + } else { + self.save_ed25519_public_key(&signing_key, &pub_path)?; + self.calculate_fingerprint_ed25519(&signing_key) + }; + + let created_at = if meta_path.exists() { + self.load_meta(&meta_path)?.created_at + } else { + let info = HostKeyInfo { + key_type: HostKeyType::Ed25519, + key_path: key_path.clone(), + created_at: SystemTime::now(), + expires_at: Some(SystemTime::now() + self.max_key_age), + fingerprint: fingerprint.clone(), + }; + self.save_meta(&meta_path, &info)?; + info.created_at + }; + + Ok(HostKey { + key_type: HostKeyType::Ed25519, + signing_key: Some(signing_key), + rsa_key: None, + fingerprint, + created_at, + }) + } + + pub fn generate_ed25519(&self) -> Result { + let key_path = self.ed25519_key_path(); + let pub_path = self.ed25519_pub_path(); + let meta_path = self.ed25519_meta_path(); + + self.ensure_keys_dir()?; + + let signing_key = SigningKey::generate(&mut OsRng); + let verifying_key = signing_key.verifying_key(); + + self.save_ed25519_private_key(&signing_key, &key_path)?; + self.save_ed25519_public_key(&signing_key, &pub_path)?; + + let fingerprint = self.calculate_fingerprint_ed25519(&signing_key); + + let created_at = SystemTime::now(); + let info = HostKeyInfo { + key_type: HostKeyType::Ed25519, + key_path: key_path.clone(), + created_at, + expires_at: Some(created_at + self.max_key_age), + fingerprint: fingerprint.clone(), + }; + self.save_meta(&meta_path, &info)?; + + info!("Generated Ed25519 host key: {}", key_path.display()); + info!("Public key saved: {}", pub_path.display()); + info!("Fingerprint: SHA256:{}", fingerprint); + + Ok(HostKey { + key_type: HostKeyType::Ed25519, + signing_key: Some(signing_key), + rsa_key: None, + fingerprint, + created_at, + }) + } + + fn parse_ed25519_private_key(&self, data: &[u8]) -> Result { + if data.len() < 32 { + return Err(anyhow!("Invalid Ed25519 private key length: {}", data.len())); + } + + if data.len() == 32 { + let bytes: [u8; 32] = data[..32].try_into()?; + Ok(SigningKey::from_bytes(&bytes)) + } else if data.len() == 64 { + let bytes: [u8; 32] = data[..32].try_into()?; + Ok(SigningKey::from_bytes(&bytes)) + } else if data.starts_with(b"-----BEGIN OPENSSH PRIVATE KEY-----") { + self.parse_openssh_ed25519_key(data) + } else { + Err(anyhow!("Unknown Ed25519 private key format")) + } + } + + fn parse_openssh_ed25519_key(&self, data: &[u8]) -> Result { + let base64_start = data + .iter() + .position(|&b| b == b'\n') + .map(|p| p + 1) + .unwrap_or(0); + + let base64_end = data + .iter() + .rposition(|&b| b == b'\n') + .unwrap_or(data.len()); + + let base64_data = &data[base64_start..base64_end]; + let decoded = base64_decode(base64_data)?; + + if decoded.len() < 64 { + return Err(anyhow!("Decoded Ed25519 key too short")); + } + + let key_bytes: [u8; 32] = decoded[..32].try_into()?; + Ok(SigningKey::from_bytes(&key_bytes)) + } + + fn save_ed25519_private_key(&self, key: &SigningKey, path: &Path) -> Result<()> { + let key_bytes = key.to_bytes(); + + fs::write(path, &key_bytes)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(path, perms)?; + } + + Ok(()) + } + + fn save_ed25519_public_key(&self, key: &SigningKey, path: &Path) -> Result<()> { + let verifying_key = key.verifying_key(); + let public_bytes = verifying_key.as_bytes(); + + let ssh_format = format!( + "ssh-ed25519 {} markbase_ssh_host_key\n", + base64_encode_ssh_ed25519(public_bytes) + ); + + fs::write(path, ssh_format)?; + + Ok(()) + } + + fn calculate_fingerprint_ed25519(&self, key: &SigningKey) -> String { + let verifying_key = key.verifying_key(); + let public_bytes = verifying_key.as_bytes(); + + use sha2::{Digest, Sha256}; + let hash = Sha256::digest(public_bytes); + base64_encode(&hash) + } + + pub fn should_rotate(&self, key: &HostKey) -> bool { + if let Ok(age) = key.created_at.elapsed() { + age > self.max_key_age + } else { + false + } + } + + pub fn rotate_ed25519(&self) -> Result<()> { + let key_path = self.ed25519_key_path(); + let pub_path = self.ed25519_pub_path(); + let meta_path = self.ed25519_meta_path(); + + if key_path.exists() { + let backup_path = self.keys_dir.join(format!( + "ssh_host_ed25519_key.backup.{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() + )); + fs::rename(&key_path, &backup_path)?; + info!("Backed up old key to {}", backup_path.display()); + } + + if pub_path.exists() { + let backup_path = self.keys_dir.join(format!( + "ssh_host_ed25519_key.pub.backup.{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() + )); + fs::rename(&pub_path, &backup_path)?; + } + + if meta_path.exists() { + fs::remove_file(&meta_path)?; + } + + self.generate_ed25519()?; + info!("Rotated Ed25519 host key successfully"); + + Ok(()) + } + + fn save_meta(&self, path: &Path, info: &HostKeyInfo) -> Result<()> { + let created_secs = info + .created_at + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs(); + + let expires_secs = info + .expires_at + .map(|t| { + t.duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() + }); + + let meta = serde_json::json!({ + "key_type": match info.key_type { + HostKeyType::Ed25519 => "ed25519", + HostKeyType::Rsa => "rsa", + }, + "created_at": created_secs, + "expires_at": expires_secs, + "fingerprint": info.fingerprint, + }); + + fs::write(path, serde_json::to_string_pretty(&meta)?)?; + Ok(()) + } + + fn load_meta(&self, path: &Path) -> Result { + let data = fs::read_to_string(path)?; + let meta: serde_json::Value = serde_json::from_str(&data)?; + + let key_type = match meta["key_type"].as_str() { + Some("ed25519") => HostKeyType::Ed25519, + Some("rsa") => HostKeyType::Rsa, + _ => HostKeyType::Ed25519, + }; + + let created_at = SystemTime::UNIX_EPOCH + Duration::from_secs(meta["created_at"].as_u64().unwrap_or(0)); + + let expires_at = meta["expires_at"] + .as_u64() + .map(|s| SystemTime::UNIX_EPOCH + Duration::from_secs(s)); + + let fingerprint = meta["fingerprint"].as_str().unwrap_or("").to_string(); + + Ok(HostKeyInfo { + key_type, + key_path: path.with_extension("").to_path_buf(), + created_at, + expires_at, + fingerprint, + }) + } + + pub fn get_key_info(&self) -> Result> { + self.ensure_keys_dir()?; + + let mut infos = Vec::new(); + + let ed25519_meta = self.ed25519_meta_path(); + if ed25519_meta.exists() { + infos.push(self.load_meta(&ed25519_meta)?); + } + + Ok(infos) + } + + pub fn check_rotation_needed(&self) -> Result> { + let infos = self.get_key_info()?; + let mut need_rotation = Vec::new(); + + for info in infos { + if let Some(expires_at) = info.expires_at { + if SystemTime::now() > expires_at { + need_rotation.push(info.key_type); + } + } + } + + Ok(need_rotation) + } +} + +pub struct HostKey { + pub key_type: HostKeyType, + pub signing_key: Option, + pub rsa_key: Option, + pub fingerprint: String, + pub created_at: SystemTime, +} + +impl HostKey { + pub fn ed25519(signing_key: SigningKey) -> Self { + let verifying_key = signing_key.verifying_key(); + let public_bytes = verifying_key.as_bytes(); + + use sha2::{Digest, Sha256}; + let hash = Sha256::digest(public_bytes); + + Self { + key_type: HostKeyType::Ed25519, + signing_key: Some(signing_key), + rsa_key: None, + fingerprint: base64_encode(&hash), + created_at: SystemTime::now(), + } + } + + pub fn sign(&self, data: &[u8]) -> Result> { + match self.key_type { + HostKeyType::Ed25519 => { + if let Some(key) = &self.signing_key { + let signature = key.sign(data); + Ok(signature.to_bytes().to_vec()) + } else { + Err(anyhow!("Ed25519 signing key not available")) + } + } + HostKeyType::Rsa => { + if let Some(key) = &self.rsa_key { + key.sign(data) + } else { + Err(anyhow!("RSA signing key not available")) + } + } + } + } + + pub fn public_key_bytes(&self) -> Result> { + match self.key_type { + HostKeyType::Ed25519 => { + if let Some(key) = &self.signing_key { + Ok(key.verifying_key().as_bytes().to_vec()) + } else { + Err(anyhow!("Ed25519 public key not available")) + } + } + HostKeyType::Rsa => { + if let Some(key) = &self.rsa_key { + Ok(key.public_key_bytes()) + } else { + Err(anyhow!("RSA public key not available")) + } + } + } + } + + pub fn ssh_public_key_format(&self) -> Result { + match self.key_type { + HostKeyType::Ed25519 => { + let pub_bytes = self.public_key_bytes()?; + Ok(format!( + "ssh-ed25519 {}", + base64_encode_ssh_ed25519(&pub_bytes) + )) + } + HostKeyType::Rsa => Err(anyhow!("RSA SSH public key format not implemented")), + } + } +} + +pub struct RsaKeyPair { + private_key: Vec, + public_key: Vec, +} + +impl RsaKeyPair { + pub fn sign(&self, _data: &[u8]) -> Result> { + warn!("RSA signing not implemented, use Ed25519 instead"); + Err(anyhow!("RSA signing not implemented")) + } + + pub fn public_key_bytes(&self) -> Vec { + self.public_key.clone() + } +} + +fn base64_encode(data: &[u8]) -> String { + use base64::{engine::general_purpose::STANDARD, Engine as _}; + STANDARD.encode(data) +} + +fn base64_decode(data: &[u8]) -> Result> { + use base64::{engine::general_purpose::STANDARD, Engine as _}; + STANDARD + .decode(data) + .map_err(|e| anyhow!("Base64 decode error: {}", e)) +} + +fn base64_encode_ssh_ed25519(public_bytes: &[u8]) -> String { + let mut ssh_format = Vec::new(); + + ssh_format.extend_from_slice(&build_ssh_string(b"ssh-ed25519")); + ssh_format.extend_from_slice(&build_ssh_string(public_bytes)); + + base64_encode(&ssh_format) +} + +fn build_ssh_string(data: &[u8]) -> Vec { + let len = data.len() as u32; + let mut result = Vec::with_capacity(4 + data.len()); + result.extend_from_slice(&len.to_be_bytes()); + result.extend_from_slice(data); + result +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_generate_ed25519_key() { + let temp_dir = TempDir::new().unwrap(); + let manager = HostKeyManager::new(temp_dir.path().to_path_buf()); + + let key = manager.generate_ed25519().unwrap(); + assert_eq!(key.key_type, HostKeyType::Ed25519); + assert!(key.signing_key.is_some()); + assert!(!key.fingerprint.is_empty()); + } + + #[test] + fn test_load_ed25519_key() { + let temp_dir = TempDir::new().unwrap(); + let manager = HostKeyManager::new(temp_dir.path().to_path_buf()); + + manager.generate_ed25519().unwrap(); + let loaded_key = manager.load_ed25519().unwrap(); + + assert_eq!(loaded_key.key_type, HostKeyType::Ed25519); + assert!(loaded_key.signing_key.is_some()); + } + + #[test] + fn test_sign_with_ed25519() { + let temp_dir = TempDir::new().unwrap(); + let manager = HostKeyManager::new(temp_dir.path().to_path_buf()); + + let key = manager.generate_ed25519().unwrap(); + let data = b"test data for signing"; + + let signature = key.sign(data).unwrap(); + assert_eq!(signature.len(), 64); + } + + #[test] + fn test_fingerprint() { + let temp_dir = TempDir::new().unwrap(); + let manager = HostKeyManager::new(temp_dir.path().to_path_buf()); + + let key1 = manager.generate_ed25519().unwrap(); + manager.rotate_ed25519().unwrap(); + let key2 = manager.load_ed25519().unwrap(); + + assert_ne!(key1.fingerprint, key2.fingerprint); + } + + #[test] + fn test_rotation_check() { + let temp_dir = TempDir::new().unwrap(); + let manager = HostKeyManager::new(temp_dir.path().to_path_buf()) + .with_max_key_age(Duration::from_secs(1)); + + manager.generate_ed25519().unwrap(); + + std::thread::sleep(Duration::from_secs(2)); + + let need_rotation = manager.check_rotation_needed().unwrap(); + assert!(need_rotation.contains(&HostKeyType::Ed25519)); + } + + #[test] + fn test_ssh_public_key_format() { + let temp_dir = TempDir::new().unwrap(); + let manager = HostKeyManager::new(temp_dir.path().to_path_buf()); + + let key = manager.generate_ed25519().unwrap(); + let ssh_pub = key.ssh_public_key_format().unwrap(); + + assert!(ssh_pub.starts_with("ssh-ed25519 ")); + } +} \ No newline at end of file diff --git a/markbase-core/src/ssh_server/kex_exchange.rs b/markbase-core/src/ssh_server/kex_exchange.rs index 683bf87..9a98601 100644 --- a/markbase-core/src/ssh_server/kex_exchange.rs +++ b/markbase-core/src/ssh_server/kex_exchange.rs @@ -1,7 +1,8 @@ // SSH密钥交换流程实现(Phase 3) // 参考OpenSSH kex.c: kex_input_kex_init(), kex_send_kex_reply() -use crate::ssh_server::crypto::{Curve25519Kex, Ed25519HostKey, SessionKeys}; +use crate::ssh_server::crypto::{Curve25519Kex, SessionKeys}; +use crate::ssh_server::host_key::{HostKey, HostKeyManager, HostKeyType}; use crate::ssh_server::kex::KexResult; use crate::ssh_server::packet::{PacketType, SshPacket}; use anyhow::{anyhow, Result}; @@ -27,14 +28,14 @@ fn cipher_key_len(algorithm: &str) -> usize { /// SSH密钥交换流程处理器(参考OpenSSH kex.c) pub struct KexExchangeHandler { kex_algorithm: String, - encryption_ctos: String, // Phase fix: store encryption algorithm for key length - encryption_stoc: String, // Phase fix: store encryption algorithm for key length + encryption_ctos: String, + encryption_stoc: String, server_kex: Option, - host_key: Ed25519HostKey, + host_key: HostKey, shared_secret: Option>, client_public_key: Option>, server_public_key: Option>, - exchange_hash: Option>, // 保存exchange hash(H参数) + exchange_hash: Option>, client_version: Option, server_version: Option, client_kexinit_payload: Option>, @@ -42,10 +43,10 @@ pub struct KexExchangeHandler { } impl KexExchangeHandler { - /// 创建密钥交换处理器 pub fn new(kex_result: KexResult) -> Result { - // 加载或生成服务器主机密钥 - let host_key = Ed25519HostKey::load_or_generate("config/ssh_host_ed25519_key")?; + let keys_dir = std::path::PathBuf::from("config/ssh_host_keys"); + let manager = HostKeyManager::new(keys_dir); + let host_key = manager.load_or_generate_ed25519()?; Ok(Self { kex_algorithm: kex_result.kex_algorithm, @@ -180,7 +181,7 @@ impl KexExchangeHandler { blob.write_all("ssh-ed25519".as_bytes())?; // Ed25519公钥(32字节) - let public_key = self.host_key.public_key_bytes(); + let public_key = self.host_key.public_key_bytes()?; blob.write_u32::(32)?; blob.write_all(&public_key)?; @@ -437,6 +438,6 @@ mod tests { let kex_result = KexResult::choose_algorithms(&server_proposal, &client_proposal).unwrap(); let handler = KexExchangeHandler::new(kex_result).unwrap(); - assert!(handler.host_key.public_key_bytes().len() == 32); + assert!(handler.host_key.public_key_bytes().unwrap().len() == 32); } } diff --git a/markbase-core/src/ssh_server/mod.rs b/markbase-core/src/ssh_server/mod.rs index 07dba3e..14ef7ec 100644 --- a/markbase-core/src/ssh_server/mod.rs +++ b/markbase-core/src/ssh_server/mod.rs @@ -4,9 +4,10 @@ pub mod auth; pub mod channel; pub mod cipher; -pub mod compression; // SSH Compression support (RFC 4253 §6.2) +pub mod compression; pub mod crypto; pub mod data_forwarder; +pub mod host_key; pub mod kex; pub mod kex_complete; pub mod kex_exchange; @@ -17,13 +18,13 @@ pub mod rsync_handler; pub mod scp_handler; pub mod server; pub mod sftp_handler; -pub mod ssh_config; // SSH config file support (~/.ssh/config) +pub mod ssh_config; pub mod ssh_security_config; pub mod sshbuf; pub mod upload_hook; pub mod version; pub mod window_manager; -pub mod x11_forward; // SSH X11 forwarding (RFC 4254 §7.2) +pub mod x11_forward; pub use packet::{PacketType, SshPacket}; pub use server::SshServer;