Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions common/client-core/src/client/base_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -195,6 +196,10 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,

derivation_material: Option<DerivationMaterial>,
// Shared derivation material wrapped in Arc<Mutex<>> 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<Arc<Mutex<DerivationMaterial>>>,
}

impl<C, S> BaseClientBuilder<C, S>
Expand All @@ -220,6 +225,7 @@ where
#[cfg(unix)]
connection_fd_callback: None,
derivation_material: None,
shared_derivation_material: None,
}
}

Expand All @@ -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<Mutex<>>.
#[must_use]
pub fn with_shared_derivation_material(
mut self,
derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
) -> 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;
Expand Down Expand Up @@ -704,6 +722,7 @@ where
key_store: &S::KeyStore,
details_store: &S::GatewaysDetailsStore,
derivation_material: Option<DerivationMaterial>,
shared_derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
) -> Result<InitialisationResult, ClientCoreError>
where
<S::KeyStore as KeyStore>::StorageError: Sync + Send,
Expand All @@ -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?;
}

Expand All @@ -741,6 +772,7 @@ where
self.client_store.key_store(),
self.client_store.gateway_details_store(),
self.derivation_material,
self.shared_derivation_material,
)
.await?;

Expand Down
3 changes: 2 additions & 1 deletion common/crypto/src/hkdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use hkdf::{
},
Hkdf,
};
use serde::{Deserialize, Serialize};
use sha2::{Sha256, Sha512};

pub use hkdf::InvalidLength;
Expand Down Expand Up @@ -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,
Expand Down
52 changes: 46 additions & 6 deletions nym-network-monitor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ 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};
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::{
Expand All @@ -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;
Expand All @@ -45,6 +48,7 @@ async fn make_clients(
n_clients: usize,
lifetime: u64,
topology: NymTopology,
derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
) {
loop {
let spawned_clients = clients.read().await.len();
Expand All @@ -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);
Expand All @@ -85,16 +94,29 @@ async fn make_clients(
}
}

async fn make_client(topology: NymTopology) -> Result<MixnetClient> {
/// 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<Arc<Mutex<DerivationMaterial>>>,
) -> Result<MixnetClient> {
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)
Expand Down Expand Up @@ -138,6 +160,12 @@ struct Args {

#[arg(long, env = "DATABASE_URL")]
database_url: Option<String>,

/// 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<PathBuf>,
}

fn generate_key_pair() -> Result<()> {
Expand Down Expand Up @@ -214,12 +242,24 @@ 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)?;
let derivation_material: DerivationMaterial = serde_json::from_reader(file)?;
Ok::<_, anyhow::Error>(Arc::new(Mutex::new(derivation_material)))
})
.transpose()?;

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();
Expand Down
29 changes: 28 additions & 1 deletion sdk/rust/nym-sdk/src/mixnet/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -67,6 +68,9 @@ pub struct MixnetClientBuilder<S: MixnetClientStorage = Ephemeral> {
forget_me: ForgetMe,
remember_me: RememberMe,
derivation_material: Option<DerivationMaterial>,
// Shared derivation material for thread-safe access across multiple clients
// Wrapped in Arc<Mutex<>> to allow concurrent access while maintaining safety
shared_derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
}

impl MixnetClientBuilder<Ephemeral> {
Expand Down Expand Up @@ -106,6 +110,7 @@ impl MixnetClientBuilder<OnDiskPersistent> {
forget_me: Default::default(),
remember_me: Default::default(),
derivation_material: None,
shared_derivation_material: None,
})
}
}
Expand Down Expand Up @@ -140,6 +145,7 @@ where
forget_me: Default::default(),
remember_me: Default::default(),
derivation_material: None,
shared_derivation_material: None,
}
}

Expand All @@ -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,
}
}

Expand All @@ -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<Mutex<>>.
#[must_use]
pub fn with_shared_derivation_material(
mut self,
derivation_material: Arc<Mutex<DerivationMaterial>>,
) -> 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(
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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<DerivationMaterial>,

/// 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<Mutex<>> wrapping.
shared_derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
}

impl<S> DisconnectedMixnetClient<S>
Expand Down Expand Up @@ -451,6 +476,7 @@ where
forget_me,
remember_me,
derivation_material: None,
shared_derivation_material: None,
})
}

Expand Down Expand Up @@ -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);
Expand Down
Loading