diff --git a/Cargo.lock b/Cargo.lock index 83f908bb..7f08c78d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1727,6 +1727,7 @@ dependencies = [ "cb-pbs", "cb-signer", "eyre", + "rcgen", "reqwest 0.12.23", "serde_json", "tempfile", @@ -3581,22 +3582,6 @@ dependencies = [ "tokio-native-tls", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.7.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.17" @@ -3616,11 +3601,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.0", - "system-configuration 0.6.1", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -4615,6 +4598,16 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -5127,6 +5120,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -5207,7 +5213,7 @@ dependencies = [ "http-body 0.4.6", "hyper 0.14.32", "hyper-rustls 0.24.2", - "hyper-tls 0.5.0", + "hyper-tls", "ipnet", "js-sys", "log", @@ -5222,7 +5228,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration 0.5.1", + "system-configuration", "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", @@ -5245,21 +5251,16 @@ checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", "futures-util", - "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.7.0", "hyper-rustls 0.27.7", - "hyper-tls 0.6.0", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -5270,7 +5271,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tokio-native-tls", "tokio-rustls 0.26.3", "tokio-util", "tower 0.5.2", @@ -6223,18 +6223,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys 0.5.0", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.4", - "core-foundation", - "system-configuration-sys 0.6.0", + "system-configuration-sys", ] [[package]] @@ -6247,16 +6236,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tap" version = "1.0.1" @@ -7375,8 +7354,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-result", + "windows-strings", ] [[package]] @@ -7413,26 +7392,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.0" @@ -7442,15 +7401,6 @@ dependencies = [ "windows-link 0.2.0", ] -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-strings" version = "0.5.0" @@ -7750,6 +7700,15 @@ dependencies = [ "tap", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 23679360..63599b3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,8 @@ prometheus = "0.14.0" prost = "0.13.4" rand = { version = "0.9", features = ["os_rng"] } rayon = "1.10.0" -reqwest = { version = "0.12.4", features = ["json", "stream"] } +rcgen = "0.14.5" +reqwest = { version = "0.12.4", default-features = false, features = ["json", "stream", "rustls-tls"] } serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.117" serde_yaml = "0.9.33" diff --git a/config.example.toml b/config.example.toml index f15b2262..90fdf458 100644 --- a/config.example.toml +++ b/config.example.toml @@ -190,6 +190,13 @@ jwt_auth_fail_timeout_seconds = 300 # [signer.remote] # URL of the Web3Signer instance # url = "https://remote.signer.url" +# Path to the client certificate for client authentication +# OPTIONAL +# cert_path = "/path/to/client.crt" +# Path to the client key for client authentication +# OPTIONAL +# key_path = "/path/to/client.key" + # For Dirk signer: # [signer.dirk] # Path to the client certificate to authenticate with Dirk diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index b535e54d..e8f289c1 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -74,7 +74,9 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // address for signer API communication let signer_port = cb_config.signer.as_ref().map(|s| s.port).unwrap_or(SIGNER_PORT_DEFAULT); let signer_server = - if let Some(SignerConfig { inner: SignerType::Remote { url }, .. }) = &cb_config.signer { + if let Some(SignerConfig { inner: SignerType::Remote { url, client_auth: _ }, .. }) = + &cb_config.signer + { url.to_string() } else { format!("http://cb_signer:{signer_port}") diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 4e8e0961..15b34108 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -2,7 +2,10 @@ use std::time::{Duration, Instant}; use alloy::primitives::Address; use eyre::WrapErr; -use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue}; +use reqwest::{ + Identity, + header::{AUTHORIZATION, HeaderMap, HeaderValue}, +}; use serde::Deserialize; use url::Url; @@ -16,6 +19,7 @@ use super::{ }; use crate::{ DEFAULT_REQUEST_TIMEOUT, + config::ClientAuthConfig, constants::SIGNER_JWT_EXPIRATION, signer::EcdsaSignature, types::{BlsPublicKey, BlsSignature, Jwt, ModuleId}, @@ -35,7 +39,12 @@ pub struct SignerClient { impl SignerClient { /// Create a new SignerClient - pub fn new(signer_server_url: Url, jwt_secret: Jwt, module_id: ModuleId) -> eyre::Result { + pub fn new( + signer_server_url: Url, + jwt_secret: Jwt, + module_id: ModuleId, + client_auth: Option, + ) -> eyre::Result { let jwt = create_jwt(&module_id, &jwt_secret)?; let mut auth_value = @@ -45,10 +54,18 @@ impl SignerClient { let mut headers = HeaderMap::new(); headers.insert(AUTHORIZATION, auth_value); - let client = reqwest::Client::builder() - .timeout(DEFAULT_REQUEST_TIMEOUT) - .default_headers(headers) - .build()?; + let mut client = + reqwest::Client::builder().timeout(DEFAULT_REQUEST_TIMEOUT).default_headers(headers); + + if let Some(ClientAuthConfig { cert_path, key_path }) = client_auth { + let cert = std::fs::read_to_string(cert_path)?; + let key = std::fs::read_to_string(key_path)?; + let buffer = format!("{cert}\n{key}"); + let identity = Identity::from_pem(buffer.as_bytes())?; + client = client.identity(identity); + } + + let client = client.build()?; Ok(Self { url: signer_server_url, diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index 02fa90da..e8e14277 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -7,6 +7,7 @@ use toml::Table; use crate::{ commit::client::SignerClient, config::{ + SignerConfig, SignerType, constants::{CONFIG_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, SIGNER_URL_ENV}, load_env_var, utils::load_file_from_env, @@ -79,6 +80,7 @@ pub fn load_commit_module_config() -> Result { chain: Chain, modules: Vec>, + signer: Option, } // load module config including the extra data (if any) @@ -101,7 +103,16 @@ pub fn load_commit_module_config() -> Result client_auth, + _ => None, + } + } else { + None + }; + + let signer_client = SignerClient::new(signer_server_url, module_jwt, module_id, client_auth)?; Ok(StartCommitModuleConfig { id: module_config.static_config.id, diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index 7bcf91e3..44d47425 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -22,7 +22,7 @@ use crate::{ commit::client::SignerClient, config::{ CONFIG_ENV, MODULE_JWT_ENV, MuxKeysLoader, PBS_MODULE_NAME, PbsMuxes, SIGNER_URL_ENV, - load_env_var, load_file_from_env, + SignerConfig, SignerType, load_env_var, load_file_from_env, }, pbs::{ DEFAULT_PBS_PORT, DEFAULT_REGISTRY_REFRESH_SECONDS, DefaultTimeout, LATE_IN_SLOT_TIME_MS, @@ -323,6 +323,7 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC relays: Vec, pbs: CustomPbsConfig, muxes: Option, + signer: Option, } // load module config including the extra data (if any) @@ -375,13 +376,22 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC let all_relays = all_relays.into_values().collect(); let signer_client = if cb_config.pbs.static_config.with_signer { - // if custom pbs requires a signer client, load jwt + // if custom pbs requires a signer client, load jwt and client auth info let module_jwt = Jwt(load_env_var(MODULE_JWT_ENV)?); let signer_server_url = load_env_var(SIGNER_URL_ENV)?.parse()?; + let client_auth = if let Some(signer) = cb_config.signer { + match signer.inner { + SignerType::Remote { url: _, client_auth } => client_auth, + _ => None, + } + } else { + None + }; Some(SignerClient::new( signer_server_url, module_jwt, ModuleId(PBS_MODULE_NAME.to_string()), + client_auth, )?) } else { None diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index bc1d2c45..9cb22c7b 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -83,6 +83,16 @@ pub struct DirkHostConfig { pub wallets: Vec, } +/// Client authentication configuration for remote signers +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct ClientAuthConfig { + /// Path to the client certificate + pub cert_path: PathBuf, + /// Path to the client key + pub key_path: PathBuf, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub enum SignerType { @@ -97,6 +107,9 @@ pub enum SignerType { Remote { /// Complete URL of the base API endpoint url: Url, + /// Client authentication configuration + #[serde(flatten)] + client_auth: Option, }, /// Dirk remote signer module Dirk { diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 74fb3f24..c9b96cc9 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -318,6 +318,15 @@ Web3Signer implements the same API as Commit-Boost, so there's no need to set up url = "https://remote.signer.url" ``` +Optionally, you can also provide a client certificate and corresponding private key if the remote signer requires client authentication: + +```toml +[signer.remote] +url = "https://remote.signer.url" +cert_path = "/path/to/client.crt" +key_path = "/path/to/client.key" +``` + #### Dirk Dirk is a distributed key management system that can be used to sign transactions. In this case the Signer module is needed as an intermediary between the modules and Dirk. The following parameters are needed: diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 5e8e1596..2bdbd25b 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -23,4 +23,5 @@ tree_hash.workspace = true url.workspace = true [dev-dependencies] -cb-common = { path = "../crates/common", features = ["testing-flags"] } \ No newline at end of file +cb-common = { path = "../crates/common", features = ["testing-flags"] } +rcgen.workspace = true diff --git a/tests/tests/web3-signer-client-auth.rs b/tests/tests/web3-signer-client-auth.rs new file mode 100644 index 00000000..b403b555 --- /dev/null +++ b/tests/tests/web3-signer-client-auth.rs @@ -0,0 +1,47 @@ +use std::io::Write; + +use cb_common::{ + commit::client::SignerClient, + config::ClientAuthConfig, + types::{Jwt, ModuleId}, +}; +use cb_tests::utils::setup_test_env; +use eyre::Result; +use rcgen::{CertificateParams, KeyPair}; + +const JWT_MODULE: &str = "test-module"; +const JWT_SECRET: &str = "test-jwt-secret"; + +/// Test that the SignerClient can be created with client authentication +#[tokio::test] +async fn test_web3_signer_client_auth() -> Result<()> { + setup_test_env(); + + // Create a keypair first (default: ECDSA P-256) + let key_pair = KeyPair::generate().unwrap(); + + // Create the certificate + let params = CertificateParams::new(vec!["web3signer-client-test".to_string()])?; + let cert = params.self_signed(&key_pair)?; + + // PEM-encode the key and certificate to temp files + let mut cert_file = tempfile::NamedTempFile::new()?; + let mut key_file = tempfile::NamedTempFile::new()?; + write!(cert_file, "{}", cert.pem())?; + write!(key_file, "{}", key_pair.serialize_pem())?; + + // Create the signer config with client auth - this will create a new client + // that has client auth enabled, so if it fails anywhere then it'll fail + // here + let _client = SignerClient::new( + "http://localhost:0".parse()?, + Jwt(JWT_SECRET.to_string()), + ModuleId(JWT_MODULE.to_string()), + Some(ClientAuthConfig { + cert_path: cert_file.path().to_path_buf(), + key_path: key_file.path().to_path_buf(), + }), + )?; + + Ok(()) +}