Skip to content

Commit 38387e2

Browse files
authored
Add CLI command to export bundler private key (#3752)
* feat: add export-bundler-key CLI command Add new CLI command to export bundler private key using the omni_exportBundlerPrivateKey RPC method. The command: - Loads authorized ECDSA private key from file - Connects to worker via WebSocket - Retrieves worker's RSA shielding public key - Generates and RSA-encrypts AES key - Signs timestamp with authorized key - Calls RPC to get encrypted bundler key - Decrypts and displays the bundler private key Usage: executor-worker export-bundler-key \ --authorized-key-path <path> \ --worker-url <url> * feat: add print_ecdsa_pubkey helper utility Add example program to extract compressed ECDSA public key from a 32-byte seed file. Useful for generating the public key needed to configure OE_BUNDLER_KEY_EXPORT_AUTHORIZED_PUBKEY. Usage: cargo run --example print_ecdsa_pubkey <path_to_seed_file> * test: add automated test script for export-bundler-key Add end-to-end test script that: - Generates test authorized key - Extracts public key and configures worker - Builds and starts worker in background - Runs export-bundler-key command - Validates successful key export - Cleans up resources automatically Usage: cd executor-worker && ./test-export-bundler-key.sh * fixing fmt issue
1 parent ca03a20 commit 38387e2

File tree

6 files changed

+357
-1
lines changed

6 files changed

+357
-1
lines changed

tee-worker/omni-executor/Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tee-worker/omni-executor/executor-worker/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ edition.workspace = true
77
[dependencies]
88
alloy = { workspace = true, features = ["signer-local"] }
99
clap = { workspace = true, features = ["derive"] }
10+
ethers = { workspace = true }
1011
hex = { workspace = true }
12+
jsonrpsee = { workspace = true, features = ["ws-client"] }
1113
metrics-exporter-prometheus = { workspace = true }
14+
rand = { workspace = true }
15+
rsa = { workspace = true }
1216
rust_decimal = { workspace = true }
1317
scale-encode = { workspace = true }
18+
serde = { workspace = true }
1419
serde_json = { workspace = true }
20+
sha2 = { workspace = true }
1521
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] }
1622
tracing = { workspace = true }
1723
tracing-subscriber = { workspace = true }
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use executor_crypto::{ecdsa, PairTrait};
2+
use std::env;
3+
use std::fs;
4+
use std::process;
5+
6+
fn main() {
7+
let args: Vec<String> = env::args().collect();
8+
9+
if args.len() != 2 {
10+
eprintln!("Usage: {} <path_to_32_byte_seed_file>", args[0]);
11+
eprintln!("\nExample:");
12+
eprintln!(" {} /tmp/test-keys/authorized_key.bin", args[0]);
13+
process::exit(1);
14+
}
15+
16+
let key_path = &args[1];
17+
18+
let seed_bytes = fs::read(key_path).unwrap_or_else(|e| {
19+
eprintln!("❌ Error: Failed to read key file '{}': {}", key_path, e);
20+
process::exit(1);
21+
});
22+
23+
if seed_bytes.len() != 32 {
24+
eprintln!("❌ Error: Invalid key file length. Expected 32 bytes, got {}", seed_bytes.len());
25+
process::exit(1);
26+
}
27+
28+
let mut seed = [0u8; 32];
29+
seed.copy_from_slice(&seed_bytes);
30+
31+
let pair = ecdsa::Pair::from_seed_slice(&seed).unwrap_or_else(|e| {
32+
eprintln!("❌ Error: Failed to create keypair from seed: {:?}", e);
33+
process::exit(1);
34+
});
35+
36+
let pubkey = pair.public();
37+
let pubkey_hex = hex::encode(pubkey.0);
38+
39+
println!("Compressed ECDSA Public Key (33 bytes):");
40+
println!("{}", pubkey_hex);
41+
println!("\nEnvironment Variable:");
42+
println!("OE_BUNDLER_KEY_EXPORT_AUTHORIZED_PUBKEY={}", pubkey_hex);
43+
}

tee-worker/omni-executor/executor-worker/src/cli.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct Cli {
1111
#[derive(Subcommand)]
1212
pub enum Commands {
1313
Run(Box<RunArgs>),
14+
ExportBundlerKey(ExportBundlerKeyArgs),
1415
}
1516

1617
#[derive(Args)]
@@ -47,3 +48,11 @@ pub struct RunArgs {
4748
#[arg(long, default_value = "3456", value_name = "mock server port")]
4849
pub mock_server_port: u16,
4950
}
51+
52+
#[derive(Args)]
53+
pub struct ExportBundlerKeyArgs {
54+
#[arg(short, long, value_name = "path to authorized ECDSA private key file (32-byte seed)")]
55+
pub authorized_key_path: String,
56+
#[arg(short, long, default_value = "ws://127.0.0.1:3456", value_name = "worker WebSocket URL")]
57+
pub worker_url: String,
58+
}

tee-worker/omni-executor/executor-worker/src/main.rs

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use alloy::primitives::Address;
2323
use alloy::signers::local::PrivateKeySigner;
2424
use binance_api::BinanceApiClient;
2525
use clap::Parser;
26-
use cli::{Cli, Commands};
26+
use cli::{Cli, Commands, ExportBundlerKeyArgs};
2727
use config_loader::ConfigLoader;
2828
use cross_chain_intent_executor::{Chain, CrossChainIntentExecutor, RpcEndpointRegistry};
2929
use ethereum_intent_executor::EthereumIntentExecutor;
@@ -73,6 +73,9 @@ async fn main() -> Result<(), ()> {
7373
let cli = Cli::parse();
7474

7575
match cli.cmd {
76+
Commands::ExportBundlerKey(args) => {
77+
export_bundler_key(args).await?;
78+
},
7679
Commands::Run(args) => {
7780
if args.enable_mock_server {
7881
#[cfg(feature = "mock-server")]
@@ -555,3 +558,144 @@ async fn main() -> Result<(), ()> {
555558

556559
Ok(())
557560
}
561+
562+
async fn export_bundler_key(args: ExportBundlerKeyArgs) -> Result<(), ()> {
563+
use alloy::primitives::keccak256;
564+
use executor_crypto::{
565+
aes256::{aes_decrypt, Aes256Key, Aes256KeyNonce, AesOutput},
566+
ecdsa, PairTrait,
567+
};
568+
use jsonrpsee::{core::client::ClientT, rpc_params, ws_client::WsClientBuilder};
569+
use rsa::{Oaep, RsaPublicKey};
570+
use sha2::Sha256;
571+
use std::time::{SystemTime, UNIX_EPOCH};
572+
573+
info!("Loading authorized key from: {}", args.authorized_key_path);
574+
575+
let authorized_key_bytes = std::fs::read(&args.authorized_key_path).map_err(|e| {
576+
error!("Failed to read authorized key file: {:?}", e);
577+
eprintln!("❌ Error: Failed to read authorized key file: {}", e);
578+
})?;
579+
580+
if authorized_key_bytes.len() != 32 {
581+
error!(
582+
"Invalid authorized key length: expected 32 bytes, got {}",
583+
authorized_key_bytes.len()
584+
);
585+
eprintln!("❌ Error: Invalid authorized key file (expected 32 bytes)");
586+
return Err(());
587+
}
588+
589+
let mut authorized_seed = [0u8; 32];
590+
authorized_seed.copy_from_slice(&authorized_key_bytes);
591+
592+
let authorized_pair = ecdsa::Pair::from_seed_slice(&authorized_seed).map_err(|e| {
593+
error!("Failed to create keypair from seed: {:?}", e);
594+
eprintln!("❌ Error: Failed to create keypair from authorized key");
595+
})?;
596+
597+
info!("Authorized public key: {:?}", authorized_pair.public());
598+
599+
info!("Connecting to worker at: {}", args.worker_url);
600+
let client = WsClientBuilder::default().build(&args.worker_url).await.map_err(|e| {
601+
error!("Failed to connect to worker: {:?}", e);
602+
eprintln!("❌ Error: Failed to connect to worker at {}: {}", args.worker_url, e);
603+
})?;
604+
605+
info!("Getting shielding key from worker...");
606+
#[derive(serde::Deserialize, Debug)]
607+
struct ShieldingKeyResponse {
608+
n: ethers::types::Bytes,
609+
e: ethers::types::Bytes,
610+
}
611+
612+
let shielding_key: ShieldingKeyResponse =
613+
client.request("omni_getShieldingKey", rpc_params![]).await.map_err(|e| {
614+
error!("Failed to get shielding key: {:?}", e);
615+
eprintln!("❌ Error: Failed to get shielding key from worker: {}", e);
616+
})?;
617+
618+
let mut n_bytes = shielding_key.n.to_vec();
619+
n_bytes.reverse();
620+
let mut e_bytes = shielding_key.e.to_vec();
621+
e_bytes.reverse();
622+
623+
let rsa_public_key = RsaPublicKey::new(
624+
rsa::BigUint::from_bytes_be(&n_bytes),
625+
rsa::BigUint::from_bytes_be(&e_bytes),
626+
)
627+
.map_err(|e| {
628+
error!("Failed to create RSA public key: {:?}", e);
629+
eprintln!("❌ Error: Failed to create RSA public key: {}", e);
630+
})?;
631+
632+
info!("Generating random AES key...");
633+
let aes_key: Aes256Key = rand::random();
634+
635+
info!("RSA-encrypting AES key...");
636+
let encrypted_aes_key = rsa_public_key
637+
.encrypt(&mut rand::thread_rng(), Oaep::new::<Sha256>(), &aes_key)
638+
.map_err(|e| {
639+
error!("Failed to RSA-encrypt AES key: {:?}", e);
640+
eprintln!("❌ Error: Failed to RSA-encrypt AES key: {}", e);
641+
})?;
642+
643+
let timestamp = SystemTime::now()
644+
.duration_since(UNIX_EPOCH)
645+
.map_err(|e| {
646+
error!("Failed to get current time: {:?}", e);
647+
eprintln!("❌ Error: System time error");
648+
})?
649+
.as_millis() as u64;
650+
651+
info!("Signing timestamp: {}", timestamp);
652+
let timestamp_str = timestamp.to_string();
653+
let challenge_hash = keccak256(timestamp_str.as_bytes());
654+
let signature = authorized_pair.sign_prehashed(&challenge_hash.0);
655+
656+
let signature_hex = format!("0x{}", hex::encode(signature.0));
657+
let encrypted_key_hex = format!("0x{}", hex::encode(&encrypted_aes_key));
658+
659+
info!("Calling omni_exportBundlerPrivateKey RPC...");
660+
#[derive(serde::Deserialize, Debug)]
661+
struct SerdeAesOutput {
662+
ciphertext: ethers::types::Bytes,
663+
aad: ethers::types::Bytes,
664+
nonce: ethers::types::Bytes,
665+
}
666+
667+
let encrypted_response: SerdeAesOutput = client
668+
.request(
669+
"omni_exportBundlerPrivateKey",
670+
rpc_params![timestamp, signature_hex, encrypted_key_hex],
671+
)
672+
.await
673+
.map_err(|e| {
674+
error!("RPC call failed: {:?}", e);
675+
eprintln!("❌ Error: RPC call failed: {}", e);
676+
})?;
677+
678+
info!("Decrypting bundler private key...");
679+
let nonce: Aes256KeyNonce = encrypted_response.nonce.to_vec().try_into().map_err(|_| {
680+
error!("Invalid nonce length in response");
681+
eprintln!("❌ Error: Invalid response nonce length");
682+
})?;
683+
684+
let mut aes_output = AesOutput {
685+
ciphertext: encrypted_response.ciphertext.to_vec(),
686+
aad: encrypted_response.aad.to_vec(),
687+
nonce,
688+
};
689+
690+
let bundler_key = aes_decrypt(&aes_key, &mut aes_output).ok_or_else(|| {
691+
error!("Failed to decrypt bundler private key");
692+
eprintln!("❌ Error: Failed to decrypt bundler private key");
693+
})?;
694+
695+
let bundler_key_hex = format!("0x{}", hex::encode(&bundler_key));
696+
697+
println!("\n✅ Bundler Private Key: {}", bundler_key_hex);
698+
println!("⚠️ WARNING: This is a sensitive key. Store it securely and never share it.\n");
699+
700+
Ok(())
701+
}

0 commit comments

Comments
 (0)