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    /// Get the ML-DSA-65 secret key.
189    ///
190    /// Used to seed the transport endpoint's TLS identity so a node presents its
191    /// *persistent* fingerprint across restarts (ADR-011), rather than a fresh
192    /// per-process key.
193    pub fn secret_key(&self) -> &MlDsaSecretKey {
194        &self.secret_key
195    }
196
197    /// Sign a message
198    pub fn sign(&self, message: &[u8]) -> Result<MlDsaSignature> {
199        crate::quantum_crypto::ml_dsa_sign(&self.secret_key, message).map_err(|e| {
200            P2PError::Identity(IdentityError::InvalidFormat(
201                format!("ML-DSA signing failed: {:?}", e).into(),
202            ))
203        })
204    }
205
206    /// Verify a signature
207    pub fn verify(&self, message: &[u8], signature: &MlDsaSignature) -> Result<bool> {
208        crate::quantum_crypto::ml_dsa_verify(&self.public_key, message, signature).map_err(|e| {
209            P2PError::Identity(IdentityError::InvalidFormat(
210                format!("ML-DSA verification failed: {:?}", e).into(),
211            ))
212        })
213    }
214
215    /// Create a public version of this identity (safe to clone)
216    pub fn to_public(&self) -> PublicNodeIdentity {
217        PublicNodeIdentity {
218            public_key: self.public_key.clone(),
219            peer_id: self.peer_id,
220        }
221    }
222}
223
224impl NodeIdentity {
225    /// Create an identity from an existing secret key
226    /// Note: Currently not supported as saorsa-transport doesn't provide public key derivation from secret key
227    /// This would require storing both keys together
228    pub fn from_secret_key(_secret_key: MlDsaSecretKey) -> Result<Self> {
229        Err(P2PError::Identity(IdentityError::InvalidFormat(
230            "Creating identity from secret key alone is not supported"
231                .to_string()
232                .into(),
233        )))
234    }
235}
236
237impl NodeIdentity {
238    /// Save identity to a JSON file (async)
239    pub async fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
240        use tokio::fs;
241        let data = self.export();
242        let json = serde_json::to_string_pretty(&data).map_err(|e| {
243            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
244                format!("Failed to serialize identity: {}", e).into(),
245            ))
246        })?;
247
248        if let Some(parent) = path.parent() {
249            fs::create_dir_all(parent).await.map_err(|e| {
250                P2PError::Identity(crate::error::IdentityError::InvalidFormat(
251                    format!("Failed to create directory: {}", e).into(),
252                ))
253            })?;
254        }
255
256        tokio::fs::write(path, json).await.map_err(|e| {
257            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
258                format!("Failed to write identity file: {}", e).into(),
259            ))
260        })?;
261        Ok(())
262    }
263
264    /// Load identity from a JSON file (async)
265    pub async fn load_from_file(path: &std::path::Path) -> Result<Self> {
266        let json = tokio::fs::read_to_string(path).await.map_err(|e| {
267            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
268                format!("Failed to read identity file: {}", e).into(),
269            ))
270        })?;
271        let data: IdentityData = serde_json::from_str(&json).map_err(|e| {
272            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
273                format!("Failed to deserialize identity: {}", e).into(),
274            ))
275        })?;
276        Self::import(&data)
277    }
278}
279
280/// Serializable identity data for persistence
281#[derive(Serialize, Deserialize)]
282pub struct IdentityData {
283    /// ML-DSA secret key bytes (4032 bytes for ML-DSA-65)
284    pub secret_key: Vec<u8>,
285    /// ML-DSA public key bytes (1952 bytes for ML-DSA-65)
286    pub public_key: Vec<u8>,
287}
288
289impl NodeIdentity {
290    /// Export identity for persistence
291    pub fn export(&self) -> IdentityData {
292        IdentityData {
293            secret_key: self.secret_key.as_bytes().to_vec(),
294            public_key: self.public_key.as_bytes().to_vec(),
295        }
296    }
297
298    /// Import identity from persisted data
299    pub fn import(data: &IdentityData) -> Result<Self> {
300        // Reconstruct keys from bytes
301        let secret_key =
302            crate::quantum_crypto::saorsa_transport_integration::MlDsaSecretKey::from_bytes(
303                &data.secret_key,
304            )
305            .map_err(|e| {
306                P2PError::Identity(IdentityError::InvalidFormat(
307                    format!("Invalid ML-DSA secret key: {e}").into(),
308                ))
309            })?;
310        let public_key =
311            crate::quantum_crypto::saorsa_transport_integration::MlDsaPublicKey::from_bytes(
312                &data.public_key,
313            )
314            .map_err(|e| {
315                P2PError::Identity(IdentityError::InvalidFormat(
316                    format!("Invalid ML-DSA public key: {e}").into(),
317                ))
318            })?;
319
320        let peer_id = peer_id_from_public_key(&public_key);
321
322        Ok(Self {
323            secret_key,
324            public_key,
325            peer_id,
326        })
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_peer_id_generation() {
336        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
337            .expect("ML-DSA key generation should succeed");
338        let peer_id = peer_id_from_public_key(&public_key);
339
340        // Should be 32 bytes
341        assert_eq!(peer_id.to_bytes().len(), 32);
342
343        // Should be deterministic
344        let peer_id2 = peer_id_from_public_key(&public_key);
345        assert_eq!(peer_id, peer_id2);
346    }
347
348    #[test]
349    fn test_xor_distance() {
350        let id1 = PeerId([0u8; 32]);
351        let mut id2_bytes = [0u8; 32];
352        id2_bytes[0] = 0xFF;
353        let id2 = PeerId(id2_bytes);
354
355        let distance = id1.xor_distance(&id2);
356        assert_eq!(distance[0], 0xFF);
357        for byte in &distance[1..] {
358            assert_eq!(*byte, 0);
359        }
360    }
361
362    #[test]
363    fn test_proof_of_work() {
364        // PoW removed: this test no longer applicable
365    }
366
367    #[test]
368    fn test_identity_generation() {
369        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
370
371        // Test signing and verification
372        let message = b"Hello, P2P!";
373        let signature = identity.sign(message).unwrap();
374        assert!(identity.verify(message, &signature).unwrap());
375
376        // Wrong message should fail with original signature
377        assert!(!identity.verify(b"Wrong message", &signature).unwrap());
378    }
379
380    #[test]
381    fn test_deterministic_generation() {
382        let seed = [0x42; 32];
383        let identity1 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
384        let identity2 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
385
386        // Should generate same identity
387        assert_eq!(identity1.peer_id, identity2.peer_id);
388        assert_eq!(
389            identity1.public_key().as_bytes(),
390            identity2.public_key().as_bytes()
391        );
392    }
393
394    #[test]
395    fn test_identity_persistence() {
396        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
397
398        // Export
399        let data = identity.export();
400
401        // Import
402        let imported = NodeIdentity::import(&data).expect("Import should succeed with valid data");
403
404        // Should be the same
405        assert_eq!(identity.peer_id, imported.peer_id);
406        assert_eq!(
407            identity.public_key().as_bytes(),
408            imported.public_key().as_bytes()
409        );
410
411        // Should be able to sign with imported identity
412        let message = b"Test message";
413        let signature = imported.sign(message);
414        assert!(identity.verify(message, &signature.unwrap()).unwrap());
415    }
416
417    #[test]
418    fn test_peer_id_display_full_hex() {
419        let id = PeerId([0xAB; 32]);
420        let display = format!("{}", id);
421        assert_eq!(display.len(), 64);
422        assert_eq!(display, "ab".repeat(32));
423    }
424
425    #[test]
426    fn test_peer_id_ord() {
427        let a = PeerId([0x00; 32]);
428        let b = PeerId([0xFF; 32]);
429        assert!(a < b);
430    }
431
432    #[test]
433    fn test_peer_id_from_str() {
434        let hex = "ab".repeat(32);
435        let id: PeerId = hex.parse().expect("should parse valid hex");
436        assert_eq!(id.0, [0xAB; 32]);
437    }
438
439    #[test]
440    fn test_peer_id_json_roundtrip() {
441        let id = PeerId([0xAB; 32]);
442        let json = serde_json::to_string(&id).expect("serialize");
443        assert_eq!(json, format!("\"{}\"", "ab".repeat(32)));
444        let deserialized: PeerId = serde_json::from_str(&json).expect("deserialize");
445        assert_eq!(id, deserialized);
446    }
447
448    #[test]
449    fn test_peer_id_postcard_roundtrip() {
450        let id = PeerId([0xAB; 32]);
451        let bytes = postcard::to_stdvec(&id).expect("serialize");
452        let deserialized: PeerId = postcard::from_bytes(&bytes).expect("deserialize");
453        assert_eq!(id, deserialized);
454    }
455}