//! Node identity and cryptographic keys //! //! Each node has an Ed25519 keypair: //! - Private key: stored locally in `identity.key` (never replicated) //! - Public key: serves as the node's identity (32 bytes) use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use rand::rngs::OsRng; use std::fs; use std::io::{self, Read, Write}; use std::path::Path; use thiserror::Error; /// Errors that can occur during node operations #[derive(Error, Debug)] pub enum NodeError { #[error("IO error: {0}")] Io(#[from] io::Error), #[error("Invalid key length: expected 32 bytes, got {0}")] InvalidKeyLength(usize), #[error("Invalid signature")] InvalidSignature, } /// A node in the Lattice mesh. /// /// Each node has an Ed25519 keypair used for signing sigchain entries /// and establishing trust within the network. #[derive(Clone)] pub struct NodeIdentity { signing_key: SigningKey, } impl NodeIdentity { /// Generate a new node with a random keypair. pub fn generate() -> Self { let signing_key = SigningKey::generate(&mut OsRng); Self { signing_key } } /// Create a node from an existing signing key. pub fn from_signing_key(signing_key: SigningKey) -> Self { Self { signing_key } } /// Load a node's identity from a key file, or generate and save if it doesn't exist. pub fn load_or_generate(path: impl AsRef) -> Result { let path = path.as_ref(); if path.exists() { Self::load(path) } else { let node = Self::generate(); node.save(path)?; Ok(node) } } /// Load a node's identity from a key file. pub fn load(path: impl AsRef) -> Result { let mut file = fs::File::open(path)?; let mut bytes = Vec::new(); file.read_to_end(&mut bytes)?; if bytes.len() != 32 { return Err(NodeError::InvalidKeyLength(bytes.len())); } let key_bytes: [u8; 32] = bytes.try_into().unwrap(); let signing_key = SigningKey::from_bytes(&key_bytes); Ok(Self { signing_key }) } /// Save the node's private key to a file. pub fn save(&self, path: impl AsRef) -> Result<(), NodeError> { let path = path.as_ref(); // Create parent directories if they don't exist if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let mut file = fs::File::create(path)?; file.write_all(self.signing_key.as_bytes())?; Ok(()) } /// Get the node's public key (identity). pub fn public_key(&self) -> VerifyingKey { self.signing_key.verifying_key() } /// Get the node's public key as bytes (32 bytes). pub fn public_key_bytes(&self) -> [u8; 32] { self.signing_key.verifying_key().to_bytes() } /// Get the signing key for creating signatures. pub fn signing_key(&self) -> &SigningKey { &self.signing_key } /// Get the secret key bytes (32 bytes) for Iroh integration. /// WARNING: Handle with care - this exposes the private key material. pub fn secret_key_bytes(&self) -> [u8; 32] { self.signing_key.to_bytes() } /// Sign a message. pub fn sign(&self, message: &[u8]) -> Signature { self.signing_key.sign(message) } /// Verify a signature against this node's public key. pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), NodeError> { self.public_key() .verify(message, signature) .map_err(|_| NodeError::InvalidSignature) } /// Verify a signature using a raw public key. pub fn verify_with_key( public_key: &VerifyingKey, message: &[u8], signature: &Signature, ) -> Result<(), NodeError> { public_key .verify(message, signature) .map_err(|_| NodeError::InvalidSignature) } } /// Peer status values used across the system #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PeerStatus { /// Peer has been invited but hasn't joined yet Invited, /// Peer is active and can sync Active, /// Peer has been removed from the mesh Removed, } impl PeerStatus { pub fn as_str(&self) -> &'static str { match self { PeerStatus::Invited => "invited", PeerStatus::Active => "active", PeerStatus::Removed => "removed", } } pub fn from_str(s: &str) -> Option { match s { "invited" => Some(PeerStatus::Invited), "active" => Some(PeerStatus::Active), "removed" => Some(PeerStatus::Removed), _ => None, } } } #[cfg(test)] mod tests { use super::*; use std::env::temp_dir; #[test] fn test_generate() { let node = NodeIdentity::generate(); let pk = node.public_key_bytes(); assert_eq!(pk.len(), 32); } #[test] fn test_sign_and_verify() { let node = NodeIdentity::generate(); let message = b"hello lattice"; let signature = node.sign(message); assert!(node.verify(message, &signature).is_ok()); } #[test] fn test_verify_wrong_message() { let node = NodeIdentity::generate(); let signature = node.sign(b"original"); assert!(node.verify(b"tampered", &signature).is_err()); } #[test] fn test_verify_with_different_key() { let node1 = NodeIdentity::generate(); let node2 = NodeIdentity::generate(); let signature = node1.sign(b"message"); assert!(node2.verify(b"message", &signature).is_err()); } #[test] fn test_save_and_load() { let temp_path = temp_dir().join("lattice_test_identity.key"); // Generate and save let node1 = NodeIdentity::generate(); let pk1 = node1.public_key_bytes(); node1.save(&temp_path).unwrap(); // Load and verify same key let node2 = NodeIdentity::load(&temp_path).unwrap(); let pk2 = node2.public_key_bytes(); assert_eq!(pk1, pk2); // Cleanup fs::remove_file(&temp_path).ok(); } #[test] fn test_load_or_generate() { let temp_path = temp_dir().join("lattice_test_identity2.key"); // Remove if exists fs::remove_file(&temp_path).ok(); // First call: generates let node1 = NodeIdentity::load_or_generate(&temp_path).unwrap(); let pk1 = node1.public_key_bytes(); // Second call: loads existing let node2 = NodeIdentity::load_or_generate(&temp_path).unwrap(); let pk2 = node2.public_key_bytes(); assert_eq!(pk1, pk2); // Cleanup fs::remove_file(&temp_path).ok(); } #[test] fn test_verify_with_key_static() { let node = NodeIdentity::generate(); let pk = node.public_key(); let message = b"test message"; let signature = node.sign(message); assert!(NodeIdentity::verify_with_key(&pk, message, &signature).is_ok()); } }