Skip to main content

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//! Peer Identity
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 saorsa_pqc::HkdfSha3_256;
29use saorsa_pqc::api::sig::{MlDsa, MlDsaVariant};
30use saorsa_pqc::api::traits::Kdf;
31use serde::{Deserialize, Serialize};
32use std::fmt;
33
34// Import PQC types from saorsa_transport via quantum_crypto module
35use crate::quantum_crypto::saorsa_transport_integration::{
36    MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature,
37};
38
39// Re-export canonical PeerId from the peer_id module.
40pub use super::peer_id::{PEER_ID_BYTE_LEN, PeerId, PeerIdParseError};
41
42/// Create a [`PeerId`] from an ML-DSA public key.
43///
44/// This is a standalone function because it depends on `MlDsaPublicKey`
45/// from `saorsa-pqc`, which `saorsa-types` does not (and should not)
46/// depend on.
47pub fn peer_id_from_public_key(public_key: &MlDsaPublicKey) -> PeerId {
48    let hash = blake3::hash(public_key.as_bytes());
49    PeerId(*hash.as_bytes())
50}
51
52/// ML-DSA-65 public key length in bytes.
53const ML_DSA_PUB_KEY_LEN: usize = 1952;
54
55/// Create a [`PeerId`] from raw ML-DSA public key bytes.
56///
57/// # Errors
58///
59/// Returns an error if the byte slice is not exactly 1952 bytes or
60/// cannot be parsed as a valid ML-DSA-65 public key.
61pub fn peer_id_from_public_key_bytes(bytes: &[u8]) -> Result<PeerId> {
62    if bytes.len() != ML_DSA_PUB_KEY_LEN {
63        return Err(P2PError::Identity(IdentityError::InvalidFormat(
64            "Invalid ML-DSA public key length".to_string().into(),
65        )));
66    }
67
68    let public_key = MlDsaPublicKey::from_bytes(bytes).map_err(|e| {
69        IdentityError::InvalidFormat(format!("Invalid ML-DSA public key: {:?}", e).into())
70    })?;
71
72    Ok(peer_id_from_public_key(&public_key))
73}
74
75/// Public node identity information (without secret keys) - safe to clone
76#[derive(Clone)]
77pub struct PublicNodeIdentity {
78    /// ML-DSA public key
79    public_key: MlDsaPublicKey,
80    /// Peer ID derived from public key
81    peer_id: PeerId,
82}
83
84impl PublicNodeIdentity {
85    /// Get peer ID
86    pub fn peer_id(&self) -> &PeerId {
87        &self.peer_id
88    }
89
90    /// Get public key
91    pub fn public_key(&self) -> &MlDsaPublicKey {
92        &self.public_key
93    }
94
95    // Word addresses are not part of identity; use bootstrap/transport layers
96}
97
98/// Core node identity with cryptographic keys
99///
100/// `Debug` is manually implemented to redact secret key material.
101pub struct NodeIdentity {
102    /// ML-DSA-65 secret key (private)
103    secret_key: MlDsaSecretKey,
104    /// ML-DSA-65 public key
105    public_key: MlDsaPublicKey,
106    /// Peer ID derived from public key
107    peer_id: PeerId,
108}
109
110impl fmt::Debug for NodeIdentity {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.debug_struct("NodeIdentity")
113            .field("peer_id", &self.peer_id)
114            .field("secret_key", &"[REDACTED]")
115            .finish()
116    }
117}
118
119impl NodeIdentity {
120    /// Generate new identity
121    pub fn generate() -> Result<Self> {
122        // Generate ML-DSA-65 key pair (saorsa-transport integration)
123        let (public_key, secret_key) =
124            crate::quantum_crypto::generate_ml_dsa_keypair().map_err(|e| {
125                P2PError::Identity(IdentityError::InvalidFormat(
126                    format!("Failed to generate ML-DSA key pair: {}", e).into(),
127                ))
128            })?;
129
130        let peer_id = peer_id_from_public_key(&public_key);
131
132        Ok(Self {
133            secret_key,
134            public_key,
135            peer_id,
136        })
137    }
138
139    /// Generate from seed (deterministic)
140    pub fn from_seed(seed: &[u8; 32]) -> Result<Self> {
141        // Derive a 32-byte ML-DSA seed from the input via HKDF-SHA3
142        let mut xi = [0u8; 32];
143        HkdfSha3_256::derive(seed, None, b"saorsa-node-identity-seed", &mut xi).map_err(|_| {
144            P2PError::Identity(IdentityError::InvalidFormat("HKDF expand failed".into()))
145        })?;
146
147        // Generate a real ML-DSA-65 keypair deterministically from the seed
148        let dsa = MlDsa::new(MlDsaVariant::MlDsa65);
149        let (pk, sk) = dsa.generate_keypair_from_seed(&xi);
150
151        let public_key = MlDsaPublicKey::from_bytes(&pk.to_bytes()).map_err(|e| {
152            P2PError::Identity(IdentityError::InvalidFormat(
153                format!("Invalid ML-DSA public key bytes: {e}").into(),
154            ))
155        })?;
156        let secret_key = MlDsaSecretKey::from_bytes(&sk.to_bytes()).map_err(|e| {
157            P2PError::Identity(IdentityError::InvalidFormat(
158                format!("Invalid ML-DSA secret key bytes: {e}").into(),
159            ))
160        })?;
161
162        let peer_id = peer_id_from_public_key(&public_key);
163
164        Ok(Self {
165            secret_key,
166            public_key,
167            peer_id,
168        })
169    }
170
171    /// Get peer ID
172    pub fn peer_id(&self) -> &PeerId {
173        &self.peer_id
174    }
175
176    /// Get public key
177    pub fn public_key(&self) -> &MlDsaPublicKey {
178        &self.public_key
179    }
180
181    // No Proof-of-Work in this crate
182
183    /// Get secret key bytes (for raw key authentication)
184    pub fn secret_key_bytes(&self) -> &[u8] {
185        self.secret_key.as_bytes()
186    }
187
188    /// Sign a message
189    pub fn sign(&self, message: &[u8]) -> Result<MlDsaSignature> {
190        crate::quantum_crypto::ml_dsa_sign(&self.secret_key, message).map_err(|e| {
191            P2PError::Identity(IdentityError::InvalidFormat(
192                format!("ML-DSA signing failed: {:?}", e).into(),
193            ))
194        })
195    }
196
197    /// Verify a signature
198    pub fn verify(&self, message: &[u8], signature: &MlDsaSignature) -> Result<bool> {
199        crate::quantum_crypto::ml_dsa_verify(&self.public_key, message, signature).map_err(|e| {
200            P2PError::Identity(IdentityError::InvalidFormat(
201                format!("ML-DSA verification failed: {:?}", e).into(),
202            ))
203        })
204    }
205
206    /// Create a public version of this identity (safe to clone)
207    pub fn to_public(&self) -> PublicNodeIdentity {
208        PublicNodeIdentity {
209            public_key: self.public_key.clone(),
210            peer_id: self.peer_id,
211        }
212    }
213}
214
215impl NodeIdentity {
216    /// Create an identity from an existing secret key
217    /// Note: Currently not supported as saorsa-transport doesn't provide public key derivation from secret key
218    /// This would require storing both keys together
219    pub fn from_secret_key(_secret_key: MlDsaSecretKey) -> Result<Self> {
220        Err(P2PError::Identity(IdentityError::InvalidFormat(
221            "Creating identity from secret key alone is not supported"
222                .to_string()
223                .into(),
224        )))
225    }
226}
227
228impl NodeIdentity {
229    /// Save identity to a JSON file (async)
230    pub async fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
231        use tokio::fs;
232        let data = self.export();
233        let json = serde_json::to_string_pretty(&data).map_err(|e| {
234            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
235                format!("Failed to serialize identity: {}", e).into(),
236            ))
237        })?;
238
239        if let Some(parent) = path.parent() {
240            fs::create_dir_all(parent).await.map_err(|e| {
241                P2PError::Identity(crate::error::IdentityError::InvalidFormat(
242                    format!("Failed to create directory: {}", e).into(),
243                ))
244            })?;
245        }
246
247        tokio::fs::write(path, json).await.map_err(|e| {
248            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
249                format!("Failed to write identity file: {}", e).into(),
250            ))
251        })?;
252        Ok(())
253    }
254
255    /// Load identity from a JSON file (async)
256    pub async fn load_from_file(path: &std::path::Path) -> Result<Self> {
257        let json = tokio::fs::read_to_string(path).await.map_err(|e| {
258            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
259                format!("Failed to read identity file: {}", e).into(),
260            ))
261        })?;
262        let data: IdentityData = serde_json::from_str(&json).map_err(|e| {
263            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
264                format!("Failed to deserialize identity: {}", e).into(),
265            ))
266        })?;
267        Self::import(&data)
268    }
269}
270
271/// Serializable identity data for persistence
272#[derive(Serialize, Deserialize)]
273pub struct IdentityData {
274    /// ML-DSA secret key bytes (4032 bytes for ML-DSA-65)
275    pub secret_key: Vec<u8>,
276    /// ML-DSA public key bytes (1952 bytes for ML-DSA-65)
277    pub public_key: Vec<u8>,
278}
279
280impl NodeIdentity {
281    /// Export identity for persistence
282    pub fn export(&self) -> IdentityData {
283        IdentityData {
284            secret_key: self.secret_key.as_bytes().to_vec(),
285            public_key: self.public_key.as_bytes().to_vec(),
286        }
287    }
288
289    /// Import identity from persisted data
290    pub fn import(data: &IdentityData) -> Result<Self> {
291        // Reconstruct keys from bytes
292        let secret_key =
293            crate::quantum_crypto::saorsa_transport_integration::MlDsaSecretKey::from_bytes(
294                &data.secret_key,
295            )
296            .map_err(|e| {
297                P2PError::Identity(IdentityError::InvalidFormat(
298                    format!("Invalid ML-DSA secret key: {e}").into(),
299                ))
300            })?;
301        let public_key =
302            crate::quantum_crypto::saorsa_transport_integration::MlDsaPublicKey::from_bytes(
303                &data.public_key,
304            )
305            .map_err(|e| {
306                P2PError::Identity(IdentityError::InvalidFormat(
307                    format!("Invalid ML-DSA public key: {e}").into(),
308                ))
309            })?;
310
311        let peer_id = peer_id_from_public_key(&public_key);
312
313        Ok(Self {
314            secret_key,
315            public_key,
316            peer_id,
317        })
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_peer_id_generation() {
327        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
328            .expect("ML-DSA key generation should succeed");
329        let peer_id = peer_id_from_public_key(&public_key);
330
331        // Should be 32 bytes
332        assert_eq!(peer_id.to_bytes().len(), 32);
333
334        // Should be deterministic
335        let peer_id2 = peer_id_from_public_key(&public_key);
336        assert_eq!(peer_id, peer_id2);
337    }
338
339    #[test]
340    fn test_xor_distance() {
341        let id1 = PeerId([0u8; 32]);
342        let mut id2_bytes = [0u8; 32];
343        id2_bytes[0] = 0xFF;
344        let id2 = PeerId(id2_bytes);
345
346        let distance = id1.xor_distance(&id2);
347        assert_eq!(distance[0], 0xFF);
348        for byte in &distance[1..] {
349            assert_eq!(*byte, 0);
350        }
351    }
352
353    #[test]
354    fn test_proof_of_work() {
355        // PoW removed: this test no longer applicable
356    }
357
358    #[test]
359    fn test_identity_generation() {
360        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
361
362        // Test signing and verification
363        let message = b"Hello, P2P!";
364        let signature = identity.sign(message).unwrap();
365        assert!(identity.verify(message, &signature).unwrap());
366
367        // Wrong message should fail with original signature
368        assert!(!identity.verify(b"Wrong message", &signature).unwrap());
369    }
370
371    #[test]
372    fn test_deterministic_generation() {
373        let seed = [0x42; 32];
374        let identity1 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
375        let identity2 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
376
377        // Should generate same identity
378        assert_eq!(identity1.peer_id, identity2.peer_id);
379        assert_eq!(
380            identity1.public_key().as_bytes(),
381            identity2.public_key().as_bytes()
382        );
383    }
384
385    #[test]
386    fn test_identity_persistence() {
387        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
388
389        // Export
390        let data = identity.export();
391
392        // Import
393        let imported = NodeIdentity::import(&data).expect("Import should succeed with valid data");
394
395        // Should be the same
396        assert_eq!(identity.peer_id, imported.peer_id);
397        assert_eq!(
398            identity.public_key().as_bytes(),
399            imported.public_key().as_bytes()
400        );
401
402        // Should be able to sign with imported identity
403        let message = b"Test message";
404        let signature = imported.sign(message);
405        assert!(identity.verify(message, &signature.unwrap()).unwrap());
406    }
407
408    #[test]
409    fn test_peer_id_display_full_hex() {
410        let id = PeerId([0xAB; 32]);
411        let display = format!("{}", id);
412        assert_eq!(display.len(), 64);
413        assert_eq!(display, "ab".repeat(32));
414    }
415
416    #[test]
417    fn test_peer_id_ord() {
418        let a = PeerId([0x00; 32]);
419        let b = PeerId([0xFF; 32]);
420        assert!(a < b);
421    }
422
423    #[test]
424    fn test_peer_id_from_str() {
425        let hex = "ab".repeat(32);
426        let id: PeerId = hex.parse().expect("should parse valid hex");
427        assert_eq!(id.0, [0xAB; 32]);
428    }
429
430    #[test]
431    fn test_peer_id_json_roundtrip() {
432        let id = PeerId([0xAB; 32]);
433        let json = serde_json::to_string(&id).expect("serialize");
434        assert_eq!(json, format!("\"{}\"", "ab".repeat(32)));
435        let deserialized: PeerId = serde_json::from_str(&json).expect("deserialize");
436        assert_eq!(id, deserialized);
437    }
438
439    #[test]
440    fn test_peer_id_postcard_roundtrip() {
441        let id = PeerId([0xAB; 32]);
442        let bytes = postcard::to_stdvec(&id).expect("serialize");
443        let deserialized: PeerId = postcard::from_bytes(&bytes).expect("deserialize");
444        assert_eq!(id, deserialized);
445    }
446}