@@ -300,6 +300,22 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 300 | 300 | info!("TLS is disabled (plain TCP mode for backwards compatibility)"); |
| 301 | 301 | } |
| 302 | 302 | |
| 303 | + // Load known hosts for TOFU |
| 304 | + let known_hosts_path = network::KnownHosts::default_path(); |
| 305 | + let known_hosts = match network::KnownHosts::load(&known_hosts_path) { |
| 306 | + Ok(kh) => { |
| 307 | + if !kh.hosts.is_empty() { |
| 308 | + info!("Loaded {} known host(s) from {:?}", kh.hosts.len(), known_hosts_path); |
| 309 | + } |
| 310 | + Arc::new(RwLock::new(kh)) |
| 311 | + } |
| 312 | + Err(e) => { |
| 313 | + tracing::warn!("Failed to load known hosts: {}, starting fresh", e); |
| 314 | + Arc::new(RwLock::new(network::KnownHosts::default())) |
| 315 | + } |
| 316 | + }; |
| 317 | + let tofu_enabled = config.network.tls.tofu; |
| 318 | + |
| 303 | 319 | // Start network server |
| 304 | 320 | let listen_addr: SocketAddr = format!("0.0.0.0:{}", config.network.listen_port).parse()?; |
| 305 | 321 | let server = if tls_enabled { |
@@ -315,6 +331,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 315 | 331 | let machine_name = config.machines.self_name.clone(); |
| 316 | 332 | let neighbors_for_accept = config.machines.neighbors.clone(); |
| 317 | 333 | let peers_for_accept = peers.clone(); |
| 334 | + let known_hosts_for_accept = known_hosts.clone(); |
| 335 | + let known_hosts_path_for_accept = known_hosts_path.clone(); |
| 318 | 336 | let accept_handle = tokio::spawn(async move { |
| 319 | 337 | loop { |
| 320 | 338 | match server.accept().await { |
@@ -327,6 +345,36 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 327 | 345 | Ok(Some(Message::Hello(hello))) => { |
| 328 | 346 | info!("Peer {} connected (protocol v{})", hello.machine_name, hello.protocol_version); |
| 329 | 347 | |
| 348 | + // Verify TLS fingerprint if this is a TLS connection |
| 349 | + if let Some(peer_fp) = conn.peer_fingerprint() { |
| 350 | + let mut kh = known_hosts_for_accept.write().await; |
| 351 | + match kh.is_trusted(&hello.machine_name, peer_fp) { |
| 352 | + network::TrustStatus::Trusted => { |
| 353 | + info!("Peer {} fingerprint verified", hello.machine_name); |
| 354 | + kh.touch(&hello.machine_name); |
| 355 | + } |
| 356 | + network::TrustStatus::Unknown => { |
| 357 | + if tofu_enabled { |
| 358 | + info!("TOFU: Trusting new peer {} with fingerprint {}", hello.machine_name, peer_fp); |
| 359 | + kh.trust_host(&hello.machine_name, peer_fp); |
| 360 | + if let Err(e) = kh.save(&known_hosts_path_for_accept) { |
| 361 | + tracing::error!("Failed to save known hosts: {}", e); |
| 362 | + } |
| 363 | + } else { |
| 364 | + tracing::warn!("Unknown peer {} fingerprint and TOFU disabled, rejecting", hello.machine_name); |
| 365 | + continue; |
| 366 | + } |
| 367 | + } |
| 368 | + network::TrustStatus::Changed { old_fingerprint, new_fingerprint } => { |
| 369 | + tracing::error!( |
| 370 | + "SECURITY WARNING: Peer {} fingerprint CHANGED!\n Old: {}\n New: {}\n This could indicate a man-in-the-middle attack!", |
| 371 | + hello.machine_name, old_fingerprint, new_fingerprint |
| 372 | + ); |
| 373 | + continue; // Reject the connection |
| 374 | + } |
| 375 | + } |
| 376 | + } |
| 377 | + |
| 330 | 378 | // Send HelloAck |
| 331 | 379 | let ack = Message::HelloAck(hyprkvm_common::protocol::HelloAckPayload { |
| 332 | 380 | accepted: true, |
@@ -393,6 +441,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 393 | 441 | let use_tls = neighbor.tls.unwrap_or(tls_enabled); |
| 394 | 442 | let fingerprint = neighbor.fingerprint.clone(); |
| 395 | 443 | let tofu_enabled = config.network.tls.tofu; |
| 444 | + let known_hosts_clone = known_hosts.clone(); |
| 445 | + let known_hosts_path_clone = known_hosts_path.clone(); |
| 396 | 446 | |
| 397 | 447 | tokio::spawn(async move { |
| 398 | 448 | loop { |
@@ -440,6 +490,38 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 440 | 490 | match conn.recv().await { |
| 441 | 491 | Ok(Some(Message::HelloAck(ack))) => { |
| 442 | 492 | if ack.accepted { |
| 493 | + // Verify TLS fingerprint if this is a TLS connection |
| 494 | + if let Some(peer_fp) = conn.peer_fingerprint() { |
| 495 | + let mut kh = known_hosts_clone.write().await; |
| 496 | + match kh.is_trusted(&neighbor_name, peer_fp) { |
| 497 | + network::TrustStatus::Trusted => { |
| 498 | + info!("Peer {} fingerprint verified", neighbor_name); |
| 499 | + kh.touch(&neighbor_name); |
| 500 | + } |
| 501 | + network::TrustStatus::Unknown => { |
| 502 | + if tofu_enabled { |
| 503 | + info!("TOFU: Trusting new peer {} with fingerprint {}", neighbor_name, peer_fp); |
| 504 | + kh.trust_host(&neighbor_name, peer_fp); |
| 505 | + if let Err(e) = kh.save(&known_hosts_path_clone) { |
| 506 | + tracing::error!("Failed to save known hosts: {}", e); |
| 507 | + } |
| 508 | + } else { |
| 509 | + tracing::warn!("Unknown peer {} fingerprint and TOFU disabled, rejecting", neighbor_name); |
| 510 | + tokio::time::sleep(std::time::Duration::from_secs(3)).await; |
| 511 | + continue; |
| 512 | + } |
| 513 | + } |
| 514 | + network::TrustStatus::Changed { old_fingerprint, new_fingerprint } => { |
| 515 | + tracing::error!( |
| 516 | + "SECURITY WARNING: Peer {} fingerprint CHANGED!\n Old: {}\n New: {}\n This could indicate a man-in-the-middle attack!", |
| 517 | + neighbor_name, old_fingerprint, new_fingerprint |
| 518 | + ); |
| 519 | + tokio::time::sleep(std::time::Duration::from_secs(3)).await; |
| 520 | + continue; // Reject and retry |
| 521 | + } |
| 522 | + } |
| 523 | + } |
| 524 | + |
| 443 | 525 | let mut peers = peers_clone.write().await; |
| 444 | 526 | if peers.contains_key(&direction) { |
| 445 | 527 | info!("Already have connection for {:?}, dropping outbound to {}", direction, ack.machine_name); |