//! Self-sovereign mesh identity backed by quicproquo-core Ed25519 keypairs. //! //! A [`MeshIdentity`] wraps an [`IdentityKeypair`] with a peer directory, //! enabling P2P nodes to persist identity and track known peers across //! restarts. use std::collections::HashMap; use std::path::Path; use quicproquo_core::IdentityKeypair; use serde::{Deserialize, Serialize}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; /// Information about a known peer in the mesh network. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PeerInfo { /// Raw Ed25519 public key (32 bytes). pub public_key: Vec, /// Unix timestamp of last observed activity. pub last_seen: u64, /// Known network addresses (e.g. iroh `NodeAddr` serializations). pub addresses: Vec, } /// Persisted form of a mesh identity (JSON on disk). #[derive(Serialize, Deserialize)] struct IdentityFile { /// Hex-encoded 32-byte Ed25519 seed. seed: String, /// Known peers, keyed by hex-encoded peer public key. peers: HashMap, } /// A self-sovereign mesh identity: an Ed25519 keypair + a known-peers directory. pub struct MeshIdentity { keypair: IdentityKeypair, known_peers: HashMap, } impl MeshIdentity { /// Generate a fresh random mesh identity. pub fn generate() -> Self { Self { keypair: IdentityKeypair::generate(), known_peers: HashMap::new(), } } /// Recreate a mesh identity from a 32-byte Ed25519 seed. pub fn from_seed(seed: [u8; 32]) -> Self { Self { keypair: IdentityKeypair::from_seed(seed), known_peers: HashMap::new(), } } /// Load a mesh identity from a JSON file. pub fn load(path: &Path) -> anyhow::Result { let data = std::fs::read_to_string(path)?; let file: IdentityFile = serde_json::from_str(&data)?; let seed_bytes = hex::decode(&file.seed)?; let seed: [u8; 32] = seed_bytes .as_slice() .try_into() .map_err(|_| anyhow::anyhow!("seed must be 32 bytes"))?; Ok(Self { keypair: IdentityKeypair::from_seed(seed), known_peers: file.peers, }) } /// Save this mesh identity to a JSON file with restrictive permissions. /// /// On Unix, the file is set to `0o600` (owner read/write only) since it /// contains the Ed25519 seed in the clear. pub fn save(&self, path: &Path) -> anyhow::Result<()> { let file = IdentityFile { seed: hex::encode(self.keypair.seed_bytes()), peers: self.known_peers.clone(), }; let json = serde_json::to_string_pretty(&file)?; std::fs::write(path, json)?; // Restrict permissions to owner-only on Unix. #[cfg(unix)] { let perms = std::fs::Permissions::from_mode(0o600); std::fs::set_permissions(path, perms)?; } Ok(()) } /// Return the raw 32-byte Ed25519 public key. pub fn public_key(&self) -> [u8; 32] { self.keypair.public_key_bytes() } /// Sign arbitrary bytes, returning a 64-byte Ed25519 signature. pub fn sign(&self, message: &[u8]) -> [u8; 64] { self.keypair.sign_raw(message) } /// Return the underlying seed (for deriving iroh `SecretKey`, etc.). pub fn seed_bytes(&self) -> [u8; 32] { *self.keypair.seed_bytes() } /// Register or update a known peer. pub fn add_peer(&mut self, id: String, info: PeerInfo) { self.known_peers.insert(id, info); } /// Immutable view of the known-peers directory. pub fn known_peers(&self) -> &HashMap { &self.known_peers } } #[cfg(test)] mod tests { use super::*; use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn generate_and_sign_verify() { let id = MeshIdentity::generate(); let msg = b"test message"; let sig = id.sign(msg); // Verify through quicproquo_core let pk = id.public_key(); IdentityKeypair::verify_raw(&pk, msg, &sig).expect("valid signature"); } #[test] fn from_seed_deterministic() { let seed = [42u8; 32]; let a = MeshIdentity::from_seed(seed); let b = MeshIdentity::from_seed(seed); assert_eq!(a.public_key(), b.public_key()); } #[test] fn save_and_load_roundtrip() { let dir = tempfile::tempdir().expect("tmp dir"); let path = dir.path().join("mesh_id.json"); let mut original = MeshIdentity::generate(); original.add_peer( "deadbeef".into(), PeerInfo { public_key: vec![0xde, 0xad], last_seen: SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time") .as_secs(), addresses: vec!["127.0.0.1:4433".into()], }, ); original.save(&path).expect("save"); let loaded = MeshIdentity::load(&path).expect("load"); assert_eq!(original.public_key(), loaded.public_key()); assert_eq!(loaded.known_peers().len(), 1); assert!(loaded.known_peers().contains_key("deadbeef")); } #[test] fn add_and_query_peers() { let mut id = MeshIdentity::generate(); assert!(id.known_peers().is_empty()); id.add_peer( "peer1".into(), PeerInfo { public_key: vec![1; 32], last_seen: 0, addresses: vec![], }, ); assert_eq!(id.known_peers().len(), 1); assert_eq!(id.known_peers()["peer1"].public_key, vec![1; 32]); } }