diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 45989eab67..d8aba6c268 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -63,6 +63,7 @@ use std::os::raw::c_int as RawFd; use std::path::Path; use std::sync::Arc; use tokio::sync::mpsc::Sender; +use tokio::sync::Mutex; use url::Url; #[cfg(all( @@ -195,6 +196,10 @@ pub struct BaseClientBuilder { connection_fd_callback: Option>, derivation_material: Option, + // Shared derivation material wrapped in Arc> for thread-safe access + // across multiple clients. This allows multiple clients to share the same + // derivation source while maintaining safe concurrent access. + shared_derivation_material: Option>>, } impl BaseClientBuilder @@ -220,6 +225,7 @@ where #[cfg(unix)] connection_fd_callback: None, derivation_material: None, + shared_derivation_material: None, } } @@ -232,6 +238,18 @@ where self } + /// Set shared derivation material for thread-safe sharing across multiple clients. + /// This is useful when multiple clients need to derive keys from the same source + /// while ensuring thread-safe access through Arc>. + #[must_use] + pub fn with_shared_derivation_material( + mut self, + derivation_material: Option>>, + ) -> Self { + self.shared_derivation_material = derivation_material; + self + } + #[must_use] pub fn with_forget_me(mut self, forget_me: &ForgetMe) -> Self { self.config.debug.forget_me = *forget_me; @@ -704,6 +722,7 @@ where key_store: &S::KeyStore, details_store: &S::GatewaysDetailsStore, derivation_material: Option, + shared_derivation_material: Option>>, ) -> Result where ::StorageError: Sync + Send, @@ -713,12 +732,24 @@ where if key_store.load_keys().await.is_err() { info!("could not find valid client keys - a new set will be generated"); let mut rng = OsRng; - let keys = if let Some(derivation_material) = derivation_material { - ClientKeys::from_master_key(&mut rng, &derivation_material) - .map_err(|_| ClientCoreError::HkdfDerivationError {})? - } else { - ClientKeys::generate_new(&mut rng) + + // Key generation priority: individual derivation material > shared derivation material > random generation + let keys = match (derivation_material, shared_derivation_material) { + // Individual derivation material takes precedence if provided + (Some(derivation_material), _) => { + ClientKeys::from_master_key(&mut rng, &derivation_material) + .map_err(|_| ClientCoreError::HkdfDerivationError {})? + } + // Use shared derivation material if no individual material is provided + (None, Some(shared_derivation_material)) => { + let shared_derivation_material = shared_derivation_material.lock().await; + ClientKeys::from_master_key(&mut rng, &shared_derivation_material) + .map_err(|_| ClientCoreError::HkdfDerivationError {})? + } + // Fall back to random key generation if no derivation material is available + (None, None) => ClientKeys::generate_new(&mut rng), }; + store_client_keys(keys, key_store).await?; } @@ -741,6 +772,7 @@ where self.client_store.key_store(), self.client_store.gateway_details_store(), self.derivation_material, + self.shared_derivation_material, ) .await?; diff --git a/common/crypto/src/hkdf.rs b/common/crypto/src/hkdf.rs index d12fadfac5..7e21089b5d 100644 --- a/common/crypto/src/hkdf.rs +++ b/common/crypto/src/hkdf.rs @@ -8,6 +8,7 @@ use hkdf::{ }, Hkdf, }; +use serde::{Deserialize, Serialize}; use sha2::{Sha256, Sha512}; pub use hkdf::InvalidLength; @@ -60,7 +61,7 @@ where /// // Prepare for the next derivation /// let next_material = material.next(); /// ``` -#[derive(ZeroizeOnDrop)] +#[derive(ZeroizeOnDrop, Serialize, Deserialize)] pub struct DerivationMaterial { master_key: [u8; 32], index: u32, diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index 3442cfe7ea..ff5ed1dd20 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -6,6 +6,7 @@ use log::{info, warn}; use nym_bin_common::bin_info; use nym_client_core::config::ForgetMe; use nym_crypto::asymmetric::ed25519::PrivateKey; +use nym_crypto::hkdf::DerivationMaterial; use nym_network_defaults::setup_env; use nym_network_defaults::var_names::NYM_API; use nym_sdk::mixnet::{self, MixnetClient}; @@ -13,6 +14,7 @@ use nym_sphinx::chunking::monitoring; use nym_topology::{HardcodedTopologyProvider, NymTopology, NymTopologyMetadata}; use std::fs::File; use std::io::Write; +use std::path::PathBuf; use std::sync::LazyLock; use std::time::Duration; use std::{ @@ -21,6 +23,7 @@ use std::{ str::FromStr, sync::Arc, }; +use tokio::sync::Mutex; use tokio::sync::OnceCell; use tokio::{signal::ctrl_c, sync::RwLock}; use tokio_util::sync::CancellationToken; @@ -45,6 +48,7 @@ async fn make_clients( n_clients: usize, lifetime: u64, topology: NymTopology, + derivation_material: Option>>, ) { loop { let spawned_clients = clients.read().await.len(); @@ -71,7 +75,12 @@ async fn make_clients( } } info!("Spawning new client"); - let client = match make_client(topology.clone()).await { + let client = match make_client( + topology.clone(), + derivation_material.as_ref().map(Arc::clone), + ) + .await + { Ok(client) => client, Err(err) => { warn!("{}, moving on", err); @@ -85,16 +94,29 @@ async fn make_clients( } } -async fn make_client(topology: NymTopology) -> Result { +/// Creates a new mixnet client, optionally using shared derivation material +/// for deterministic key generation. This allows multiple monitor clients +/// to derive keys from the same source while maintaining thread safety. +async fn make_client( + topology: NymTopology, + derivation_material: Option>>, +) -> Result { let net = mixnet::NymNetworkDetails::new_from_env(); let topology_provider = Box::new(HardcodedTopologyProvider::new(topology)); - let mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral() + let mut mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral() .network_details(net) .custom_topology_provider(topology_provider) .debug_config(mixnet_debug_config(0)) - .with_forget_me(ForgetMe::new_all()) - // .enable_credentials_mode() - .build()?; + .with_forget_me(ForgetMe::new_all()); + + // Configure the client with shared derivation material if available + // This ensures all monitor clients use the same key derivation source + if let Some(derivation_material) = derivation_material { + mixnet_client = + mixnet_client.with_shared_derivation_material(Arc::clone(&derivation_material)); + } + + let mixnet_client = mixnet_client.build()?; let client = mixnet_client.connect_to_mixnet().await?; Ok(client) @@ -138,6 +160,12 @@ struct Args { #[arg(long, env = "DATABASE_URL")] database_url: Option, + + /// Path to a JSON file containing serialized DerivationMaterial for deterministic + /// client key generation. When provided, all monitor clients will derive keys + /// from this shared material instead of generating random keys. + #[arg(long, env = "DERIVATION_MATERIAL_PATH")] + derivation_material_path: Option, } fn generate_key_pair() -> Result<()> { @@ -214,12 +242,23 @@ async fn main() -> Result<()> { MIXNET_TIMEOUT.set(args.mixnet_timeout).ok(); + // Load shared derivation material from file if provided + // This enables deterministic key generation across all monitor clients + let derivation_material = args + .derivation_material_path + .map(|derivation_material_path| { + let file = File::open(derivation_material_path).unwrap(); + let derivation_material: DerivationMaterial = serde_json::from_reader(file).unwrap(); + Arc::new(Mutex::new(derivation_material)) + }); + let spawn_clients = Arc::clone(&clients); tokio::spawn(make_clients( spawn_clients, args.n_clients, args.client_lifetime, TOPOLOGY.get().expect("Topology not set yet!").clone(), + derivation_material, )); let clients_server = clients.clone(); diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 47e958cb38..408b5a0860 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -39,6 +39,7 @@ use std::path::Path; use std::path::PathBuf; #[cfg(unix)] use std::sync::Arc; +use tokio::sync::Mutex; use url::Url; use zeroize::Zeroizing; @@ -67,6 +68,9 @@ pub struct MixnetClientBuilder { forget_me: ForgetMe, remember_me: RememberMe, derivation_material: Option, + // Shared derivation material for thread-safe access across multiple clients + // Wrapped in Arc> to allow concurrent access while maintaining safety + shared_derivation_material: Option>>, } impl MixnetClientBuilder { @@ -106,6 +110,7 @@ impl MixnetClientBuilder { forget_me: Default::default(), remember_me: Default::default(), derivation_material: None, + shared_derivation_material: None, }) } } @@ -140,6 +145,7 @@ where forget_me: Default::default(), remember_me: Default::default(), derivation_material: None, + shared_derivation_material: None, } } @@ -163,6 +169,7 @@ where forget_me: self.forget_me, remember_me: self.remember_me, derivation_material: self.derivation_material, + shared_derivation_material: self.shared_derivation_material, } } @@ -172,6 +179,18 @@ where self } + /// Set shared derivation material for deterministic key generation across multiple clients. + /// This allows multiple client instances to derive keys from the same source material + /// while ensuring thread-safe access through Arc>. + #[must_use] + pub fn with_shared_derivation_material( + mut self, + derivation_material: Arc>, + ) -> Self { + self.shared_derivation_material = Some(derivation_material); + self + } + /// Change the underlying storage of this builder to use default implementation of on-disk disk_persistence. #[must_use] pub fn set_default_storage( @@ -335,6 +354,7 @@ where client.forget_me = self.forget_me; client.remember_me = self.remember_me; client.derivation_material = self.derivation_material; + client.shared_derivation_material = self.shared_derivation_material; Ok(client) } } @@ -394,6 +414,11 @@ where /// The derivation material to use for the client keys, its up to the caller to save this for rederivation later derivation_material: Option, + + /// Shared derivation material that can be safely accessed across multiple threads/clients. + /// This is useful when multiple clients need to derive keys from the same source while + /// maintaining thread safety through Arc> wrapping. + shared_derivation_material: Option>>, } impl DisconnectedMixnetClient @@ -451,6 +476,7 @@ where forget_me, remember_me, derivation_material: None, + shared_derivation_material: None, }) } @@ -678,7 +704,8 @@ where .with_wait_for_gateway(self.wait_for_gateway) .with_forget_me(&self.forget_me) .with_remember_me(&self.remember_me) - .with_derivation_material(self.derivation_material); + .with_derivation_material(self.derivation_material) + .with_shared_derivation_material(self.shared_derivation_material); if let Some(user_agent) = self.user_agent { base_builder = base_builder.with_user_agent(user_agent);