diff --git a/bolt-cli/Cargo.lock b/bolt-cli/Cargo.lock index 6f7692d1..4a111268 100644 --- a/bolt-cli/Cargo.lock +++ b/bolt-cli/Cargo.lock @@ -1359,6 +1359,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "cipher" version = "0.3.0" @@ -2167,8 +2173,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -2391,6 +2399,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -3511,6 +3520,58 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.0", + "rustls", + "socket2", + "thiserror 2.0.3", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom", + "rand", + "ring 0.17.8", + "rustc-hash 2.1.0", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.3", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -3637,7 +3698,10 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -3645,11 +3709,13 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "windows-registry", ] @@ -3829,6 +3895,9 @@ name = "rustls-pki-types" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -5041,6 +5110,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.7" diff --git a/bolt-cli/Cargo.toml b/bolt-cli/Cargo.toml index e88d1559..6c46c4cf 100644 --- a/bolt-cli/Cargo.toml +++ b/bolt-cli/Cargo.toml @@ -38,7 +38,7 @@ thiserror = "1.0" hex = "0.4.3" tracing = "0.1.40" tracing-subscriber = "0.3.18" -reqwest = "0.12.8" +reqwest = { version = "0.12.9", features = ["rustls-tls"] } rand = "0.8.5" [dev-dependencies] diff --git a/bolt-cli/README.md b/bolt-cli/README.md index 65facf53..e1bf3e0f 100644 --- a/bolt-cli/README.md +++ b/bolt-cli/README.md @@ -42,11 +42,12 @@ Available commands: The `delegate` command generates signed delegation messages for the Constraints API. To learn more about the Constraints API, please refer to the [Bolt documentation][bolt-docs]. -The `delegate` command supports three key sources: +The `delegate` command supports different key sources: - Local BLS secret keys (as hex-encoded strings) via `secret-keys` - Local EIP-2335 filesystem keystore directories via `local-keystore` - Remote Dirk keystore via `dirk` (requires TLS credentials) +- Remote Web3Signer keystore via `web3signer`
Usage @@ -62,6 +63,7 @@ Commands: secret-keys Use local secret keys to generate the signed messages local-keystore Use an EIP-2335 filesystem keystore directory to generate the signed messages dirk Use a remote DIRK keystore to generate the signed messages +web3signer Use a remote web3signer keystore to generate the signed messages help Print this message or the help of the given subcommand(s) Options: @@ -133,6 +135,17 @@ bolt delegate \ --wallet-path wallet1 --passphrases secret ``` +4. Generating a delegation using a remote Web3Signer keystore + +```text +bolt delegate \ + --delegatee-pubkey 0x83eeddfac5e60f8fe607ee8713efb8877c295ad9f8ca075f4d8f6f2ae241a30dd57f78f6f3863a9fe0d5b5db9d550b93 \ + --chain holesky \ + web3signer --url https://localhost:9000 \ + --ca-cert-path ./test_data/web3signer/tls/web3signer.crt \ + --combined_pem_path ./test_data/web3signer/tls/combined.pem +``` +
--- @@ -144,6 +157,7 @@ The `pubkeys` command lists available BLS public keys from different key sources - Local BLS secret keys (as hex-encoded strings) via `secret-keys` - Local EIP-2335 filesystem keystore directories via `local-keystore` - Remote Dirk keystore via `dirk` (requires TLS credentials) +- Remote Web3Signer via `web3signer`
Usage @@ -159,6 +173,7 @@ Commands: secret-keys Use local secret keys to generate the signed messages local-keystore Use an EIP-2335 filesystem keystore directory to generate the signed messages dirk Use a remote DIRK keystore to generate the signed messages + web3signer Use a remote web3signer keystore to generate the signed messages help Print this message or the help of the given subcommand(s) Options: @@ -195,6 +210,14 @@ bolt pubkeys dirk --url https://localhost:9091 \ --wallet-path wallet1 --passphrases secret ``` +4. Listing BLS public keys from a remote Web3Signer keystore + +```text +bolt pubkeys web3signer --url https://localhost:9000 \ + --ca-cert-path ./test_data/web3signer/tls/web3signer.crt \ + --combined_pem_path ./test_data/web3signer/tls/combined.pem +``` +
--- diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 64190493..4bcf16c7 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -354,6 +354,14 @@ pub enum KeysSource { #[clap(flatten)] opts: DirkOpts, }, + + /// Use a remote web3signer keystore as source for the public keys. + #[clap(name = "web3signer")] + Web3Signer { + /// The options for connecting to the web3signer keystore. + #[clap(flatten)] + opts: Web3SignerOpts, + }, } #[derive(Debug, Clone, Parser)] @@ -379,6 +387,13 @@ pub enum SecretsSource { #[clap(flatten)] opts: DirkOpts, }, + + /// Use a remote Web3Signer keystore to generate the signed messages. + #[clap(name = "web3signer")] + Web3Signer { + #[clap(flatten)] + opts: Web3SignerOpts, + }, } /// Options for reading a keystore folder. @@ -426,12 +441,35 @@ pub struct DirkOpts { /// The TLS credentials for connecting to the DIRK keystore. #[clap(flatten)] - pub tls_credentials: TlsCredentials, + pub tls_credentials: DirkTlsCredentials, +} + +/// Options for connecting to a Web3Signer keystore. +#[derive(Debug, Clone, Parser)] +pub struct Web3SignerOpts { + /// The URL of the Web3Signer keystore. + #[clap(long, env = "WEB3SIGNER_URL")] + pub url: String, + + /// The TLS credentials for connecting to the Web3Signer keystore. + #[clap(flatten)] + pub tls_credentials: Web3SignerTlsCredentials, +} + +/// TLS credentials for connecting to a remote Web3Signer server. +#[derive(Debug, Clone, PartialEq, Eq, Parser)] +pub struct Web3SignerTlsCredentials { + /// Path to the CA certificate file. (.crt) + #[clap(long, env = "CA_CERT_PATH")] + pub ca_cert_path: String, + /// Path to the PEM encoded private key and certificate file. (.pem) + #[clap(long, env = "CLIENT_COMBINED_PEM_PATH")] + pub combined_pem_path: String, } -/// TLS credentials for connecting to a remote server. +/// TLS credentials for connecting to a remote Dirk server. #[derive(Debug, Clone, PartialEq, Eq, Parser)] -pub struct TlsCredentials { +pub struct DirkTlsCredentials { /// Path to the client certificate file. (.crt) #[clap(long, env = "CLIENT_CERT_PATH")] pub client_cert_path: String, diff --git a/bolt-cli/src/commands/delegate/mod.rs b/bolt-cli/src/commands/delegate/mod.rs index 68dbaf38..6fe44c77 100644 --- a/bolt-cli/src/commands/delegate/mod.rs +++ b/bolt-cli/src/commands/delegate/mod.rs @@ -18,6 +18,9 @@ mod keystore; /// Create delegations from remote Dirk signers. mod dirk; +/// Create delegations from remote Web3Signers. +mod web3signer; + impl DelegateCommand { /// Run the `delegate` command. pub async fn run(self) -> Result<()> { @@ -46,6 +49,10 @@ impl DelegateCommand { let delegatee_pubkey = parse_bls_public_key(&self.delegatee_pubkey)?; dirk::generate_from_dirk(opts, delegatee_pubkey, self.chain, self.action).await? } + SecretsSource::Web3Signer { opts } => { + let delegatee_pubkey = parse_bls_public_key(&self.delegatee_pubkey)?; + web3signer::generate_from_web3signer(opts, delegatee_pubkey, self.action).await? + } }; debug!("Generated {} signed messages", signed_messages.len()); diff --git a/bolt-cli/src/commands/delegate/web3signer.rs b/bolt-cli/src/commands/delegate/web3signer.rs new file mode 100644 index 00000000..f0c98fe8 --- /dev/null +++ b/bolt-cli/src/commands/delegate/web3signer.rs @@ -0,0 +1,109 @@ +use crate::{ + cli::{Action, Web3SignerOpts}, + commands::delegate::types::{ + DelegationMessage, RevocationMessage, SignedDelegation, SignedRevocation, + }, + common::web3signer::Web3Signer, +}; +use ethereum_consensus::crypto::{PublicKey as BlsPublicKey, Signature as BlsSignature}; +use eyre::Result; +use tracing::debug; + +use super::types::SignedMessage; + +/// Generate signed delegations/recovations using a remote Web3Signer. +pub async fn generate_from_web3signer( + opts: Web3SignerOpts, + delegatee_pubkey: BlsPublicKey, + action: Action, +) -> Result> { + // Connect to web3signer. + let mut web3signer = Web3Signer::connect(opts.url, opts.tls_credentials).await?; + + // Read in the accounts from the remote keystore. + let accounts = web3signer.list_accounts().await?; + debug!("Found {} remote accounts to sign with", accounts.len()); + + let mut signed_messages = Vec::with_capacity(accounts.len()); + + for account in accounts { + // Parse the BLS key of the account. + // Trim the pre-pended 0x. + let trimmed_account = trim_hex_prefix(&account)?; + let pubkey = BlsPublicKey::try_from(hex::decode(trimmed_account)?.as_slice())?; + + match action { + Action::Delegate => { + let message = DelegationMessage::new(pubkey.clone(), delegatee_pubkey.clone()); + // Web3Signer expects the pre-pended 0x. + let signing_root = format!("0x{}", &hex::encode(message.digest())); + let returned_signature = + web3signer.request_signature(&account, &signing_root).await?; + // Trim the 0x. + let trimmed_signature = trim_hex_prefix(&returned_signature)?; + let signature = BlsSignature::try_from(hex::decode(trimmed_signature)?.as_slice())?; + let signed = SignedDelegation { message, signature }; + signed_messages.push(SignedMessage::Delegation(signed)); + } + Action::Revoke => { + let message = RevocationMessage::new(pubkey.clone(), delegatee_pubkey.clone()); + // Web3Signer expects the pre-pended 0x. + let signing_root = format!("0x{}", &hex::encode(message.digest())); + let returned_signature = + web3signer.request_signature(&account, &signing_root).await?; + // Trim the 0x. + let trimmed_signature = trim_hex_prefix(&returned_signature)?; + let signature = BlsSignature::try_from(trimmed_signature.as_bytes())?; + let signed = SignedRevocation { message, signature }; + signed_messages.push(SignedMessage::Revocation(signed)); + } + } + } + + Ok(signed_messages) +} + +/// A utility function to trim the pre-pended 0x prefix for hex strings. +fn trim_hex_prefix(hex: &str) -> Result { + let trimmed = hex.get(2..).ok_or_else(|| eyre::eyre!("Invalid hex string: {hex}"))?; + Ok(trimmed.to_string()) +} + +#[cfg(test)] +mod tests { + use crate::{ + cli::{Action, Chain, Web3SignerOpts}, + commands::delegate::web3signer::generate_from_web3signer, + common::{parse_bls_public_key, web3signer::test_util::start_web3signer_test_server}, + }; + + /// Test generating signed delegations using a remote Web3Signer signer. + /// + /// ```shell + /// cargo test --package bolt --bin bolt -- commands::delegate::web3signer::tests::test_delegation_web3signer + /// --exact --show-output --ignored --nocapture + /// ``` + #[tokio::test] + #[ignore = "Requires Web3Signer to be installed on the system"] + async fn test_delegation_web3signer() -> eyre::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let (url, mut web3signer_proc, creds) = start_web3signer_test_server().await?; + + let delegatee_pubkey = "0x83eeddfac5e60f8fe607ee8713efb8877c295ad9f8ca075f4d8f6f2ae241a30dd57f78f6f3863a9fe0d5b5db9d550b93"; + let delegatee_pubkey = parse_bls_public_key(delegatee_pubkey)?; + let chain = Chain::Mainnet; + + let opts = Web3SignerOpts { url, tls_credentials: creds }; + + let signed_delegations = + generate_from_web3signer(opts, delegatee_pubkey, Action::Delegate).await?; + + let signed_message = signed_delegations.first().expect("to get signed delegation"); + + signed_message.verify_signature(chain)?; + + web3signer_proc.kill()?; + + Ok(()) + } +} diff --git a/bolt-cli/src/commands/pubkeys.rs b/bolt-cli/src/commands/pubkeys.rs index 1d74ec38..d618c6e7 100644 --- a/bolt-cli/src/commands/pubkeys.rs +++ b/bolt-cli/src/commands/pubkeys.rs @@ -7,6 +7,7 @@ use crate::{ common::{ dirk::Dirk, keystore::{keystore_paths, KeystoreError}, + web3signer::Web3Signer, write_to_file, }, pb::eth2_signer_api::ListAccountsResponse, @@ -41,6 +42,15 @@ impl PubkeysCommand { write_to_file(&self.out, &pubkeys)?; println!("Pubkeys generated from Dirk and saved to {}", self.out); } + KeysSource::Web3Signer { opts } => { + let mut web3signer = Web3Signer::connect(opts.url, opts.tls_credentials).await?; + + let accounts = web3signer.list_accounts().await?; + let pubkeys = list_from_web3signer_accounts(&accounts)?; + + write_to_file(&self.out, &pubkeys)?; + println!("Pubkeys generated from Web3signer and saved to {}", self.out); + } } Ok(()) @@ -95,3 +105,16 @@ pub fn list_from_dirk_accounts(accounts: ListAccountsResponse) -> Result Result> { + let mut pubkeys = Vec::with_capacity(accounts.len()); + + for acc in accounts { + let trimmed_account = &acc.clone()[2..]; + let pubkey = BlsPublicKey::try_from(hex::decode(trimmed_account)?.as_slice())?; + pubkeys.push(pubkey); + } + + Ok(pubkeys) +} diff --git a/bolt-cli/src/common/dirk/distributed.rs b/bolt-cli/src/common/dirk/distributed.rs index 26dfa8c6..6103bef9 100644 --- a/bolt-cli/src/common/dirk/distributed.rs +++ b/bolt-cli/src/common/dirk/distributed.rs @@ -5,7 +5,7 @@ use futures::{stream, StreamExt}; use tracing::{debug, warn}; use crate::{ - cli::TlsCredentials, + cli::DirkTlsCredentials, common::dirk::recover_signature::recover_signature_from_shards, pb::eth2_signer_api::{self, Endpoint}, }; @@ -14,7 +14,7 @@ use super::Dirk; #[derive(Debug)] pub struct DistributedDirkAccount { - credentials: TlsCredentials, + credentials: DirkTlsCredentials, participants: Vec, threshold: usize, composite_public_key: BlsPublicKey, @@ -24,7 +24,7 @@ impl DistributedDirkAccount { /// Create a new distributed account. pub fn new( acc: eth2_signer_api::DistributedAccount, - credentials: TlsCredentials, + credentials: DirkTlsCredentials, ) -> Result { let composite_public_key = BlsPublicKey::try_from(acc.composite_public_key.as_ref())?; diff --git a/bolt-cli/src/common/dirk/mod.rs b/bolt-cli/src/common/dirk/mod.rs index 53e5acdf..939ab434 100644 --- a/bolt-cli/src/common/dirk/mod.rs +++ b/bolt-cli/src/common/dirk/mod.rs @@ -7,7 +7,7 @@ use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use tracing::debug; use crate::{ - cli::TlsCredentials, + cli::DirkTlsCredentials, pb::eth2_signer_api::{ AccountManagerClient, ListAccountsRequest, ListAccountsResponse, ListerClient, LockAccountRequest, ResponseState, SignRequest, SignRequestId, SignerClient, @@ -42,7 +42,7 @@ pub struct Dirk { impl Dirk { /// Connect to the DIRK server with the given address and TLS credentials. - pub async fn connect(addr: String, credentials: TlsCredentials) -> Result { + pub async fn connect(addr: String, credentials: DirkTlsCredentials) -> Result { let addr = addr.parse()?; let tls_config = compose_credentials(credentials)?; let conn = Channel::builder(addr).tls_config(tls_config)?.connect().await?; @@ -169,8 +169,8 @@ impl Dirk { } } -/// Compose the TLS credentials from the given paths. -fn compose_credentials(creds: TlsCredentials) -> Result { +/// Compose the TLS credentials for Dirk from the given paths. +fn compose_credentials(creds: DirkTlsCredentials) -> Result { let client_cert = fs::read(creds.client_cert_path).wrap_err("Failed to read client cert")?; let client_key = fs::read(creds.client_key_path).wrap_err("Failed to read client key")?; diff --git a/bolt-cli/src/common/dirk/test_util.rs b/bolt-cli/src/common/dirk/test_util.rs index 0cb1e7c9..e90ebf6d 100644 --- a/bolt-cli/src/common/dirk/test_util.rs +++ b/bolt-cli/src/common/dirk/test_util.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use crate::cli::TlsCredentials; +use crate::cli::DirkTlsCredentials; /// Initialize the default TLS provider for the tests if not already set. pub fn try_init_tls_provider() { @@ -20,7 +20,7 @@ pub fn try_init_tls_provider() { /// This is a single instance (non distributed). /// /// Returns the DIRK client URL and credentials, and the corresponding server process handle. -pub async fn start_single_dirk_test_server() -> eyre::Result<(String, TlsCredentials, Child)> { +pub async fn start_single_dirk_test_server() -> eyre::Result<(String, DirkTlsCredentials, Child)> { try_init_tls_provider(); // Check if dirk is installed (in $PATH) @@ -47,7 +47,7 @@ pub async fn start_single_dirk_test_server() -> eyre::Result<(String, TlsCredent let url = "https://localhost:9091".to_string(); - let cred = TlsCredentials { + let cred = DirkTlsCredentials { client_cert_path: test_data_dir.clone() + "/client1.crt", client_key_path: test_data_dir.clone() + "/client1.key", ca_cert_path: Some(test_data_dir.clone() + "/security/ca.crt"), @@ -71,7 +71,8 @@ pub async fn start_single_dirk_test_server() -> eyre::Result<(String, TlsCredent /// /// This is because we need to map 3 different server certificates to localhost /// to simulate multiple servers with their own hostnames. -pub async fn start_multi_dirk_test_server() -> eyre::Result<(String, TlsCredentials, Vec)> { +pub async fn start_multi_dirk_test_server() -> eyre::Result<(String, DirkTlsCredentials, Vec)> +{ try_init_tls_provider(); // Check if dirk is installed (in $PATH) @@ -107,7 +108,7 @@ pub async fn start_multi_dirk_test_server() -> eyre::Result<(String, TlsCredenti // Note: the first server is used for the client connection let url = "https://localhost-1:8881".to_string(); - let cred = TlsCredentials { + let cred = DirkTlsCredentials { client_cert_path: test_data_dir.clone() + "/client/localhost.crt", client_key_path: test_data_dir.clone() + "/client/localhost.key", ca_cert_path: Some(test_data_dir.clone() + "/1/security/ca.crt"), diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs index dfd67941..a1f23b6d 100644 --- a/bolt-cli/src/common/mod.rs +++ b/bolt-cli/src/common/mod.rs @@ -20,6 +20,9 @@ pub mod signing; /// Utilities for hashing messages and custom types. pub mod hash; +/// Utilities for working with Consensys' Web3Signer remote keystore. +pub mod web3signer; + /// Parse a BLS public key from a string pub fn parse_bls_public_key(delegatee_pubkey: &str) -> Result { let hex_pk = delegatee_pubkey.strip_prefix("0x").unwrap_or(delegatee_pubkey); diff --git a/bolt-cli/src/common/web3signer.rs b/bolt-cli/src/common/web3signer.rs new file mode 100644 index 00000000..0d24e97a --- /dev/null +++ b/bolt-cli/src/common/web3signer.rs @@ -0,0 +1,200 @@ +use std::fs; + +use eyre::{Context, Result}; +use reqwest::{Certificate, Identity, Url}; +use serde::{Deserialize, Serialize}; + +use crate::cli::Web3SignerTlsCredentials; + +/// Web3Signer remote server. +/// +/// Functionality: +/// - List consensus accounts in the keystore. +/// - Sign roots over the consensus type. +/// +/// Reference: https://docs.web3signer.consensys.io/reference +#[derive(Clone)] +pub struct Web3Signer { + base_url: Url, + client: reqwest::Client, +} + +impl Web3Signer { + /// Establish connection to a remote Web3Signer instance with TLS credentials. + pub async fn connect(addr: String, credentials: Web3SignerTlsCredentials) -> Result { + let base_url = addr.parse()?; + let (cert, identity) = compose_credentials(credentials)?; + + let client = reqwest::Client::builder() + .add_root_certificate(cert) + .identity(identity) + .use_rustls_tls() + .build()?; + + Ok(Self { base_url, client }) + } + + /// List the consensus accounts of the keystore. + /// + /// Only the consensus keys are returned. + /// This is due to signing only being over the consensus type. + /// + /// Reference: https://commit-boost.github.io/commit-boost-client/api/ + pub async fn list_accounts(&mut self) -> Result> { + let path = self.base_url.join("/signer/v1/get_pubkeys")?; + let resp = self.client.get(path).send().await?.json::().await?; + + let consensus_keys: Vec = + resp.keys.into_iter().map(|key_set| key_set.consensus).collect(); + + Ok(consensus_keys) + } + + /// Request a signature from the remote signer. + /// + /// This will sign an arbituary root over the consensus type. + /// + /// Reference: https://commit-boost.github.io/commit-boost-client/api/ + pub async fn request_signature(&mut self, pub_key: &str, object_root: &str) -> Result { + let path = self.base_url.join("/signer/v1/request_signature")?; + let body = CommitBoostSignatureRequest { + type_: "consensus".to_string(), + pubkey: pub_key.to_string(), + object_root: object_root.to_string(), + }; + + let resp = self.client.post(path).json(&body).send().await?.json::().await?; + + Ok(resp) + } +} + +/// Compose the TLS credentials for the Web3Signer. +/// +/// Returns the CA certificate and the identity (combined PEM). +fn compose_credentials(credentials: Web3SignerTlsCredentials) -> Result<(Certificate, Identity)> { + let ca_cert = fs::read(credentials.ca_cert_path).wrap_err("Failed to read CA cert")?; + let ca_cert = Certificate::from_pem(&ca_cert)?; + + let identity = fs::read(credentials.combined_pem_path).wrap_err("Failed to read PEM")?; + let identity = Identity::from_pem(&identity)?; + + Ok((ca_cert, identity)) +} + +#[derive(Serialize, Deserialize)] +struct Keys { + /// The consensus keys stored in the Web3Signer. + pub consensus: String, + /// The two below proxy fields are here for deserialisation purposes. + /// They are not used as signing is only over the consensus type. + #[allow(unused)] + pub proxy_bls: Vec, + #[allow(unused)] + pub proxy_ecdsa: Vec, +} + +/// Outer container for response. +#[derive(Serialize, Deserialize)] +struct CommitBoostKeys { + keys: Vec, +} + +/// Request signature from the Web3Signer. +#[derive(Serialize, Deserialize)] +struct CommitBoostSignatureRequest { + #[serde(rename = "type")] + pub type_: String, + pub pubkey: String, + pub object_root: String, +} + +#[cfg(test)] +pub mod test_util { + use std::{ + process::{Child, Command}, + time::Duration, + }; + + use crate::cli::Web3SignerTlsCredentials; + use eyre::{bail, Ok}; + + /// Start a Web3Signer server for testing. + /// + /// This will start a Web3Signer server and return its URL, process handle, and TLS credentials. + pub async fn start_web3signer_test_server( + ) -> eyre::Result<(String, Child, Web3SignerTlsCredentials)> { + let test_data_dir = env!("CARGO_MANIFEST_DIR").to_string() + "/test_data/web3signer"; + + // Keystore test data. + let keystore_dir = test_data_dir.clone() + "/keystore"; + + // TLS test data. + let tls_dir = test_data_dir.clone() + "/tls"; + let tls_keystore = tls_dir.clone() + "/key.p12"; + let tls_password = tls_dir.clone() + "/password.txt"; + let ca_cert_path = tls_dir.clone() + "/web3signer.crt"; + let combined_pem_path = tls_dir.clone() + "/combined.pem"; + + // Check if web3signer is installed (in $PATH). + if Command::new("web3signer").spawn().is_err() { + bail!("Web3Signer is not installed in $PATH"); + } + + // Start the web3signer server. + let web3signer_proc = Command::new("web3signer") + .arg("--key-store-path") + .arg(keystore_dir.clone()) + .arg("--tls-keystore-file") + .arg(tls_keystore) + .arg("--tls-allow-any-client") + .arg("true") + .arg("--tls-keystore-password-file") + .arg(tls_password.clone()) + .arg("eth2") + .arg("--network") + .arg("mainnet") + .arg("--slashing-protection-enabled") + .arg("false") + .arg("--commit-boost-api-enabled") + .arg("true") + .arg("--proxy-keystores-path") + .arg(keystore_dir.clone()) + .arg("--proxy-keystores-password-file") + .arg(tls_password) + .spawn()?; + + // Allow the server to start up. + tokio::time::sleep(Duration::from_secs(5)).await; + + let credentials = Web3SignerTlsCredentials { ca_cert_path, combined_pem_path }; + let url = "https://127.0.0.1:9000".to_string(); + + Ok((url, web3signer_proc, credentials)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Test for connecting to the Web3Signer and listing accounts. + /// + /// ```shell + /// cargo test --package bolt --bin bolt -- common::web3signer::tests::test_web3signer_connection_e2e + /// --exact --show-output --ignored + /// ``` + #[tokio::test] + #[ignore = "Requires Web3Signer to be installed on the system"] + async fn test_web3signer_connection_e2e() -> eyre::Result<()> { + let (url, mut web3signer_proc, creds) = test_util::start_web3signer_test_server().await?; + let mut web3signer = Web3Signer::connect(url, creds).await?; + + let accounts = web3signer.list_accounts().await?; + println!("Web3Signer Accounts: {:?}", accounts); + + web3signer_proc.kill()?; + + Ok(()) + } +} diff --git a/bolt-cli/test_data/web3signer/keystore/keystore-m_12381_3600_0_0_0-1733041966.json b/bolt-cli/test_data/web3signer/keystore/keystore-m_12381_3600_0_0_0-1733041966.json new file mode 100644 index 00000000..5303dfb2 --- /dev/null +++ b/bolt-cli/test_data/web3signer/keystore/keystore-m_12381_3600_0_0_0-1733041966.json @@ -0,0 +1 @@ +{"crypto": {"kdf": {"function": "scrypt", "params": {"dklen": 32, "n": 262144, "r": 8, "p": 1, "salt": "622f70de380282d6bcacea3a70154d990a2367bae7560ebe07f2357907fbeb2b"}, "message": ""}, "checksum": {"function": "sha256", "params": {}, "message": "463544f14320114a2be597f43f7da2bbf758f17d19af5b961c90925533b6b42d"}, "cipher": {"function": "aes-128-ctr", "params": {"iv": "3de45e2f98b2c29cf988d69ede95d9ab"}, "message": "8c18c17bb9a185e6a72c246bee6f3efcf263b96dccc2e78fa382b8984e1f3311"}}, "description": "", "pubkey": "967dbf953e4e93f11fa23f45ca4f6eb092cad028786bd507557bd0d1053116183093ba7dfdcbfee742af37606f5458eb", "path": "m/12381/3600/0/0/0", "uuid": "258d5f1f-116c-4ae9-9ead-ecfcb3daa6a3", "version": 4} \ No newline at end of file diff --git a/bolt-cli/test_data/web3signer/keystore/password.txt b/bolt-cli/test_data/web3signer/keystore/password.txt new file mode 100644 index 00000000..830f5817 --- /dev/null +++ b/bolt-cli/test_data/web3signer/keystore/password.txt @@ -0,0 +1 @@ +testdata \ No newline at end of file diff --git a/bolt-cli/test_data/web3signer/keystore/validators.yaml b/bolt-cli/test_data/web3signer/keystore/validators.yaml new file mode 100644 index 00000000..1d2035ab --- /dev/null +++ b/bolt-cli/test_data/web3signer/keystore/validators.yaml @@ -0,0 +1,4 @@ +type: "file-keystore" +#pool shield render south used december repair injury gym surface extra true chat narrow carry lizard body table borrow ripple swallow soda any vehicle +keystoreFile: "./keystore-m_12381_3600_0_0_0-1733041966.json" +keystorePasswordFile: "./password.txt" \ No newline at end of file diff --git a/bolt-cli/test_data/web3signer/tls/combined.pem b/bolt-cli/test_data/web3signer/tls/combined.pem new file mode 100644 index 00000000..b37613b8 --- /dev/null +++ b/bolt-cli/test_data/web3signer/tls/combined.pem @@ -0,0 +1,83 @@ +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgIUS6WAuJjrNKU6qzOR3/cxTooU9/IwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDEwMjIwOTA2MjFaFw0yOTEw +MjEwOTA2MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANyTDRfa37FhTG2qEPckYPR6vP1tFcUqatK1V5mhKk7p +U38dF5YtCwuNX0FH/iG4fe7w3Xavt86aVVOpUF/Vxy3becv+RThpmUbRzCN/FlfT +ZzxsEMN/VFABRarxQK24UCbjnQ7p/yHiXOeRE5pAgBanp5uKKkSq+qEo9sb7s0w9 +c6ifgXQR/GuX68yui1XiJgE3WlkTk/hvKbbOAAhuzE0x9atndbFfBRMO1Sz7zScH +HF0cp/YI2SRx5cx7ovj9biOR0hi0Af+DuHT15vznDkfMo4sAT28CeBjtXBM1eYY9 +cm6gD7zm35+kcD2xulVkw4a/AueBLOwiszclnIKULC1t4ubuwYx6Be2uGrwBy2x3 +sJh8NoMVqGqZLfVmZ7qpah1iBMsuGhH+NjZtdiKeFtU2KhOkbpUlSVHCoL3Qn8xY +eytg7IqyfXZ0YSVnE3YsAJDCOjh1vQXTK0FMkt4z4uaM8UepG5ucurhQpGcEbcSj +DLxLbHMlR4dMFNlR825OPBBVpNLxXyfsHNuftJmX0OziSS2glVjYERqRWpkglgh2 +EHL2P2R63LRMNr9qKJBdZbHzschURvcZLuNr8dqkCS83vtOhHhCH9Hyl+QZC9w+F +MgIgdbxyE0iSUwVu2gwWqoPLC9gf0rssLxySkaKXUEizpbcuyb9qnT/JlNxahaDd +AgMBAAGjcDBuMB8GA1UdIwQYMBaAFO3Lnl1Id4OcqYwhtgbFXrx6min4MAkGA1Ud +EwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAdBgNVHQ4E +FgQUWlWshAUj6JXYhYHPlSKkBCx2A9swDQYJKoZIhvcNAQELBQADggIBACuvuqA1 +aL+iiyZw8DUnPgBFHoFRbViLKLxg6Qiqv/ldvoLYKYzE5rlCuSoSRoh2cd7Du3i8 +66cRrp8eh70XiHM5P7034BdYzIo7oCHfEmVPJpo1+G85+WbNzTf2ZLNFGoeudOGw +Kvwkxb4gr2xX03EOms/w2EyVycdQ0u7Y0yaDluDhXQF2ynJYnXDGx5oIrwp9YQWF +neGSyCTP4A67QybKYaA1p1Jibhv3i4QPcFHFOSOFOidqWn01zY/TS0kxQvFpX9cB +7LjMTujHiTdZGZVHFAf9gAlMXQ1jvYdUf9r1qOXvnBOLI/aqV5fLKqgXXmqt5WFZ +bhtbUVhEu4OEavgboPOt3WUpbXSnHyVR7t/f/wtycOnfTcYcQb2dZAGRyLP/cVQt +nDBElrpUHizysNuNMa1rfd/3WC0FvC0/M2OwNK1ad9itctGKel0UrTvCU59/GO3C +P6Q2EFxyQc1wAgekkBZQX5se9RcTo8VJlV16ZMlo4tm3l57YtHwipKCVVf7EUgxu +Bsj21nBY5nOIHzaYAqF5Bz7/Lg/9IFuVx/sPatcMYmUcYkPSvm/1uRB17lr30sQp +bOKGbxzv00JFBwF0t6ZO/XnpLQiHLBtbwgGKMUak0/wZiFiZPmcdaljHLqq08fPV +S3zuTPjwU2aaQsAqAu9F/B1Cn2/gFiJEL/Mg +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +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 +nJqYM6m13Q+F0Wu8krDMA65Wi5nA +-----END PRIVATE KEY----- diff --git a/bolt-cli/test_data/web3signer/tls/key.p12 b/bolt-cli/test_data/web3signer/tls/key.p12 new file mode 100644 index 00000000..792dc197 Binary files /dev/null and b/bolt-cli/test_data/web3signer/tls/key.p12 differ diff --git a/bolt-cli/test_data/web3signer/tls/password.txt b/bolt-cli/test_data/web3signer/tls/password.txt new file mode 100644 index 00000000..0a47d365 --- /dev/null +++ b/bolt-cli/test_data/web3signer/tls/password.txt @@ -0,0 +1 @@ +meow \ No newline at end of file diff --git a/bolt-cli/test_data/web3signer/tls/web3signer.crt b/bolt-cli/test_data/web3signer/tls/web3signer.crt new file mode 100644 index 00000000..676c8115 --- /dev/null +++ b/bolt-cli/test_data/web3signer/tls/web3signer.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- \ No newline at end of file