Skip to content

Commit

Permalink
Add Network keypair to enclave init. (#209)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hmzakhalid authored Dec 19, 2024
1 parent 44aac5b commit 5491353
Show file tree
Hide file tree
Showing 18 changed files with 215 additions and 73 deletions.
1 change: 1 addition & 0 deletions packages/ciphernode/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/ciphernode/enclave/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
43 changes: 24 additions & 19 deletions packages/ciphernode/enclave/src/commands/init.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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()))]
Expand All @@ -50,6 +41,8 @@ pub async fn execute(
eth_address: Option<String>,
password: Option<String>,
skip_eth: bool,
net_keypair: Option<String>,
generate_net_keypair: bool,
) -> Result<()> {
let rpc_url = match rpc_url {
Some(url) => {
Expand Down Expand Up @@ -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`");

Expand Down
8 changes: 8 additions & 0 deletions packages/ciphernode/enclave/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Generate a new network keypair
#[arg(long = "generate-net-keypair")]
generate_net_keypair: bool,
},
}
37 changes: 37 additions & 0 deletions packages/ciphernode/enclave/src/commands/net/generate.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
15 changes: 14 additions & 1 deletion packages/ciphernode/enclave/src/commands/net/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod generate;
mod purge;
mod set;
use anyhow::*;
use clap::Subcommand;
use config::AppConfig;
Expand All @@ -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<String>,
},
}

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(())
Expand Down
2 changes: 1 addition & 1 deletion packages/ciphernode/enclave/src/commands/net/purge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
59 changes: 59 additions & 0 deletions packages/ciphernode/enclave/src/commands/net/set.rs
Original file line number Diff line number Diff line change
@@ -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<Keypair> {
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<String>) -> 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(())
}
2 changes: 1 addition & 1 deletion packages/ciphernode/enclave/src/commands/password/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 6 additions & 25 deletions packages/ciphernode/enclave/src/commands/wallet/set.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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::<Result<Vec<u8>, _>>()
.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<String>) -> Result<()> {
let input = if let Some(private_key) = private_key {
validate_private_key(&private_key)?;
Expand Down
18 changes: 15 additions & 3 deletions packages/ciphernode/enclave/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
2 changes: 1 addition & 1 deletion packages/ciphernode/enclave_node/src/aggregator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ pub async fn setup_aggregator(
&cipher,
config.quic_port(),
config.enable_mdns(),
repositories.libp2pid(),
repositories.libp2p_keypair(),
)
.await?;

Expand Down
2 changes: 1 addition & 1 deletion packages/ciphernode/enclave_node/src/ciphernode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub async fn setup_ciphernode(
&cipher,
config.quic_port(),
config.enable_mdns(),
repositories.libp2pid(),
repositories.libp2p_keypair(),
)
.await?;

Expand Down
33 changes: 14 additions & 19 deletions packages/ciphernode/net/src/network_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -75,35 +74,31 @@ impl NetworkManager {
enable_mdns: bool,
repository: Repository<Vec<u8>>,
) -> Result<(Addr<Self>, tokio::task::JoinHandle<Result<()>>, 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,
Some(quic_port),
"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()))
}
}
Expand Down
Loading

0 comments on commit 5491353

Please sign in to comment.