trusted_bootstrap.rs
1 //! Trusted Bootstrap Node Verification 2 //! 3 //! Provides cryptographic verification of bootstrap nodes to prevent Sybil attacks 4 //! during network entry. New nodes verify they're connecting to legitimate network 5 //! infrastructure, not attacker-controlled nodes impersonating bootstrap services. 6 //! 7 //! ## Trust Modes 8 //! 9 //! - **Pinned**: Require Ed25519 public keys to match hardcoded values (most secure) 10 //! - **TrustOnFirstUse**: Remember keys on first connect, warn on change (TOFU) 11 //! - **Any**: Accept any bootstrap node (insecure, shows warning) 12 //! 13 //! ## Challenge-Response Protocol 14 //! 15 //! ```text 16 //! Client Bootstrap Node 17 //! | | 18 //! |-------- Hello + challenge ---------->| 19 //! | | 20 //! |<-- HelloAck + identity_signature ----| 21 //! | | 22 //! |-- verify(signature, expected_key) --| 23 //! ``` 24 25 use ed25519_dalek::{Signature, Verifier, VerifyingKey}; 26 use serde::{Deserialize, Serialize}; 27 use std::collections::HashMap; 28 use std::path::Path; 29 use thiserror::Error; 30 31 // ───────────────────────────────────────────────────────────────────────────── 32 // Trust Mode 33 // ───────────────────────────────────────────────────────────────────────────── 34 35 /// Trust mode for bootstrap nodes 36 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] 37 pub enum BootstrapTrustMode { 38 /// Require pinned Ed25519 public keys (most secure) 39 /// Connection fails if bootstrap node's key doesn't match pinned value. 40 Pinned, 41 42 /// Trust on first use, warn on key change (TOFU) 43 /// First connection to an address stores its key; subsequent connections 44 /// must present the same key or a warning is logged. 45 TrustOnFirstUse, 46 47 /// Accept any bootstrap (insecure, shows warning) 48 /// Backward compatible with existing configurations but logs a security warning. 49 #[default] 50 Any, 51 } 52 53 // ───────────────────────────────────────────────────────────────────────────── 54 // Trusted Node Entry 55 // ───────────────────────────────────────────────────────────────────────────── 56 57 /// A trusted bootstrap node entry 58 #[derive(Debug, Clone, Serialize, Deserialize)] 59 pub struct TrustedBootstrapNode { 60 /// Network address (host:port) 61 pub address: String, 62 /// Ed25519 public key (32 bytes) 63 pub pubkey: [u8; 32], 64 /// Optional human-readable label (e.g., "Official EU Node 1") 65 pub label: Option<String>, 66 } 67 68 impl TrustedBootstrapNode { 69 /// Create a new trusted node entry 70 pub fn new(address: impl Into<String>, pubkey: [u8; 32]) -> Self { 71 Self { 72 address: address.into(), 73 pubkey, 74 label: None, 75 } 76 } 77 78 /// Create with a label 79 pub fn with_label(address: impl Into<String>, pubkey: [u8; 32], label: impl Into<String>) -> Self { 80 Self { 81 address: address.into(), 82 pubkey, 83 label: Some(label.into()), 84 } 85 } 86 } 87 88 // ───────────────────────────────────────────────────────────────────────────── 89 // Trust Configuration 90 // ───────────────────────────────────────────────────────────────────────────── 91 92 /// Bootstrap trust configuration 93 #[derive(Debug, Clone, Default)] 94 pub struct BootstrapTrustConfig { 95 /// Trust mode 96 pub mode: BootstrapTrustMode, 97 /// Pinned bootstrap nodes (for Pinned mode) 98 pub pinned_nodes: Vec<TrustedBootstrapNode>, 99 /// Path to TOFU keystore (for TrustOnFirstUse mode) 100 /// Defaults to `{data_dir}/bootstrap_keys.json` if not set 101 pub tofu_keystore_path: Option<String>, 102 } 103 104 impl BootstrapTrustConfig { 105 /// Create a pinned configuration with the given trusted nodes 106 pub fn pinned(nodes: Vec<TrustedBootstrapNode>) -> Self { 107 Self { 108 mode: BootstrapTrustMode::Pinned, 109 pinned_nodes: nodes, 110 tofu_keystore_path: None, 111 } 112 } 113 114 /// Create a TOFU configuration 115 pub fn trust_on_first_use(keystore_path: impl Into<String>) -> Self { 116 Self { 117 mode: BootstrapTrustMode::TrustOnFirstUse, 118 pinned_nodes: Vec::new(), 119 tofu_keystore_path: Some(keystore_path.into()), 120 } 121 } 122 123 /// Create a TOFU configuration with automatic path 124 /// 125 /// The keystore will be created in the node's data directory. 126 /// This is the recommended option for most applications. 127 pub fn trust_on_first_use_auto() -> Self { 128 Self { 129 mode: BootstrapTrustMode::TrustOnFirstUse, 130 pinned_nodes: Vec::new(), 131 tofu_keystore_path: None, // Will use node's data_dir 132 } 133 } 134 135 /// Create an insecure "any" configuration (with warning) 136 pub fn any() -> Self { 137 Self { 138 mode: BootstrapTrustMode::Any, 139 pinned_nodes: Vec::new(), 140 tofu_keystore_path: None, 141 } 142 } 143 144 /// Look up the expected public key for an address (Pinned mode) 145 pub fn get_pinned_key(&self, address: &str) -> Option<&[u8; 32]> { 146 self.pinned_nodes 147 .iter() 148 .find(|n| n.address == address) 149 .map(|n| &n.pubkey) 150 } 151 } 152 153 // ───────────────────────────────────────────────────────────────────────────── 154 // TOFU Keystore 155 // ───────────────────────────────────────────────────────────────────────────── 156 157 /// Trust-on-first-use keystore 158 /// Persists learned bootstrap keys across restarts 159 #[derive(Debug, Clone, Default, Serialize, Deserialize)] 160 pub struct TofuKeystore { 161 /// Learned keys: address → (pubkey, first_seen_timestamp) 162 keys: HashMap<String, ([u8; 32], u64)>, 163 } 164 165 impl TofuKeystore { 166 /// Load from disk, or create empty if file doesn't exist 167 pub fn load(path: &Path) -> Result<Self, TofuError> { 168 if !path.exists() { 169 return Ok(Self::default()); 170 } 171 let data = std::fs::read_to_string(path)?; 172 let keystore: Self = serde_json::from_str(&data)?; 173 Ok(keystore) 174 } 175 176 /// Save to disk 177 pub fn save(&self, path: &Path) -> Result<(), TofuError> { 178 let data = serde_json::to_string_pretty(self)?; 179 std::fs::write(path, data)?; 180 Ok(()) 181 } 182 183 /// Check if we have a stored key for this address 184 pub fn get(&self, address: &str) -> Option<&[u8; 32]> { 185 self.keys.get(address).map(|(k, _)| k) 186 } 187 188 /// Store a key for an address (first use) 189 pub fn store(&mut self, address: String, pubkey: [u8; 32], timestamp: u64) { 190 self.keys.entry(address).or_insert((pubkey, timestamp)); 191 } 192 193 /// Check if a key matches the stored key (returns None if no stored key) 194 pub fn verify(&self, address: &str, pubkey: &[u8; 32]) -> Option<bool> { 195 self.keys.get(address).map(|(stored, _)| stored == pubkey) 196 } 197 198 /// Get the number of stored keys 199 pub fn len(&self) -> usize { 200 self.keys.len() 201 } 202 203 /// Check if keystore is empty 204 pub fn is_empty(&self) -> bool { 205 self.keys.is_empty() 206 } 207 } 208 209 /// TOFU keystore errors 210 #[derive(Debug, Error)] 211 pub enum TofuError { 212 #[error("IO error: {0}")] 213 Io(#[from] std::io::Error), 214 #[error("JSON error: {0}")] 215 Json(#[from] serde_json::Error), 216 } 217 218 // ───────────────────────────────────────────────────────────────────────────── 219 // Verification 220 // ───────────────────────────────────────────────────────────────────────────── 221 222 /// Errors from bootstrap identity verification 223 #[derive(Debug, Error)] 224 pub enum BootstrapVerifyError { 225 #[error("Invalid public key format")] 226 InvalidPublicKey, 227 228 #[error("Invalid signature format (expected 64 bytes, got {0})")] 229 InvalidSignature(usize), 230 231 #[error("Signature verification failed")] 232 SignatureVerificationFailed, 233 234 #[error("Bootstrap node not in trusted list: {0}")] 235 NotTrusted(String), 236 237 #[error("Identity key mismatch for {address} (expected {expected}, got {actual})")] 238 KeyMismatch { 239 address: String, 240 expected: String, 241 actual: String, 242 }, 243 244 #[error("TOFU keystore error: {0}")] 245 TofuError(#[from] TofuError), 246 } 247 248 /// Verify a bootstrap node's identity signature 249 /// 250 /// The signature is over: `challenge || ephemeral_pub || timestamp` 251 /// 252 /// # Arguments 253 /// * `challenge` - 32-byte random nonce sent by client 254 /// * `ephemeral_pub` - Bootstrap node's X25519 ephemeral public key for this session 255 /// * `timestamp` - Timestamp from HelloAck 256 /// * `identity_pubkey` - Bootstrap node's Ed25519 identity public key 257 /// * `signature` - Ed25519 signature over the message 258 /// 259 /// # Returns 260 /// * `Ok(())` if signature is valid 261 /// * `Err(BootstrapVerifyError)` if verification fails 262 pub fn verify_bootstrap_identity( 263 challenge: &[u8; 32], 264 ephemeral_pub: &[u8; 32], 265 timestamp: u64, 266 identity_pubkey: &[u8; 32], 267 signature: &[u8], 268 ) -> Result<(), BootstrapVerifyError> { 269 // Build signed message: challenge || ephemeral_pub || timestamp (72 bytes) 270 let mut message = Vec::with_capacity(72); 271 message.extend_from_slice(challenge); 272 message.extend_from_slice(ephemeral_pub); 273 message.extend_from_slice(×tamp.to_le_bytes()); 274 275 // Parse public key 276 let verifying_key = VerifyingKey::from_bytes(identity_pubkey) 277 .map_err(|_| BootstrapVerifyError::InvalidPublicKey)?; 278 279 // Parse signature (must be exactly 64 bytes) 280 let sig_bytes: [u8; 64] = signature 281 .try_into() 282 .map_err(|_| BootstrapVerifyError::InvalidSignature(signature.len()))?; 283 let sig = Signature::from_bytes(&sig_bytes); 284 285 // Verify 286 verifying_key 287 .verify(&message, &sig) 288 .map_err(|_| BootstrapVerifyError::SignatureVerificationFailed)?; 289 290 Ok(()) 291 } 292 293 /// Create the signed message for bootstrap identity verification 294 /// 295 /// Bootstrap nodes use this to create their identity signature. 296 pub fn create_identity_message(challenge: &[u8; 32], ephemeral_pub: &[u8; 32], timestamp: u64) -> Vec<u8> { 297 let mut message = Vec::with_capacity(72); 298 message.extend_from_slice(challenge); 299 message.extend_from_slice(ephemeral_pub); 300 message.extend_from_slice(×tamp.to_le_bytes()); 301 message 302 } 303 304 // ───────────────────────────────────────────────────────────────────────────── 305 // Utility 306 // ───────────────────────────────────────────────────────────────────────────── 307 308 /// Format a public key as a hex string (for error messages) 309 pub fn format_pubkey(key: &[u8; 32]) -> String { 310 hex::encode(&key[..8]) + "..." 311 } 312 313 // ───────────────────────────────────────────────────────────────────────────── 314 // Tests 315 // ───────────────────────────────────────────────────────────────────────────── 316 317 #[cfg(test)] 318 mod tests { 319 use super::*; 320 use ed25519_dalek::{Signer, SigningKey}; 321 use rand::rngs::OsRng; 322 323 fn generate_keypair() -> (SigningKey, VerifyingKey) { 324 let signing_key = SigningKey::generate(&mut OsRng); 325 let verifying_key = signing_key.verifying_key(); 326 (signing_key, verifying_key) 327 } 328 329 #[test] 330 fn test_verify_valid_signature() { 331 let (signing_key, verifying_key) = generate_keypair(); 332 333 let challenge = [0xAB; 32]; 334 let ephemeral_pub = [0xCD; 32]; 335 let timestamp = 1234567890u64; 336 337 // Create and sign the message 338 let message = create_identity_message(&challenge, &ephemeral_pub, timestamp); 339 let signature = signing_key.sign(&message); 340 341 // Verify 342 let result = verify_bootstrap_identity( 343 &challenge, 344 &ephemeral_pub, 345 timestamp, 346 verifying_key.as_bytes(), 347 signature.to_bytes().as_slice(), 348 ); 349 350 assert!(result.is_ok()); 351 } 352 353 #[test] 354 fn test_verify_invalid_signature() { 355 let (signing_key, verifying_key) = generate_keypair(); 356 357 let challenge = [0xAB; 32]; 358 let ephemeral_pub = [0xCD; 32]; 359 let timestamp = 1234567890u64; 360 361 // Create and sign the message 362 let message = create_identity_message(&challenge, &ephemeral_pub, timestamp); 363 let mut signature = signing_key.sign(&message).to_bytes(); 364 365 // Tamper with the signature 366 signature[0] ^= 0xFF; 367 368 // Verify should fail 369 let result = verify_bootstrap_identity( 370 &challenge, 371 &ephemeral_pub, 372 timestamp, 373 verifying_key.as_bytes(), 374 &signature, 375 ); 376 377 assert!(matches!( 378 result, 379 Err(BootstrapVerifyError::SignatureVerificationFailed) 380 )); 381 } 382 383 #[test] 384 fn test_verify_wrong_pubkey() { 385 let (signing_key, _verifying_key) = generate_keypair(); 386 let (_, wrong_verifying_key) = generate_keypair(); // Different key 387 388 let challenge = [0xAB; 32]; 389 let ephemeral_pub = [0xCD; 32]; 390 let timestamp = 1234567890u64; 391 392 // Sign with correct key 393 let message = create_identity_message(&challenge, &ephemeral_pub, timestamp); 394 let signature = signing_key.sign(&message); 395 396 // Verify with wrong key should fail 397 let result = verify_bootstrap_identity( 398 &challenge, 399 &ephemeral_pub, 400 timestamp, 401 wrong_verifying_key.as_bytes(), 402 signature.to_bytes().as_slice(), 403 ); 404 405 assert!(matches!( 406 result, 407 Err(BootstrapVerifyError::SignatureVerificationFailed) 408 )); 409 } 410 411 #[test] 412 fn test_verify_invalid_pubkey_format() { 413 let challenge = [0xAB; 32]; 414 let ephemeral_pub = [0xCD; 32]; 415 let timestamp = 1234567890u64; 416 417 // Invalid public key (0xFF repeated is not a valid Ed25519 point) 418 // The Ed25519 curve doesn't accept arbitrary 32-byte values 419 let invalid_pubkey = [0xFF; 32]; 420 let fake_signature = [0u8; 64]; 421 422 let result = verify_bootstrap_identity( 423 &challenge, 424 &ephemeral_pub, 425 timestamp, 426 &invalid_pubkey, 427 &fake_signature, 428 ); 429 430 // Either InvalidPublicKey (if parsing fails) or SignatureVerificationFailed (if it parses but sig fails) 431 // Both are acceptable - the key is invalid and won't verify 432 assert!(result.is_err()); 433 } 434 435 #[test] 436 fn test_verify_invalid_signature_length() { 437 let (_, verifying_key) = generate_keypair(); 438 439 let challenge = [0xAB; 32]; 440 let ephemeral_pub = [0xCD; 32]; 441 let timestamp = 1234567890u64; 442 443 // Wrong signature length 444 let short_signature = [0u8; 32]; 445 446 let result = verify_bootstrap_identity( 447 &challenge, 448 &ephemeral_pub, 449 timestamp, 450 verifying_key.as_bytes(), 451 &short_signature, 452 ); 453 454 assert!(matches!( 455 result, 456 Err(BootstrapVerifyError::InvalidSignature(32)) 457 )); 458 } 459 460 #[test] 461 fn test_challenge_binds_to_session() { 462 let (signing_key, verifying_key) = generate_keypair(); 463 464 let challenge1 = [0xAB; 32]; 465 let challenge2 = [0xCD; 32]; // Different challenge 466 let ephemeral_pub = [0xEF; 32]; 467 let timestamp = 1234567890u64; 468 469 // Sign with challenge1 470 let message = create_identity_message(&challenge1, &ephemeral_pub, timestamp); 471 let signature = signing_key.sign(&message); 472 473 // Verify with challenge2 should fail 474 let result = verify_bootstrap_identity( 475 &challenge2, 476 &ephemeral_pub, 477 timestamp, 478 verifying_key.as_bytes(), 479 signature.to_bytes().as_slice(), 480 ); 481 482 assert!(matches!( 483 result, 484 Err(BootstrapVerifyError::SignatureVerificationFailed) 485 )); 486 } 487 488 #[test] 489 fn test_trusted_node_lookup() { 490 let config = BootstrapTrustConfig::pinned(vec![ 491 TrustedBootstrapNode::new("bootstrap1.abzu.network:4433", [0xAA; 32]), 492 TrustedBootstrapNode::new("bootstrap2.abzu.network:4433", [0xBB; 32]), 493 ]); 494 495 assert_eq!( 496 config.get_pinned_key("bootstrap1.abzu.network:4433"), 497 Some(&[0xAA; 32]) 498 ); 499 assert_eq!( 500 config.get_pinned_key("bootstrap2.abzu.network:4433"), 501 Some(&[0xBB; 32]) 502 ); 503 assert_eq!(config.get_pinned_key("unknown:4433"), None); 504 } 505 506 #[test] 507 fn test_tofu_keystore() { 508 let mut keystore = TofuKeystore::default(); 509 510 // First use stores the key 511 keystore.store("node1:4433".to_string(), [0xAA; 32], 1000); 512 513 // Verify returns Some(true) for matching key 514 assert_eq!(keystore.verify("node1:4433", &[0xAA; 32]), Some(true)); 515 516 // Verify returns Some(false) for mismatching key 517 assert_eq!(keystore.verify("node1:4433", &[0xBB; 32]), Some(false)); 518 519 // Verify returns None for unknown address 520 assert_eq!(keystore.verify("unknown:4433", &[0xAA; 32]), None); 521 } 522 }