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.
76pub(crate) fn peer_id_from_public_key_spki(spki_bytes: &[u8]) -> Result<PeerId> {
77    let public_key =
78        saorsa_transport::crypto::raw_public_keys::pqc::extract_public_key_from_spki(spki_bytes)
79            .map_err(|e| {
80                P2PError::Identity(IdentityError::InvalidFormat(
81                    format!("Invalid ML-DSA SubjectPublicKeyInfo: {e}").into(),
82                ))
83            })?;
84
85    peer_id_from_public_key_bytes(public_key.as_bytes())
86}
87
88/// Public node identity information (without secret keys) - safe to clone
89#[derive(Clone)]
90pub struct PublicNodeIdentity {
91    /// ML-DSA public key
92    public_key: MlDsaPublicKey,
93    /// Peer ID derived from public key
94    peer_id: PeerId,
95}
96
97impl PublicNodeIdentity {
98    /// Get peer ID
99    pub fn peer_id(&self) -> &PeerId {
100        &self.peer_id
101    }
102
103    /// Get public key
104    pub fn public_key(&self) -> &MlDsaPublicKey {
105        &self.public_key
106    }
107
108    // Word addresses are not part of identity; use bootstrap/transport layers
109}
110
111/// Core node identity with cryptographic keys
112///
113/// `Debug` is manually implemented to redact secret key material.
114pub struct NodeIdentity {
115    /// ML-DSA-65 secret key (private)
116    secret_key: MlDsaSecretKey,
117    /// ML-DSA-65 public key
118    public_key: MlDsaPublicKey,
119    /// Peer ID derived from public key
120    peer_id: PeerId,
121}
122
123impl fmt::Debug for NodeIdentity {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.debug_struct("NodeIdentity")
126            .field("peer_id", &self.peer_id)
127            .field("secret_key", &"[REDACTED]")
128            .finish()
129    }
130}
131
132impl NodeIdentity {
133    /// Generate new identity
134    pub fn generate() -> Result<Self> {
135        // Generate ML-DSA-65 key pair (saorsa-transport integration)
136        let (public_key, secret_key) =
137            crate::quantum_crypto::generate_ml_dsa_keypair().map_err(|e| {
138                P2PError::Identity(IdentityError::InvalidFormat(
139                    format!("Failed to generate ML-DSA key pair: {}", e).into(),
140                ))
141            })?;
142
143        let peer_id = peer_id_from_public_key(&public_key);
144
145        Ok(Self {
146            secret_key,
147            public_key,
148            peer_id,
149        })
150    }
151
152    /// Generate from seed (deterministic)
153    pub fn from_seed(seed: &[u8; 32]) -> Result<Self> {
154        // Derive a 32-byte ML-DSA seed from the input via HKDF-SHA3
155        let mut xi = [0u8; 32];
156        HkdfSha3_256::derive(seed, None, b"saorsa-node-identity-seed", &mut xi).map_err(|_| {
157            P2PError::Identity(IdentityError::InvalidFormat("HKDF expand failed".into()))
158        })?;
159
160        // Generate a real ML-DSA-65 keypair deterministically from the seed
161        let dsa = MlDsa::new(MlDsaVariant::MlDsa65);
162        let (pk, sk) = dsa.generate_keypair_from_seed(&xi);
163
164        let public_key = MlDsaPublicKey::from_bytes(&pk.to_bytes()).map_err(|e| {
165            P2PError::Identity(IdentityError::InvalidFormat(
166                format!("Invalid ML-DSA public key bytes: {e}").into(),
167            ))
168        })?;
169        let secret_key = MlDsaSecretKey::from_bytes(&sk.to_bytes()).map_err(|e| {
170            P2PError::Identity(IdentityError::InvalidFormat(
171                format!("Invalid ML-DSA secret key bytes: {e}").into(),
172            ))
173        })?;
174
175        let peer_id = peer_id_from_public_key(&public_key);
176
177        Ok(Self {
178            secret_key,
179            public_key,
180            peer_id,
181        })
182    }
183
184    /// Get peer ID
185    pub fn peer_id(&self) -> &PeerId {
186        &self.peer_id
187    }
188
189    /// Get public key
190    pub fn public_key(&self) -> &MlDsaPublicKey {
191        &self.public_key
192    }
193
194    // No Proof-of-Work in this crate
195
196    /// Get secret key bytes (for raw key authentication)
197    pub fn secret_key_bytes(&self) -> &[u8] {
198        self.secret_key.as_bytes()
199    }
200
201    /// Get the ML-DSA-65 secret key.
202    ///
203    /// Used to seed the transport endpoint's TLS identity so a node presents its
204    /// *persistent* fingerprint across restarts (ADR-011), rather than a fresh
205    /// per-process key.
206    pub fn secret_key(&self) -> &MlDsaSecretKey {
207        &self.secret_key
208    }
209
210    /// Sign a message
211    pub fn sign(&self, message: &[u8]) -> Result<MlDsaSignature> {
212        crate::quantum_crypto::ml_dsa_sign(&self.secret_key, message).map_err(|e| {
213            P2PError::Identity(IdentityError::InvalidFormat(
214                format!("ML-DSA signing failed: {:?}", e).into(),
215            ))
216        })
217    }
218
219    /// Verify a signature
220    pub fn verify(&self, message: &[u8], signature: &MlDsaSignature) -> Result<bool> {
221        crate::quantum_crypto::ml_dsa_verify(&self.public_key, message, signature).map_err(|e| {
222            P2PError::Identity(IdentityError::InvalidFormat(
223                format!("ML-DSA verification failed: {:?}", e).into(),
224            ))
225        })
226    }
227
228    /// Create a public version of this identity (safe to clone)
229    pub fn to_public(&self) -> PublicNodeIdentity {
230        PublicNodeIdentity {
231            public_key: self.public_key.clone(),
232            peer_id: self.peer_id,
233        }
234    }
235}
236
237impl NodeIdentity {
238    /// Create an identity from an existing secret key
239    /// Note: Currently not supported as saorsa-transport doesn't provide public key derivation from secret key
240    /// This would require storing both keys together
241    pub fn from_secret_key(_secret_key: MlDsaSecretKey) -> Result<Self> {
242        Err(P2PError::Identity(IdentityError::InvalidFormat(
243            "Creating identity from secret key alone is not supported"
244                .to_string()
245                .into(),
246        )))
247    }
248}
249
250impl NodeIdentity {
251    /// Save identity to a JSON file (async)
252    pub async fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
253        use tokio::fs;
254        let data = self.export();
255        let json = serde_json::to_string_pretty(&data).map_err(|e| {
256            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
257                format!("Failed to serialize identity: {}", e).into(),
258            ))
259        })?;
260
261        if let Some(parent) = path.parent() {
262            fs::create_dir_all(parent).await.map_err(|e| {
263                P2PError::Identity(crate::error::IdentityError::InvalidFormat(
264                    format!("Failed to create directory: {}", e).into(),
265                ))
266            })?;
267        }
268
269        tokio::fs::write(path, json).await.map_err(|e| {
270            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
271                format!("Failed to write identity file: {}", e).into(),
272            ))
273        })?;
274        Ok(())
275    }
276
277    /// Load identity from a JSON file (async)
278    pub async fn load_from_file(path: &std::path::Path) -> Result<Self> {
279        let json = tokio::fs::read_to_string(path).await.map_err(|e| {
280            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
281                format!("Failed to read identity file: {}", e).into(),
282            ))
283        })?;
284        let data: IdentityData = serde_json::from_str(&json).map_err(|e| {
285            P2PError::Identity(crate::error::IdentityError::InvalidFormat(
286                format!("Failed to deserialize identity: {}", e).into(),
287            ))
288        })?;
289        Self::import(&data)
290    }
291}
292
293/// Serializable identity data for persistence
294#[derive(Serialize, Deserialize)]
295pub struct IdentityData {
296    /// ML-DSA secret key bytes (4032 bytes for ML-DSA-65)
297    pub secret_key: Vec<u8>,
298    /// ML-DSA public key bytes (1952 bytes for ML-DSA-65)
299    pub public_key: Vec<u8>,
300}
301
302impl NodeIdentity {
303    /// Export identity for persistence
304    pub fn export(&self) -> IdentityData {
305        IdentityData {
306            secret_key: self.secret_key.as_bytes().to_vec(),
307            public_key: self.public_key.as_bytes().to_vec(),
308        }
309    }
310
311    /// Import identity from persisted data
312    pub fn import(data: &IdentityData) -> Result<Self> {
313        // Reconstruct keys from bytes
314        let secret_key =
315            crate::quantum_crypto::saorsa_transport_integration::MlDsaSecretKey::from_bytes(
316                &data.secret_key,
317            )
318            .map_err(|e| {
319                P2PError::Identity(IdentityError::InvalidFormat(
320                    format!("Invalid ML-DSA secret key: {e}").into(),
321                ))
322            })?;
323        let public_key =
324            crate::quantum_crypto::saorsa_transport_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 peer_id = peer_id_from_public_key(&public_key);
334
335        Ok(Self {
336            secret_key,
337            public_key,
338            peer_id,
339        })
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    fn ml_dsa_65_spki(public_key: &[u8]) -> Vec<u8> {
348        const OID: [u8; 9] = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12];
349        let bit_string_len = public_key.len() + 1;
350        let algorithm_len = 2 + OID.len();
351        let algorithm_total_len = 2 + algorithm_len;
352        let bit_string_total_len = 4 + bit_string_len;
353        let outer_len = algorithm_total_len + bit_string_total_len;
354
355        let mut encoded = Vec::with_capacity(4 + outer_len);
356        encoded.extend_from_slice(&[
357            0x30,
358            0x82,
359            (outer_len >> 8) as u8,
360            outer_len as u8,
361            0x30,
362            algorithm_len as u8,
363            0x06,
364            OID.len() as u8,
365        ]);
366        encoded.extend_from_slice(&OID);
367        encoded.extend_from_slice(&[
368            0x03,
369            0x82,
370            (bit_string_len >> 8) as u8,
371            bit_string_len as u8,
372            0x00,
373        ]);
374        encoded.extend_from_slice(public_key);
375        encoded
376    }
377
378    #[test]
379    fn test_peer_id_generation() {
380        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
381            .expect("ML-DSA key generation should succeed");
382        let peer_id = peer_id_from_public_key(&public_key);
383
384        // Should be 32 bytes
385        assert_eq!(peer_id.to_bytes().len(), 32);
386
387        // Should be deterministic
388        let peer_id2 = peer_id_from_public_key(&public_key);
389        assert_eq!(peer_id, peer_id2);
390    }
391
392    #[test]
393    fn transport_spki_derives_same_peer_id_as_raw_key() {
394        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
395            .expect("ML-DSA key generation should succeed");
396        let spki = ml_dsa_65_spki(public_key.as_bytes());
397
398        let from_spki =
399            peer_id_from_public_key_spki(&spki).expect("valid ML-DSA SPKI should parse");
400
401        assert_eq!(from_spki, peer_id_from_public_key(&public_key));
402    }
403
404    #[test]
405    fn transport_spki_rejects_wrong_algorithm() {
406        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
407            .expect("ML-DSA key generation should succeed");
408        let mut spki = ml_dsa_65_spki(public_key.as_bytes());
409        let oid_last_byte = 16;
410        spki[oid_last_byte] = 0x11;
411
412        assert!(peer_id_from_public_key_spki(&spki).is_err());
413    }
414
415    #[test]
416    fn transport_spki_rejects_raw_public_key_bytes() {
417        let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
418            .expect("ML-DSA key generation should succeed");
419
420        assert!(peer_id_from_public_key_spki(public_key.as_bytes()).is_err());
421    }
422
423    #[test]
424    fn test_xor_distance() {
425        let id1 = PeerId([0u8; 32]);
426        let mut id2_bytes = [0u8; 32];
427        id2_bytes[0] = 0xFF;
428        let id2 = PeerId(id2_bytes);
429
430        let distance = id1.xor_distance(&id2);
431        assert_eq!(distance[0], 0xFF);
432        for byte in &distance[1..] {
433            assert_eq!(*byte, 0);
434        }
435    }
436
437    #[test]
438    fn test_proof_of_work() {
439        // PoW removed: this test no longer applicable
440    }
441
442    #[test]
443    fn test_identity_generation() {
444        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
445
446        // Test signing and verification
447        let message = b"Hello, P2P!";
448        let signature = identity.sign(message).unwrap();
449        assert!(identity.verify(message, &signature).unwrap());
450
451        // Wrong message should fail with original signature
452        assert!(!identity.verify(b"Wrong message", &signature).unwrap());
453    }
454
455    #[test]
456    fn test_deterministic_generation() {
457        let seed = [0x42; 32];
458        let identity1 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
459        let identity2 = NodeIdentity::from_seed(&seed).expect("Identity from seed should succeed");
460
461        // Should generate same identity
462        assert_eq!(identity1.peer_id, identity2.peer_id);
463        assert_eq!(
464            identity1.public_key().as_bytes(),
465            identity2.public_key().as_bytes()
466        );
467    }
468
469    #[test]
470    fn test_identity_persistence() {
471        let identity = NodeIdentity::generate().expect("Identity generation should succeed");
472
473        // Export
474        let data = identity.export();
475
476        // Import
477        let imported = NodeIdentity::import(&data).expect("Import should succeed with valid data");
478
479        // Should be the same
480        assert_eq!(identity.peer_id, imported.peer_id);
481        assert_eq!(
482            identity.public_key().as_bytes(),
483            imported.public_key().as_bytes()
484        );
485
486        // Should be able to sign with imported identity
487        let message = b"Test message";
488        let signature = imported.sign(message);
489        assert!(identity.verify(message, &signature.unwrap()).unwrap());
490    }
491
492    #[test]
493    fn test_peer_id_display_full_hex() {
494        let id = PeerId([0xAB; 32]);
495        let display = format!("{}", id);
496        assert_eq!(display.len(), 64);
497        assert_eq!(display, "ab".repeat(32));
498    }
499
500    #[test]
501    fn test_peer_id_ord() {
502        let a = PeerId([0x00; 32]);
503        let b = PeerId([0xFF; 32]);
504        assert!(a < b);
505    }
506
507    #[test]
508    fn test_peer_id_from_str() {
509        let hex = "ab".repeat(32);
510        let id: PeerId = hex.parse().expect("should parse valid hex");
511        assert_eq!(id.0, [0xAB; 32]);
512    }
513
514    #[test]
515    fn test_peer_id_json_roundtrip() {
516        let id = PeerId([0xAB; 32]);
517        let json = serde_json::to_string(&id).expect("serialize");
518        assert_eq!(json, format!("\"{}\"", "ab".repeat(32)));
519        let deserialized: PeerId = serde_json::from_str(&json).expect("deserialize");
520        assert_eq!(id, deserialized);
521    }
522
523    #[test]
524    fn test_peer_id_postcard_roundtrip() {
525        let id = PeerId([0xAB; 32]);
526        let bytes = postcard::to_stdvec(&id).expect("serialize");
527        let deserialized: PeerId = postcard::from_bytes(&bytes).expect("deserialize");
528        assert_eq!(id, deserialized);
529    }
530}