Skip to main content

saorsa_core/attestation/
entangled_id.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: david@saorsalabs.com
9
10//! Entangled Identity implementation.
11//!
12//! An Entangled Identity binds a node's cryptographic identity to:
13//! - Its public key (ML-DSA-65)
14//! - The hash of its executing binary
15//! - A unique nonce
16//!
17//! The derivation formula is:
18//! ```text
19//! N_ID = BLAKE3(PK || binary_hash || nonce)
20//! ```
21//!
22//! ## zkVM Compatibility
23//!
24//! The core derivation logic is provided by `saorsa-logic`, which is
25//! `no_std` compatible and can run inside zkVMs (SP1, RISC Zero).
26//! This allows nodes to generate zero-knowledge proofs of correct
27//! identity derivation.
28
29use crate::identity::node_identity::NodeId;
30use crate::quantum_crypto::ant_quic_integration::MlDsaPublicKey;
31use serde::{Deserialize, Serialize};
32use std::fmt;
33
34// Re-export constants from saorsa-logic for consistency and downstream use
35#[allow(unused_imports)]
36pub use saorsa_logic::attestation::{ENTANGLED_ID_SIZE, HASH_SIZE, ML_DSA_65_PUBLIC_KEY_SIZE};
37
38/// An Entangled Identity that binds a node's ID to its software.
39///
40/// This structure represents a node identity that is cryptographically
41/// entangled with the binary it is running, preventing identity spoofing
42/// while running modified software.
43#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub struct EntangledId {
45    /// The derived 32-byte identity: BLAKE3(PK || binary_hash || nonce)
46    id: [u8; 32],
47
48    /// The hash of the binary this identity is bound to
49    binary_hash: [u8; 32],
50
51    /// The nonce used in derivation
52    nonce: u64,
53}
54
55impl EntangledId {
56    /// Derive an entangled identity from its components.
57    ///
58    /// The derivation formula is:
59    /// ```text
60    /// N_ID = BLAKE3(PK || binary_hash || nonce)
61    /// ```
62    ///
63    /// # Arguments
64    ///
65    /// * `public_key` - The ML-DSA-65 public key
66    /// * `binary_hash` - The BLAKE3 hash of the binary
67    /// * `nonce` - A unique nonce (e.g., timestamp or random)
68    ///
69    /// # Returns
70    ///
71    /// A new `EntangledId` with the derived identity.
72    ///
73    /// # zkVM Compatibility
74    ///
75    /// This method uses `saorsa-logic` for the core derivation, which is
76    /// `no_std` compatible and can run inside zkVMs.
77    #[must_use]
78    pub fn derive(public_key: &MlDsaPublicKey, binary_hash: &[u8; 32], nonce: u64) -> Self {
79        // Delegate to saorsa-logic for zkVM-compatible derivation
80        let id = saorsa_logic::attestation::derive_entangled_id(
81            public_key.as_bytes(),
82            binary_hash,
83            nonce,
84        );
85        Self {
86            id,
87            binary_hash: *binary_hash,
88            nonce,
89        }
90    }
91
92    /// Verify that this entangled ID matches the given public key.
93    ///
94    /// This re-derives the ID from the public key and the stored binary hash/nonce,
95    /// then compares with the stored ID.
96    ///
97    /// # Arguments
98    ///
99    /// * `public_key` - The ML-DSA-65 public key to verify against
100    ///
101    /// # Returns
102    ///
103    /// `true` if the ID was derived from this public key and the stored binary hash/nonce.
104    ///
105    /// # zkVM Compatibility
106    ///
107    /// This method uses `saorsa-logic` for verification, enabling the same
108    /// logic to be proven inside a zkVM.
109    #[must_use]
110    pub fn verify(&self, public_key: &MlDsaPublicKey) -> bool {
111        // Delegate to saorsa-logic for zkVM-compatible verification
112        saorsa_logic::attestation::verify_entangled_id(
113            &self.id,
114            public_key.as_bytes(),
115            &self.binary_hash,
116            self.nonce,
117        )
118    }
119
120    /// Verify that this entangled ID matches the given public key and binary hash.
121    ///
122    /// This is a stricter verification that also checks the binary hash matches.
123    ///
124    /// # Arguments
125    ///
126    /// * `public_key` - The ML-DSA-65 public key to verify against
127    /// * `binary_hash` - The expected binary hash
128    ///
129    /// # Returns
130    ///
131    /// `true` if the ID was derived from this public key, this binary hash, and the stored nonce.
132    #[must_use]
133    pub fn verify_with_binary(&self, public_key: &MlDsaPublicKey, binary_hash: &[u8; 32]) -> bool {
134        // First check that the binary hash matches what we stored
135        if &self.binary_hash != binary_hash {
136            return false;
137        }
138
139        // Then verify the full derivation
140        self.verify(public_key)
141    }
142
143    /// Get the raw 32-byte identity.
144    #[must_use]
145    pub fn id(&self) -> &[u8; 32] {
146        &self.id
147    }
148
149    /// Get the binary hash this identity is bound to.
150    #[must_use]
151    pub fn binary_hash(&self) -> &[u8; 32] {
152        &self.binary_hash
153    }
154
155    /// Get the nonce used in derivation.
156    #[must_use]
157    pub fn nonce(&self) -> u64 {
158        self.nonce
159    }
160
161    /// Convert to a `NodeId` for use in DHT routing.
162    ///
163    /// The `NodeId` is simply the entangled ID bytes.
164    #[must_use]
165    pub fn to_node_id(&self) -> NodeId {
166        NodeId::from_bytes(self.id)
167    }
168
169    /// Calculate XOR distance to another entangled ID.
170    ///
171    /// This is used for Kademlia routing.
172    ///
173    /// # zkVM Compatibility
174    ///
175    /// This method uses `saorsa-logic` for the calculation, enabling
176    /// consistent distance computation across native and zkVM contexts.
177    #[must_use]
178    pub fn xor_distance(&self, other: &EntangledId) -> [u8; 32] {
179        saorsa_logic::attestation::xor_distance(&self.id, &other.id)
180    }
181
182    /// Create from raw bytes (for deserialization/testing).
183    ///
184    /// # Warning
185    ///
186    /// This bypasses the derivation process and should only be used
187    /// for deserialization or testing purposes.
188    #[must_use]
189    pub fn from_raw(id: [u8; 32], binary_hash: [u8; 32], nonce: u64) -> Self {
190        Self {
191            id,
192            binary_hash,
193            nonce,
194        }
195    }
196}
197
198impl fmt::Display for EntangledId {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        // Display first 8 bytes as hex for brevity
201        write!(f, "{}", hex::encode(&self.id[..8]))
202    }
203}
204
205impl fmt::Debug for EntangledId {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        f.debug_struct("EntangledId")
208            .field("id", &hex::encode(&self.id[..8]))
209            .field("binary_hash", &hex::encode(&self.binary_hash[..8]))
210            .field("nonce", &self.nonce)
211            .finish()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::quantum_crypto::generate_ml_dsa_keypair;
219
220    #[test]
221    fn test_derive_deterministic() {
222        let (pk, _) = generate_ml_dsa_keypair().unwrap();
223        let binary_hash = [0x42u8; 32];
224        let nonce = 12345u64;
225
226        let id1 = EntangledId::derive(&pk, &binary_hash, nonce);
227        let id2 = EntangledId::derive(&pk, &binary_hash, nonce);
228
229        assert_eq!(id1, id2);
230    }
231
232    #[test]
233    fn test_different_keys_different_ids() {
234        let (pk1, _) = generate_ml_dsa_keypair().unwrap();
235        let (pk2, _) = generate_ml_dsa_keypair().unwrap();
236        let binary_hash = [0x42u8; 32];
237        let nonce = 12345u64;
238
239        let id1 = EntangledId::derive(&pk1, &binary_hash, nonce);
240        let id2 = EntangledId::derive(&pk2, &binary_hash, nonce);
241
242        assert_ne!(id1.id(), id2.id());
243    }
244
245    #[test]
246    fn test_different_binaries_different_ids() {
247        let (pk, _) = generate_ml_dsa_keypair().unwrap();
248        let binary_hash1 = [0x42u8; 32];
249        let binary_hash2 = [0x43u8; 32];
250        let nonce = 12345u64;
251
252        let id1 = EntangledId::derive(&pk, &binary_hash1, nonce);
253        let id2 = EntangledId::derive(&pk, &binary_hash2, nonce);
254
255        assert_ne!(id1.id(), id2.id());
256    }
257
258    #[test]
259    fn test_verification() {
260        let (pk, _) = generate_ml_dsa_keypair().unwrap();
261        let binary_hash = [0x42u8; 32];
262        let nonce = 12345u64;
263
264        let id = EntangledId::derive(&pk, &binary_hash, nonce);
265
266        assert!(id.verify(&pk));
267    }
268
269    #[test]
270    fn test_verification_wrong_key() {
271        let (pk1, _) = generate_ml_dsa_keypair().unwrap();
272        let (pk2, _) = generate_ml_dsa_keypair().unwrap();
273        let binary_hash = [0x42u8; 32];
274        let nonce = 12345u64;
275
276        let id = EntangledId::derive(&pk1, &binary_hash, nonce);
277
278        assert!(!id.verify(&pk2));
279    }
280
281    #[test]
282    fn test_xor_distance_self() {
283        let (pk, _) = generate_ml_dsa_keypair().unwrap();
284        let id = EntangledId::derive(&pk, &[0u8; 32], 0);
285
286        let distance = id.xor_distance(&id);
287        assert_eq!(distance, [0u8; 32]);
288    }
289
290    #[test]
291    fn test_to_node_id() {
292        let (pk, _) = generate_ml_dsa_keypair().unwrap();
293        let id = EntangledId::derive(&pk, &[0u8; 32], 0);
294        let node_id = id.to_node_id();
295
296        assert_eq!(node_id.to_bytes(), id.id());
297    }
298
299    #[test]
300    fn test_serialization_roundtrip() {
301        let (pk, _) = generate_ml_dsa_keypair().unwrap();
302        let id = EntangledId::derive(&pk, &[0x42u8; 32], 12345);
303
304        let json = serde_json::to_string(&id).unwrap();
305        let restored: EntangledId = serde_json::from_str(&json).unwrap();
306
307        assert_eq!(id, restored);
308    }
309}