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 crate::quantum_crypto::ant_quic_integration::{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/// Core node identity with cryptographic keys
119pub struct NodeIdentity {
120    /// ML-DSA-65 secret key (private)
121    secret_key: MlDsaSecretKey,
122    /// ML-DSA-65 public key
123    public_key: MlDsaPublicKey,
124    /// Node ID derived from public key
125    node_id: NodeId,
126}
127
128impl NodeIdentity {
129    /// Generate new identity
130    pub fn generate() -> Result<Self> {
131        // Generate ML-DSA-65 key pair (ant-quic integration)
132        let (public_key, secret_key) =
133            crate::quantum_crypto::generate_ml_dsa_keypair().map_err(|e| {
134                P2PError::Identity(IdentityError::InvalidFormat(
135                    format!("Failed to generate ML-DSA key pair: {}", e).into(),
136                ))
137            })?;
138
139        let node_id = NodeId::from_public_key(&public_key);
140
141        Ok(Self {
142            secret_key,
143            public_key,
144            node_id,
145        })
146    }
147
148    /// Convert this identity's NodeId to a UserId for use in adaptive modules
149    pub fn to_user_id(&self) -> crate::peer_record::UserId {
150        crate::peer_record::UserId::from_bytes(self.node_id.0)
151    }
152
153    /// Generate from seed (deterministic)
154    pub fn from_seed(seed: &[u8; 32]) -> Result<Self> {
155        // Deterministically derive key material via HKDF-SHA3
156        use saorsa_pqc::{HkdfSha3_256, api::traits::Kdf};
157
158        // ML-DSA-65 public/secret key sizes (bytes)
159        const ML_DSA_PUB_LEN: usize = 1952;
160        const ML_DSA_SEC_LEN: usize = 4032;
161
162        let mut derived = vec![0u8; ML_DSA_PUB_LEN + ML_DSA_SEC_LEN];
163        HkdfSha3_256::derive(seed, None, b"saorsa-node-identity-seed", &mut derived).map_err(
164            |_| P2PError::Identity(IdentityError::InvalidFormat("HKDF expand failed".into())),
165        )?;
166
167        let pub_bytes = &derived[..ML_DSA_PUB_LEN];
168        let sec_bytes = &derived[ML_DSA_PUB_LEN..];
169
170        // Construct keys from bytes; these constructors accept byte slices in our integration
171        let public_key =
172            crate::quantum_crypto::ant_quic_integration::MlDsaPublicKey::from_bytes(pub_bytes)
173                .map_err(|e| {
174                    P2PError::Identity(IdentityError::InvalidFormat(
175                        format!("Invalid ML-DSA public key bytes: {e}").into(),
176                    ))
177                })?;
178        let secret_key =
179            crate::quantum_crypto::ant_quic_integration::MlDsaSecretKey::from_bytes(sec_bytes)
180                .map_err(|e| {
181                    P2PError::Identity(IdentityError::InvalidFormat(
182                        format!("Invalid ML-DSA secret key bytes: {e}").into(),
183                    ))
184                })?;
185
186        let node_id = NodeId::from_public_key(&public_key);
187
188        Ok(Self {
189            secret_key,
190            public_key,
191            node_id,
192        })
193    }
194
195    /// Get node ID
196    pub fn node_id(&self) -> &NodeId {
197        &self.node_id
198    }
199
200    /// Get public key
201    pub fn public_key(&self) -> &MlDsaPublicKey {
202        &self.public_key
203    }
204
205    // No Proof-of-Work in this crate
206
207    /// Get secret key bytes (for raw key authentication)
208    pub fn secret_key_bytes(&self) -> &[u8] {
209        self.secret_key.as_bytes()
210    }
211
212    /// Sign a message
213    pub fn sign(&self, message: &[u8]) -> Result<MlDsaSignature> {
214        crate::quantum_crypto::ml_dsa_sign(&self.secret_key, message).map_err(|e| {
215            P2PError::Identity(IdentityError::InvalidFormat(
216                format!("ML-DSA signing failed: {:?}", e).into(),
217            ))
218        })
219    }
220
221    /// Verify a signature
222    pub fn verify(&self, message: &[u8], signature: &MlDsaSignature) -> Result<bool> {
223        crate::quantum_crypto::ml_dsa_verify(&self.public_key, message, signature).map_err(|e| {
224            P2PError::Identity(IdentityError::InvalidFormat(
225                format!("ML-DSA verification failed: {:?}", e).into(),
226            ))
227        })
228    }
229
230    /// Create a public version of this identity (safe to clone)
231    pub fn to_public(&self) -> PublicNodeIdentity {
232        PublicNodeIdentity {
233            public_key: self.public_key.clone(),
234            node_id: self.node_id.clone(),
235        }
236    }
237}
238
239impl NodeIdentity {
240    /// Create an identity from an existing secret key
241    /// Note: Currently not supported as ant-quic doesn't provide public key derivation from secret key
242    /// This would require storing both keys together
243    pub fn from_secret_key(_secret_key: MlDsaSecretKey) -> Result<Self> {
244        Err(P2PError::Identity(IdentityError::InvalidFormat(
245            "Creating identity from secret key alone is not supported"
246                .to_string()
247                .into(),
248        )))
249    }
250}
251
252impl NodeIdentity {
253    /// Save identity to a JSON file (async)
254    pub async fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
255        use tokio::fs;
256        let data = self.export();
257        let json = serde_json::to_string_pretty(&data).map_err(|e| {
258            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
259                format!("Failed to serialize identity: {}", e).into(),
260            ))
261        })?;
262
263        if let Some(parent) = path.parent() {
264            fs::create_dir_all(parent).await.map_err(|e| {
265                P2PError::Identity(crate::error::IdentityError::InvalidFormat(
266                    format!("Failed to create directory: {}", e).into(),
267                ))
268            })?;
269        }
270
271        tokio::fs::write(path, json).await.map_err(|e| {
272            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
273                format!("Failed to write identity file: {}", e).into(),
274            ))
275        })?;
276        Ok(())
277    }
278
279    /// Load identity from a JSON file (async)
280    pub async fn load_from_file(path: &std::path::Path) -> Result<Self> {
281        let json = tokio::fs::read_to_string(path).await.map_err(|e| {
282            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
283                format!("Failed to read identity file: {}", e).into(),
284            ))
285        })?;
286        let data: IdentityData = serde_json::from_str(&json).map_err(|e| {
287            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
288                format!("Failed to deserialize identity: {}", e).into(),
289            ))
290        })?;
291        Self::import(&data)
292    }
293}
294
295/// Serializable identity data for persistence
296#[derive(Serialize, Deserialize)]
297pub struct IdentityData {
298    /// ML-DSA secret key bytes (4032 bytes for ML-DSA-65)
299    pub secret_key: Vec<u8>,
300    /// ML-DSA public key bytes (1952 bytes for ML-DSA-65)
301    pub public_key: Vec<u8>,
302}
303
304impl NodeIdentity {
305    /// Export identity for persistence
306    pub fn export(&self) -> IdentityData {
307        IdentityData {
308            secret_key: self.secret_key.as_bytes().to_vec(),
309            public_key: self.public_key.as_bytes().to_vec(),
310        }
311    }
312
313    /// Import identity from persisted data
314    pub fn import(data: &IdentityData) -> Result<Self> {
315        // Reconstruct keys from bytes
316        let secret_key = crate::quantum_crypto::ant_quic_integration::MlDsaSecretKey::from_bytes(
317            &data.secret_key,
318        )
319        .map_err(|e| {
320            P2PError::Identity(IdentityError::InvalidFormat(
321                format!("Invalid ML-DSA secret key: {e}").into(),
322            ))
323        })?;
324        let public_key = crate::quantum_crypto::ant_quic_integration::MlDsaPublicKey::from_bytes(
325            &data.public_key,
326        )
327        .map_err(|e| {
328            P2PError::Identity(IdentityError::InvalidFormat(
329                format!("Invalid ML-DSA public key: {e}").into(),
330            ))
331        })?;
332
333        let node_id = NodeId::from_public_key(&public_key);
334        Ok(Self {
335            secret_key,
336            public_key,
337            node_id,
338        })
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_node_id_generation() {
348        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
349            .expect("ML-DSA key generation should succeed");
350        let node_id = NodeId::from_public_key(&public_key);
351
352        // Should be 32 bytes
353        assert_eq!(node_id.to_bytes().len(), 32);
354
355        // Should be deterministic
356        let node_id2 = NodeId::from_public_key(&public_key);
357        assert_eq!(node_id, node_id2);
358    }
359
360    #[test]
361    fn test_xor_distance() {
362        let id1 = NodeId([0u8; 32]);
363        let mut id2_bytes = [0u8; 32];
364        id2_bytes[0] = 0xFF;
365        let id2 = NodeId(id2_bytes);
366
367        let distance = id1.xor_distance(&id2);
368        assert_eq!(distance[0], 0xFF);
369        for byte in &distance[1..] {
370            assert_eq!(*byte, 0);
371        }
372    }
373
374    #[test]
375    fn test_proof_of_work() {
376        // PoW removed: this test no longer applicable
377    }
378
379    #[test]
380    fn test_identity_generation() {
381        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
382
383        // Test signing and verification
384        let message = b"Hello, P2P!";
385        let signature = identity.sign(message).unwrap();
386        assert!(identity.verify(message, &signature).unwrap());
387
388        // Wrong message should fail with original signature
389        assert!(!identity.verify(b"Wrong message", &signature).unwrap());
390    }
391
392    #[test]
393    fn test_deterministic_generation() {
394        let seed = [0x42; 32];
395        let identity1 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
396        let identity2 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
397
398        // Should generate same identity
399        assert_eq!(identity1.node_id, identity2.node_id);
400        assert_eq!(
401            identity1.public_key().as_bytes(),
402            identity2.public_key().as_bytes()
403        );
404    }
405
406    #[test]
407    fn test_identity_persistence() {
408        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
409
410        // Export
411        let data = identity.export();
412
413        // Import
414        let imported = NodeIdentity::import(&data).expect("Import should succeed with valid data");
415
416        // Should be the same
417        assert_eq!(identity.node_id, imported.node_id);
418        assert_eq!(
419            identity.public_key().as_bytes(),
420            imported.public_key().as_bytes()
421        );
422
423        // Should be able to sign with imported identity
424        let message = b"Test message";
425        let signature = imported.sign(message);
426        assert!(identity.verify(message, &signature.unwrap()).unwrap());
427    }
428}