Implement SSH Host Key Management (Phase 1): Generate/Load/Rotate Ed25519 keys

This commit is contained in:
Warren
2026-06-21 04:57:15 +08:00
parent bb886449d7
commit 56e73ad8a4
7 changed files with 640 additions and 13 deletions

20
Cargo.lock generated
View File

@@ -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"

View File

@@ -0,0 +1,2 @@
O<EFBFBD><EFBFBD>7<0F>.J<>BK<42>6<><36>w<EFBFBD><77>
<11><>螎N<E89E8E>t&<26>

View File

@@ -0,0 +1,6 @@
{
"created_at": 1781989019,
"expires_at": 1813525019,
"fingerprint": "ROdbODpphK5Kg7obS0fqzJyZJDpo5qszDrNvph/DqxQ=",
"key_type": "ed25519"
}

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/pxhXVCfsQXAOGk6/QBTZf4HPMwfLwqc63Prps4366 markbase_ssh_host_key

View File

@@ -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<SystemTime>,
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<Vec<HostKey>> {
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<HostKey> {
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<HostKey> {
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<HostKey> {
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<SigningKey> {
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<SigningKey> {
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<HostKeyInfo> {
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<Vec<HostKeyInfo>> {
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<Vec<HostKeyType>> {
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<SigningKey>,
pub rsa_key: Option<RsaKeyPair>,
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<Vec<u8>> {
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<Vec<u8>> {
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<String> {
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<u8>,
public_key: Vec<u8>,
}
impl RsaKeyPair {
pub fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> {
warn!("RSA signing not implemented, use Ed25519 instead");
Err(anyhow!("RSA signing not implemented"))
}
pub fn public_key_bytes(&self) -> Vec<u8> {
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<Vec<u8>> {
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<u8> {
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 "));
}
}

View File

@@ -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<Curve25519Kex>,
host_key: Ed25519HostKey,
host_key: HostKey,
shared_secret: Option<Vec<u8>>,
client_public_key: Option<Vec<u8>>,
server_public_key: Option<Vec<u8>>,
exchange_hash: Option<Vec<u8>>, // 保存exchange hashH参数
exchange_hash: Option<Vec<u8>>,
client_version: Option<String>,
server_version: Option<String>,
client_kexinit_payload: Option<Vec<u8>>,
@@ -42,10 +43,10 @@ pub struct KexExchangeHandler {
}
impl KexExchangeHandler {
/// 创建密钥交换处理器
pub fn new(kex_result: KexResult) -> Result<Self> {
// 加载或生成服务器主机密钥
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::<BigEndian>(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);
}
}

View File

@@ -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;