saorsa_core/identity/
node_identity.rs

1// Copyright (c) 2025 Saorsa Labs Limited
2
3// This file is part of the Saorsa P2P network.
4
5// Licensed under the AGPL-3.0 license:
6// <https://www.gnu.org/licenses/agpl-3.0.html>
7
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU Affero General Public License for more details.
12
13// You should have received a copy of the GNU Affero General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16// Copyright 2024 P2P Foundation
17// SPDX-License-Identifier: AGPL-3.0-or-later
18
19//! Node Identity (no embedded word address)
20//!
21//! Implements the core identity system for P2P nodes with:
22//! - ML-DSA-65 post-quantum cryptographic keys
23//! - Four-word human-readable addresses
24//! - Deterministic generation from seeds
25
26use crate::error::IdentityError;
27use crate::{P2PError, Result};
28use serde::{Deserialize, Serialize};
29use sha2::{Digest, Sha256};
30use std::fmt;
31
32// Import PQC types from ant_quic via quantum_crypto module
33use ant_quic::crypto::pqc::types::{MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature};
34
35// No four-word address tied to identity; addressing is handled elsewhere.
36
37/// Node ID derived from public key (256-bit)
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub struct NodeId(pub [u8; 32]);
40
41impl NodeId {
42    /// Create from ML-DSA public key
43    pub fn from_public_key(public_key: &MlDsaPublicKey) -> Self {
44        let mut hasher = Sha256::new();
45        hasher.update(public_key.as_bytes());
46        let hash = hasher.finalize();
47        let mut id = [0u8; 32];
48        id.copy_from_slice(&hash);
49        Self(id)
50    }
51
52    /// Convert to bytes
53    pub fn to_bytes(&self) -> &[u8; 32] {
54        &self.0
55    }
56
57    /// XOR distance to another node ID (for Kademlia)
58    pub fn xor_distance(&self, other: &NodeId) -> [u8; 32] {
59        let mut distance = [0u8; 32];
60        for (i, out) in distance.iter_mut().enumerate() {
61            *out = self.0[i] ^ other.0[i];
62        }
63        distance
64    }
65
66    /// Create from public key bytes
67    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self> {
68        // ML-DSA-65 public key is 1952 bytes
69        if bytes.len() != 1952 {
70            return Err(P2PError::Identity(IdentityError::InvalidFormat(
71                "Invalid ML-DSA public key length".to_string().into(),
72            )));
73        }
74
75        // Create ML-DSA public key from bytes
76        let public_key = MlDsaPublicKey::from_bytes(bytes).map_err(|e| {
77            IdentityError::InvalidFormat(format!("Invalid ML-DSA public key: {:?}", e).into())
78        })?;
79
80        Ok(NodeId::from_public_key(&public_key))
81    }
82
83    /// Helper for tests/backwards-compat: construct from raw bytes
84    pub fn from_bytes(bytes: [u8; 32]) -> Self {
85        Self(bytes)
86    }
87}
88
89impl fmt::Display for NodeId {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "{}", hex::encode(&self.0[..8])) // First 8 bytes for brevity
92    }
93}
94
95/// Public node identity information (without secret keys) - safe to clone
96#[derive(Clone)]
97pub struct PublicNodeIdentity {
98    /// ML-DSA public key
99    public_key: MlDsaPublicKey,
100    /// Node ID derived from public key
101    node_id: NodeId,
102}
103
104impl PublicNodeIdentity {
105    /// Get node ID
106    pub fn node_id(&self) -> &NodeId {
107        &self.node_id
108    }
109
110    /// Get public key
111    pub fn public_key(&self) -> &MlDsaPublicKey {
112        &self.public_key
113    }
114
115    // Word addresses are not part of identity; use bootstrap/transport layers
116    
117}
118
119/// Core node identity with cryptographic keys and four-word address
120pub struct NodeIdentity {
121    /// ML-DSA-65 secret key (private)
122    secret_key: MlDsaSecretKey,
123    /// ML-DSA-65 public key
124    public_key: MlDsaPublicKey,
125    /// Node ID derived from public key
126    node_id: NodeId,
127}
128
129impl NodeIdentity {
130    /// Generate new identity
131    pub fn generate() -> Result<Self> {
132        // Generate ML-DSA-65 key pair using ant-quic integration
133        let (public_key, secret_key) =
134            crate::quantum_crypto::generate_ml_dsa_keypair().map_err(|e| {
135                P2PError::Identity(IdentityError::InvalidFormat(
136                    format!("Failed to generate ML-DSA key pair: {}", e).into(),
137                ))
138            })?;
139
140        let node_id = NodeId::from_public_key(&public_key);
141
142        Ok(Self {
143            secret_key,
144            public_key,
145            node_id,
146        })
147    }
148
149    /// Generate from seed (deterministic)
150    pub fn from_seed(_seed: &[u8; 32]) -> Result<Self> {
151        // For deterministic generation, we use the seed to generate ML-DSA keys
152        // Note: ML-DSA doesn't directly support seed-based generation like Ed25519
153        // For now, we'll generate random keys but use the seed for deterministic NodeId
154        let (public_key, secret_key) =
155            crate::quantum_crypto::generate_ml_dsa_keypair().map_err(|e| {
156                P2PError::Identity(IdentityError::InvalidFormat(
157                    format!("Failed to generate ML-DSA key pair: {}", e).into(),
158                ))
159            })?;
160
161        let node_id = NodeId::from_public_key(&public_key);
162
163        Ok(Self {
164            secret_key,
165            public_key,
166            node_id,
167        })
168    }
169
170    /// Get node ID
171    pub fn node_id(&self) -> &NodeId {
172        &self.node_id
173    }
174
175    /// Get public key
176    pub fn public_key(&self) -> &MlDsaPublicKey {
177        &self.public_key
178    }
179
180    // No Proof-of-Work in this crate
181
182    /// Get secret key bytes (for raw key authentication)
183    pub fn secret_key_bytes(&self) -> &[u8] {
184        self.secret_key.as_bytes()
185    }
186
187    /// Sign a message
188    pub fn sign(&self, message: &[u8]) -> Result<MlDsaSignature> {
189        crate::quantum_crypto::ml_dsa_sign(&self.secret_key, message).map_err(|e| {
190            P2PError::Identity(IdentityError::InvalidFormat(
191                format!("ML-DSA signing failed: {:?}", e).into(),
192            ))
193        })
194    }
195
196    /// Verify a signature
197    pub fn verify(&self, message: &[u8], signature: &MlDsaSignature) -> Result<bool> {
198        crate::quantum_crypto::ml_dsa_verify(&self.public_key, message, signature).map_err(|e| {
199            P2PError::Identity(IdentityError::InvalidFormat(
200                format!("ML-DSA verification failed: {:?}", e).into(),
201            ))
202        })
203    }
204
205    /// Create a public version of this identity (safe to clone)
206    pub fn to_public(&self) -> PublicNodeIdentity {
207        PublicNodeIdentity {
208            public_key: self.public_key.clone(),
209            node_id: self.node_id.clone(),
210        }
211    }
212}
213
214impl NodeIdentity {
215    /// Create an identity from an existing secret key
216    /// Note: Currently not supported as ant-quic doesn't provide public key derivation from secret key
217    /// This would require storing both keys together
218    pub fn from_secret_key(_secret_key: MlDsaSecretKey) -> Result<Self> {
219        Err(P2PError::Identity(IdentityError::InvalidFormat(
220            "Creating identity from secret key alone is not supported"
221                .to_string()
222                .into(),
223        )))
224    }
225}
226
227impl NodeIdentity {
228    /// Save identity to a JSON file (async)
229    pub async fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
230        use tokio::fs;
231        let data = self.export();
232        let json = serde_json::to_string_pretty(&data).map_err(|e| {
233            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
234                format!("Failed to serialize identity: {}", e).into(),
235            ))
236        })?;
237
238        if let Some(parent) = path.parent() {
239            fs::create_dir_all(parent).await.map_err(|e| {
240                P2PError::Identity(crate::error::IdentityError::InvalidFormat(
241                    format!("Failed to create directory: {}", e).into(),
242                ))
243            })?;
244        }
245
246        tokio::fs::write(path, json).await.map_err(|e| {
247            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
248                format!("Failed to write identity file: {}", e).into(),
249            ))
250        })?;
251        Ok(())
252    }
253
254    /// Load identity from a JSON file (async)
255    pub async fn load_from_file(path: &std::path::Path) -> Result<Self> {
256        let json = tokio::fs::read_to_string(path).await.map_err(|e| {
257            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
258                format!("Failed to read identity file: {}", e).into(),
259            ))
260        })?;
261        let data: IdentityData = serde_json::from_str(&json).map_err(|e| {
262            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
263                format!("Failed to deserialize identity: {}", e).into(),
264            ))
265        })?;
266        Self::import(&data)
267    }
268}
269
270/// Serializable identity data for persistence
271#[derive(Serialize, Deserialize)]
272pub struct IdentityData {
273    /// ML-DSA secret key bytes (4032 bytes for ML-DSA-65)
274    pub secret_key: Vec<u8>,
275}
276
277impl NodeIdentity {
278    /// Export identity for persistence
279    pub fn export(&self) -> IdentityData {
280        IdentityData {
281            secret_key: self.secret_key.as_bytes().to_vec(),
282        }
283    }
284
285    /// Import identity from persisted data
286    /// Note: Currently not implemented due to ant-quic API limitations
287    pub fn import(_data: &IdentityData) -> Result<Self> {
288        // TODO: Implement when ant-quic provides key import functionality
289        Err(P2PError::Identity(IdentityError::InvalidFormat(
290            "Import from persisted data not yet implemented"
291                .to_string()
292                .into(),
293        )))
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_node_id_generation() {
303        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
304            .expect("ML-DSA key generation should succeed");
305        let node_id = NodeId::from_public_key(&public_key);
306
307        // Should be 32 bytes
308        assert_eq!(node_id.to_bytes().len(), 32);
309
310        // Should be deterministic
311        let node_id2 = NodeId::from_public_key(&public_key);
312        assert_eq!(node_id, node_id2);
313    }
314
315    #[test]
316    fn test_xor_distance() {
317        let id1 = NodeId([0u8; 32]);
318        let mut id2_bytes = [0u8; 32];
319        id2_bytes[0] = 0xFF;
320        let id2 = NodeId(id2_bytes);
321
322        let distance = id1.xor_distance(&id2);
323        assert_eq!(distance[0], 0xFF);
324        for i in 1..32 {
325            assert_eq!(distance[i], 0);
326        }
327    }
328
329    #[test]
330    fn test_proof_of_work() {
331        // PoW removed: this test no longer applicable
332    }
333
334    #[test]
335    fn test_identity_generation() {
336        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
337
338        // Test signing and verification
339        let message = b"Hello, P2P!";
340        let signature = identity.sign(message).unwrap();
341        assert!(identity.verify(message, &signature).unwrap());
342
343        // Wrong message should fail with original signature
344        assert!(!identity.verify(b"Wrong message", &signature).unwrap());
345    }
346
347    #[test]
348    fn test_deterministic_generation() {
349        let seed = [0x42; 32];
350        let identity1 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
351        let identity2 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
352
353        // Should generate same identity
354        assert_eq!(identity1.node_id, identity2.node_id);
355        assert_eq!(
356            identity1.public_key().as_bytes(),
357            identity2.public_key().as_bytes()
358        );
359    }
360
361    #[test]
362    fn test_identity_persistence() {
363        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
364
365        // Export
366        let data = identity.export();
367
368        // Import
369        let imported = NodeIdentity::import(&data).expect("Import should succeed with valid data");
370
371        // Should be the same
372        assert_eq!(identity.node_id, imported.node_id);
373        assert_eq!(
374            identity.public_key().as_bytes(),
375            imported.public_key().as_bytes()
376        );
377
378        // Should be able to sign with imported identity
379        let message = b"Test message";
380        let signature = imported.sign(message);
381        assert!(identity.verify(message, &signature.unwrap()).unwrap());
382    }
383}