89de989ad1
* WIP * NS agent calls probe as lib * Clippy: ns agent * Fix submit_v2 on API * Adjust dockerfile, deployment details for the new flow * Bump package versions * PR feedback * Fix CI * Final version
231 lines
8.5 KiB
Rust
231 lines
8.5 KiB
Rust
/// Proof-of-concept: two mixnet clients sharing the same identity,
|
|
/// where the second client reuses the first client's gateway registration
|
|
/// (shared key) without re-registering.
|
|
///
|
|
/// Could be used in the NS API optimization where
|
|
/// multiple gateway probes share a single identity and registration cache.
|
|
///
|
|
/// Run from the sdk/rust/nym-sdk directory:
|
|
/// cargo run --example shared_identity
|
|
///
|
|
/// Flow:
|
|
/// - Client-A registers with a gateway (DH handshake → shared key)
|
|
/// - Client-B uses the SAME identity and the SAME shared key
|
|
/// - Client-B authenticates (no DH) and gets connected
|
|
/// - Both clients get the same Nym address (proves shared identity works)
|
|
use nym_client_core::client::base_client::storage::gateways_storage::{
|
|
GatewayDetails, GatewaysDetailsStore, InMemGatewaysDetails,
|
|
};
|
|
use nym_client_core::client::base_client::storage::{Ephemeral, MixnetClientStorage};
|
|
use nym_client_core::client::key_manager::persistence::{InMemEphemeralKeys, KeyStore};
|
|
use nym_client_core::client::replies::reply_storage;
|
|
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
|
|
use nym_credentials_interface::TicketType;
|
|
use nym_gateway_requests::shared_key::SharedSymmetricKey;
|
|
use nym_sdk::mixnet;
|
|
use nym_sdk::mixnet::MixnetMessageSender;
|
|
use rand::rngs::OsRng;
|
|
use std::sync::Arc;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
nym_bin_common::logging::setup_tracing_logger();
|
|
|
|
// export ENVIRONMENT=sandbox
|
|
let sandbox_network = mixnet::NymNetworkDetails::new_from_env();
|
|
|
|
println!("=== Phase 1: Client-A registers with a gateway ===\n");
|
|
|
|
// create client-A with ephemeral storage (will register fresh)
|
|
let storage_a = Ephemeral::default();
|
|
|
|
// save a handle to the stores so we can extract data after connection
|
|
let keys_store_a = storage_a.key_store().clone();
|
|
let gw_store_a = storage_a.gateway_details_store().clone();
|
|
|
|
let client_a_builder = mixnet::MixnetClientBuilder::new_with_storage(storage_a)
|
|
.network_details(sandbox_network.clone())
|
|
.enable_credentials_mode()
|
|
.build()?;
|
|
|
|
let mnemonic = std::env::var("SANDBOX_MNEMONIC").expect("Set mnemonic in the env variable");
|
|
let bandwidth_client_a = client_a_builder
|
|
.create_bandwidth_client(mnemonic, TicketType::V1MixnetEntry)
|
|
.await?;
|
|
|
|
// get a bandwidth credential for the mixnet_client
|
|
bandwidth_client_a.acquire().await?;
|
|
|
|
// connect: this triggers gateway registration (DH handshake)
|
|
let mut client_a = client_a_builder.connect_to_mixnet().await?;
|
|
let address_a = *client_a.nym_address();
|
|
println!("Client-A address: {address_a}");
|
|
|
|
// extract the identity keys and gateway registration from client-A
|
|
let client_keys = keys_store_a.load_keys().await?;
|
|
let active_gw = gw_store_a.active_gateway().await?;
|
|
let registration = active_gw
|
|
.registration
|
|
.expect("client-A should have an active gateway registration");
|
|
|
|
// extract the shared key and gateway info
|
|
let shared_key_bytes = registration
|
|
.details
|
|
.shared_key()
|
|
.expect("remote gateway should have a shared key")
|
|
.to_bytes();
|
|
let gateway_id = registration.details.gateway_id();
|
|
let published_data = match ®istration.details {
|
|
GatewayDetails::Remote(r) => r.published_data.clone(),
|
|
_ => panic!("expected remote gateway"),
|
|
};
|
|
|
|
println!("Gateway ID: {}", gateway_id.to_base58_string());
|
|
println!(
|
|
"Shared key (first 8 bytes): {:02x?}",
|
|
&shared_key_bytes[..8]
|
|
);
|
|
println!("Gateway listener: {}", published_data.listeners.primary);
|
|
|
|
// send a self-ping to prove client-A works
|
|
client_a
|
|
.send_plain_message(address_a, "hello from client-A")
|
|
.await?;
|
|
println!("\nClient-A sent self-ping...");
|
|
if let Some(msgs) = client_a.wait_for_messages().await {
|
|
for m in &msgs {
|
|
println!("Client-A received: {}", String::from_utf8_lossy(&m.message));
|
|
}
|
|
}
|
|
|
|
// disconnect client-A (don't ForgetMe: keep registration alive)
|
|
client_a.disconnect().await;
|
|
println!("\nClient-A disconnected (gateway keeps registration)\n");
|
|
|
|
// ================================================================
|
|
println!("=== Phase 2: Client-B reuses Client-A's identity + registration ===\n");
|
|
|
|
// reconstruct the shared key from raw bytes
|
|
let shared_key = SharedSymmetricKey::try_from_bytes(&shared_key_bytes)?;
|
|
|
|
// build a pre-populated gateway registration
|
|
let gw_details = GatewayDetails::new_remote(gateway_id, Arc::new(shared_key), published_data);
|
|
let gw_registration = gw_details.into();
|
|
|
|
// create a new ephemeral storage and pre-populate it
|
|
let keys_store_b = InMemEphemeralKeys::new(&mut OsRng);
|
|
// inject the SAME identity keys from client-A
|
|
keys_store_b.store_keys(&client_keys).await?;
|
|
|
|
let gw_store_b = InMemGatewaysDetails::default();
|
|
// inject the gateway registration (shared key) from client-A
|
|
gw_store_b.store_gateway_details(&gw_registration).await?;
|
|
gw_store_b
|
|
.set_active_gateway(&gateway_id.to_base58_string())
|
|
.await?;
|
|
|
|
// wrap into a custom storage that the SDK can use
|
|
let storage_b = PrePopulatedStorage {
|
|
key_store: keys_store_b,
|
|
reply_store: reply_storage::Empty::default(),
|
|
credential_store: EphemeralCredentialStorage::default(),
|
|
gateway_details_store: gw_store_b,
|
|
};
|
|
|
|
// Build client-B with the pre-populated storage
|
|
// request_gateway forces the builder to use our pre-registered gateway
|
|
let client_b_builder = mixnet::MixnetClientBuilder::new_with_storage(storage_b)
|
|
.request_gateway(gateway_id.to_base58_string())
|
|
.network_details(sandbox_network)
|
|
.enable_credentials_mode()
|
|
.build()?;
|
|
|
|
// === what we'd do when claiming bandwidth: ===
|
|
// but we do NOT do this because we want to reuse registration from client A
|
|
// let bandwidth_client_b = client_b_builder
|
|
// .create_bandwidth_client(mnemonic, TicketType::V1MixnetEntry)
|
|
// .await?;
|
|
// bandwidth_client_b.acquire().await?;
|
|
|
|
// Connect: this should AUTHENTICATE (not register) because the
|
|
// gateway details store already has a registration for this gateway
|
|
println!("Client-B connecting (should authenticate, not register)...");
|
|
let mut client_b = client_b_builder.connect_to_mixnet().await?;
|
|
let address_b = *client_b.nym_address();
|
|
println!("Client-B address: {address_b}");
|
|
|
|
// Verify: both clients get the same Nym address
|
|
if address_a == address_b {
|
|
println!("\n*** SUCCESS: Both clients have the SAME Nym address ***");
|
|
println!("*** Client-B authenticated using Client-A's shared key ***");
|
|
} else {
|
|
println!("\n*** UNEXPECTED: Addresses differ ***");
|
|
println!(" A: {address_a}");
|
|
println!(" B: {address_b}");
|
|
}
|
|
|
|
// send a self-ping to prove client-B works
|
|
client_b
|
|
.send_plain_message(address_b, "hello from client-B")
|
|
.await?;
|
|
println!("\nClient-B sent self-ping...");
|
|
if let Some(msgs) = client_b.wait_for_messages().await {
|
|
for m in &msgs {
|
|
println!("Client-B received: {}", String::from_utf8_lossy(&m.message));
|
|
}
|
|
}
|
|
|
|
client_b.disconnect().await;
|
|
println!("\nClient-B disconnected. Done!");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// A wrapper around in-memory stores to implement MixnetClientStorage.
|
|
// This is the pattern an NS Agent would use to create probes with
|
|
// pre-populated identity and gateway registration.
|
|
#[derive(Clone)]
|
|
struct PrePopulatedStorage {
|
|
key_store: InMemEphemeralKeys,
|
|
reply_store: reply_storage::Empty,
|
|
credential_store: EphemeralCredentialStorage,
|
|
gateway_details_store: InMemGatewaysDetails,
|
|
}
|
|
|
|
impl MixnetClientStorage for PrePopulatedStorage {
|
|
type KeyStore = InMemEphemeralKeys;
|
|
type ReplyStore = reply_storage::Empty;
|
|
type CredentialStore = EphemeralCredentialStorage;
|
|
type GatewaysDetailsStore = InMemGatewaysDetails;
|
|
|
|
fn into_runtime_stores(
|
|
self,
|
|
) -> (
|
|
Self::ReplyStore,
|
|
Self::CredentialStore,
|
|
Self::GatewaysDetailsStore,
|
|
) {
|
|
(
|
|
self.reply_store,
|
|
self.credential_store,
|
|
self.gateway_details_store,
|
|
)
|
|
}
|
|
|
|
fn key_store(&self) -> &Self::KeyStore {
|
|
&self.key_store
|
|
}
|
|
|
|
fn reply_store(&self) -> &Self::ReplyStore {
|
|
&self.reply_store
|
|
}
|
|
|
|
fn credential_store(&self) -> &Self::CredentialStore {
|
|
&self.credential_store
|
|
}
|
|
|
|
fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore {
|
|
&self.gateway_details_store
|
|
}
|
|
}
|