tenseleyflow/hyprkvm / bf0c444

Browse files

feat: add TOFU persistence for certificate fingerprints

- Load known_hosts.toml on daemon startup
- Verify peer fingerprints on incoming and outgoing TLS connections
- Auto-save new peer fingerprints when TOFU is enabled
- Reject connections if fingerprint changed (potential MITM warning)
- Touch last_seen timestamp on successful verification
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
bf0c4448b20f1ec39858a12949b5c2ee4d6b8d00
Parents
0bbef70
Tree
b008ba6

1 changed file

StatusFile+-
M hyprkvm-daemon/src/main.rs 82 0
hyprkvm-daemon/src/main.rsmodified
@@ -300,6 +300,22 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
300300
         info!("TLS is disabled (plain TCP mode for backwards compatibility)");
301301
     }
302302
 
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
+
303319
     // Start network server
304320
     let listen_addr: SocketAddr = format!("0.0.0.0:{}", config.network.listen_port).parse()?;
305321
     let server = if tls_enabled {
@@ -315,6 +331,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
315331
     let machine_name = config.machines.self_name.clone();
316332
     let neighbors_for_accept = config.machines.neighbors.clone();
317333
     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();
318336
     let accept_handle = tokio::spawn(async move {
319337
         loop {
320338
             match server.accept().await {
@@ -327,6 +345,36 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
327345
                         Ok(Some(Message::Hello(hello))) => {
328346
                             info!("Peer {} connected (protocol v{})", hello.machine_name, hello.protocol_version);
329347
 
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
+
330378
                             // Send HelloAck
331379
                             let ack = Message::HelloAck(hyprkvm_common::protocol::HelloAckPayload {
332380
                                 accepted: true,
@@ -393,6 +441,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
393441
         let use_tls = neighbor.tls.unwrap_or(tls_enabled);
394442
         let fingerprint = neighbor.fingerprint.clone();
395443
         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();
396446
 
397447
         tokio::spawn(async move {
398448
             loop {
@@ -440,6 +490,38 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
440490
                         match conn.recv().await {
441491
                             Ok(Some(Message::HelloAck(ack))) => {
442492
                                 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
+
443525
                                     let mut peers = peers_clone.write().await;
444526
                                     if peers.contains_key(&direction) {
445527
                                         info!("Already have connection for {:?}, dropping outbound to {}", direction, ack.machine_name);