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//! 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        crate::quantum_crypto::ant_quic_integration::register_debug_ml_dsa_keypair(
142            &secret_key,
143            &public_key,
144        );
145
146        Ok(Self {
147            secret_key,
148            public_key,
149            node_id,
150        })
151    }
152
153    /// Convert this identity's NodeId to a UserId for use in adaptive modules
154    pub fn to_user_id(&self) -> crate::peer_record::UserId {
155        crate::peer_record::UserId::from_bytes(self.node_id.0)
156    }
157
158    /// Generate from seed (deterministic)
159    pub fn from_seed(seed: &[u8; 32]) -> Result<Self> {
160        // Deterministically derive key material via HKDF-SHA3
161        use saorsa_pqc::{HkdfSha3_256, api::traits::Kdf};
162
163        // ML-DSA-65 public/secret key sizes (bytes)
164        const ML_DSA_PUB_LEN: usize = 1952;
165        const ML_DSA_SEC_LEN: usize = 4032;
166
167        let mut derived = vec![0u8; ML_DSA_PUB_LEN + ML_DSA_SEC_LEN];
168        HkdfSha3_256::derive(seed, None, b"saorsa-node-identity-seed", &mut derived).map_err(
169            |_| P2PError::Identity(IdentityError::InvalidFormat("HKDF expand failed".into())),
170        )?;
171
172        let pub_bytes = &derived[..ML_DSA_PUB_LEN];
173        let sec_bytes = &derived[ML_DSA_PUB_LEN..];
174
175        // Construct keys from bytes; these constructors accept byte slices in our integration
176        let public_key =
177            crate::quantum_crypto::ant_quic_integration::MlDsaPublicKey::from_bytes(pub_bytes)
178                .map_err(|e| {
179                    P2PError::Identity(IdentityError::InvalidFormat(
180                        format!("Invalid ML-DSA public key bytes: {e}").into(),
181                    ))
182                })?;
183        let secret_key =
184            crate::quantum_crypto::ant_quic_integration::MlDsaSecretKey::from_bytes(sec_bytes)
185                .map_err(|e| {
186                    P2PError::Identity(IdentityError::InvalidFormat(
187                        format!("Invalid ML-DSA secret key bytes: {e}").into(),
188                    ))
189                })?;
190
191        let node_id = NodeId::from_public_key(&public_key);
192
193        crate::quantum_crypto::ant_quic_integration::register_debug_ml_dsa_keypair(
194            &secret_key,
195            &public_key,
196        );
197
198        Ok(Self {
199            secret_key,
200            public_key,
201            node_id,
202        })
203    }
204
205    /// Get node ID
206    pub fn node_id(&self) -> &NodeId {
207        &self.node_id
208    }
209
210    /// Get public key
211    pub fn public_key(&self) -> &MlDsaPublicKey {
212        &self.public_key
213    }
214
215    // No Proof-of-Work in this crate
216
217    /// Get secret key bytes (for raw key authentication)
218    pub fn secret_key_bytes(&self) -> &[u8] {
219        self.secret_key.as_bytes()
220    }
221
222    /// Sign a message
223    pub fn sign(&self, message: &[u8]) -> Result<MlDsaSignature> {
224        crate::quantum_crypto::ml_dsa_sign(&self.secret_key, message).map_err(|e| {
225            P2PError::Identity(IdentityError::InvalidFormat(
226                format!("ML-DSA signing failed: {:?}", e).into(),
227            ))
228        })
229    }
230
231    /// Verify a signature
232    pub fn verify(&self, message: &[u8], signature: &MlDsaSignature) -> Result<bool> {
233        crate::quantum_crypto::ml_dsa_verify(&self.public_key, message, signature).map_err(|e| {
234            P2PError::Identity(IdentityError::InvalidFormat(
235                format!("ML-DSA verification failed: {:?}", e).into(),
236            ))
237        })
238    }
239
240    /// Create a public version of this identity (safe to clone)
241    pub fn to_public(&self) -> PublicNodeIdentity {
242        PublicNodeIdentity {
243            public_key: self.public_key.clone(),
244            node_id: self.node_id.clone(),
245        }
246    }
247}
248
249impl NodeIdentity {
250    /// Create an identity from an existing secret key
251    /// Note: Currently not supported as ant-quic doesn't provide public key derivation from secret key
252    /// This would require storing both keys together
253    pub fn from_secret_key(_secret_key: MlDsaSecretKey) -> Result<Self> {
254        Err(P2PError::Identity(IdentityError::InvalidFormat(
255            "Creating identity from secret key alone is not supported"
256                .to_string()
257                .into(),
258        )))
259    }
260}
261
262// =============================================================================
263// Entangled Attestation Integration
264// =============================================================================
265
266impl NodeIdentity {
267    /// Create an entangled identity from this node identity.
268    ///
269    /// An entangled identity binds this node's cryptographic identity to
270    /// the hash of the executing binary and a unique nonce. This ensures
271    /// that any modification to the software forces a change in identity,
272    /// preventing attackers from maintaining reputation while running
273    /// malicious code.
274    ///
275    /// # Arguments
276    ///
277    /// * `binary_hash` - BLAKE3 hash of the executing binary
278    /// * `nonce` - Unique nonce (e.g., timestamp or random value)
279    ///
280    /// # Example
281    ///
282    /// ```rust,ignore
283    /// let identity = NodeIdentity::generate()?;
284    /// let binary_hash = compute_binary_hash(); // BLAKE3 hash of executable
285    /// let nonce = std::time::SystemTime::now()
286    ///     .duration_since(std::time::UNIX_EPOCH)
287    ///     .unwrap()
288    ///     .as_secs();
289    /// let entangled_id = identity.to_entangled_id(&binary_hash, nonce);
290    /// ```
291    #[must_use]
292    pub fn to_entangled_id(
293        &self,
294        binary_hash: &[u8; 32],
295        nonce: u64,
296    ) -> crate::attestation::EntangledId {
297        crate::attestation::EntangledId::derive(&self.public_key, binary_hash, nonce)
298    }
299
300    /// Verify that an entangled ID was derived from this identity.
301    ///
302    /// This re-derives the entangled ID using this node's public key
303    /// and the stored binary hash/nonce from the provided EntangledId,
304    /// then compares with the stored ID.
305    ///
306    /// # Arguments
307    ///
308    /// * `entangled_id` - The entangled ID to verify
309    ///
310    /// # Returns
311    ///
312    /// `true` if the entangled ID was derived from this identity.
313    #[must_use]
314    pub fn verify_entangled_id(&self, entangled_id: &crate::attestation::EntangledId) -> bool {
315        entangled_id.verify(&self.public_key)
316    }
317
318    /// Verify that an entangled ID was derived from this identity with a specific binary.
319    ///
320    /// This is a stricter verification that also checks the binary hash matches.
321    ///
322    /// # Arguments
323    ///
324    /// * `entangled_id` - The entangled ID to verify
325    /// * `binary_hash` - The expected binary hash
326    ///
327    /// # Returns
328    ///
329    /// `true` if the entangled ID was derived from this identity with the given binary hash.
330    #[must_use]
331    pub fn verify_entangled_id_with_binary(
332        &self,
333        entangled_id: &crate::attestation::EntangledId,
334        binary_hash: &[u8; 32],
335    ) -> bool {
336        entangled_id.verify_with_binary(&self.public_key, binary_hash)
337    }
338}
339
340impl NodeIdentity {
341    /// Save identity to a JSON file (async)
342    pub async fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
343        use tokio::fs;
344        let data = self.export();
345        let json = serde_json::to_string_pretty(&data).map_err(|e| {
346            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
347                format!("Failed to serialize identity: {}", e).into(),
348            ))
349        })?;
350
351        if let Some(parent) = path.parent() {
352            fs::create_dir_all(parent).await.map_err(|e| {
353                P2PError::Identity(crate::error::IdentityError::InvalidFormat(
354                    format!("Failed to create directory: {}", e).into(),
355                ))
356            })?;
357        }
358
359        tokio::fs::write(path, json).await.map_err(|e| {
360            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
361                format!("Failed to write identity file: {}", e).into(),
362            ))
363        })?;
364        Ok(())
365    }
366
367    /// Load identity from a JSON file (async)
368    pub async fn load_from_file(path: &std::path::Path) -> Result<Self> {
369        let json = tokio::fs::read_to_string(path).await.map_err(|e| {
370            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
371                format!("Failed to read identity file: {}", e).into(),
372            ))
373        })?;
374        let data: IdentityData = serde_json::from_str(&json).map_err(|e| {
375            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
376                format!("Failed to deserialize identity: {}", e).into(),
377            ))
378        })?;
379        Self::import(&data)
380    }
381}
382
383/// Serializable identity data for persistence
384#[derive(Serialize, Deserialize)]
385pub struct IdentityData {
386    /// ML-DSA secret key bytes (4032 bytes for ML-DSA-65)
387    pub secret_key: Vec<u8>,
388    /// ML-DSA public key bytes (1952 bytes for ML-DSA-65)
389    pub public_key: Vec<u8>,
390}
391
392impl NodeIdentity {
393    /// Export identity for persistence
394    pub fn export(&self) -> IdentityData {
395        IdentityData {
396            secret_key: self.secret_key.as_bytes().to_vec(),
397            public_key: self.public_key.as_bytes().to_vec(),
398        }
399    }
400
401    /// Import identity from persisted data
402    pub fn import(data: &IdentityData) -> Result<Self> {
403        // Reconstruct keys from bytes
404        let secret_key = crate::quantum_crypto::ant_quic_integration::MlDsaSecretKey::from_bytes(
405            &data.secret_key,
406        )
407        .map_err(|e| {
408            P2PError::Identity(IdentityError::InvalidFormat(
409                format!("Invalid ML-DSA secret key: {e}").into(),
410            ))
411        })?;
412        let public_key = crate::quantum_crypto::ant_quic_integration::MlDsaPublicKey::from_bytes(
413            &data.public_key,
414        )
415        .map_err(|e| {
416            P2PError::Identity(IdentityError::InvalidFormat(
417                format!("Invalid ML-DSA public key: {e}").into(),
418            ))
419        })?;
420
421        let node_id = NodeId::from_public_key(&public_key);
422
423        crate::quantum_crypto::ant_quic_integration::register_debug_ml_dsa_keypair(
424            &secret_key,
425            &public_key,
426        );
427
428        Ok(Self {
429            secret_key,
430            public_key,
431            node_id,
432        })
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn test_node_id_generation() {
442        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
443            .expect("ML-DSA key generation should succeed");
444        let node_id = NodeId::from_public_key(&public_key);
445
446        // Should be 32 bytes
447        assert_eq!(node_id.to_bytes().len(), 32);
448
449        // Should be deterministic
450        let node_id2 = NodeId::from_public_key(&public_key);
451        assert_eq!(node_id, node_id2);
452    }
453
454    #[test]
455    fn test_xor_distance() {
456        let id1 = NodeId([0u8; 32]);
457        let mut id2_bytes = [0u8; 32];
458        id2_bytes[0] = 0xFF;
459        let id2 = NodeId(id2_bytes);
460
461        let distance = id1.xor_distance(&id2);
462        assert_eq!(distance[0], 0xFF);
463        for byte in &distance[1..] {
464            assert_eq!(*byte, 0);
465        }
466    }
467
468    #[test]
469    fn test_proof_of_work() {
470        // PoW removed: this test no longer applicable
471    }
472
473    #[test]
474    fn test_identity_generation() {
475        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
476
477        // Test signing and verification
478        let message = b"Hello, P2P!";
479        let signature = identity.sign(message).unwrap();
480        assert!(identity.verify(message, &signature).unwrap());
481
482        // Wrong message should fail with original signature
483        assert!(!identity.verify(b"Wrong message", &signature).unwrap());
484    }
485
486    #[test]
487    fn test_deterministic_generation() {
488        let seed = [0x42; 32];
489        let identity1 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
490        let identity2 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
491
492        // Should generate same identity
493        assert_eq!(identity1.node_id, identity2.node_id);
494        assert_eq!(
495            identity1.public_key().as_bytes(),
496            identity2.public_key().as_bytes()
497        );
498    }
499
500    #[test]
501    fn test_identity_persistence() {
502        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
503
504        // Export
505        let data = identity.export();
506
507        // Import
508        let imported = NodeIdentity::import(&data).expect("Import should succeed with valid data");
509
510        // Should be the same
511        assert_eq!(identity.node_id, imported.node_id);
512        assert_eq!(
513            identity.public_key().as_bytes(),
514            imported.public_key().as_bytes()
515        );
516
517        // Should be able to sign with imported identity
518        let message = b"Test message";
519        let signature = imported.sign(message);
520        assert!(identity.verify(message, &signature.unwrap()).unwrap());
521    }
522
523    // =========================================================================
524    // Entangled Attestation Integration Tests
525    // =========================================================================
526
527    #[test]
528    fn test_entangled_id_creation() {
529        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
530        let binary_hash = [0x42u8; 32];
531        let nonce = 12345u64;
532
533        let entangled_id = identity.to_entangled_id(&binary_hash, nonce);
534
535        // The entangled ID should be different from the node ID
536        assert_ne!(
537            entangled_id.id(),
538            identity.node_id().to_bytes(),
539            "Entangled ID should differ from plain NodeId"
540        );
541
542        // Should be deterministic
543        let entangled_id2 = identity.to_entangled_id(&binary_hash, nonce);
544        assert_eq!(entangled_id.id(), entangled_id2.id());
545    }
546
547    #[test]
548    fn test_entangled_id_verification() {
549        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
550        let binary_hash = [0x42u8; 32];
551        let nonce = 12345u64;
552
553        let entangled_id = identity.to_entangled_id(&binary_hash, nonce);
554
555        // Should verify against the same identity
556        assert!(
557            identity.verify_entangled_id(&entangled_id),
558            "Entangled ID should verify against its creating identity"
559        );
560    }
561
562    #[test]
563    fn test_entangled_id_rejects_different_identity() {
564        let identity1 = NodeIdentity::generate().expect("Identity generation should succeed");
565        let identity2 = NodeIdentity::generate().expect("Identity generation should succeed");
566        let binary_hash = [0x42u8; 32];
567        let nonce = 12345u64;
568
569        let entangled_id = identity1.to_entangled_id(&binary_hash, nonce);
570
571        // Should NOT verify against a different identity
572        assert!(
573            !identity2.verify_entangled_id(&entangled_id),
574            "Entangled ID should NOT verify against a different identity"
575        );
576    }
577
578    #[test]
579    fn test_entangled_id_with_binary_verification() {
580        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
581        let binary_hash = [0x42u8; 32];
582        let wrong_binary_hash = [0x43u8; 32];
583        let nonce = 12345u64;
584
585        let entangled_id = identity.to_entangled_id(&binary_hash, nonce);
586
587        // Should verify with correct binary hash
588        assert!(
589            identity.verify_entangled_id_with_binary(&entangled_id, &binary_hash),
590            "Should verify with correct binary hash"
591        );
592
593        // Should NOT verify with wrong binary hash
594        assert!(
595            !identity.verify_entangled_id_with_binary(&entangled_id, &wrong_binary_hash),
596            "Should NOT verify with wrong binary hash"
597        );
598    }
599
600    #[test]
601    fn test_entangled_id_to_node_id_consistency() {
602        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
603        let binary_hash = [0x42u8; 32];
604        let nonce = 12345u64;
605
606        let entangled_id = identity.to_entangled_id(&binary_hash, nonce);
607        let derived_node_id = entangled_id.to_node_id();
608
609        // The derived NodeId should have the same bytes as the entangled ID
610        assert_eq!(
611            derived_node_id.to_bytes(),
612            entangled_id.id(),
613            "NodeId derived from EntangledId should have same bytes"
614        );
615    }
616}