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// This software is licensed under the MIT license <LICENSE-MIT or
6// https://opensource.org/licenses/MIT> or the Apache License, Version 2.0
7// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, at your
8// option. This file may not be copied, modified, or distributed except
9// according to those terms.
10
11// Copyright 2024 P2P Foundation
12// SPDX-License-Identifier: MIT OR Apache-2.0
13
14//! Peer Identity
15//!
16//! Implements the core identity system for P2P nodes with:
17//! - ML-DSA-65 post-quantum cryptographic keys
18//! - Four-word human-readable addresses
19//! - Deterministic generation from seeds
20
21use crate::error::IdentityError;
22use crate::{P2PError, Result};
23use saorsa_pqc::HkdfSha3_256;
24use saorsa_pqc::api::sig::{MlDsa, MlDsaVariant};
25use saorsa_pqc::api::traits::Kdf;
26use serde::{Deserialize, Serialize};
27use std::fmt;
28
29// Import PQC types from saorsa_transport via quantum_crypto module
30use crate::quantum_crypto::saorsa_transport_integration::{
31    MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature,
32};
33
34// Re-export canonical PeerId from the peer_id module.
35pub use super::peer_id::{PEER_ID_BYTE_LEN, PeerId, PeerIdParseError};
36
37/// Create a [`PeerId`] from an ML-DSA public key.
38///
39/// This is a standalone function because it depends on `MlDsaPublicKey`
40/// from `saorsa-pqc`, which `saorsa-types` does not (and should not)
41/// depend on.
42pub fn peer_id_from_public_key(public_key: &MlDsaPublicKey) -> PeerId {
43    let hash = blake3::hash(public_key.as_bytes());
44    PeerId(*hash.as_bytes())
45}
46
47/// ML-DSA-65 public key length in bytes.
48const ML_DSA_PUB_KEY_LEN: usize = 1952;
49
50/// Create a [`PeerId`] from raw ML-DSA public key bytes.
51///
52/// # Errors
53///
54/// Returns an error if the byte slice is not exactly 1952 bytes or
55/// cannot be parsed as a valid ML-DSA-65 public key.
56pub fn peer_id_from_public_key_bytes(bytes: &[u8]) -> Result<PeerId> {
57    if bytes.len() != ML_DSA_PUB_KEY_LEN {
58        return Err(P2PError::Identity(IdentityError::InvalidFormat(
59            "Invalid ML-DSA public key length".to_string().into(),
60        )));
61    }
62
63    let public_key = MlDsaPublicKey::from_bytes(bytes).map_err(|e| {
64        IdentityError::InvalidFormat(format!("Invalid ML-DSA public key: {:?}", e).into())
65    })?;
66
67    Ok(peer_id_from_public_key(&public_key))
68}
69
70/// Create a [`PeerId`] from an authenticated ML-DSA-65 TLS SPKI.
71///
72/// `saorsa-transport` exposes the exact peer certificate identity from a
73/// completed QUIC/TLS handshake as DER-encoded SubjectPublicKeyInfo. Validate
74/// the DER shape, algorithm identifier, absent ML-DSA parameters, and
75/// byte-aligned key before deriving the overlay identity from the raw key.
76#[cfg(feature = "native")]
77pub(crate) fn peer_id_from_public_key_spki(spki_bytes: &[u8]) -> Result<PeerId> {
78    let public_key =
79        saorsa_transport::crypto::raw_public_keys::pqc::extract_public_key_from_spki(spki_bytes)
80            .map_err(|e| {
81                P2PError::Identity(IdentityError::InvalidFormat(
82                    format!("Invalid ML-DSA SubjectPublicKeyInfo: {e}").into(),
83                ))
84            })?;
85
86    peer_id_from_public_key_bytes(public_key.as_bytes())
87}
88
89/// Public node identity information (without secret keys) - safe to clone
90#[derive(Clone)]
91pub struct PublicNodeIdentity {
92    /// ML-DSA public key
93    public_key: MlDsaPublicKey,
94    /// Peer ID derived from public key
95    peer_id: PeerId,
96}
97
98impl PublicNodeIdentity {
99    /// Get peer ID
100    pub fn peer_id(&self) -> &PeerId {
101        &self.peer_id
102    }
103
104    /// Get public key
105    pub fn public_key(&self) -> &MlDsaPublicKey {
106        &self.public_key
107    }
108
109    // Word addresses are not part of identity; use bootstrap/transport layers
110}
111
112/// Core node identity with cryptographic keys
113///
114/// `Debug` is manually implemented to redact secret key material.
115pub struct NodeIdentity {
116    /// ML-DSA-65 secret key (private)
117    secret_key: MlDsaSecretKey,
118    /// ML-DSA-65 public key
119    public_key: MlDsaPublicKey,
120    /// Peer ID derived from public key
121    peer_id: PeerId,
122}
123
124impl fmt::Debug for NodeIdentity {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        f.debug_struct("NodeIdentity")
127            .field("peer_id", &self.peer_id)
128            .field("secret_key", &"[REDACTED]")
129            .finish()
130    }
131}
132
133impl NodeIdentity {
134    /// Generate new identity
135    pub fn generate() -> Result<Self> {
136        // Generate ML-DSA-65 key pair (saorsa-transport integration)
137        let (public_key, secret_key) =
138            crate::quantum_crypto::generate_ml_dsa_keypair().map_err(|e| {
139                P2PError::Identity(IdentityError::InvalidFormat(
140                    format!("Failed to generate ML-DSA key pair: {}", e).into(),
141                ))
142            })?;
143
144        let peer_id = peer_id_from_public_key(&public_key);
145
146        Ok(Self {
147            secret_key,
148            public_key,
149            peer_id,
150        })
151    }
152
153    /// Generate from seed (deterministic)
154    pub fn from_seed(seed: &[u8; 32]) -> Result<Self> {
155        // Derive a 32-byte ML-DSA seed from the input via HKDF-SHA3
156        let mut xi = [0u8; 32];
157        HkdfSha3_256::derive(seed, None, b"saorsa-node-identity-seed", &mut xi).map_err(|_| {
158            P2PError::Identity(IdentityError::InvalidFormat("HKDF expand failed".into()))
159        })?;
160
161        // Generate a real ML-DSA-65 keypair deterministically from the seed
162        let dsa = MlDsa::new(MlDsaVariant::MlDsa65);
163        let (pk, sk) = dsa.generate_keypair_from_seed(&xi);
164
165        let public_key = MlDsaPublicKey::from_bytes(&pk.to_bytes()).map_err(|e| {
166            P2PError::Identity(IdentityError::InvalidFormat(
167                format!("Invalid ML-DSA public key bytes: {e}").into(),
168            ))
169        })?;
170        let secret_key = MlDsaSecretKey::from_bytes(&sk.to_bytes()).map_err(|e| {
171            P2PError::Identity(IdentityError::InvalidFormat(
172                format!("Invalid ML-DSA secret key bytes: {e}").into(),
173            ))
174        })?;
175
176        let peer_id = peer_id_from_public_key(&public_key);
177
178        Ok(Self {
179            secret_key,
180            public_key,
181            peer_id,
182        })
183    }
184
185    /// Get peer ID
186    pub fn peer_id(&self) -> &PeerId {
187        &self.peer_id
188    }
189
190    /// Get public key
191    pub fn public_key(&self) -> &MlDsaPublicKey {
192        &self.public_key
193    }
194
195    // No Proof-of-Work in this crate
196
197    /// Get secret key bytes (for raw key authentication)
198    pub fn secret_key_bytes(&self) -> &[u8] {
199        self.secret_key.as_bytes()
200    }
201
202    /// Get the ML-DSA-65 secret key.
203    ///
204    /// Used to seed the transport endpoint's TLS identity so a node presents its
205    /// *persistent* fingerprint across restarts (ADR-011), rather than a fresh
206    /// per-process key.
207    pub fn secret_key(&self) -> &MlDsaSecretKey {
208        &self.secret_key
209    }
210
211    /// Sign a message
212    pub fn sign(&self, message: &[u8]) -> Result<MlDsaSignature> {
213        crate::quantum_crypto::ml_dsa_sign(&self.secret_key, message).map_err(|e| {
214            P2PError::Identity(IdentityError::InvalidFormat(
215                format!("ML-DSA signing failed: {:?}", e).into(),
216            ))
217        })
218    }
219
220    /// Verify a signature
221    pub fn verify(&self, message: &[u8], signature: &MlDsaSignature) -> Result<bool> {
222        crate::quantum_crypto::ml_dsa_verify(&self.public_key, message, signature).map_err(|e| {
223            P2PError::Identity(IdentityError::InvalidFormat(
224                format!("ML-DSA verification failed: {:?}", e).into(),
225            ))
226        })
227    }
228
229    /// Create a public version of this identity (safe to clone)
230    pub fn to_public(&self) -> PublicNodeIdentity {
231        PublicNodeIdentity {
232            public_key: self.public_key.clone(),
233            peer_id: self.peer_id,
234        }
235    }
236}
237
238impl NodeIdentity {
239    /// Create an identity from an existing secret key
240    /// Note: Currently not supported as saorsa-transport doesn't provide public key derivation from secret key
241    /// This would require storing both keys together
242    pub fn from_secret_key(_secret_key: MlDsaSecretKey) -> Result<Self> {
243        Err(P2PError::Identity(IdentityError::InvalidFormat(
244            "Creating identity from secret key alone is not supported"
245                .to_string()
246                .into(),
247        )))
248    }
249}
250
251impl NodeIdentity {
252    /// Save identity to a JSON file (async)
253    #[cfg(feature = "native")]
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    #[cfg(feature = "native")]
281    pub async fn load_from_file(path: &std::path::Path) -> Result<Self> {
282        let json = tokio::fs::read_to_string(path).await.map_err(|e| {
283            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
284                format!("Failed to read identity file: {}", e).into(),
285            ))
286        })?;
287        let data: IdentityData = serde_json::from_str(&json).map_err(|e| {
288            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
289                format!("Failed to deserialize identity: {}", e).into(),
290            ))
291        })?;
292        Self::import(&data)
293    }
294}
295
296/// Serializable identity data for persistence
297#[derive(Serialize, Deserialize)]
298pub struct IdentityData {
299    /// ML-DSA secret key bytes (4032 bytes for ML-DSA-65)
300    pub secret_key: Vec<u8>,
301    /// ML-DSA public key bytes (1952 bytes for ML-DSA-65)
302    pub public_key: Vec<u8>,
303}
304
305impl NodeIdentity {
306    /// Export identity for persistence
307    pub fn export(&self) -> IdentityData {
308        IdentityData {
309            secret_key: self.secret_key.as_bytes().to_vec(),
310            public_key: self.public_key.as_bytes().to_vec(),
311        }
312    }
313
314    /// Import identity from persisted data
315    pub fn import(data: &IdentityData) -> Result<Self> {
316        // Reconstruct keys from bytes
317        let secret_key =
318            crate::quantum_crypto::saorsa_transport_integration::MlDsaSecretKey::from_bytes(
319                &data.secret_key,
320            )
321            .map_err(|e| {
322                P2PError::Identity(IdentityError::InvalidFormat(
323                    format!("Invalid ML-DSA secret key: {e}").into(),
324                ))
325            })?;
326        let public_key =
327            crate::quantum_crypto::saorsa_transport_integration::MlDsaPublicKey::from_bytes(
328                &data.public_key,
329            )
330            .map_err(|e| {
331                P2PError::Identity(IdentityError::InvalidFormat(
332                    format!("Invalid ML-DSA public key: {e}").into(),
333                ))
334            })?;
335
336        let peer_id = peer_id_from_public_key(&public_key);
337
338        Ok(Self {
339            secret_key,
340            public_key,
341            peer_id,
342        })
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[cfg(feature = "native")]
351    fn ml_dsa_65_spki(public_key: &[u8]) -> Vec<u8> {
352        const OID: [u8; 9] = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12];
353        let bit_string_len = public_key.len() + 1;
354        let algorithm_len = 2 + OID.len();
355        let algorithm_total_len = 2 + algorithm_len;
356        let bit_string_total_len = 4 + bit_string_len;
357        let outer_len = algorithm_total_len + bit_string_total_len;
358
359        let mut encoded = Vec::with_capacity(4 + outer_len);
360        encoded.extend_from_slice(&[
361            0x30,
362            0x82,
363            (outer_len >> 8) as u8,
364            outer_len as u8,
365            0x30,
366            algorithm_len as u8,
367            0x06,
368            OID.len() as u8,
369        ]);
370        encoded.extend_from_slice(&OID);
371        encoded.extend_from_slice(&[
372            0x03,
373            0x82,
374            (bit_string_len >> 8) as u8,
375            bit_string_len as u8,
376            0x00,
377        ]);
378        encoded.extend_from_slice(public_key);
379        encoded
380    }
381
382    #[test]
383    fn test_peer_id_generation() {
384        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
385            .expect("ML-DSA key generation should succeed");
386        let peer_id = peer_id_from_public_key(&public_key);
387
388        // Should be 32 bytes
389        assert_eq!(peer_id.to_bytes().len(), 32);
390
391        // Should be deterministic
392        let peer_id2 = peer_id_from_public_key(&public_key);
393        assert_eq!(peer_id, peer_id2);
394    }
395
396    #[test]
397    #[cfg(feature = "native")]
398    fn transport_spki_derives_same_peer_id_as_raw_key() {
399        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
400            .expect("ML-DSA key generation should succeed");
401        let spki = ml_dsa_65_spki(public_key.as_bytes());
402
403        let from_spki =
404            peer_id_from_public_key_spki(&spki).expect("valid ML-DSA SPKI should parse");
405
406        assert_eq!(from_spki, peer_id_from_public_key(&public_key));
407    }
408
409    #[test]
410    #[cfg(feature = "native")]
411    fn transport_spki_rejects_wrong_algorithm() {
412        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
413            .expect("ML-DSA key generation should succeed");
414        let mut spki = ml_dsa_65_spki(public_key.as_bytes());
415        let oid_last_byte = 16;
416        spki[oid_last_byte] = 0x11;
417
418        assert!(peer_id_from_public_key_spki(&spki).is_err());
419    }
420
421    #[test]
422    #[cfg(feature = "native")]
423    fn transport_spki_rejects_raw_public_key_bytes() {
424        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
425            .expect("ML-DSA key generation should succeed");
426
427        assert!(peer_id_from_public_key_spki(public_key.as_bytes()).is_err());
428    }
429
430    #[test]
431    fn test_xor_distance() {
432        let id1 = PeerId([0u8; 32]);
433        let mut id2_bytes = [0u8; 32];
434        id2_bytes[0] = 0xFF;
435        let id2 = PeerId(id2_bytes);
436
437        let distance = id1.xor_distance(&id2);
438        assert_eq!(distance[0], 0xFF);
439        for byte in &distance[1..] {
440            assert_eq!(*byte, 0);
441        }
442    }
443
444    #[test]
445    fn test_proof_of_work() {
446        // PoW removed: this test no longer applicable
447    }
448
449    #[test]
450    fn test_identity_generation() {
451        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
452
453        // Test signing and verification
454        let message = b"Hello, P2P!";
455        let signature = identity.sign(message).unwrap();
456        assert!(identity.verify(message, &signature).unwrap());
457
458        // Wrong message should fail with original signature
459        assert!(!identity.verify(b"Wrong message", &signature).unwrap());
460    }
461
462    #[test]
463    fn test_deterministic_generation() {
464        let seed = [0x42; 32];
465        let identity1 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
466        let identity2 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
467
468        // Should generate same identity
469        assert_eq!(identity1.peer_id, identity2.peer_id);
470        assert_eq!(
471            identity1.public_key().as_bytes(),
472            identity2.public_key().as_bytes()
473        );
474    }
475
476    #[test]
477    fn test_identity_persistence() {
478        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
479
480        // Export
481        let data = identity.export();
482
483        // Import
484        let imported = NodeIdentity::import(&data).expect("Import should succeed with valid data");
485
486        // Should be the same
487        assert_eq!(identity.peer_id, imported.peer_id);
488        assert_eq!(
489            identity.public_key().as_bytes(),
490            imported.public_key().as_bytes()
491        );
492
493        // Should be able to sign with imported identity
494        let message = b"Test message";
495        let signature = imported.sign(message);
496        assert!(identity.verify(message, &signature.unwrap()).unwrap());
497    }
498
499    #[test]
500    fn test_peer_id_display_full_hex() {
501        let id = PeerId([0xAB; 32]);
502        let display = format!("{}", id);
503        assert_eq!(display.len(), 64);
504        assert_eq!(display, "ab".repeat(32));
505    }
506
507    #[test]
508    fn test_peer_id_ord() {
509        let a = PeerId([0x00; 32]);
510        let b = PeerId([0xFF; 32]);
511        assert!(a < b);
512    }
513
514    #[test]
515    fn test_peer_id_from_str() {
516        let hex = "ab".repeat(32);
517        let id: PeerId = hex.parse().expect("should parse valid hex");
518        assert_eq!(id.0, [0xAB; 32]);
519    }
520
521    #[test]
522    fn test_peer_id_json_roundtrip() {
523        let id = PeerId([0xAB; 32]);
524        let json = serde_json::to_string(&id).expect("serialize");
525        assert_eq!(json, format!("\"{}\"", "ab".repeat(32)));
526        let deserialized: PeerId = serde_json::from_str(&json).expect("deserialize");
527        assert_eq!(id, deserialized);
528    }
529
530    #[test]
531    fn test_peer_id_postcard_roundtrip() {
532        let id = PeerId([0xAB; 32]);
533        let bytes = postcard::to_stdvec(&id).expect("serialize");
534        let deserialized: PeerId = postcard::from_bytes(&bytes).expect("deserialize");
535        assert_eq!(id, deserialized);
536    }
537}