From 2cab5f44e914ee40fac4c1f98ea4f93ac6972dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 25 Mar 2025 16:53:36 -0300 Subject: [PATCH 01/19] Add JWT creation and verification --- Cargo.lock | 17 ++++++++ Cargo.toml | 1 + crates/cli/src/docker_init.rs | 4 +- crates/common/Cargo.toml | 1 + crates/common/src/commit/constants.rs | 1 + crates/common/src/utils.rs | 27 ++++++++++++- crates/signer/Cargo.toml | 1 + crates/signer/src/service.rs | 57 +++++++++++++++++++-------- 8 files changed, 89 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7db6ff67..5ebc811a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,6 +1493,7 @@ dependencies = [ "ethereum_ssz 0.8.3", "ethereum_ssz_derive", "eyre", + "jsonwebtoken", "pbkdf2 0.12.2", "rand 0.9.0", "reqwest", @@ -1566,6 +1567,7 @@ dependencies = [ "eyre", "futures", "headers", + "jsonwebtoken", "lazy_static", "prometheus", "prost", @@ -2574,8 +2576,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -3167,6 +3171,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "ring 0.17.14", + "serde", + "serde_json", +] + [[package]] name = "k256" version = "0.13.4" diff --git a/Cargo.toml b/Cargo.toml index 43bf3991..aef26a94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,4 @@ typenum = "1.17.0" unicode-normalization = "0.1.24" url = { version = "2.5.0", features = ["serde"] } uuid = { version = "1.8.0", features = ["fast-rng", "serde", "v4"] } +jsonwebtoken = { version = "9.3.1", default-features = false } diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 768c45b6..5b352859 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -20,7 +20,7 @@ use cb_common::{ pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, types::ModuleId, - utils::random_jwt, + utils::create_jwt, }; use docker_compose_types::{ Compose, DependsCondition, DependsOnOptions, EnvFile, Environment, Healthcheck, @@ -99,7 +99,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut ports = vec![]; needs_signer_module = true; - let jwt = random_jwt(); + let jwt = create_jwt(&module.id)?; let jwt_name = format!("CB_JWT_{}", module.id.to_uppercase()); // module ids are assumed unique, so envs dont override each other diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 6f5dd36d..df78b046 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -40,3 +40,4 @@ tree_hash.workspace = true tree_hash_derive.workspace = true unicode-normalization.workspace = true url.workspace = true +jsonwebtoken.workspace = true diff --git a/crates/common/src/commit/constants.rs b/crates/common/src/commit/constants.rs index 7c9f948c..a9d9a9ce 100644 --- a/crates/common/src/commit/constants.rs +++ b/crates/common/src/commit/constants.rs @@ -3,3 +3,4 @@ pub const REQUEST_SIGNATURE_PATH: &str = "/signer/v1/request_signature"; pub const GENERATE_PROXY_KEY_PATH: &str = "/signer/v1/generate_proxy_key"; pub const STATUS_PATH: &str = "/status"; pub const RELOAD_PATH: &str = "/reload"; +pub const REFRESH_TOKEN_PATH: &str = "/refresh_token"; diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 108690a2..9cee5ef6 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -11,7 +11,7 @@ use axum::http::HeaderValue; use blst::min_pk::{PublicKey, Signature}; use rand::{distr::Alphanumeric, Rng}; use reqwest::header::HeaderMap; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use ssz::{Decode, Encode}; use tracing::Level; @@ -22,7 +22,11 @@ use tracing_subscriber::{ EnvFilter, }; -use crate::{config::LogsSettings, pbs::HEADER_VERSION_VALUE, types::Chain}; +use crate::{ + config::LogsSettings, + pbs::HEADER_VERSION_VALUE, + types::{Chain, ModuleId}, +}; const MILLIS_PER_SECOND: u64 = 1_000; @@ -269,6 +273,25 @@ pub fn blst_pubkey_to_alloy(pubkey: &PublicKey) -> BlsPublicKey { BlsPublicKey::from_slice(&pubkey.to_bytes()) } +#[derive(Debug, Serialize, Deserialize)] +pub struct JwtClaims { + pub exp: u64, + pub module: String, +} + +/// Create a JWT for the given module id with a 5 minutes expiration +pub fn create_jwt(module_id: &ModuleId) -> eyre::Result { + jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &JwtClaims { + module: module_id.to_string(), + exp: jsonwebtoken::get_current_timestamp() + 300, // 5 minutes + }, + &jsonwebtoken::EncodingKey::from_secret("secret".as_ref()), + ) + .map_err(Into::into) +} + /// Generates a random string pub fn random_jwt() -> String { rand::rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect() diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 2b108bd2..69f92886 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -26,6 +26,7 @@ tonic.workspace = true tracing.workspace = true tree_hash.workspace = true uuid.workspace = true +jsonwebtoken.workspace = true [build-dependencies] tonic-build.workspace = true diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 99f8e09e..35ee1800 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{collections::HashSet, net::SocketAddr, sync::Arc}; use axum::{ extract::{Request, State}, @@ -9,12 +9,11 @@ use axum::{ Extension, Json, }; use axum_extra::TypedHeader; -use bimap::BiHashMap; use cb_common::{ commit::{ constants::{ - GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, RELOAD_PATH, REQUEST_SIGNATURE_PATH, - STATUS_PATH, + GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REFRESH_TOKEN_PATH, RELOAD_PATH, + REQUEST_SIGNATURE_PATH, STATUS_PATH, }, request::{ EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, SignConsensusRequest, @@ -24,10 +23,12 @@ use cb_common::{ config::StartSignerConfig, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, types::{Chain, Jwt, ModuleId}, + utils::{create_jwt, JwtClaims}, }; use cb_metrics::provider::MetricsProvider; use eyre::Context; use headers::{authorization::Bearer, Authorization}; +use jsonwebtoken::{DecodingKey, Validation}; use tokio::{net::TcpListener, sync::RwLock}; use tracing::{debug, error, info, warn}; use uuid::Uuid; @@ -45,9 +46,8 @@ pub struct SigningService; struct SigningState { /// Manager handling different signing methods manager: Arc>, - /// Map of JWTs to module ids. This also acts as registry of all modules - /// running - jwts: Arc>, + /// Registry of all modules running + modules: Arc>, } impl SigningService { @@ -57,11 +57,11 @@ impl SigningService { return Ok(()); } - let module_ids: Vec = config.jwts.left_values().cloned().map(Into::into).collect(); + let module_ids: HashSet = config.jwts.left_values().cloned().collect(); let state = SigningState { manager: Arc::new(RwLock::new(start_manager(config.clone()).await?)), - jwts: config.jwts.into(), + modules: Arc::new(module_ids.clone()), }; let loaded_consensus = state.manager.read().await.available_consensus_signers(); @@ -75,18 +75,17 @@ impl SigningService { .route(REQUEST_SIGNATURE_PATH, post(handle_request_signature)) .route(GET_PUBKEYS_PATH, get(handle_get_pubkeys)) .route(GENERATE_PROXY_KEY_PATH, post(handle_generate_proxy)) + .route(REFRESH_TOKEN_PATH, post(handle_refresh_token)) .route_layer(middleware::from_fn_with_state(state.clone(), jwt_auth)) .route(RELOAD_PATH, post(handle_reload)) .with_state(state.clone()) - .route_layer(middleware::from_fn(log_request)); - let status_router = axum::Router::new().route(STATUS_PATH, get(handle_status)); + .route_layer(middleware::from_fn(log_request)) + .route(STATUS_PATH, get(handle_status)); let address = SocketAddr::from(([0, 0, 0, 0], config.server_port)); let listener = TcpListener::bind(address).await?; - axum::serve(listener, axum::Router::new().merge(app).merge(status_router)) - .await - .wrap_err("signer server exited") + axum::serve(listener, app).await.wrap_err("signer server exited") } fn init_metrics(network: Chain) -> eyre::Result<()> { @@ -103,12 +102,28 @@ async fn jwt_auth( ) -> Result { let jwt: Jwt = auth.token().to_string().into(); - let module_id = state.jwts.get_by_right(&jwt).ok_or_else(|| { + let mut validation = Validation::default(); + validation.leeway = 10; + + let module_id: ModuleId = jsonwebtoken::decode::( + &jwt, + &DecodingKey::from_secret("secret".as_ref()), + &validation, + ) + .map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })? + .claims + .module + .into(); + + state.modules.get(&module_id).ok_or_else(|| { error!("Unauthorized request. Was the module started correctly?"); SignerModuleError::Unauthorized })?; - req.extensions_mut().insert(module_id.clone()); + req.extensions_mut().insert(module_id); Ok(next.run(req).await) } @@ -273,6 +288,16 @@ async fn handle_reload( Ok(StatusCode::OK) } +async fn handle_refresh_token( + Extension(module_id): Extension, + State(_state): State, +) -> Result { + let new_token = create_jwt(&module_id) + .map_err(|_| SignerModuleError::Internal("Failed to generate new JWT".to_string()))?; + + Ok(Json(new_token).into_response()) +} + async fn start_manager(config: StartSignerConfig) -> eyre::Result { let proxy_store = if let Some(store) = config.store.clone() { Some(store.init_from_env()?) From f74bea7efe7538a4464a31ecbf9534a81fbdf84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 25 Mar 2025 17:34:50 -0300 Subject: [PATCH 02/19] Replace CB_JWTS with CB_MODULES --- crates/cli/src/docker_init.rs | 28 ++++++++--------- crates/common/src/config/constants.rs | 4 +-- crates/common/src/config/signer.rs | 18 +++++------ crates/common/src/config/utils.rs | 43 ++++++++++----------------- crates/signer/src/service.rs | 8 ++--- 5 files changed, 42 insertions(+), 59 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 5b352859..c5404f26 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -9,8 +9,8 @@ use cb_common::{ CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, - DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, - LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, + DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, + METRICS_PORT_ENV, MODULES_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, @@ -19,7 +19,6 @@ use cb_common::{ }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, - types::ModuleId, utils::create_jwt, }; use docker_compose_types::{ @@ -330,7 +329,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re SignerType::Local { loader, store } => { let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), - get_env_same(JWTS_ENV), + get_env_same(MODULES_ENV), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), ]); @@ -355,8 +354,11 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re signer_envs.insert(key, val); } - // write jwts to env - envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); + // write modules to env + envs.insert( + MODULES_ENV.into(), + jwts.keys().map(|module| module.0.clone()).collect::>().join(","), + ); // volumes let mut volumes = vec![config_volume.clone()]; @@ -455,7 +457,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re SignerType::Dirk { cert_path, key_path, secrets_path, ca_cert_path, store, .. } => { let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), - get_env_same(JWTS_ENV), + get_env_same(MODULES_ENV), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), get_env_val(DIRK_CERT_ENV, DIRK_CERT_DEFAULT), get_env_val(DIRK_KEY_ENV, DIRK_KEY_DEFAULT), @@ -483,8 +485,11 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re signer_envs.insert(key, val); } - // write jwts to env - envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); + // write modules to env + envs.insert( + MODULES_ENV.into(), + jwts.keys().map(|module| module.0.clone()).collect::>().join(","), + ); // volumes let mut volumes = vec![ @@ -664,8 +669,3 @@ fn get_log_volume(config: &LogsSettings, module_id: &str) -> Option { )) }) } - -/// Formats as a comma separated list of key=value -fn format_comma_separated(map: &IndexMap) -> String { - map.iter().map(|(k, v)| format!("{}={}", k, v)).collect::>().join(",") -} diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 31580cd8..538baafd 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -35,8 +35,8 @@ pub const SIGNER_MODULE_NAME: &str = "signer"; /// Where the signer module should open the server pub const SIGNER_PORT_ENV: &str = "CB_SIGNER_PORT"; -/// Comma separated list module_id=jwt_secret -pub const JWTS_ENV: &str = "CB_JWTS"; +/// Comma separated list of module ids +pub const MODULES_ENV: &str = "CB_MODULES"; /// Path to json file with plaintext keys (testing only) pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_LOADER_FILE"; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 3d1dccc9..3e6c1286 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -1,20 +1,18 @@ -use std::path::PathBuf; +use std::{collections::HashSet, path::PathBuf}; -use bimap::BiHashMap; use eyre::{bail, OptionExt, Result}; use serde::{Deserialize, Serialize}; use tonic::transport::{Certificate, Identity}; use url::Url; use super::{ - constants::SIGNER_IMAGE_DEFAULT, - utils::{load_env_var, load_jwts}, - CommitBoostConfig, SIGNER_PORT_ENV, + constants::SIGNER_IMAGE_DEFAULT, load_modules, utils::load_env_var, CommitBoostConfig, + SIGNER_PORT_ENV, }; use crate::{ config::{DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV}, signer::{ProxyStore, SignerLoader}, - types::{Chain, Jwt, ModuleId}, + types::{Chain, ModuleId}, }; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -90,7 +88,7 @@ pub struct StartSignerConfig { pub loader: Option, pub store: Option, pub server_port: u16, - pub jwts: BiHashMap, + pub modules: HashSet, pub dirk: Option, } @@ -98,7 +96,7 @@ impl StartSignerConfig { pub fn load_from_env() -> Result { let config = CommitBoostConfig::from_env_path()?; - let jwts = load_jwts()?; + let modules = load_modules()?; let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; let signer = config.signer.ok_or_eyre("Signer config is missing")?.inner; @@ -108,7 +106,7 @@ impl StartSignerConfig { chain: config.chain, loader: Some(loader), server_port, - jwts, + modules, store, dirk: None, }), @@ -136,7 +134,7 @@ impl StartSignerConfig { Ok(StartSignerConfig { chain: config.chain, server_port, - jwts, + modules, loader: None, store, dirk: Some(DirkConfig { diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index 538bee83..edad81a2 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -1,11 +1,10 @@ -use std::path::Path; +use std::{collections::HashSet, path::Path}; -use bimap::BiHashMap; -use eyre::{bail, Context, Ok, Result}; +use eyre::{Context, Ok, Result}; use serde::de::DeserializeOwned; -use super::constants::JWTS_ENV; -use crate::types::{Jwt, ModuleId}; +use super::MODULES_ENV; +use crate::types::ModuleId; pub fn load_env_var(env: &str) -> Result { std::env::var(env).wrap_err(format!("{env} is not set")) @@ -25,24 +24,10 @@ pub fn load_file_from_env(env: &str) -> Result { load_from_file(&path) } -/// Loads a bidirectional map of module id <-> jwt token from a json env -pub fn load_jwts() -> Result> { - let jwts = std::env::var(JWTS_ENV).wrap_err(format!("{JWTS_ENV} is not set"))?; - decode_string_to_map(&jwts) -} - -fn decode_string_to_map(raw: &str) -> Result> { - // trim the string and split for comma - raw.trim() - .split(',') - .map(|pair| { - let mut parts = pair.trim().split('='); - match (parts.next(), parts.next()) { - (Some(key), Some(value)) => Ok((ModuleId(key.into()), Jwt(value.into()))), - _ => bail!("Invalid key-value pair: {pair}"), - } - }) - .collect() +/// Loads a set of module ids from a comma separated string +pub fn load_modules() -> Result> { + let modules = std::env::var(MODULES_ENV).wrap_err(format!("{MODULES_ENV} is not set"))?; + Ok(modules.split(',').map(|id| ModuleId(id.into())).collect()) } #[cfg(test)] @@ -50,12 +35,14 @@ mod tests { use super::*; #[test] - fn test_decode_string_to_map() { - let raw = " KEY=VALUE , KEY2=value2 "; + fn test_load_modules() { + std::env::set_var(MODULES_ENV, "module1,module2,module3"); - let map = decode_string_to_map(raw).unwrap(); + let modules = load_modules().unwrap(); - assert_eq!(map.get_by_left(&ModuleId("KEY".into())), Some(&Jwt("VALUE".into()))); - assert_eq!(map.get_by_left(&ModuleId("KEY2".into())), Some(&Jwt("value2".into()))); + assert_eq!(modules.len(), 3); + assert!(modules.contains(&ModuleId("module1".into()))); + assert!(modules.contains(&ModuleId("module2".into()))); + assert!(modules.contains(&ModuleId("module3".into()))); } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 35ee1800..6e8b626b 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -52,22 +52,20 @@ struct SigningState { impl SigningService { pub async fn run(config: StartSignerConfig) -> eyre::Result<()> { - if config.jwts.is_empty() { + if config.modules.is_empty() { warn!("Signing service was started but no module is registered. Exiting"); return Ok(()); } - let module_ids: HashSet = config.jwts.left_values().cloned().collect(); - let state = SigningState { manager: Arc::new(RwLock::new(start_manager(config.clone()).await?)), - modules: Arc::new(module_ids.clone()), + modules: Arc::new(config.modules.clone()), }; let loaded_consensus = state.manager.read().await.available_consensus_signers(); let loaded_proxies = state.manager.read().await.available_proxy_signers(); - info!(version = COMMIT_BOOST_VERSION, commit_hash = COMMIT_BOOST_COMMIT, modules =? module_ids, port =? config.server_port, loaded_consensus, loaded_proxies, "Starting signing service"); + info!(version = COMMIT_BOOST_VERSION, commit_hash = COMMIT_BOOST_COMMIT, modules =? config.modules, port =? config.server_port, loaded_consensus, loaded_proxies, "Starting signing service"); SigningService::init_metrics(config.chain)?; From 9647ef248b2a2a7835c1d62d22bfbead2cecea41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 25 Mar 2025 17:50:49 -0300 Subject: [PATCH 03/19] Remove CB_MODULES --- crates/cli/src/docker_init.rs | 26 ++++++----------------- crates/common/src/config/constants.rs | 3 --- crates/common/src/config/signer.rs | 6 +++--- crates/common/src/config/utils.rs | 30 ++------------------------- crates/signer/src/service.rs | 14 ++++++++++++- 5 files changed, 24 insertions(+), 55 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index c5404f26..344ceb0d 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -10,12 +10,11 @@ use cb_common::{ BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, - METRICS_PORT_ENV, MODULES_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, - PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, - PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, - SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, - SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV, - SIGNER_URL_ENV, + METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, + PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, + PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, + SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, + SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV, }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, @@ -329,7 +328,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re SignerType::Local { loader, store } => { let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), - get_env_same(MODULES_ENV), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), ]); @@ -354,12 +352,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re signer_envs.insert(key, val); } - // write modules to env - envs.insert( - MODULES_ENV.into(), - jwts.keys().map(|module| module.0.clone()).collect::>().join(","), - ); - // volumes let mut volumes = vec![config_volume.clone()]; volumes.extend(chain_spec_volume.clone()); @@ -457,7 +449,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re SignerType::Dirk { cert_path, key_path, secrets_path, ca_cert_path, store, .. } => { let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), - get_env_same(MODULES_ENV), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), get_env_val(DIRK_CERT_ENV, DIRK_CERT_DEFAULT), get_env_val(DIRK_KEY_ENV, DIRK_KEY_DEFAULT), @@ -485,12 +476,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re signer_envs.insert(key, val); } - // write modules to env - envs.insert( - MODULES_ENV.into(), - jwts.keys().map(|module| module.0.clone()).collect::>().join(","), - ); - // volumes let mut volumes = vec![ config_volume.clone(), @@ -637,6 +622,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re } /// FOO=${FOO} +#[allow(dead_code)] fn get_env_same(k: &str) -> (String, Option) { get_env_interp(k, k) } diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 538baafd..7362fea8 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -35,9 +35,6 @@ pub const SIGNER_MODULE_NAME: &str = "signer"; /// Where the signer module should open the server pub const SIGNER_PORT_ENV: &str = "CB_SIGNER_PORT"; -/// Comma separated list of module ids -pub const MODULES_ENV: &str = "CB_MODULES"; - /// Path to json file with plaintext keys (testing only) pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_LOADER_FILE"; pub const SIGNER_DEFAULT: &str = "/keys.json"; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 3e6c1286..3cd8c90a 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -6,8 +6,7 @@ use tonic::transport::{Certificate, Identity}; use url::Url; use super::{ - constants::SIGNER_IMAGE_DEFAULT, load_modules, utils::load_env_var, CommitBoostConfig, - SIGNER_PORT_ENV, + constants::SIGNER_IMAGE_DEFAULT, utils::load_env_var, CommitBoostConfig, SIGNER_PORT_ENV, }; use crate::{ config::{DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV}, @@ -96,7 +95,8 @@ impl StartSignerConfig { pub fn load_from_env() -> Result { let config = CommitBoostConfig::from_env_path()?; - let modules = load_modules()?; + let modules = + config.modules.unwrap_or(Vec::new()).iter().map(|module| module.id.clone()).collect(); let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; let signer = config.signer.ok_or_eyre("Signer config is missing")?.inner; diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index edad81a2..5cb7e9d0 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -1,11 +1,8 @@ -use std::{collections::HashSet, path::Path}; +use std::path::Path; -use eyre::{Context, Ok, Result}; +use eyre::{Context, Result}; use serde::de::DeserializeOwned; -use super::MODULES_ENV; -use crate::types::ModuleId; - pub fn load_env_var(env: &str) -> Result { std::env::var(env).wrap_err(format!("{env} is not set")) } @@ -23,26 +20,3 @@ pub fn load_file_from_env(env: &str) -> Result { let path = std::env::var(env).wrap_err(format!("{env} is not set"))?; load_from_file(&path) } - -/// Loads a set of module ids from a comma separated string -pub fn load_modules() -> Result> { - let modules = std::env::var(MODULES_ENV).wrap_err(format!("{MODULES_ENV} is not set"))?; - Ok(modules.split(',').map(|id| ModuleId(id.into())).collect()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_load_modules() { - std::env::set_var(MODULES_ENV, "module1,module2,module3"); - - let modules = load_modules().unwrap(); - - assert_eq!(modules.len(), 3); - assert!(modules.contains(&ModuleId("module1".into()))); - assert!(modules.contains(&ModuleId("module2".into()))); - assert!(modules.contains(&ModuleId("module3".into()))); - } -} diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 6e8b626b..7a172416 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -65,7 +65,19 @@ impl SigningService { let loaded_consensus = state.manager.read().await.available_consensus_signers(); let loaded_proxies = state.manager.read().await.available_proxy_signers(); - info!(version = COMMIT_BOOST_VERSION, commit_hash = COMMIT_BOOST_COMMIT, modules =? config.modules, port =? config.server_port, loaded_consensus, loaded_proxies, "Starting signing service"); + info!( + version = COMMIT_BOOST_VERSION, + commit_hash = COMMIT_BOOST_COMMIT, + modules =? config + .modules + .iter() + .map(|module| module.to_string()) + .collect::>(), + port =? config.server_port, + loaded_consensus, + loaded_proxies, + "Starting signing service" + ); SigningService::init_metrics(config.chain)?; From 57abb931aaa0a923d66febbee7c7b3034b019541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Mar 2025 10:47:03 -0300 Subject: [PATCH 04/19] Add jwt secret to config --- crates/cli/src/docker_init.rs | 43 ++++++++++++++++++--------- crates/common/src/config/constants.rs | 3 ++ crates/common/src/config/signer.rs | 16 ++++++++-- crates/common/src/utils.rs | 4 +-- crates/signer/src/manager/dirk.rs | 5 +++- crates/signer/src/manager/local.rs | 13 ++++++-- crates/signer/src/manager/mod.rs | 7 +++++ crates/signer/src/service.rs | 16 ++++++---- 8 files changed, 79 insertions(+), 28 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 344ceb0d..6db648d3 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -6,14 +6,15 @@ use std::{ use cb_common::{ config::{ - CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, BUILDER_PORT_ENV, - BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, - DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, - DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, - METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, - PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, - PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, - SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, + load_optional_env_var, CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, + SignerType, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, + DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, + DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, + LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, + PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, + PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, + PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, + SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_JWT_SECRET_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV, }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, @@ -25,7 +26,7 @@ use docker_compose_types::{ HealthcheckTest, MapOrEmpty, NetworkSettings, Networks, Ports, Service, Services, SingleValue, Volumes, }; -use eyre::Result; +use eyre::{bail, Result}; use indexmap::IndexMap; /// Name of the docker compose file @@ -64,7 +65,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re Some(get_env_val(CHAIN_SPEC_ENV, &format!("/{file_name}"))) }); - let mut jwts = IndexMap::new(); // envs to write in .env file let mut envs = IndexMap::new(); // targets to pass to prometheus @@ -84,7 +84,20 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut warnings = Vec::new(); - let mut needs_signer_module = cb_config.pbs.with_signer; + let needs_signer_module = cb_config.pbs.with_signer || + cb_config.modules.as_ref().is_some_and(|modules| { + modules.iter().any(|module| matches!(module.kind, ModuleKind::Commit)) + }); + + let secret = load_optional_env_var(SIGNER_JWT_SECRET_ENV) + .or(cb_config.signer.as_ref().and_then(|signer_config| signer_config.jwt_secret.clone())); + let jwt_secret = match (needs_signer_module, secret) { + (true, Some(secret)) => secret, + (true, None) => { + bail!("No JWT secret provided for the signer. Set it in the environment or config") + } + (false, _) => String::new(), // not needed + }; // setup modules if let Some(modules_config) = cb_config.modules { @@ -95,9 +108,8 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // a commit module needs a JWT and access to the signer network ModuleKind::Commit => { let mut ports = vec![]; - needs_signer_module = true; - let jwt = create_jwt(&module.id)?; + let jwt = create_jwt(&module.id, &jwt_secret)?; let jwt_name = format!("CB_JWT_{}", module.id.to_uppercase()); // module ids are assumed unique, so envs dont override each other @@ -145,7 +157,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re } envs.insert(jwt_name.clone(), jwt.clone()); - jwts.insert(module.id.clone(), jwt); // networks let module_networks = vec![SIGNER_NETWORK.to_owned()]; @@ -324,9 +335,12 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re panic!("Signer module required but no signer config provided"); }; + envs.insert(SIGNER_JWT_SECRET_ENV.to_string(), jwt_secret.clone()); + match signer_config.inner { SignerType::Local { loader, store } => { let mut signer_envs = IndexMap::from([ + get_env_same(SIGNER_JWT_SECRET_ENV), get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), ]); @@ -450,6 +464,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), + get_env_same(SIGNER_JWT_SECRET_ENV), get_env_val(DIRK_CERT_ENV, DIRK_CERT_DEFAULT), get_env_val(DIRK_KEY_ENV, DIRK_KEY_DEFAULT), get_env_val(DIRK_DIR_SECRETS_ENV, DIRK_DIR_SECRETS_DEFAULT), diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 7362fea8..dac5c830 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -35,6 +35,9 @@ pub const SIGNER_MODULE_NAME: &str = "signer"; /// Where the signer module should open the server pub const SIGNER_PORT_ENV: &str = "CB_SIGNER_PORT"; +/// The JWT secret for the signer to validate the modules requests +pub const SIGNER_JWT_SECRET_ENV: &str = "CB_SIGNER_JWT_SECRET"; + /// Path to json file with plaintext keys (testing only) pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_LOADER_FILE"; pub const SIGNER_DEFAULT: &str = "/keys.json"; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 3cd8c90a..889e51a9 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -6,7 +6,8 @@ use tonic::transport::{Certificate, Identity}; use url::Url; use super::{ - constants::SIGNER_IMAGE_DEFAULT, utils::load_env_var, CommitBoostConfig, SIGNER_PORT_ENV, + constants::SIGNER_IMAGE_DEFAULT, load_optional_env_var, utils::load_env_var, CommitBoostConfig, + SIGNER_JWT_SECRET_ENV, SIGNER_PORT_ENV, }; use crate::{ config::{DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV}, @@ -20,6 +21,8 @@ pub struct SignerConfig { /// Docker image of the module #[serde(default = "default_signer")] pub docker_image: String, + /// Secret used to sign JWTs + pub jwt_secret: Option, /// Inner type-specific configuration #[serde(flatten)] pub inner: SignerType, @@ -89,6 +92,7 @@ pub struct StartSignerConfig { pub server_port: u16, pub modules: HashSet, pub dirk: Option, + pub jwt_secret: String, } impl StartSignerConfig { @@ -99,9 +103,13 @@ impl StartSignerConfig { config.modules.unwrap_or(Vec::new()).iter().map(|module| module.id.clone()).collect(); let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; - let signer = config.signer.ok_or_eyre("Signer config is missing")?.inner; + let signer = config.signer.ok_or_eyre("Signer config is missing")?; + let jwt_secret = + load_optional_env_var(SIGNER_JWT_SECRET_ENV).or(signer.jwt_secret).ok_or_eyre( + "No JWT secret provided for the signer. Set it in the environment or config", + )?; - match signer { + match signer.inner { SignerType::Local { loader, store, .. } => Ok(StartSignerConfig { chain: config.chain, loader: Some(loader), @@ -109,6 +117,7 @@ impl StartSignerConfig { modules, store, dirk: None, + jwt_secret, }), SignerType::Dirk { @@ -151,6 +160,7 @@ impl StartSignerConfig { None => None, }, }), + jwt_secret, }) } diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 9cee5ef6..d61e28ef 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -280,14 +280,14 @@ pub struct JwtClaims { } /// Create a JWT for the given module id with a 5 minutes expiration -pub fn create_jwt(module_id: &ModuleId) -> eyre::Result { +pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { jsonwebtoken::encode( &jsonwebtoken::Header::default(), &JwtClaims { module: module_id.to_string(), exp: jsonwebtoken::get_current_timestamp() + 300, // 5 minutes }, - &jsonwebtoken::EncodingKey::from_secret("secret".as_ref()), + &jsonwebtoken::EncodingKey::from_secret(secret.as_ref()), ) .map_err(Into::into) } diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 0b9c9855..c23271c4 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -82,6 +82,8 @@ struct ProxyAccount { pub struct DirkManager { /// Chain config for the manager chain: Chain, + /// Secret used to sign JWTs + pub(super) jwt_secret: String, /// Consensus accounts available for signing. The key is the public key of /// the account. consensus_accounts: HashMap, @@ -95,7 +97,7 @@ pub struct DirkManager { } impl DirkManager { - pub async fn new(chain: Chain, config: DirkConfig) -> eyre::Result { + pub async fn new(chain: Chain, jwt_secret: String, config: DirkConfig) -> eyre::Result { let mut consensus_accounts = HashMap::new(); for host in config.hosts { @@ -163,6 +165,7 @@ impl DirkManager { Ok(Self { chain, + jwt_secret, consensus_accounts, proxy_accounts: HashMap::new(), secrets_path: config.secrets_path, diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index d04b2fcd..717cd362 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -19,6 +19,8 @@ use crate::error::SignerModuleError; #[derive(Clone)] pub struct LocalSigningManager { chain: Chain, + /// Secret used to sign JWTs + pub(super) jwt_secret: String, proxy_store: Option, consensus_signers: HashMap, proxy_signers: ProxySigners, @@ -30,9 +32,14 @@ pub struct LocalSigningManager { } impl LocalSigningManager { - pub fn new(chain: Chain, proxy_store: Option) -> eyre::Result { + pub fn new( + chain: Chain, + jwt_secret: String, + proxy_store: Option, + ) -> eyre::Result { let mut manager = Self { chain, + jwt_secret, proxy_store, consensus_signers: Default::default(), proxy_signers: Default::default(), @@ -271,13 +278,15 @@ mod tests { use super::*; const CHAIN: Chain = Chain::Holesky; + const JWT_SECRET: &str = "secret"; lazy_static! { static ref MODULE_ID: ModuleId = ModuleId("SAMPLE_MODULE".to_string()); } fn init_signing_manager() -> (LocalSigningManager, BlsPublicKey) { - let mut signing_manager = LocalSigningManager::new(CHAIN, None).unwrap(); + let mut signing_manager = + LocalSigningManager::new(CHAIN, JWT_SECRET.to_string(), None).unwrap(); let consensus_signer = ConsensusSigner::new_random(); let consensus_pk = consensus_signer.pubkey(); diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index 2e608674..35bf0e92 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -43,4 +43,11 @@ impl SigningManager { } } } + + pub(super) fn jwt_secret(&self) -> &str { + match self { + SigningManager::Local(local_manager) => local_manager.jwt_secret.as_str(), + SigningManager::Dirk(dirk_manager) => dirk_manager.jwt_secret.as_str(), + } + } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 7a172416..1334c426 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -117,7 +117,7 @@ async fn jwt_auth( let module_id: ModuleId = jsonwebtoken::decode::( &jwt, - &DecodingKey::from_secret("secret".as_ref()), + &DecodingKey::from_secret(state.manager.read().await.jwt_secret().as_ref()), &validation, ) .map_err(|e| { @@ -300,10 +300,13 @@ async fn handle_reload( async fn handle_refresh_token( Extension(module_id): Extension, - State(_state): State, + State(state): State, ) -> Result { - let new_token = create_jwt(&module_id) - .map_err(|_| SignerModuleError::Internal("Failed to generate new JWT".to_string()))?; + let new_token = + create_jwt(&module_id, state.manager.read().await.jwt_secret()).map_err(|err| { + error!(event = "refresh_token", ?module_id, error = ?err, "Failed to generate new JWT"); + SignerModuleError::Internal("Failed to generate new JWT".to_string()) + })?; Ok(Json(new_token).into_response()) } @@ -318,7 +321,7 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result { - let mut manager = DirkManager::new(config.chain, dirk).await?; + let mut manager = DirkManager::new(config.chain, config.jwt_secret, dirk).await?; if let Some(store) = config.store { manager = manager.with_proxy_store(store.init_from_env()?)?; } @@ -326,7 +329,8 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result { - let mut manager = LocalSigningManager::new(config.chain, proxy_store)?; + let mut manager = + LocalSigningManager::new(config.chain, config.jwt_secret, proxy_store)?; let Some(loader) = config.loader.clone() else { warn!("No loader configured."); return Err(eyre::eyre!("No loader configured")); From 1df6272c188b20a4e826ce5a2acf22752c584f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Mar 2025 12:05:07 -0300 Subject: [PATCH 05/19] Refresh JWT in DA_COMMIT example --- bin/src/lib.rs | 1 + crates/common/src/commit/client.rs | 33 +++++++++++++++++++++++++++++- crates/common/src/constants.rs | 1 + crates/common/src/utils.rs | 5 +++-- examples/da_commit/src/main.rs | 15 +++++++++++--- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 126847b6..5fac235b 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -9,6 +9,7 @@ pub mod prelude { load_builder_module_config, load_commit_module_config, load_pbs_config, load_pbs_custom_config, LogsSettings, StartCommitModuleConfig, PBS_MODULE_NAME, }, + constants::SIGNER_JWT_EXPIRATION, pbs::{BuilderEvent, BuilderEventClient, OnBuilderApiEvent}, signer::{BlsPublicKey, BlsSignature, EcdsaSignature}, types::Chain, diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 8f1a4e3b..44380841 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -7,7 +7,9 @@ use serde::Deserialize; use url::Url; use super::{ - constants::{GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH}, + constants::{ + GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REFRESH_TOKEN_PATH, REQUEST_SIGNATURE_PATH, + }, error::SignerClientError, request::{ EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, ProxyId, SignConsensusRequest, @@ -151,4 +153,33 @@ impl SignerClient { Ok(ecdsa_signed_proxy_delegation) } + + pub async fn refresh_token(&mut self) -> Result<(), SignerClientError> { + let url = self.url.join(REFRESH_TOKEN_PATH)?; + let res = self.client.post(url).send().await?; + + let status = res.status(); + let response_bytes = res.bytes().await?; + + if !status.is_success() { + return Err(SignerClientError::FailedRequest { + status: status.as_u16(), + error_msg: String::from_utf8_lossy(&response_bytes).into_owned(), + }); + } + + let new_jwt: String = serde_json::from_slice(&response_bytes)?; + let mut headers = HeaderMap::new(); + let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", new_jwt)) + .map_err(|e| SignerClientError::InvalidHeader(e))?; + auth_value.set_sensitive(true); + headers.insert(AUTHORIZATION, auth_value); + + self.client = reqwest::Client::builder() + .timeout(DEFAULT_REQUEST_TIMEOUT) + .default_headers(headers) + .build()?; + + Ok(()) + } } diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index d03904b3..c075ed13 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -3,3 +3,4 @@ pub const GENESIS_VALIDATORS_ROOT: [u8; 32] = [0; 32]; pub const COMMIT_BOOST_DOMAIN: [u8; 4] = [109, 109, 111, 67]; pub const COMMIT_BOOST_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const COMMIT_BOOST_COMMIT: &str = env!("GIT_HASH"); +pub const SIGNER_JWT_EXPIRATION: u64 = 300; // 5 minutes diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index d61e28ef..2bd05a64 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -24,6 +24,7 @@ use tracing_subscriber::{ use crate::{ config::LogsSettings, + constants::SIGNER_JWT_EXPIRATION, pbs::HEADER_VERSION_VALUE, types::{Chain, ModuleId}, }; @@ -279,13 +280,13 @@ pub struct JwtClaims { pub module: String, } -/// Create a JWT for the given module id with a 5 minutes expiration +/// Create a JWT for the given module id with expiration pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { jsonwebtoken::encode( &jsonwebtoken::Header::default(), &JwtClaims { module: module_id.to_string(), - exp: jsonwebtoken::get_current_timestamp() + 300, // 5 minutes + exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, }, &jsonwebtoken::EncodingKey::from_secret(secret.as_ref()), ) diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index b1c65338..2a571e7a 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -7,7 +7,7 @@ use lazy_static::lazy_static; use prometheus::{IntCounter, Registry}; use serde::Deserialize; use tokio::time::sleep; -use tracing::{error, info}; +use tracing::{debug, error, info}; // You can define custom metrics and a custom registry for the business logic of // your module. These will be automatically scaped by the Prometheus server @@ -43,7 +43,7 @@ fn default_ecdsa() -> bool { } impl DaCommitService { - pub async fn run(self) -> Result<()> { + pub async fn run(&mut self) -> Result<()> { // the config has the signer_client already setup, we can use it to interact // with the Signer API let pubkeys = self.config.signer_client.get_pubkeys().await?.keys; @@ -66,11 +66,20 @@ impl DaCommitService { }; let mut data = 0; + let mut token_time = 0; loop { + // Refresh JWT if it's about to expire + if token_time >= SIGNER_JWT_EXPIRATION * 3 / 4 { + self.config.signer_client.refresh_token().await?; + debug!("Token refreshed"); + token_time = 0; + } + self.send_request(data, pubkey, proxy_bls, proxy_ecdsa).await?; sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; data += 1; + token_time += self.config.extra.sleep_secs; } } @@ -134,7 +143,7 @@ async fn main() -> Result<()> { "Starting module with custom data" ); - let service = DaCommitService { config }; + let mut service = DaCommitService { config }; if let Err(err) = service.run().await { error!(%err, "Service failed"); From 2e098f7e1f2bb91145941ed154624b1eed584ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Mar 2025 12:09:27 -0300 Subject: [PATCH 06/19] Update example configs --- config.example.toml | 4 +++- examples/configs/dirk_signer.toml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config.example.toml b/config.example.toml index e5e7f777..4b01009d 100644 --- a/config.example.toml +++ b/config.example.toml @@ -141,10 +141,12 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # - Dirk: a remote Dirk instance # - Local: a local Signer module # More details on the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration/#signer-module) +# [signer] # Docker image to use for the Signer module. # OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest -# [signer] # docker_image = "ghcr.io/commit-boost/signer:latest" +# Secret to use for JWT authentication with the Signer module +# jwt_secret = "secret" # For Remote signer: # [signer.remote] # URL of the Web3Signer instance diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index 5dd1c9d3..411e9231 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -8,6 +8,7 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f [signer] docker_image = "commitboost_signer" +jwt_secret = "secret" [signer.dirk] cert_path = "/path/to/client.crt" From 8239a5a89c1fe899a4e9aa5404f067ca504b7f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Mar 2025 12:22:18 -0300 Subject: [PATCH 07/19] Update docs --- docs/docs/developing/commit-module.md | 6 ++++++ docs/docs/get_started/running/binary.md | 4 ++-- docs/package.json | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/docs/developing/commit-module.md b/docs/docs/developing/commit-module.md index 3ad46c03..0d36fc7e 100644 --- a/docs/docs/developing/commit-module.md +++ b/docs/docs/developing/commit-module.md @@ -58,6 +58,12 @@ let pubkeys = config.signer_client.get_pubkeys().await.unwrap(); Which will call the `get_pubkeys` endpoint of the [SignerAPI](/api), returning all the consensus pubkeys and the corresponding proxy keys, of your module. +Note that the requests are authenticated using a JWT, that must be regularly refreshed as it expires after a certain time. To do so, you can call: +```rust +config.signer_client.refresh_token().await.unwrap(); +``` +You have the `SIGNER_JWT_EXPIRATION` constant available in the `commit-boost` crate, which is the time in seconds after which the JWT will expire. + Then, we can request a signature either with a consensus key or with a proxy key: ### With a consensus key diff --git a/docs/docs/get_started/running/binary.md b/docs/docs/get_started/running/binary.md index b5c962d1..3708ab19 100644 --- a/docs/docs/get_started/running/binary.md +++ b/docs/docs/get_started/running/binary.md @@ -26,7 +26,7 @@ Modules need some environment variables to work correctly. - `CB_MUX_PATH_{ID}`: optional, override where to load mux validator keys for mux with `id=\{ID\}`. ### Signer Module -- `CB_JWTS`: required, comma-separated list of `MODULE_ID=JWT` to process signature requests. +- `CB_SIGNER_JWT_SECRET`: secret to use for JWT authentication with the Signer module. - `CB_SIGNER_PORT`: required, port to open the signer server on. - For loading keys we currently support: - `CB_SIGNER_LOADER_FILE`: path to a `.json` with plaintext keys (for testing purposes only). @@ -45,7 +45,7 @@ Modules need some environment variables to work correctly. #### Commit modules - `CB_SIGNER_URL`: required, url to the signer module server. -- `CB_SIGNER_JWT`: required, jwt to use for signature requests (needs to match what is in `CB_JWTS`). +- `CB_SIGNER_JWT`: required, jwt to use for signature requests. #### Events modules - `CB_BUILDER_PORT`: required, port to open to receive builder events from the PBS module. diff --git a/docs/package.json b/docs/package.json index efc2b9bd..36a22a33 100644 --- a/docs/package.json +++ b/docs/package.json @@ -41,5 +41,6 @@ }, "engines": { "node": ">=18.0" - } -} \ No newline at end of file + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} From 2d35982372304c6f0212a9ea0f9ce708c196a18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Mar 2025 12:24:30 -0300 Subject: [PATCH 08/19] Update comment --- config.example.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.example.toml b/config.example.toml index 4b01009d..3b0f0fe5 100644 --- a/config.example.toml +++ b/config.example.toml @@ -145,7 +145,7 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # Docker image to use for the Signer module. # OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest # docker_image = "ghcr.io/commit-boost/signer:latest" -# Secret to use for JWT authentication with the Signer module +# Secret to use for JWT authentication with the Signer module. This can also be set in the `CB_SIGNER_JWT_SECRET` env var. # jwt_secret = "secret" # For Remote signer: # [signer.remote] From 6ccf5a7be84ee8f99d1eb61f208cf738c47a1bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Mar 2025 13:59:53 -0300 Subject: [PATCH 09/19] Add tests --- crates/cli/src/docker_init.rs | 2 +- crates/common/src/utils.rs | 48 +++++++++++++++++++++++++++++++++-- crates/signer/src/service.rs | 23 +++++------------ 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 6db648d3..3a16c724 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -156,7 +156,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re module_envs.insert(key, val); } - envs.insert(jwt_name.clone(), jwt.clone()); + envs.insert(jwt_name.clone(), jwt.to_string()); // networks let module_networks = vec![SIGNER_NETWORK.to_owned()]; diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 2bd05a64..20552151 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -26,7 +26,7 @@ use crate::{ config::LogsSettings, constants::SIGNER_JWT_EXPIRATION, pbs::HEADER_VERSION_VALUE, - types::{Chain, ModuleId}, + types::{Chain, Jwt, ModuleId}, }; const MILLIS_PER_SECOND: u64 = 1_000; @@ -281,7 +281,7 @@ pub struct JwtClaims { } /// Create a JWT for the given module id with expiration -pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { +pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { jsonwebtoken::encode( &jsonwebtoken::Header::default(), &JwtClaims { @@ -291,6 +291,24 @@ pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { &jsonwebtoken::EncodingKey::from_secret(secret.as_ref()), ) .map_err(Into::into) + .map(Jwt::from) +} + +/// Decode a JWT and return the module id +pub fn decode_jwt(jwt: Jwt, secret: &str) -> eyre::Result { + let mut validation = jsonwebtoken::Validation::default(); + validation.leeway = 10; + + let module = jsonwebtoken::decode::( + jwt.as_str(), + &jsonwebtoken::DecodingKey::from_secret(secret.as_ref()), + &validation, + )? + .claims + .module + .into(); + + Ok(module) } /// Generates a random string @@ -333,3 +351,29 @@ pub async fn wait_for_signal() -> eyre::Result<()> { tokio::signal::ctrl_c().await?; Ok(()) } + +#[cfg(test)] +mod test { + use super::{create_jwt, decode_jwt}; + use crate::types::{Jwt, ModuleId}; + + #[test] + fn test_jwt_validation() { + // Check valid JWT + let jwt = create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret").unwrap(); + let module = decode_jwt(jwt, "secret".as_ref()).unwrap(); + assert_eq!(module, ModuleId("DA_COMMIT".to_string())); + + // Check expired JWT + let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.iiq4Z2ed2hk3c3c-cn2QOQJWE5XUOc5BoaIPT-I8q-s".to_string()); + let response = decode_jwt(expired_jwt, "secret"); + assert!(response.is_err()); + assert_eq!(response.unwrap_err().to_string(), "ExpiredSignature"); + + // Check invalid signature JWT + let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.w9WYdDNzgDjYTvjBkk4GGzywGNBYPxnzU2uJWzPUT1s".to_string()); + let response = decode_jwt(invalid_jwt, "secret"); + assert!(response.is_err()); + assert_eq!(response.unwrap_err().to_string(), "InvalidSignature"); + } +} diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 1334c426..91026dcf 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -23,12 +23,11 @@ use cb_common::{ config::StartSignerConfig, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, types::{Chain, Jwt, ModuleId}, - utils::{create_jwt, JwtClaims}, + utils::{create_jwt, decode_jwt}, }; use cb_metrics::provider::MetricsProvider; use eyre::Context; use headers::{authorization::Bearer, Authorization}; -use jsonwebtoken::{DecodingKey, Validation}; use tokio::{net::TcpListener, sync::RwLock}; use tracing::{debug, error, info, warn}; use uuid::Uuid; @@ -112,21 +111,11 @@ async fn jwt_auth( ) -> Result { let jwt: Jwt = auth.token().to_string().into(); - let mut validation = Validation::default(); - validation.leeway = 10; - - let module_id: ModuleId = jsonwebtoken::decode::( - &jwt, - &DecodingKey::from_secret(state.manager.read().await.jwt_secret().as_ref()), - &validation, - ) - .map_err(|e| { - error!("Unauthorized request. Invalid JWT: {e}"); - SignerModuleError::Unauthorized - })? - .claims - .module - .into(); + let module_id: ModuleId = + decode_jwt(jwt, state.manager.read().await.jwt_secret()).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })?; state.modules.get(&module_id).ok_or_else(|| { error!("Unauthorized request. Was the module started correctly?"); From d5076a4329fb3aa7c46a82c60b2eca4f93f89f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Mar 2025 15:12:29 -0300 Subject: [PATCH 10/19] Change refresh_token endpoint method to GET --- crates/common/src/commit/client.rs | 4 ++-- crates/signer/src/service.rs | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 44380841..9f017bc7 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -156,7 +156,7 @@ impl SignerClient { pub async fn refresh_token(&mut self) -> Result<(), SignerClientError> { let url = self.url.join(REFRESH_TOKEN_PATH)?; - let res = self.client.post(url).send().await?; + let res = self.client.get(url).send().await?; let status = res.status(); let response_bytes = res.bytes().await?; @@ -171,7 +171,7 @@ impl SignerClient { let new_jwt: String = serde_json::from_slice(&response_bytes)?; let mut headers = HeaderMap::new(); let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", new_jwt)) - .map_err(|e| SignerClientError::InvalidHeader(e))?; + .map_err(SignerClientError::InvalidHeader)?; auth_value.set_sensitive(true); headers.insert(AUTHORIZATION, auth_value); diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 91026dcf..e5daa2a0 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -84,7 +84,7 @@ impl SigningService { .route(REQUEST_SIGNATURE_PATH, post(handle_request_signature)) .route(GET_PUBKEYS_PATH, get(handle_get_pubkeys)) .route(GENERATE_PROXY_KEY_PATH, post(handle_generate_proxy)) - .route(REFRESH_TOKEN_PATH, post(handle_refresh_token)) + .route(REFRESH_TOKEN_PATH, get(handle_refresh_token)) .route_layer(middleware::from_fn_with_state(state.clone(), jwt_auth)) .route(RELOAD_PATH, post(handle_reload)) .with_state(state.clone()) @@ -111,11 +111,10 @@ async fn jwt_auth( ) -> Result { let jwt: Jwt = auth.token().to_string().into(); - let module_id: ModuleId = - decode_jwt(jwt, state.manager.read().await.jwt_secret()).map_err(|e| { - error!("Unauthorized request. Invalid JWT: {e}"); - SignerModuleError::Unauthorized - })?; + let module_id = decode_jwt(jwt, state.manager.read().await.jwt_secret()).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })?; state.modules.get(&module_id).ok_or_else(|| { error!("Unauthorized request. Was the module started correctly?"); From bb073a0351c3777ce3e790676961afdade785749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Mar 2025 15:32:59 -0300 Subject: [PATCH 11/19] Add logs --- crates/signer/src/service.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index e5daa2a0..7835d031 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -290,6 +290,9 @@ async fn handle_refresh_token( Extension(module_id): Extension, State(state): State, ) -> Result { + let req_id = Uuid::new_v4(); + debug!(event = "refresh_token", ?req_id, ?module_id, "New request"); + let new_token = create_jwt(&module_id, state.manager.read().await.jwt_secret()).map_err(|err| { error!(event = "refresh_token", ?module_id, error = ?err, "Failed to generate new JWT"); From 85e8e8d289baf55ffbd32ca4af65cc93e12bc3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Mar 2025 13:12:26 -0300 Subject: [PATCH 12/19] Fail if not modules in signer start --- crates/common/src/config/signer.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 889e51a9..9fdb8964 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -99,8 +99,12 @@ impl StartSignerConfig { pub fn load_from_env() -> Result { let config = CommitBoostConfig::from_env_path()?; - let modules = - config.modules.unwrap_or(Vec::new()).iter().map(|module| module.id.clone()).collect(); + let modules = config + .modules + .ok_or_eyre("No modules were detected in config")? + .iter() + .map(|module| module.id.clone()) + .collect(); let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; let signer = config.signer.ok_or_eyre("Signer config is missing")?; From 3835a3f991877247e6296970ebd0f9562fa39ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Mar 2025 13:21:04 -0300 Subject: [PATCH 13/19] Removed JWT secret from config. Generate random secret if env var not present --- config.example.toml | 2 -- crates/cli/src/docker_init.rs | 16 ++++------------ crates/common/src/config/signer.rs | 9 +++------ crates/common/src/utils.rs | 2 +- examples/configs/dirk_signer.toml | 1 - 5 files changed, 8 insertions(+), 22 deletions(-) diff --git a/config.example.toml b/config.example.toml index 3b0f0fe5..a832a945 100644 --- a/config.example.toml +++ b/config.example.toml @@ -145,8 +145,6 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # Docker image to use for the Signer module. # OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest # docker_image = "ghcr.io/commit-boost/signer:latest" -# Secret to use for JWT authentication with the Signer module. This can also be set in the `CB_SIGNER_JWT_SECRET` env var. -# jwt_secret = "secret" # For Remote signer: # [signer.remote] # URL of the Web3Signer instance diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 3a16c724..0604f01a 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -19,14 +19,14 @@ use cb_common::{ }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, - utils::create_jwt, + utils::{create_jwt, random_jwt_secret}, }; use docker_compose_types::{ Compose, DependsCondition, DependsOnOptions, EnvFile, Environment, Healthcheck, HealthcheckTest, MapOrEmpty, NetworkSettings, Networks, Ports, Service, Services, SingleValue, Volumes, }; -use eyre::{bail, Result}; +use eyre::Result; use indexmap::IndexMap; /// Name of the docker compose file @@ -89,15 +89,8 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re modules.iter().any(|module| matches!(module.kind, ModuleKind::Commit)) }); - let secret = load_optional_env_var(SIGNER_JWT_SECRET_ENV) - .or(cb_config.signer.as_ref().and_then(|signer_config| signer_config.jwt_secret.clone())); - let jwt_secret = match (needs_signer_module, secret) { - (true, Some(secret)) => secret, - (true, None) => { - bail!("No JWT secret provided for the signer. Set it in the environment or config") - } - (false, _) => String::new(), // not needed - }; + let jwt_secret = + load_optional_env_var(SIGNER_JWT_SECRET_ENV).unwrap_or_else(|| random_jwt_secret()); // setup modules if let Some(modules_config) = cb_config.modules { @@ -637,7 +630,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re } /// FOO=${FOO} -#[allow(dead_code)] fn get_env_same(k: &str) -> (String, Option) { get_env_interp(k, k) } diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 9fdb8964..beeeb9a4 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -21,8 +21,6 @@ pub struct SignerConfig { /// Docker image of the module #[serde(default = "default_signer")] pub docker_image: String, - /// Secret used to sign JWTs - pub jwt_secret: Option, /// Inner type-specific configuration #[serde(flatten)] pub inner: SignerType, @@ -108,10 +106,9 @@ impl StartSignerConfig { let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; let signer = config.signer.ok_or_eyre("Signer config is missing")?; - let jwt_secret = - load_optional_env_var(SIGNER_JWT_SECRET_ENV).or(signer.jwt_secret).ok_or_eyre( - "No JWT secret provided for the signer. Set it in the environment or config", - )?; + let jwt_secret = load_optional_env_var(SIGNER_JWT_SECRET_ENV).ok_or(eyre::eyre!( + "No JWT secret provided for the signer. Set it in the {SIGNER_JWT_SECRET_ENV} env var" + ))?; match signer.inner { SignerType::Local { loader, store, .. } => Ok(StartSignerConfig { diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 20552151..7e109bab 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -312,7 +312,7 @@ pub fn decode_jwt(jwt: Jwt, secret: &str) -> eyre::Result { } /// Generates a random string -pub fn random_jwt() -> String { +pub fn random_jwt_secret() -> String { rand::rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect() } diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index 411e9231..5dd1c9d3 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -8,7 +8,6 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f [signer] docker_image = "commitboost_signer" -jwt_secret = "secret" [signer.dirk] cert_path = "/path/to/client.crt" From fc42dc7866a7491914e8757f3358b553a14c06e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Mar 2025 13:30:32 -0300 Subject: [PATCH 14/19] Fix clippy --- crates/cli/src/docker_init.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 0604f01a..fae227ed 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -89,8 +89,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re modules.iter().any(|module| matches!(module.kind, ModuleKind::Commit)) }); - let jwt_secret = - load_optional_env_var(SIGNER_JWT_SECRET_ENV).unwrap_or_else(|| random_jwt_secret()); + let jwt_secret = load_optional_env_var(SIGNER_JWT_SECRET_ENV).unwrap_or_else(random_jwt_secret); // setup modules if let Some(modules_config) = cb_config.modules { From 3f9873676ec850a24373fcef93156e04ee86b8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Mar 2025 18:03:39 -0300 Subject: [PATCH 15/19] Refactor: Use a JWT secret per module and share it with them --- bin/src/lib.rs | 1 - crates/cli/src/docker_init.rs | 24 ++++++---- crates/common/src/commit/client.rs | 33 +------------- crates/common/src/commit/constants.rs | 1 - crates/common/src/commit/error.rs | 3 ++ crates/common/src/config/constants.rs | 2 + crates/common/src/config/signer.rs | 29 ++++-------- crates/common/src/config/utils.rs | 42 ++++++++++++++++- crates/common/src/utils.rs | 41 ++++++++++++----- crates/signer/src/manager/dirk.rs | 5 +- crates/signer/src/manager/local.rs | 13 +----- crates/signer/src/manager/mod.rs | 7 --- crates/signer/src/service.rs | 66 ++++++++++----------------- examples/da_commit/src/main.rs | 11 +---- 14 files changed, 127 insertions(+), 151 deletions(-) diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 5fac235b..126847b6 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -9,7 +9,6 @@ pub mod prelude { load_builder_module_config, load_commit_module_config, load_pbs_config, load_pbs_custom_config, LogsSettings, StartCommitModuleConfig, PBS_MODULE_NAME, }, - constants::SIGNER_JWT_EXPIRATION, pbs::{BuilderEvent, BuilderEventClient, OnBuilderApiEvent}, signer::{BlsPublicKey, BlsSignature, EcdsaSignature}, types::Chain, diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index fae227ed..12944431 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -9,7 +9,7 @@ use cb_common::{ load_optional_env_var, CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, - DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, + DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, @@ -19,7 +19,8 @@ use cb_common::{ }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, - utils::{create_jwt, random_jwt_secret}, + types::ModuleId, + utils::random_jwt_secret, }; use docker_compose_types::{ Compose, DependsCondition, DependsOnOptions, EnvFile, Environment, Healthcheck, @@ -65,6 +66,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re Some(get_env_val(CHAIN_SPEC_ENV, &format!("/{file_name}"))) }); + let mut jwts = IndexMap::new(); // envs to write in .env file let mut envs = IndexMap::new(); // targets to pass to prometheus @@ -89,8 +91,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re modules.iter().any(|module| matches!(module.kind, ModuleKind::Commit)) }); - let jwt_secret = load_optional_env_var(SIGNER_JWT_SECRET_ENV).unwrap_or_else(random_jwt_secret); - // setup modules if let Some(modules_config) = cb_config.modules { for module in modules_config { @@ -101,7 +101,8 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re ModuleKind::Commit => { let mut ports = vec![]; - let jwt = create_jwt(&module.id, &jwt_secret)?; + let jwt_secret = load_optional_env_var(SIGNER_JWT_SECRET_ENV) + .unwrap_or_else(random_jwt_secret); let jwt_name = format!("CB_JWT_{}", module.id.to_uppercase()); // module ids are assumed unique, so envs dont override each other @@ -148,7 +149,8 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re module_envs.insert(key, val); } - envs.insert(jwt_name.clone(), jwt.to_string()); + jwts.insert(module.id.clone(), jwt_secret.clone()); + envs.insert(jwt_name.clone(), jwt_secret); // networks let module_networks = vec![SIGNER_NETWORK.to_owned()]; @@ -327,8 +329,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re panic!("Signer module required but no signer config provided"); }; - envs.insert(SIGNER_JWT_SECRET_ENV.to_string(), jwt_secret.clone()); - match signer_config.inner { SignerType::Local { loader, store } => { let mut signer_envs = IndexMap::from([ @@ -483,6 +483,9 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re signer_envs.insert(key, val); } + // write jwts to env + envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); + // volumes let mut volumes = vec![ config_volume.clone(), @@ -661,3 +664,8 @@ fn get_log_volume(config: &LogsSettings, module_id: &str) -> Option { )) }) } + +/// Formats as a comma separated list of key=value +fn format_comma_separated(map: &IndexMap) -> String { + map.iter().map(|(k, v)| format!("{}={}", k, v)).collect::>().join(",") +} diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 9f017bc7..8f1a4e3b 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -7,9 +7,7 @@ use serde::Deserialize; use url::Url; use super::{ - constants::{ - GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REFRESH_TOKEN_PATH, REQUEST_SIGNATURE_PATH, - }, + constants::{GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH}, error::SignerClientError, request::{ EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, ProxyId, SignConsensusRequest, @@ -153,33 +151,4 @@ impl SignerClient { Ok(ecdsa_signed_proxy_delegation) } - - pub async fn refresh_token(&mut self) -> Result<(), SignerClientError> { - let url = self.url.join(REFRESH_TOKEN_PATH)?; - let res = self.client.get(url).send().await?; - - let status = res.status(); - let response_bytes = res.bytes().await?; - - if !status.is_success() { - return Err(SignerClientError::FailedRequest { - status: status.as_u16(), - error_msg: String::from_utf8_lossy(&response_bytes).into_owned(), - }); - } - - let new_jwt: String = serde_json::from_slice(&response_bytes)?; - let mut headers = HeaderMap::new(); - let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", new_jwt)) - .map_err(SignerClientError::InvalidHeader)?; - auth_value.set_sensitive(true); - headers.insert(AUTHORIZATION, auth_value); - - self.client = reqwest::Client::builder() - .timeout(DEFAULT_REQUEST_TIMEOUT) - .default_headers(headers) - .build()?; - - Ok(()) - } } diff --git a/crates/common/src/commit/constants.rs b/crates/common/src/commit/constants.rs index a9d9a9ce..7c9f948c 100644 --- a/crates/common/src/commit/constants.rs +++ b/crates/common/src/commit/constants.rs @@ -3,4 +3,3 @@ pub const REQUEST_SIGNATURE_PATH: &str = "/signer/v1/request_signature"; pub const GENERATE_PROXY_KEY_PATH: &str = "/signer/v1/generate_proxy_key"; pub const STATUS_PATH: &str = "/status"; pub const RELOAD_PATH: &str = "/reload"; -pub const REFRESH_TOKEN_PATH: &str = "/refresh_token"; diff --git a/crates/common/src/commit/error.rs b/crates/common/src/commit/error.rs index 94a15aad..47a9f397 100644 --- a/crates/common/src/commit/error.rs +++ b/crates/common/src/commit/error.rs @@ -14,4 +14,7 @@ pub enum SignerClientError { #[error("url parse error: {0}")] ParseError(#[from] url::ParseError), + + #[error("JWT error: {0}")] + JWTError(#[from] eyre::Error), } diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index dac5c830..422af7e7 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -35,6 +35,8 @@ pub const SIGNER_MODULE_NAME: &str = "signer"; /// Where the signer module should open the server pub const SIGNER_PORT_ENV: &str = "CB_SIGNER_PORT"; +/// Comma separated list module_id=jwt_secret +pub const JWTS_ENV: &str = "CB_JWTS"; /// The JWT secret for the signer to validate the modules requests pub const SIGNER_JWT_SECRET_ENV: &str = "CB_SIGNER_JWT_SECRET"; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index beeeb9a4..9df6b948 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf}; use eyre::{bail, OptionExt, Result}; use serde::{Deserialize, Serialize}; @@ -6,8 +6,8 @@ use tonic::transport::{Certificate, Identity}; use url::Url; use super::{ - constants::SIGNER_IMAGE_DEFAULT, load_optional_env_var, utils::load_env_var, CommitBoostConfig, - SIGNER_JWT_SECRET_ENV, SIGNER_PORT_ENV, + constants::SIGNER_IMAGE_DEFAULT, load_jwt_secrets, utils::load_env_var, CommitBoostConfig, + SIGNER_PORT_ENV, }; use crate::{ config::{DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV}, @@ -88,37 +88,27 @@ pub struct StartSignerConfig { pub loader: Option, pub store: Option, pub server_port: u16, - pub modules: HashSet, + pub jwts: HashMap, pub dirk: Option, - pub jwt_secret: String, } impl StartSignerConfig { pub fn load_from_env() -> Result { let config = CommitBoostConfig::from_env_path()?; - let modules = config - .modules - .ok_or_eyre("No modules were detected in config")? - .iter() - .map(|module| module.id.clone()) - .collect(); + let jwts = load_jwt_secrets()?; let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; - let signer = config.signer.ok_or_eyre("Signer config is missing")?; - let jwt_secret = load_optional_env_var(SIGNER_JWT_SECRET_ENV).ok_or(eyre::eyre!( - "No JWT secret provided for the signer. Set it in the {SIGNER_JWT_SECRET_ENV} env var" - ))?; + let signer = config.signer.ok_or_eyre("Signer config is missing")?.inner; - match signer.inner { + match signer { SignerType::Local { loader, store, .. } => Ok(StartSignerConfig { chain: config.chain, loader: Some(loader), server_port, - modules, + jwts, store, dirk: None, - jwt_secret, }), SignerType::Dirk { @@ -144,7 +134,7 @@ impl StartSignerConfig { Ok(StartSignerConfig { chain: config.chain, server_port, - modules, + jwts, loader: None, store, dirk: Some(DirkConfig { @@ -161,7 +151,6 @@ impl StartSignerConfig { None => None, }, }), - jwt_secret, }) } diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index 5cb7e9d0..67c367c5 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -1,8 +1,11 @@ -use std::path::Path; +use std::{collections::HashMap, path::Path}; -use eyre::{Context, Result}; +use eyre::{bail, Context, Result}; use serde::de::DeserializeOwned; +use super::JWTS_ENV; +use crate::types::ModuleId; + pub fn load_env_var(env: &str) -> Result { std::env::var(env).wrap_err(format!("{env} is not set")) } @@ -20,3 +23,38 @@ pub fn load_file_from_env(env: &str) -> Result { let path = std::env::var(env).wrap_err(format!("{env} is not set"))?; load_from_file(&path) } + +/// Loads a map of module id -> jwt secret from a json env +pub fn load_jwt_secrets() -> Result> { + let jwt_secrets = std::env::var(JWTS_ENV).wrap_err(format!("{JWTS_ENV} is not set"))?; + decode_string_to_map(&jwt_secrets) +} + +fn decode_string_to_map(raw: &str) -> Result> { + // trim the string and split for comma + raw.trim() + .split(',') + .map(|pair| { + let mut parts = pair.trim().split('='); + match (parts.next(), parts.next()) { + (Some(key), Some(value)) => Ok((ModuleId(key.into()), value.into())), + _ => bail!("Invalid key-value pair: {pair}"), + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_string_to_map() { + let raw = " KEY=VALUE , KEY2=value2 "; + + let map = decode_string_to_map(raw).unwrap(); + + assert_eq!(map.get(&ModuleId("KEY".into())), Some(&"VALUE".to_string())); + assert_eq!(map.get(&ModuleId("KEY2".into())), Some(&"value2".to_string())); + } +} diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 7e109bab..3bfd3074 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -251,11 +251,11 @@ fn format_crates_filter(default_level: &str, crates_level: &str) -> EnvFilter { pub fn print_logo() { println!( - r#" ______ _ __ ____ __ + r#" ______ _ __ ____ __ / ____/___ ____ ___ ____ ___ (_) /_ / __ )____ ____ _____/ /_ / / / __ \/ __ `__ \/ __ `__ \/ / __/ / __ / __ \/ __ \/ ___/ __/ -/ /___/ /_/ / / / / / / / / / / / / /_ / /_/ / /_/ / /_/ (__ ) /_ -\____/\____/_/ /_/ /_/_/ /_/ /_/_/\__/ /_____/\____/\____/____/\__/ +/ /___/ /_/ / / / / / / / / / / / / /_ / /_/ / /_/ / /_/ (__ ) /_ +\____/\____/_/ /_/ /_/_/ /_/ /_/_/\__/ /_____/\____/\____/____/\__/ "# ) } @@ -294,14 +294,15 @@ pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { .map(Jwt::from) } -/// Decode a JWT and return the module id -pub fn decode_jwt(jwt: Jwt, secret: &str) -> eyre::Result { +/// Decode a JWT and return the module id. IMPORTANT: This function does not +/// validate the JWT, it only obtains the module id from the claims. +pub fn decode_jwt(jwt: Jwt) -> eyre::Result { let mut validation = jsonwebtoken::Validation::default(); - validation.leeway = 10; + validation.insecure_disable_signature_validation(); let module = jsonwebtoken::decode::( jwt.as_str(), - &jsonwebtoken::DecodingKey::from_secret(secret.as_ref()), + &jsonwebtoken::DecodingKey::from_secret(&[]), &validation, )? .claims @@ -311,6 +312,20 @@ pub fn decode_jwt(jwt: Jwt, secret: &str) -> eyre::Result { Ok(module) } +/// Validate a JWT with the given secret +pub fn validate_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> { + let mut validation = jsonwebtoken::Validation::default(); + validation.leeway = 10; + + jsonwebtoken::decode::( + jwt.as_str(), + &jsonwebtoken::DecodingKey::from_secret(secret.as_ref()), + &validation, + ) + .map(|_| ()) + .map_err(From::from) +} + /// Generates a random string pub fn random_jwt_secret() -> String { rand::rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect() @@ -354,25 +369,27 @@ pub async fn wait_for_signal() -> eyre::Result<()> { #[cfg(test)] mod test { - use super::{create_jwt, decode_jwt}; + use super::{create_jwt, decode_jwt, validate_jwt}; use crate::types::{Jwt, ModuleId}; #[test] fn test_jwt_validation() { // Check valid JWT let jwt = create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret").unwrap(); - let module = decode_jwt(jwt, "secret".as_ref()).unwrap(); - assert_eq!(module, ModuleId("DA_COMMIT".to_string())); + let module_id = decode_jwt(jwt.clone()).unwrap(); + assert_eq!(module_id, ModuleId("DA_COMMIT".to_string())); + let response = validate_jwt(jwt, "secret".as_ref()); + assert!(response.is_ok()); // Check expired JWT let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.iiq4Z2ed2hk3c3c-cn2QOQJWE5XUOc5BoaIPT-I8q-s".to_string()); - let response = decode_jwt(expired_jwt, "secret"); + let response = validate_jwt(expired_jwt, "secret"); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "ExpiredSignature"); // Check invalid signature JWT let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.w9WYdDNzgDjYTvjBkk4GGzywGNBYPxnzU2uJWzPUT1s".to_string()); - let response = decode_jwt(invalid_jwt, "secret"); + let response = validate_jwt(invalid_jwt, "secret"); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "InvalidSignature"); } diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index c23271c4..0b9c9855 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -82,8 +82,6 @@ struct ProxyAccount { pub struct DirkManager { /// Chain config for the manager chain: Chain, - /// Secret used to sign JWTs - pub(super) jwt_secret: String, /// Consensus accounts available for signing. The key is the public key of /// the account. consensus_accounts: HashMap, @@ -97,7 +95,7 @@ pub struct DirkManager { } impl DirkManager { - pub async fn new(chain: Chain, jwt_secret: String, config: DirkConfig) -> eyre::Result { + pub async fn new(chain: Chain, config: DirkConfig) -> eyre::Result { let mut consensus_accounts = HashMap::new(); for host in config.hosts { @@ -165,7 +163,6 @@ impl DirkManager { Ok(Self { chain, - jwt_secret, consensus_accounts, proxy_accounts: HashMap::new(), secrets_path: config.secrets_path, diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index 717cd362..d04b2fcd 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -19,8 +19,6 @@ use crate::error::SignerModuleError; #[derive(Clone)] pub struct LocalSigningManager { chain: Chain, - /// Secret used to sign JWTs - pub(super) jwt_secret: String, proxy_store: Option, consensus_signers: HashMap, proxy_signers: ProxySigners, @@ -32,14 +30,9 @@ pub struct LocalSigningManager { } impl LocalSigningManager { - pub fn new( - chain: Chain, - jwt_secret: String, - proxy_store: Option, - ) -> eyre::Result { + pub fn new(chain: Chain, proxy_store: Option) -> eyre::Result { let mut manager = Self { chain, - jwt_secret, proxy_store, consensus_signers: Default::default(), proxy_signers: Default::default(), @@ -278,15 +271,13 @@ mod tests { use super::*; const CHAIN: Chain = Chain::Holesky; - const JWT_SECRET: &str = "secret"; lazy_static! { static ref MODULE_ID: ModuleId = ModuleId("SAMPLE_MODULE".to_string()); } fn init_signing_manager() -> (LocalSigningManager, BlsPublicKey) { - let mut signing_manager = - LocalSigningManager::new(CHAIN, JWT_SECRET.to_string(), None).unwrap(); + let mut signing_manager = LocalSigningManager::new(CHAIN, None).unwrap(); let consensus_signer = ConsensusSigner::new_random(); let consensus_pk = consensus_signer.pubkey(); diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index 35bf0e92..2e608674 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -43,11 +43,4 @@ impl SigningManager { } } } - - pub(super) fn jwt_secret(&self) -> &str { - match self { - SigningManager::Local(local_manager) => local_manager.jwt_secret.as_str(), - SigningManager::Dirk(dirk_manager) => dirk_manager.jwt_secret.as_str(), - } - } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 7835d031..28a1d934 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, net::SocketAddr, sync::Arc}; +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; use axum::{ extract::{Request, State}, @@ -12,8 +12,8 @@ use axum_extra::TypedHeader; use cb_common::{ commit::{ constants::{ - GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REFRESH_TOKEN_PATH, RELOAD_PATH, - REQUEST_SIGNATURE_PATH, STATUS_PATH, + GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, RELOAD_PATH, REQUEST_SIGNATURE_PATH, + STATUS_PATH, }, request::{ EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, SignConsensusRequest, @@ -23,7 +23,7 @@ use cb_common::{ config::StartSignerConfig, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, types::{Chain, Jwt, ModuleId}, - utils::{create_jwt, decode_jwt}, + utils::{decode_jwt, validate_jwt}, }; use cb_metrics::provider::MetricsProvider; use eyre::Context; @@ -45,38 +45,29 @@ pub struct SigningService; struct SigningState { /// Manager handling different signing methods manager: Arc>, - /// Registry of all modules running - modules: Arc>, + /// Map of modules ids to JWT secrets. This also acts as registry of all + /// modules running + jwts: Arc>, } impl SigningService { pub async fn run(config: StartSignerConfig) -> eyre::Result<()> { - if config.modules.is_empty() { + if config.jwts.is_empty() { warn!("Signing service was started but no module is registered. Exiting"); return Ok(()); } + let module_ids: Vec = config.jwts.keys().cloned().map(Into::into).collect(); + let state = SigningState { manager: Arc::new(RwLock::new(start_manager(config.clone()).await?)), - modules: Arc::new(config.modules.clone()), + jwts: config.jwts.into(), }; let loaded_consensus = state.manager.read().await.available_consensus_signers(); let loaded_proxies = state.manager.read().await.available_proxy_signers(); - info!( - version = COMMIT_BOOST_VERSION, - commit_hash = COMMIT_BOOST_COMMIT, - modules =? config - .modules - .iter() - .map(|module| module.to_string()) - .collect::>(), - port =? config.server_port, - loaded_consensus, - loaded_proxies, - "Starting signing service" - ); + info!(version = COMMIT_BOOST_VERSION, commit_hash = COMMIT_BOOST_COMMIT, modules =? module_ids, port =? config.server_port, loaded_consensus, loaded_proxies, "Starting signing service"); SigningService::init_metrics(config.chain)?; @@ -84,7 +75,6 @@ impl SigningService { .route(REQUEST_SIGNATURE_PATH, post(handle_request_signature)) .route(GET_PUBKEYS_PATH, get(handle_get_pubkeys)) .route(GENERATE_PROXY_KEY_PATH, post(handle_generate_proxy)) - .route(REFRESH_TOKEN_PATH, get(handle_refresh_token)) .route_layer(middleware::from_fn_with_state(state.clone(), jwt_auth)) .route(RELOAD_PATH, post(handle_reload)) .with_state(state.clone()) @@ -111,16 +101,23 @@ async fn jwt_auth( ) -> Result { let jwt: Jwt = auth.token().to_string().into(); - let module_id = decode_jwt(jwt, state.manager.read().await.jwt_secret()).map_err(|e| { + // We first need to decode it to get the module id and then validate it + // with the secret stored in the state + let module_id = decode_jwt(jwt.clone()).map_err(|e| { error!("Unauthorized request. Invalid JWT: {e}"); SignerModuleError::Unauthorized })?; - state.modules.get(&module_id).ok_or_else(|| { + let jwt_secret = state.jwts.get(&module_id).ok_or_else(|| { error!("Unauthorized request. Was the module started correctly?"); SignerModuleError::Unauthorized })?; + validate_jwt(jwt, jwt_secret).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })?; + req.extensions_mut().insert(module_id); Ok(next.run(req).await) @@ -286,22 +283,6 @@ async fn handle_reload( Ok(StatusCode::OK) } -async fn handle_refresh_token( - Extension(module_id): Extension, - State(state): State, -) -> Result { - let req_id = Uuid::new_v4(); - debug!(event = "refresh_token", ?req_id, ?module_id, "New request"); - - let new_token = - create_jwt(&module_id, state.manager.read().await.jwt_secret()).map_err(|err| { - error!(event = "refresh_token", ?module_id, error = ?err, "Failed to generate new JWT"); - SignerModuleError::Internal("Failed to generate new JWT".to_string()) - })?; - - Ok(Json(new_token).into_response()) -} - async fn start_manager(config: StartSignerConfig) -> eyre::Result { let proxy_store = if let Some(store) = config.store.clone() { Some(store.init_from_env()?) @@ -312,7 +293,7 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result { - let mut manager = DirkManager::new(config.chain, config.jwt_secret, dirk).await?; + let mut manager = DirkManager::new(config.chain, dirk).await?; if let Some(store) = config.store { manager = manager.with_proxy_store(store.init_from_env()?)?; } @@ -320,8 +301,7 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result { - let mut manager = - LocalSigningManager::new(config.chain, config.jwt_secret, proxy_store)?; + let mut manager = LocalSigningManager::new(config.chain, proxy_store)?; let Some(loader) = config.loader.clone() else { warn!("No loader configured."); return Err(eyre::eyre!("No loader configured")); diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index 2a571e7a..a98299ed 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -7,7 +7,7 @@ use lazy_static::lazy_static; use prometheus::{IntCounter, Registry}; use serde::Deserialize; use tokio::time::sleep; -use tracing::{debug, error, info}; +use tracing::{error, info}; // You can define custom metrics and a custom registry for the business logic of // your module. These will be automatically scaped by the Prometheus server @@ -66,20 +66,11 @@ impl DaCommitService { }; let mut data = 0; - let mut token_time = 0; loop { - // Refresh JWT if it's about to expire - if token_time >= SIGNER_JWT_EXPIRATION * 3 / 4 { - self.config.signer_client.refresh_token().await?; - debug!("Token refreshed"); - token_time = 0; - } - self.send_request(data, pubkey, proxy_bls, proxy_ecdsa).await?; sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; data += 1; - token_time += self.config.extra.sleep_secs; } } From b08eab2d389eef15b9fadab11b9765a4dc94023c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Mar 2025 18:07:35 -0300 Subject: [PATCH 16/19] Fix docker_init --- crates/cli/src/docker_init.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 12944431..a419fafb 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -149,8 +149,8 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re module_envs.insert(key, val); } - jwts.insert(module.id.clone(), jwt_secret.clone()); - envs.insert(jwt_name.clone(), jwt_secret); + envs.insert(jwt_name.clone(), jwt_secret.clone()); + jwts.insert(module.id.clone(), jwt_secret); // networks let module_networks = vec![SIGNER_NETWORK.to_owned()]; @@ -332,8 +332,9 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re match signer_config.inner { SignerType::Local { loader, store } => { let mut signer_envs = IndexMap::from([ - get_env_same(SIGNER_JWT_SECRET_ENV), get_env_val(CONFIG_ENV, CONFIG_DEFAULT), + get_env_same(JWTS_ENV), + get_env_same(SIGNER_JWT_SECRET_ENV), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), ]); @@ -358,6 +359,8 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re signer_envs.insert(key, val); } + envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); + // volumes let mut volumes = vec![config_volume.clone()]; volumes.extend(chain_spec_volume.clone()); @@ -455,8 +458,9 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re SignerType::Dirk { cert_path, key_path, secrets_path, ca_cert_path, store, .. } => { let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), - get_env_uval(SIGNER_PORT_ENV, signer_port as u64), + get_env_same(JWTS_ENV), get_env_same(SIGNER_JWT_SECRET_ENV), + get_env_uval(SIGNER_PORT_ENV, signer_port as u64), get_env_val(DIRK_CERT_ENV, DIRK_CERT_DEFAULT), get_env_val(DIRK_KEY_ENV, DIRK_KEY_DEFAULT), get_env_val(DIRK_DIR_SECRETS_ENV, DIRK_DIR_SECRETS_DEFAULT), From 74a0270ae4686e1141a283eea96391530735c4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Mar 2025 18:09:51 -0300 Subject: [PATCH 17/19] More fixes --- crates/cli/src/docker_init.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index a419fafb..4453f597 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -334,7 +334,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_same(JWTS_ENV), - get_env_same(SIGNER_JWT_SECRET_ENV), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), ]); @@ -359,6 +358,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re signer_envs.insert(key, val); } + // write jwts to env envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); // volumes @@ -459,7 +459,6 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_same(JWTS_ENV), - get_env_same(SIGNER_JWT_SECRET_ENV), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), get_env_val(DIRK_CERT_ENV, DIRK_CERT_DEFAULT), get_env_val(DIRK_KEY_ENV, DIRK_KEY_DEFAULT), From d3b2f507e8258919b273c7e7bc504c007d5c5804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Mar 2025 18:13:16 -0300 Subject: [PATCH 18/19] Move JwtClaims to types --- crates/common/src/types.rs | 6 ++++++ crates/common/src/utils.rs | 10 ++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index b7d3e01a..5293a789 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -17,6 +17,12 @@ pub struct ModuleId(pub String); #[serde(transparent)] pub struct Jwt(pub String); +#[derive(Debug, Serialize, Deserialize)] +pub struct JwtClaims { + pub exp: u64, + pub module: String, +} + #[derive(Clone, Copy, PartialEq, Eq)] pub enum Chain { Mainnet, diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 3bfd3074..37119580 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -11,7 +11,7 @@ use axum::http::HeaderValue; use blst::min_pk::{PublicKey, Signature}; use rand::{distr::Alphanumeric, Rng}; use reqwest::header::HeaderMap; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Serialize}; use serde_json::Value; use ssz::{Decode, Encode}; use tracing::Level; @@ -26,7 +26,7 @@ use crate::{ config::LogsSettings, constants::SIGNER_JWT_EXPIRATION, pbs::HEADER_VERSION_VALUE, - types::{Chain, Jwt, ModuleId}, + types::{Chain, Jwt, JwtClaims, ModuleId}, }; const MILLIS_PER_SECOND: u64 = 1_000; @@ -274,12 +274,6 @@ pub fn blst_pubkey_to_alloy(pubkey: &PublicKey) -> BlsPublicKey { BlsPublicKey::from_slice(&pubkey.to_bytes()) } -#[derive(Debug, Serialize, Deserialize)] -pub struct JwtClaims { - pub exp: u64, - pub module: String, -} - /// Create a JWT for the given module id with expiration pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { jsonwebtoken::encode( From d8add4e43b32a5e97ac16e1a99dc800a88d56bc9 Mon Sep 17 00:00:00 2001 From: eltitanb Date: Fri, 28 Mar 2025 11:03:52 +0000 Subject: [PATCH 19/19] add signer client jwt refresh --- crates/common/src/commit/client.rs | 67 ++++++++++++++++++++++++------ crates/common/src/config/module.rs | 2 +- crates/common/src/config/pbs.rs | 13 ++++-- crates/common/src/signer/store.rs | 6 ++- examples/da_commit/src/main.rs | 28 ++++++------- 5 files changed, 80 insertions(+), 36 deletions(-) diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 8f1a4e3b..34413b65 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::time::{Duration, Instant}; use alloy::{primitives::Address, rpc::types::beacon::BlsSignature}; use eyre::WrapErr; @@ -15,7 +15,10 @@ use super::{ }, }; use crate::{ + constants::SIGNER_JWT_EXPIRATION, signer::{BlsPublicKey, EcdsaSignature}, + types::{Jwt, ModuleId}, + utils::create_jwt, DEFAULT_REQUEST_TIMEOUT, }; @@ -23,31 +26,65 @@ use crate::{ #[derive(Debug, Clone)] pub struct SignerClient { /// Url endpoint of the Signer Module - url: Arc, + url: Url, client: reqwest::Client, + last_jwt_refresh: Instant, + module_id: ModuleId, + jwt_secret: Jwt, } impl SignerClient { /// Create a new SignerClient - pub fn new(signer_server_url: Url, jwt: &str) -> eyre::Result { - let mut headers = HeaderMap::new(); + pub fn new(signer_server_url: Url, jwt_secret: Jwt, module_id: ModuleId) -> eyre::Result { + let jwt = create_jwt(&module_id, &jwt_secret)?; let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; auth_value.set_sensitive(true); + + let mut headers = HeaderMap::new(); headers.insert(AUTHORIZATION, auth_value); + let client = reqwest::Client::builder() .timeout(DEFAULT_REQUEST_TIMEOUT) .default_headers(headers) .build()?; - Ok(Self { url: signer_server_url.into(), client }) + Ok(Self { + url: signer_server_url, + client, + last_jwt_refresh: Instant::now(), + module_id, + jwt_secret, + }) + } + + fn refresh_jwt(&mut self) -> Result<(), SignerClientError> { + if self.last_jwt_refresh.elapsed() > Duration::from_secs(SIGNER_JWT_EXPIRATION) { + let jwt = create_jwt(&self.module_id, &self.jwt_secret)?; + + let mut auth_value = + HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; + auth_value.set_sensitive(true); + + let mut headers = HeaderMap::new(); + headers.insert(AUTHORIZATION, auth_value); + + self.client = reqwest::Client::builder() + .timeout(DEFAULT_REQUEST_TIMEOUT) + .default_headers(headers) + .build()?; + } + + Ok(()) } /// Request a list of validator pubkeys for which signatures can be /// requested. // TODO: add more docs on how proxy keys work - pub async fn get_pubkeys(&self) -> Result { + pub async fn get_pubkeys(&mut self) -> Result { + self.refresh_jwt()?; + let url = self.url.join(GET_PUBKEYS_PATH)?; let res = self.client.get(url).send().await?; @@ -62,10 +99,12 @@ impl SignerClient { } /// Send a signature request - async fn request_signature(&self, request: &SignRequest) -> Result + async fn request_signature(&mut self, request: &SignRequest) -> Result where T: for<'de> Deserialize<'de>, { + self.refresh_jwt()?; + let url = self.url.join(REQUEST_SIGNATURE_PATH)?; let res = self.client.post(url).json(&request).send().await?; @@ -85,33 +124,35 @@ impl SignerClient { } pub async fn request_consensus_signature( - &self, + &mut self, request: SignConsensusRequest, ) -> Result { self.request_signature(&request.into()).await } pub async fn request_proxy_signature_ecdsa( - &self, + &mut self, request: SignProxyRequest
, ) -> Result { self.request_signature(&request.into()).await } pub async fn request_proxy_signature_bls( - &self, + &mut self, request: SignProxyRequest, ) -> Result { self.request_signature(&request.into()).await } async fn generate_proxy_key( - &self, + &mut self, request: &GenerateProxyRequest, ) -> Result, SignerClientError> where T: ProxyId + for<'de> Deserialize<'de>, { + self.refresh_jwt()?; + let url = self.url.join(GENERATE_PROXY_KEY_PATH)?; let res = self.client.post(url).json(&request).send().await?; @@ -131,7 +172,7 @@ impl SignerClient { } pub async fn generate_proxy_key_bls( - &self, + &mut self, consensus_pubkey: BlsPublicKey, ) -> Result, SignerClientError> { let request = GenerateProxyRequest::new(consensus_pubkey, EncryptionScheme::Bls); @@ -142,7 +183,7 @@ impl SignerClient { } pub async fn generate_proxy_key_ecdsa( - &self, + &mut self, consensus_pubkey: BlsPublicKey, ) -> Result, SignerClientError> { let request = GenerateProxyRequest::new(consensus_pubkey, EncryptionScheme::Ecdsa); diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index 2476079b..16b089ca 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -104,7 +104,7 @@ pub fn load_commit_module_config() -> Result() -> Result<(PbsModuleC let signer_client = if cb_config.pbs.static_config.with_signer { // if custom pbs requires a signer client, load jwt - let module_jwt = load_env_var(MODULE_JWT_ENV)?; + let module_jwt = Jwt(load_env_var(MODULE_JWT_ENV)?); let signer_server_url = load_env_var(SIGNER_URL_ENV)?.parse()?; - Some(SignerClient::new(signer_server_url, &module_jwt)?) + Some(SignerClient::new( + signer_server_url, + module_jwt, + ModuleId(PBS_MODULE_NAME.to_string()), + )?) } else { None }; diff --git a/crates/common/src/signer/store.rs b/crates/common/src/signer/store.rs index 822c0369..d6353f4d 100644 --- a/crates/common/src/signer/store.rs +++ b/crates/common/src/signer/store.rs @@ -19,7 +19,7 @@ use eth2_keystore::{ }, Uuid, IV_SIZE, SALT_SIZE, }; -use eyre::OptionExt; +use eyre::{Context, OptionExt}; use rand::Rng; use serde::{Deserialize, Serialize}; use tracing::{trace, warn}; @@ -195,7 +195,9 @@ impl ProxyStore { let mut ecdsa_map: HashMap> = HashMap::new(); // Iterate over the entries in the base directory - for entry in std::fs::read_dir(proxy_dir)? { + for entry in std::fs::read_dir(proxy_dir) + .wrap_err_with(|| format!("failed reading proxy dir: {proxy_dir:?}"))? + { let entry = entry?; let module_path = entry.path(); diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index a98299ed..71b61c53 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -75,7 +75,7 @@ impl DaCommitService { } pub async fn send_request( - &self, + &mut self, data: u64, pubkey: BlsPublicKey, proxy_bls: BlsPublicKey, @@ -84,27 +84,23 @@ impl DaCommitService { let datagram = Datagram { data }; let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); - let signature = self.config.signer_client.request_consensus_signature(request); + let signature = self.config.signer_client.request_consensus_signature(request).await?; + + info!("Proposer commitment (consensus): {}", signature); let proxy_request_bls = SignProxyRequest::builder(proxy_bls).with_msg(&datagram); let proxy_signature_bls = - self.config.signer_client.request_proxy_signature_bls(proxy_request_bls); - - let proxy_signature_ecdsa = proxy_ecdsa.map(|proxy_ecdsa| { - let proxy_request_ecdsa = SignProxyRequest::builder(proxy_ecdsa).with_msg(&datagram); - self.config.signer_client.request_proxy_signature_ecdsa(proxy_request_ecdsa) - }); + self.config.signer_client.request_proxy_signature_bls(proxy_request_bls).await?; - let (signature, proxy_signature_bls) = { - let res = tokio::join!(signature, proxy_signature_bls); - (res.0?, res.1?) - }; - - info!("Proposer commitment (consensus): {}", signature); info!("Proposer commitment (proxy BLS): {}", proxy_signature_bls); - if let Some(proxy_signature_ecdsa) = proxy_signature_ecdsa { - let proxy_signature_ecdsa = proxy_signature_ecdsa.await?; + if let Some(proxy_ecdsa) = proxy_ecdsa { + let proxy_request_ecdsa = SignProxyRequest::builder(proxy_ecdsa).with_msg(&datagram); + let proxy_signature_ecdsa = self + .config + .signer_client + .request_proxy_signature_ecdsa(proxy_request_ecdsa) + .await?; info!("Proposer commitment (proxy ECDSA): {}", proxy_signature_ecdsa); }