From 5491353f8a55c429e2e61bf1cd5046c86faae23e Mon Sep 17 00:00:00 2001 From: Hamza Khalid <36852564+hmzakhalid@users.noreply.github.com> Date: Fri, 20 Dec 2024 02:35:53 +0500 Subject: [PATCH] Add Network keypair to enclave init. (#209) * update cargo.toml * add net-pk to cli * update validation * generate and purge keypair * set network key * update repositories * update ci tests * formatting * Zeroize bytes --- packages/ciphernode/Cargo.lock | 1 + packages/ciphernode/enclave/Cargo.toml | 1 + .../ciphernode/enclave/src/commands/init.rs | 43 ++++++++------ .../ciphernode/enclave/src/commands/mod.rs | 8 +++ .../enclave/src/commands/net/generate.rs | 37 ++++++++++++ .../enclave/src/commands/net/mod.rs | 15 ++++- .../enclave/src/commands/net/purge.rs | 2 +- .../enclave/src/commands/net/set.rs | 59 +++++++++++++++++++ .../enclave/src/commands/password/mod.rs | 2 +- .../enclave/src/commands/wallet/set.rs | 31 ++-------- packages/ciphernode/enclave/src/main.rs | 18 +++++- .../ciphernode/enclave_node/src/aggregator.rs | 2 +- .../ciphernode/enclave_node/src/ciphernode.rs | 2 +- .../ciphernode/net/src/network_manager.rs | 33 +++++------ .../ciphernode/router/src/repositories.rs | 4 +- tests/basic_integration/base.sh | 7 +++ tests/basic_integration/fns.sh | 16 +++++ tests/basic_integration/persist.sh | 7 +++ 18 files changed, 215 insertions(+), 73 deletions(-) create mode 100644 packages/ciphernode/enclave/src/commands/net/generate.rs create mode 100644 packages/ciphernode/enclave/src/commands/net/set.rs diff --git a/packages/ciphernode/Cargo.lock b/packages/ciphernode/Cargo.lock index 0684a617..9e9fc345 100644 --- a/packages/ciphernode/Cargo.lock +++ b/packages/ciphernode/Cargo.lock @@ -2171,6 +2171,7 @@ dependencies = [ "enclave-core", "enclave_node", "hex", + "libp2p", "once_cell", "phf", "router", diff --git a/packages/ciphernode/enclave/Cargo.toml b/packages/ciphernode/enclave/Cargo.toml index d38cf579..284a4d12 100644 --- a/packages/ciphernode/enclave/Cargo.toml +++ b/packages/ciphernode/enclave/Cargo.toml @@ -19,6 +19,7 @@ dialoguer = "0.11.0" enclave-core = { path = "../core" } enclave_node = { path = "../enclave_node" } hex = { workspace = true } +libp2p = { workspace = true } once_cell = "1.20.2" router = { path = "../router" } tokio = { workspace = true } diff --git a/packages/ciphernode/enclave/src/commands/init.rs b/packages/ciphernode/enclave/src/commands/init.rs index 65b16658..dbfe9952 100644 --- a/packages/ciphernode/enclave/src/commands/init.rs +++ b/packages/ciphernode/enclave/src/commands/init.rs @@ -1,7 +1,9 @@ -use crate::commands::password::{self, PasswordCommands}; -use anyhow::anyhow; -use anyhow::bail; -use anyhow::Result; +use crate::commands::{ + net, + password::{self, PasswordCommands}, +}; +use alloy::primitives::Address; +use anyhow::{anyhow, bail, Result}; use config::load_config; use config::RPC; use dialoguer::{theme::ColorfulTheme, Input}; @@ -27,21 +29,10 @@ fn validate_rpc_url(url: &String) -> Result<()> { } fn validate_eth_address(address: &String) -> Result<()> { - if address.is_empty() { - return Ok(()); + match Address::parse_checksummed(address, None) { + Ok(_) => Ok(()), + Err(e) => bail!("Invalid Ethereum address: {}", e), } - if !address.starts_with("0x") { - bail!("Address must start with '0x'") - } - if address.len() != 42 { - bail!("Address must be 42 characters long (including '0x')") - } - for c in address[2..].chars() { - if !c.is_ascii_hexdigit() { - bail!("Address must contain only hexadecimal characters") - } - } - Ok(()) } #[instrument(name = "app", skip_all, fields(id = get_tag()))] @@ -50,6 +41,8 @@ pub async fn execute( eth_address: Option, password: Option, skip_eth: bool, + net_keypair: Option, + generate_net_keypair: bool, ) -> Result<()> { let rpc_url = match rpc_url { Some(url) => { @@ -133,10 +126,22 @@ chains: password, overwrite: true, }, - config, + &config, ) .await?; + if generate_net_keypair { + net::execute(net::NetCommands::GenerateKey, &config).await?; + } else { + net::execute( + net::NetCommands::SetKey { + net_keypair: net_keypair, + }, + &config, + ) + .await?; + } + println!("Enclave configuration successfully created!"); println!("You can start your node using `enclave start`"); diff --git a/packages/ciphernode/enclave/src/commands/mod.rs b/packages/ciphernode/enclave/src/commands/mod.rs index ec26de49..92cc422b 100644 --- a/packages/ciphernode/enclave/src/commands/mod.rs +++ b/packages/ciphernode/enclave/src/commands/mod.rs @@ -56,5 +56,13 @@ pub enum Commands { /// Skip asking for eth #[arg(long = "skip-eth")] skip_eth: bool, + + /// The network private key (ed25519) + #[arg(long = "net-keypair")] + net_keypair: Option, + + /// Generate a new network keypair + #[arg(long = "generate-net-keypair")] + generate_net_keypair: bool, }, } diff --git a/packages/ciphernode/enclave/src/commands/net/generate.rs b/packages/ciphernode/enclave/src/commands/net/generate.rs new file mode 100644 index 00000000..1e747379 --- /dev/null +++ b/packages/ciphernode/enclave/src/commands/net/generate.rs @@ -0,0 +1,37 @@ +use actix::Actor; +use anyhow::{bail, Result}; +use cipher::Cipher; +use config::AppConfig; +use enclave_core::{EventBus, GetErrors}; +use enclave_node::get_repositories; +use libp2p::identity::Keypair; +use zeroize::Zeroize; + +pub async fn execute(config: &AppConfig) -> Result<()> { + let kp = Keypair::generate_ed25519(); + println!( + "Generated new keypair with peer ID: {}", + kp.public().to_peer_id() + ); + let mut bytes = kp.try_into_ed25519()?.to_bytes().to_vec(); + let cipher = Cipher::from_config(config).await?; + let encrypted = cipher.encrypt_data(&mut bytes.clone())?; + let bus = EventBus::new(true).start(); + let repositories = get_repositories(&config, &bus)?; + bytes.zeroize(); + + // NOTE: We are writing an encrypted string here + repositories.libp2p_keypair().write(&encrypted); + + let errors = bus.send(GetErrors).await?; + if errors.len() > 0 { + for error in errors.iter() { + println!("{error}"); + } + bail!("There were errors generating the network keypair") + } + + println!("Network keypair has been successfully generated and encrypted."); + + Ok(()) +} diff --git a/packages/ciphernode/enclave/src/commands/net/mod.rs b/packages/ciphernode/enclave/src/commands/net/mod.rs index 913cf69e..37b6b11e 100644 --- a/packages/ciphernode/enclave/src/commands/net/mod.rs +++ b/packages/ciphernode/enclave/src/commands/net/mod.rs @@ -1,4 +1,6 @@ +mod generate; mod purge; +mod set; use anyhow::*; use clap::Subcommand; use config::AppConfig; @@ -7,11 +9,22 @@ use config::AppConfig; pub enum NetCommands { /// Purge the current peer ID from the database. PurgeId, + + /// Generate a new network keypair + GenerateKey, + + /// Set the network private key + SetKey { + #[arg(long = "net-keypair")] + net_keypair: Option, + }, } -pub async fn execute(command: NetCommands, config: AppConfig) -> Result<()> { +pub async fn execute(command: NetCommands, config: &AppConfig) -> Result<()> { match command { NetCommands::PurgeId => purge::execute(&config).await?, + NetCommands::GenerateKey => generate::execute(&config).await?, + NetCommands::SetKey { net_keypair } => set::execute(&config, net_keypair).await?, }; Ok(()) diff --git a/packages/ciphernode/enclave/src/commands/net/purge.rs b/packages/ciphernode/enclave/src/commands/net/purge.rs index 3c2b3aae..520a75d5 100644 --- a/packages/ciphernode/enclave/src/commands/net/purge.rs +++ b/packages/ciphernode/enclave/src/commands/net/purge.rs @@ -7,7 +7,7 @@ use enclave_node::get_repositories; pub async fn execute(config: &AppConfig) -> Result<()> { let bus = EventBus::new(true).start(); let repositories = get_repositories(&config, &bus)?; - repositories.libp2pid().clear(); + repositories.libp2p_keypair().clear(); println!("Peer ID has been purged. A new Peer ID will be generated upon restart."); Ok(()) } diff --git a/packages/ciphernode/enclave/src/commands/net/set.rs b/packages/ciphernode/enclave/src/commands/net/set.rs new file mode 100644 index 00000000..853ecde0 --- /dev/null +++ b/packages/ciphernode/enclave/src/commands/net/set.rs @@ -0,0 +1,59 @@ +use actix::Actor; +use alloy::primitives::hex; +use anyhow::{bail, Result}; +use cipher::Cipher; +use config::AppConfig; +use dialoguer::{theme::ColorfulTheme, Password}; +use enclave_core::{EventBus, GetErrors}; +use enclave_node::get_repositories; +use libp2p::identity::Keypair; + +pub fn create_keypair(input: &String) -> Result { + match hex::check(input) { + Ok(()) => match Keypair::ed25519_from_bytes(hex::decode(input)?) { + Ok(kp) => Ok(kp), + Err(e) => bail!("Invalid network keypair: {}", e), + }, + Err(e) => bail!("Error decoding network keypair: {}", e), + } +} + +fn validate_keypair_input(input: &String) -> Result<()> { + create_keypair(input).map(|_| ()) +} + +pub async fn execute(config: &AppConfig, net_keypair: Option) -> Result<()> { + let input = if let Some(net_keypair) = net_keypair { + let kp = create_keypair(&net_keypair)?; + kp.try_into_ed25519()?.to_bytes().to_vec() + } else { + let kp = Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your network private key") + .validate_with(validate_keypair_input) + .interact()? + .trim() + .to_string(); + let kp = create_keypair(&kp)?; + kp.try_into_ed25519()?.to_bytes().to_vec() + }; + + let cipher = Cipher::from_config(config).await?; + let encrypted = cipher.encrypt_data(&mut input.clone())?; + let bus = EventBus::new(true).start(); + let repositories = get_repositories(&config, &bus)?; + + // NOTE: We are writing an encrypted string here + repositories.libp2p_keypair().write(&encrypted); + + let errors = bus.send(GetErrors).await?; + if errors.len() > 0 { + for error in errors.iter() { + println!("{error}"); + } + bail!("There were errors setting the network keypair") + } + + println!("Network keypair has been successfully encrypted."); + + Ok(()) +} diff --git a/packages/ciphernode/enclave/src/commands/password/mod.rs b/packages/ciphernode/enclave/src/commands/password/mod.rs index 5ebaf984..fe6259d1 100644 --- a/packages/ciphernode/enclave/src/commands/password/mod.rs +++ b/packages/ciphernode/enclave/src/commands/password/mod.rs @@ -30,7 +30,7 @@ pub enum PasswordCommands { }, } -pub async fn execute(command: PasswordCommands, config: AppConfig) -> Result<()> { +pub async fn execute(command: PasswordCommands, config: &AppConfig) -> Result<()> { match command { PasswordCommands::Create { password, diff --git a/packages/ciphernode/enclave/src/commands/wallet/set.rs b/packages/ciphernode/enclave/src/commands/wallet/set.rs index 0e1faac5..fcafd38e 100644 --- a/packages/ciphernode/enclave/src/commands/wallet/set.rs +++ b/packages/ciphernode/enclave/src/commands/wallet/set.rs @@ -1,4 +1,5 @@ use actix::Actor; +use alloy::{hex::FromHex, primitives::FixedBytes, signers::local::PrivateKeySigner}; use anyhow::{anyhow, bail, Result}; use cipher::Cipher; use config::AppConfig; @@ -7,33 +8,13 @@ use enclave_core::{EventBus, GetErrors}; use enclave_node::get_repositories; pub fn validate_private_key(input: &String) -> Result<()> { - // Require 0x prefix - if !input.starts_with("0x") { - return Err(anyhow!( - "Invalid private key format: must start with '0x' prefix" - )); - } - - // Remove 0x prefix - let key = &input[2..]; - - // Check length - if key.len() != 64 { - return Err(anyhow!( - "Invalid private key length: {}. Expected 64 characters after '0x' prefix", - key.len() - )); - } - - // Validate hex characters and convert to bytes - let _ = (0..key.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&key[i..i + 2], 16)) - .collect::, _>>() - .map_err(|e| anyhow!("Invalid hex character: {}", e))?; - + let bytes = + FixedBytes::<32>::from_hex(input).map_err(|e| anyhow!("Invalid private key: {}", e))?; + let _ = + PrivateKeySigner::from_bytes(&bytes).map_err(|e| anyhow!("Invalid private key: {}", e))?; Ok(()) } + pub async fn execute(config: &AppConfig, private_key: Option) -> Result<()> { let input = if let Some(private_key) = private_key { validate_private_key(&private_key)?; diff --git a/packages/ciphernode/enclave/src/main.rs b/packages/ciphernode/enclave/src/main.rs index 808b7629..d344ecf5 100644 --- a/packages/ciphernode/enclave/src/main.rs +++ b/packages/ciphernode/enclave/src/main.rs @@ -55,11 +55,23 @@ impl Cli { eth_address, password, skip_eth, - } => init::execute(rpc_url, eth_address, password, skip_eth).await?, - Commands::Password { command } => password::execute(command, config).await?, + net_keypair, + generate_net_keypair, + } => { + init::execute( + rpc_url, + eth_address, + password, + skip_eth, + net_keypair, + generate_net_keypair, + ) + .await? + } + Commands::Password { command } => password::execute(command, &config).await?, Commands::Aggregator { command } => aggregator::execute(command, config).await?, Commands::Wallet { command } => wallet::execute(command, config).await?, - Commands::Net { command } => net::execute(command, config).await?, + Commands::Net { command } => net::execute(command, &config).await?, } Ok(()) diff --git a/packages/ciphernode/enclave_node/src/aggregator.rs b/packages/ciphernode/enclave_node/src/aggregator.rs index b215ce8f..e9fbbf6a 100644 --- a/packages/ciphernode/enclave_node/src/aggregator.rs +++ b/packages/ciphernode/enclave_node/src/aggregator.rs @@ -83,7 +83,7 @@ pub async fn setup_aggregator( &cipher, config.quic_port(), config.enable_mdns(), - repositories.libp2pid(), + repositories.libp2p_keypair(), ) .await?; diff --git a/packages/ciphernode/enclave_node/src/ciphernode.rs b/packages/ciphernode/enclave_node/src/ciphernode.rs index 5205d9be..bf71c73c 100644 --- a/packages/ciphernode/enclave_node/src/ciphernode.rs +++ b/packages/ciphernode/enclave_node/src/ciphernode.rs @@ -72,7 +72,7 @@ pub async fn setup_ciphernode( &cipher, config.quic_port(), config.enable_mdns(), - repositories.libp2pid(), + repositories.libp2p_keypair(), ) .await?; diff --git a/packages/ciphernode/net/src/network_manager.rs b/packages/ciphernode/net/src/network_manager.rs index 8969c908..777909f3 100644 --- a/packages/ciphernode/net/src/network_manager.rs +++ b/packages/ciphernode/net/src/network_manager.rs @@ -2,8 +2,7 @@ use crate::NetworkPeer; /// Actor for connecting to an libp2p client via it's mpsc channel interface /// This Actor should be responsible for use actix::prelude::*; -use anyhow::anyhow; -use anyhow::Result; +use anyhow::{anyhow, bail, Result}; use cipher::Cipher; use data::Repository; use enclave_core::{EnclaveEvent, EventBus, EventId, Subscribe}; @@ -75,25 +74,18 @@ impl NetworkManager { enable_mdns: bool, repository: Repository>, ) -> Result<(Addr, tokio::task::JoinHandle>, String)> { - info!("Reading from repository"); - let mut bytes = if let Some(bytes) = repository.read().await? { - let decrypted = cipher.decrypt_data(&bytes)?; - info!("Found keypair in repository"); - decrypted - } else { - let kp = libp2p::identity::Keypair::generate_ed25519(); - info!("Generated new keypair {}", kp.public().to_peer_id()); - let innerkp = kp.try_into_ed25519()?; - let bytes = innerkp.to_bytes().to_vec(); - - // We need to clone here so that returned bytes are not zeroized - repository.write(&cipher.encrypt_data(&mut bytes.clone())?); - info!("Saved new keypair to repository"); - bytes + // Get existing keypair or generate a new one + let mut bytes = match repository.read().await? { + Some(bytes) => { + info!("Found keypair in repository"); + cipher.decrypt_data(&bytes)? + } + None => bail!("No network keypair found in repository, please generate a new one using `enclave net generate-key`"), }; - let ed25519_keypair = ed25519::Keypair::try_from_bytes(&mut bytes)?; - let keypair: libp2p::identity::Keypair = ed25519_keypair.try_into()?; + // Create peer from keypair + let keypair: libp2p::identity::Keypair = + ed25519::Keypair::try_from_bytes(&mut bytes)?.try_into()?; let mut peer = NetworkPeer::new( &keypair, peers, @@ -101,9 +93,12 @@ impl NetworkManager { "tmp-enclave-gossip-topic", enable_mdns, )?; + + // Setup and start network manager let rx = peer.rx().ok_or(anyhow!("Peer rx already taken"))?; let p2p_addr = NetworkManager::setup(bus, peer.tx(), rx); let handle = tokio::spawn(async move { Ok(peer.start().await?) }); + Ok((p2p_addr, handle, keypair.public().to_peer_id().to_string())) } } diff --git a/packages/ciphernode/router/src/repositories.rs b/packages/ciphernode/router/src/repositories.rs index bf785d21..d9ab879c 100644 --- a/packages/ciphernode/router/src/repositories.rs +++ b/packages/ciphernode/router/src/repositories.rs @@ -74,8 +74,8 @@ impl Repositories { Repository::new(self.store.scope(format!("//eth_private_key"))) } - pub fn libp2pid(&self) -> Repository> { - Repository::new(self.store.scope(format!("//libp2pid"))) + pub fn libp2p_keypair(&self) -> Repository> { + Repository::new(self.store.scope(format!("//libp2p/keypair"))) } pub fn enclave_sol_reader(&self, chain_id: u64) -> Repository { diff --git a/tests/basic_integration/base.sh b/tests/basic_integration/base.sh index 8765b7c5..43485526 100755 --- a/tests/basic_integration/base.sh +++ b/tests/basic_integration/base.sh @@ -24,6 +24,13 @@ set_password cn4 "$CIPHERNODE_SECRET" set_password ag "$CIPHERNODE_SECRET" set_private_key ag "$PRIVATE_KEY" +# Set the network private key for all ciphernodes +set_network_private_key cn1 "$NETWORK_PRIVATE_KEY_1" +set_network_private_key cn2 "$NETWORK_PRIVATE_KEY_2" +set_network_private_key cn3 "$NETWORK_PRIVATE_KEY_3" +set_network_private_key cn4 "$NETWORK_PRIVATE_KEY_4" +set_network_private_key ag "$NETWORK_PRIVATE_KEY_AG" + # Launch 4 ciphernodes launch_ciphernode cn1 launch_ciphernode cn2 diff --git a/tests/basic_integration/fns.sh b/tests/basic_integration/fns.sh index 720fcd80..be24e90a 100644 --- a/tests/basic_integration/fns.sh +++ b/tests/basic_integration/fns.sh @@ -16,6 +16,7 @@ fi RPC_URL="ws://localhost:8545" PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +NETWORK_PRIVATE_KEY_AG="0x51a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" CIPHERNODE_SECRET="We are the music makers and we are the dreamers of the dreams." # These contracts are based on the deterministic order of hardhat deploy @@ -31,6 +32,12 @@ CIPHERNODE_ADDRESS_2="0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" CIPHERNODE_ADDRESS_3="0xdD2FD4581271e230360230F9337D5c0430Bf44C0" CIPHERNODE_ADDRESS_4="0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" +# These are the network private keys for the ciphernodes +NETWORK_PRIVATE_KEY_1="0x11a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" +NETWORK_PRIVATE_KEY_2="0x21a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" +NETWORK_PRIVATE_KEY_3="0x31a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" +NETWORK_PRIVATE_KEY_4="0x41a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" + # Function to clean up background processes cleanup() { @@ -102,6 +109,15 @@ set_private_key() { --private-key "$private_key" } +set_network_private_key() { + local name="$1" + local private_key="$2" + + yarn enclave net set-key \ + --config "$SCRIPT_DIR/lib/$name/config.yaml" \ + --net-keypair "$private_key" +} + launch_aggregator() { local name="$1" heading "Launch aggregator $name" diff --git a/tests/basic_integration/persist.sh b/tests/basic_integration/persist.sh index d4f4f489..6372e6ee 100755 --- a/tests/basic_integration/persist.sh +++ b/tests/basic_integration/persist.sh @@ -24,6 +24,13 @@ set_password cn4 "$CIPHERNODE_SECRET" set_password ag "$CIPHERNODE_SECRET" set_private_key ag "$PRIVATE_KEY" +# Set the network private key for all ciphernodes +set_network_private_key cn1 "$NETWORK_PRIVATE_KEY_1" +set_network_private_key cn2 "$NETWORK_PRIVATE_KEY_2" +set_network_private_key cn3 "$NETWORK_PRIVATE_KEY_3" +set_network_private_key cn4 "$NETWORK_PRIVATE_KEY_4" +set_network_private_key ag "$NETWORK_PRIVATE_KEY_AG" + # Launch 4 ciphernodes launch_ciphernode cn1 launch_ciphernode cn2