Implement SSH Host Key Management (Phase 1): Generate/Load/Rotate Ed25519 keys
This commit is contained in:
2
markbase-core/config/ssh_host_keys/ssh_host_ed25519_key
Normal file
2
markbase-core/config/ssh_host_keys/ssh_host_ed25519_key
Normal file
@@ -0,0 +1,2 @@
|
||||
O<EFBFBD><EFBFBD>7<0F>.J<>BK<42>6<><36>w<EFBFBD><77>
|
||||
<11><>螎N<E89E8E>t&<26>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"created_at": 1781989019,
|
||||
"expires_at": 1813525019,
|
||||
"fingerprint": "ROdbODpphK5Kg7obS0fqzJyZJDpo5qszDrNvph/DqxQ=",
|
||||
"key_type": "ed25519"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/pxhXVCfsQXAOGk6/QBTZf4HPMwfLwqc63Prps4366 markbase_ssh_host_key
|
||||
596
markbase-core/src/ssh_server/host_key.rs
Normal file
596
markbase-core/src/ssh_server/host_key.rs
Normal 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 "));
|
||||
}
|
||||
}
|
||||
@@ -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 hash(H参数)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user