//! Identity Manager - Ed25519 + X25519 Key Management //! //! Security principles: //! - Ed25519 for signing (identity keys) //! - X25519 for key exchange (session keys) //! - Never trust pubkeys from envelopes - only from signed registry //! - Per-sender nonce tracking with timestamps for expiry //! - Separate send/receive counters use ed25519_dalek::{SigningKey, VerifyingKey, Signature, Signer, Verifier}; use x25519_dalek::{StaticSecret, PublicKey as X25519PublicKey}; use hkdf::Hkdf; use sha2::Sha256; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; use std::collections::HashMap; use std::sync::Arc; use parking_lot::RwLock; /// Ed25519 key pair for identity/signing #[derive(Clone)] pub struct KeyPair { signing_key: SigningKey, verifying_key: VerifyingKey, } impl KeyPair { pub fn generate() -> Self { let signing_key = SigningKey::generate(&mut OsRng); let verifying_key = signing_key.verifying_key(); Self { signing_key, verifying_key } } pub fn public_key_bytes(&self) -> [u8; 32] { self.verifying_key.to_bytes() } pub fn public_key_base64(&self) -> String { base64::Engine::encode(&base64::engine::general_purpose::STANDARD, self.public_key_bytes()) } pub fn sign(&self, message: &[u8]) -> Signature { self.signing_key.sign(message) } pub fn verify(public_key: &[u8; 32], message: &[u8], signature: &[u8; 64]) -> bool { let Ok(verifying_key) = VerifyingKey::from_bytes(public_key) else { return false; }; let sig = Signature::from_bytes(signature); verifying_key.verify(message, &sig).is_ok() } } /// Registered member in the swarm (from signed registry) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegisteredMember { pub agent_id: String, #[serde(with = "hex::serde")] pub ed25519_public_key: [u8; 32], #[serde(with = "hex::serde")] pub x25519_public_key: [u8; 32], pub capabilities: Vec, pub joined_at: u64, pub last_heartbeat: u64, #[serde(with = "BigArray")] pub signature: [u8; 64], } impl RegisteredMember { /// Verify the registration signature /// Signature covers: agent_id || ed25519_key || x25519_key || capabilities || joined_at pub fn verify(&self) -> bool { let canonical = self.canonical_data(); KeyPair::verify(&self.ed25519_public_key, &canonical, &self.signature) } /// Create canonical data for signing pub fn canonical_data(&self) -> Vec { let mut data = Vec::new(); data.extend_from_slice(self.agent_id.as_bytes()); data.push(0); // separator data.extend_from_slice(&self.ed25519_public_key); data.extend_from_slice(&self.x25519_public_key); for cap in &self.capabilities { data.extend_from_slice(cap.as_bytes()); data.push(0); } data.extend_from_slice(&self.joined_at.to_le_bytes()); data } } /// Per-sender nonce tracking entry struct NonceEntry { timestamp: u64, } /// Identity Manager with registry-based trust pub struct IdentityManager { /// Our Ed25519 identity keypair identity_key: KeyPair, /// Our X25519 key for ECDH x25519_secret: StaticSecret, x25519_public: X25519PublicKey, /// Derived session keys per peer (cache) session_keys: Arc>>, /// Registry of verified members - THE SOURCE OF TRUTH /// Never trust keys from envelopes, only from here member_registry: Arc>>, /// Per-sender nonce tracking: senderId -> (nonce -> entry) seen_nonces: Arc>>>, /// Local monotonic send counter send_counter: Arc>, /// Per-peer receive counters recv_counters: Arc>>, /// Max nonce age (5 minutes) max_nonce_age_ms: u64, } impl IdentityManager { pub fn new() -> Self { let identity_key = KeyPair::generate(); let x25519_secret = StaticSecret::random_from_rng(OsRng); let x25519_public = X25519PublicKey::from(&x25519_secret); Self { identity_key, x25519_secret, x25519_public, session_keys: Arc::new(RwLock::new(HashMap::new())), member_registry: Arc::new(RwLock::new(HashMap::new())), seen_nonces: Arc::new(RwLock::new(HashMap::new())), send_counter: Arc::new(RwLock::new(0)), recv_counters: Arc::new(RwLock::new(HashMap::new())), max_nonce_age_ms: 300_000, // 5 minutes } } /// Get our Ed25519 public key pub fn public_key(&self) -> [u8; 32] { self.identity_key.public_key_bytes() } /// Get our X25519 public key pub fn x25519_public_key(&self) -> [u8; 32] { self.x25519_public.to_bytes() } /// Sign data with our identity key pub fn sign(&self, data: &[u8]) -> [u8; 64] { self.identity_key.sign(data).to_bytes() } /// Verify signature using ONLY the registry key /// Never use a key from the message itself pub fn verify_from_registry(&self, sender_id: &str, data: &[u8], signature: &[u8; 64]) -> bool { let registry = self.member_registry.read(); let Some(member) = registry.get(sender_id) else { tracing::warn!("verify_from_registry: sender not in registry: {}", sender_id); return false; }; KeyPair::verify(&member.ed25519_public_key, data, signature) } /// Register a member from a signed registration /// Verifies the signature before adding pub fn register_member(&self, member: RegisteredMember) -> bool { if !member.verify() { tracing::warn!("register_member: invalid signature for {}", member.agent_id); return false; } let mut registry = self.member_registry.write(); // If already registered, only accept if from same key if let Some(existing) = registry.get(&member.agent_id) { if existing.ed25519_public_key != member.ed25519_public_key { tracing::warn!("register_member: key mismatch for {}", member.agent_id); return false; } } registry.insert(member.agent_id.clone(), member); true } /// Get registered member by ID pub fn get_member(&self, agent_id: &str) -> Option { self.member_registry.read().get(agent_id).cloned() } /// Update member heartbeat pub fn update_heartbeat(&self, agent_id: &str) { let mut registry = self.member_registry.write(); if let Some(member) = registry.get_mut(agent_id) { member.last_heartbeat = chrono::Utc::now().timestamp_millis() as u64; } } /// Get active members (heartbeat within threshold) pub fn get_active_members(&self, heartbeat_threshold_ms: u64) -> Vec { let now = chrono::Utc::now().timestamp_millis() as u64; let registry = self.member_registry.read(); registry.values() .filter(|m| now - m.last_heartbeat < heartbeat_threshold_ms) .cloned() .collect() } /// Create our registration (for publishing to registry) pub fn create_registration(&self, agent_id: &str, capabilities: Vec) -> RegisteredMember { let joined_at = chrono::Utc::now().timestamp_millis() as u64; let mut member = RegisteredMember { agent_id: agent_id.to_string(), ed25519_public_key: self.public_key(), x25519_public_key: self.x25519_public_key(), capabilities, joined_at, last_heartbeat: joined_at, signature: [0u8; 64], }; // Sign the canonical data let canonical = member.canonical_data(); member.signature = self.sign(&canonical); member } /// Derive session key with peer using X25519 ECDH + HKDF /// Uses ONLY the X25519 key from the registry pub fn derive_session_key(&self, peer_id: &str, swarm_id: &str) -> Option<[u8; 32]> { let cache_key = format!("{}:{}", peer_id, swarm_id); // Check cache { let cache = self.session_keys.read(); if let Some(key) = cache.get(&cache_key) { return Some(*key); } } // Get peer's X25519 key from registry ONLY let registry = self.member_registry.read(); let peer = registry.get(peer_id)?; let peer_x25519 = X25519PublicKey::from(peer.x25519_public_key); // X25519 ECDH let shared_secret = self.x25519_secret.diffie_hellman(&peer_x25519); // HKDF with stable salt from both public keys let my_key = self.x25519_public.as_bytes(); let peer_key = peer.x25519_public_key; // Salt = sha256(min(pubA, pubB) || max(pubA, pubB)) use sha2::Digest; let salt = if my_key < &peer_key { let mut hasher = Sha256::new(); hasher.update(my_key); hasher.update(&peer_key); hasher.finalize() } else { let mut hasher = Sha256::new(); hasher.update(&peer_key); hasher.update(my_key); hasher.finalize() }; // Info only includes swarm_id (salt already includes both parties' public keys for pair separation) // This ensures both parties derive the same key let info = format!("p2p-swarm-v2:{}", swarm_id); let hkdf = Hkdf::::new(Some(&salt), shared_secret.as_bytes()); let mut session_key = [0u8; 32]; hkdf.expand(info.as_bytes(), &mut session_key).ok()?; // Cache self.session_keys.write().insert(cache_key, session_key); Some(session_key) } /// Generate cryptographic nonce pub fn generate_nonce() -> String { let mut bytes = [0u8; 16]; rand::RngCore::fill_bytes(&mut OsRng, &mut bytes); hex::encode(bytes) } /// Check nonce validity with per-sender tracking pub fn check_nonce(&self, nonce: &str, timestamp: u64, sender_id: &str) -> bool { let now = chrono::Utc::now().timestamp_millis() as u64; // Reject old messages if now.saturating_sub(timestamp) > self.max_nonce_age_ms { return false; } // Reject future timestamps (1 minute tolerance for clock skew) if timestamp > now + 60_000 { return false; } let mut nonces = self.seen_nonces.write(); let sender_nonces = nonces.entry(sender_id.to_string()).or_insert_with(HashMap::new); // Reject replayed nonces if sender_nonces.contains_key(nonce) { return false; } // Record nonce sender_nonces.insert(nonce.to_string(), NonceEntry { timestamp }); true } /// Cleanup expired nonces pub fn cleanup_nonces(&self) { let now = chrono::Utc::now().timestamp_millis() as u64; let mut nonces = self.seen_nonces.write(); for sender_nonces in nonces.values_mut() { sender_nonces.retain(|_, entry| now - entry.timestamp < self.max_nonce_age_ms); } nonces.retain(|_, v| !v.is_empty()); } /// Get next send counter pub fn next_send_counter(&self) -> u64 { let mut counter = self.send_counter.write(); *counter += 1; *counter } /// Validate receive counter (must be > last seen from peer) pub fn validate_recv_counter(&self, peer_id: &str, counter: u64) -> bool { let mut counters = self.recv_counters.write(); let last_seen = counters.get(peer_id).copied().unwrap_or(0); if counter <= last_seen { return false; } counters.insert(peer_id.to_string(), counter); true } /// Rotate session key for peer pub fn rotate_session_key(&self, peer_id: &str) { self.session_keys.write().retain(|k, _| !k.starts_with(peer_id)); } } impl Default for IdentityManager { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_keypair_sign_verify() { let keypair = KeyPair::generate(); let message = b"test message"; let signature = keypair.sign(message); assert!(KeyPair::verify( &keypair.public_key_bytes(), message, &signature.to_bytes() )); } #[test] fn test_member_registration() { let identity = IdentityManager::new(); let registration = identity.create_registration("test-agent", vec!["executor".to_string()]); assert!(registration.verify()); assert!(identity.register_member(registration)); } #[test] fn test_session_key_derivation() { let alice = IdentityManager::new(); let bob = IdentityManager::new(); // Register each other with valid capabilities let alice_reg = alice.create_registration("alice", vec!["worker".to_string()]); let bob_reg = bob.create_registration("bob", vec!["worker".to_string()]); // Register in each other's registry alice.register_member(bob_reg.clone()); bob.register_member(alice_reg.clone()); // Derive session keys - both should derive the same key let alice_key = alice.derive_session_key("bob", "test-swarm").unwrap(); let bob_key = bob.derive_session_key("alice", "test-swarm").unwrap(); // Keys should match (symmetric ECDH) assert_eq!(alice_key, bob_key, "Session keys should be symmetric"); } #[test] fn test_nonce_replay_protection() { let identity = IdentityManager::new(); let nonce = IdentityManager::generate_nonce(); let timestamp = chrono::Utc::now().timestamp_millis() as u64; // First use should succeed assert!(identity.check_nonce(&nonce, timestamp, "sender-1")); // Replay should fail assert!(!identity.check_nonce(&nonce, timestamp, "sender-1")); // Different sender can use same nonce (per-sender tracking) assert!(identity.check_nonce(&nonce, timestamp, "sender-2")); } }