Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
2 changes: 1 addition & 1 deletion config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ 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"
# For Remote signer:
# [signer.remote]
Expand Down
35 changes: 19 additions & 16 deletions crates/cli/src/docker_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ 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, 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, 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,
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, 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,
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},
signer::{ProxyStore, SignerLoader},
types::ModuleId,
utils::random_jwt,
utils::random_jwt_secret,
};
use docker_compose_types::{
Compose, DependsCondition, DependsOnOptions, EnvFile, Environment, Healthcheck,
Expand Down Expand Up @@ -86,7 +86,10 @@ 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))
});

// setup modules
if let Some(modules_config) = cb_config.modules {
Expand All @@ -97,9 +100,9 @@ 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 = random_jwt();
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
Expand Down Expand Up @@ -146,8 +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.clone());
jwts.insert(module.id.clone(), jwt);
envs.insert(jwt_name.clone(), jwt_secret.clone());
jwts.insert(module.id.clone(), jwt_secret);

// networks
let module_networks = vec![SIGNER_NETWORK.to_owned()];
Expand Down
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ tree_hash.workspace = true
tree_hash_derive.workspace = true
unicode-normalization.workspace = true
url.workspace = true
jsonwebtoken.workspace = true
67 changes: 54 additions & 13 deletions crates/common/src/commit/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::time::{Duration, Instant};

use alloy::{primitives::Address, rpc::types::beacon::BlsSignature};
use eyre::WrapErr;
Expand All @@ -15,39 +15,76 @@ use super::{
},
};
use crate::{
constants::SIGNER_JWT_EXPIRATION,
signer::{BlsPublicKey, EcdsaSignature},
types::{Jwt, ModuleId},
utils::create_jwt,
DEFAULT_REQUEST_TIMEOUT,
};

/// Client used by commit modules to request signatures via the Signer API
#[derive(Debug, Clone)]
pub struct SignerClient {
/// Url endpoint of the Signer Module
url: Arc<Url>,
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<Self> {
let mut headers = HeaderMap::new();
pub fn new(signer_server_url: Url, jwt_secret: Jwt, module_id: ModuleId) -> eyre::Result<Self> {
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<GetPubkeysResponse, SignerClientError> {
pub async fn get_pubkeys(&mut self) -> Result<GetPubkeysResponse, SignerClientError> {
self.refresh_jwt()?;

let url = self.url.join(GET_PUBKEYS_PATH)?;
let res = self.client.get(url).send().await?;

Expand All @@ -62,10 +99,12 @@ impl SignerClient {
}

/// Send a signature request
async fn request_signature<T>(&self, request: &SignRequest) -> Result<T, SignerClientError>
async fn request_signature<T>(&mut self, request: &SignRequest) -> Result<T, SignerClientError>
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?;

Expand All @@ -85,33 +124,35 @@ impl SignerClient {
}

pub async fn request_consensus_signature(
&self,
&mut self,
request: SignConsensusRequest,
) -> Result<BlsSignature, SignerClientError> {
self.request_signature(&request.into()).await
}

pub async fn request_proxy_signature_ecdsa(
&self,
&mut self,
request: SignProxyRequest<Address>,
) -> Result<EcdsaSignature, SignerClientError> {
self.request_signature(&request.into()).await
}

pub async fn request_proxy_signature_bls(
&self,
&mut self,
request: SignProxyRequest<BlsPublicKey>,
) -> Result<BlsSignature, SignerClientError> {
self.request_signature(&request.into()).await
}

async fn generate_proxy_key<T>(
&self,
&mut self,
request: &GenerateProxyRequest,
) -> Result<SignedProxyDelegation<T>, 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?;

Expand All @@ -131,7 +172,7 @@ impl SignerClient {
}

pub async fn generate_proxy_key_bls(
&self,
&mut self,
consensus_pubkey: BlsPublicKey,
) -> Result<SignedProxyDelegation<BlsPublicKey>, SignerClientError> {
let request = GenerateProxyRequest::new(consensus_pubkey, EncryptionScheme::Bls);
Expand All @@ -142,7 +183,7 @@ impl SignerClient {
}

pub async fn generate_proxy_key_ecdsa(
&self,
&mut self,
consensus_pubkey: BlsPublicKey,
) -> Result<SignedProxyDelegation<Address>, SignerClientError> {
let request = GenerateProxyRequest::new(consensus_pubkey, EncryptionScheme::Ecdsa);
Expand Down
3 changes: 3 additions & 0 deletions crates/common/src/commit/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ pub enum SignerClientError {

#[error("url parse error: {0}")]
ParseError(#[from] url::ParseError),

#[error("JWT error: {0}")]
JWTError(#[from] eyre::Error),
}
2 changes: 2 additions & 0 deletions crates/common/src/config/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ 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";

/// Path to json file with plaintext keys (testing only)
pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_LOADER_FILE";
Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/config/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub fn load_commit_module_config<T: DeserializeOwned>() -> Result<StartCommitMod
.find(|m| m.static_config.id == module_id)
.wrap_err(format!("failed to find module for {module_id}"))?;

let signer_client = SignerClient::new(signer_server_url, &module_jwt)?;
let signer_client = SignerClient::new(signer_server_url, module_jwt, module_id)?;

Ok(StartCommitModuleConfig {
id: module_config.static_config.id,
Expand Down
13 changes: 9 additions & 4 deletions crates/common/src/config/pbs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ use super::{
use crate::{
commit::client::SignerClient,
config::{
load_env_var, load_file_from_env, PbsMuxes, CONFIG_ENV, MODULE_JWT_ENV, SIGNER_URL_ENV,
load_env_var, load_file_from_env, PbsMuxes, CONFIG_ENV, MODULE_JWT_ENV, PBS_MODULE_NAME,
SIGNER_URL_ENV,
},
pbs::{
BuilderEventPublisher, DefaultTimeout, RelayClient, RelayEntry, DEFAULT_PBS_PORT,
LATE_IN_SLOT_TIME_MS,
},
types::Chain,
types::{Chain, Jwt, ModuleId},
utils::{
as_eth_str, default_bool, default_host, default_u16, default_u256, default_u64, WEI_PER_ETH,
},
Expand Down Expand Up @@ -331,9 +332,13 @@ pub async fn load_pbs_custom_config<T: DeserializeOwned>() -> 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
};
Expand Down
14 changes: 6 additions & 8 deletions crates/common/src/config/signer.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
use std::path::PathBuf;
use std::{collections::HashMap, 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_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},
signer::{ProxyStore, SignerLoader},
types::{Chain, Jwt, ModuleId},
types::{Chain, ModuleId},
};

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -90,15 +88,15 @@ pub struct StartSignerConfig {
pub loader: Option<SignerLoader>,
pub store: Option<ProxyStore>,
pub server_port: u16,
pub jwts: BiHashMap<ModuleId, Jwt>,
pub jwts: HashMap<ModuleId, String>,
pub dirk: Option<DirkConfig>,
}

impl StartSignerConfig {
pub fn load_from_env() -> Result<Self> {
let config = CommitBoostConfig::from_env_path()?;

let jwts = load_jwts()?;
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")?.inner;
Expand Down
Loading
Loading