/ abzu-core / src / trusted_bootstrap.rs
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(&timestamp.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(&timestamp.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  }