Skip to main content

pmetal_distributed/
identity.rs

1//! Persistent node identity management.
2//!
3//! Each node in the cluster has a unique, persistent identity based on an
4//! Ed25519 keypair. The keypair is stored at `~/.pmetal/node_keypair` and
5//! loaded on startup (or generated if not present).
6//!
7//! This design ensures:
8//! - Consistent node identification across restarts
9//! - Cryptographic verification of peer identity
10//! - Compatibility with libp2p's PeerId system
11
12use crate::error::DistributedError;
13use anyhow::Result;
14use libp2p::PeerId;
15use libp2p::identity::{Keypair, ed25519};
16use std::fs::{self, File};
17use std::io::{Read, Write};
18use std::path::PathBuf;
19use tracing::{debug, info};
20
21/// Default directory name for pmetal data.
22const PMETAL_DIR: &str = ".pmetal";
23
24/// Filename for the node keypair.
25const KEYPAIR_FILE: &str = "node_keypair";
26
27/// Node identity containing the keypair and derived PeerId.
28#[derive(Clone)]
29pub struct NodeIdentity {
30    /// The Ed25519 keypair for this node.
31    keypair: Keypair,
32    /// The PeerId derived from the public key.
33    peer_id: PeerId,
34}
35
36impl NodeIdentity {
37    /// Load or generate a persistent node identity.
38    ///
39    /// The keypair is stored at `~/.pmetal/node_keypair`.
40    /// If the file doesn't exist, a new keypair is generated and saved.
41    pub fn load_or_generate() -> Result<Self> {
42        let keypair_path = Self::keypair_path()?;
43
44        let keypair = if keypair_path.exists() {
45            Self::load_keypair(&keypair_path)?
46        } else {
47            let kp = Self::generate_and_save(&keypair_path)?;
48            info!("Generated new node identity");
49            kp
50        };
51
52        let peer_id = PeerId::from(keypair.public());
53        info!("Node identity: {}", peer_id);
54
55        Ok(Self { keypair, peer_id })
56    }
57
58    /// Generate a new ephemeral identity (for testing).
59    pub fn ephemeral() -> Self {
60        let keypair = Keypair::generate_ed25519();
61        let peer_id = PeerId::from(keypair.public());
62        debug!("Generated ephemeral identity: {}", peer_id);
63        Self { keypair, peer_id }
64    }
65
66    /// Get the keypair.
67    pub fn keypair(&self) -> &Keypair {
68        &self.keypair
69    }
70
71    /// Get the PeerId.
72    pub fn peer_id(&self) -> &PeerId {
73        &self.peer_id
74    }
75
76    /// Get the PeerId as a base58 string.
77    pub fn peer_id_string(&self) -> String {
78        self.peer_id.to_base58()
79    }
80
81    /// Get the path to the keypair file.
82    fn keypair_path() -> Result<PathBuf> {
83        let home = dirs::home_dir()
84            .ok_or_else(|| DistributedError::Config("Cannot determine home directory".into()))?;
85
86        let pmetal_dir = home.join(PMETAL_DIR);
87        if !pmetal_dir.exists() {
88            fs::create_dir_all(&pmetal_dir).map_err(|e| {
89                DistributedError::Config(format!(
90                    "Failed to create {}: {}",
91                    pmetal_dir.display(),
92                    e
93                ))
94            })?;
95        }
96
97        Ok(pmetal_dir.join(KEYPAIR_FILE))
98    }
99
100    /// Load a keypair from a file.
101    fn load_keypair(path: &PathBuf) -> Result<Keypair> {
102        let mut file = File::open(path)
103            .map_err(|e| DistributedError::Config(format!("Failed to open keypair file: {}", e)))?;
104
105        let mut bytes = Vec::new();
106        file.read_to_end(&mut bytes)
107            .map_err(|e| DistributedError::Config(format!("Failed to read keypair file: {}", e)))?;
108
109        // Decode the Ed25519 secret key (32 bytes)
110        if bytes.len() != 32 {
111            return Err(DistributedError::Config(format!(
112                "Invalid keypair file: expected 32 bytes, got {}",
113                bytes.len()
114            ))
115            .into());
116        }
117
118        let secret = ed25519::SecretKey::try_from_bytes(&mut bytes)
119            .map_err(|e| DistributedError::Config(format!("Invalid Ed25519 secret key: {}", e)))?;
120
121        let keypair = ed25519::Keypair::from(secret);
122        debug!("Loaded keypair from {}", path.display());
123
124        Ok(keypair.into())
125    }
126
127    /// Generate a new keypair and save it to a file.
128    fn generate_and_save(path: &PathBuf) -> Result<Keypair> {
129        // Generate a new Ed25519 keypair using libp2p's internal method
130        let ed25519_keypair = ed25519::Keypair::generate();
131        let keypair: Keypair = ed25519_keypair.clone().into();
132
133        // Get the secret key bytes (32 bytes)
134        let secret = ed25519_keypair.secret();
135        let secret_bytes = secret.as_ref();
136
137        // Create file atomically with correct permissions from the start.
138        // O_CREAT | O_EXCL (via create_new) prevents TOCTOU: if the file
139        // already exists this errors rather than silently overwriting.
140        // Mode 0o600 means only the owner can read/write — no race window
141        // where the key is world-readable.
142        #[cfg(unix)]
143        let mut file = {
144            use std::os::unix::fs::OpenOptionsExt;
145            std::fs::OpenOptions::new()
146                .mode(0o600)
147                .create_new(true)
148                .write(true)
149                .open(path)
150                .map_err(|e| {
151                    DistributedError::Config(format!("Failed to create keypair file: {}", e))
152                })?
153        };
154
155        #[cfg(not(unix))]
156        let mut file = {
157            std::fs::OpenOptions::new()
158                .create_new(true)
159                .write(true)
160                .open(path)
161                .map_err(|e| {
162                    DistributedError::Config(format!("Failed to create keypair file: {}", e))
163                })?
164        };
165
166        file.write_all(secret_bytes)
167            .map_err(|e| DistributedError::Config(format!("Failed to write keypair: {}", e)))?;
168
169        debug!("Saved keypair to {}", path.display());
170        Ok(keypair)
171    }
172}
173
174impl std::fmt::Debug for NodeIdentity {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.debug_struct("NodeIdentity")
177            .field("peer_id", &self.peer_id.to_base58())
178            .finish()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_ephemeral_identity() {
188        let id1 = NodeIdentity::ephemeral();
189        let id2 = NodeIdentity::ephemeral();
190
191        // Each ephemeral identity should be unique
192        assert_ne!(id1.peer_id(), id2.peer_id());
193    }
194
195    #[test]
196    fn test_peer_id_string() {
197        let id = NodeIdentity::ephemeral();
198        let s = id.peer_id_string();
199
200        // Base58 encoded PeerId should be a reasonable length
201        assert!(s.len() > 40);
202        assert!(s.len() < 60);
203    }
204}