Skip to content

Commit d3244bb

Browse files
feat(signer): use real JWTs for signing (#288)
Co-authored-by: eltitanb <[email protected]>
1 parent 490adc8 commit d3244bb

File tree

22 files changed

+270
-98
lines changed

22 files changed

+270
-98
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,4 @@ typenum = "1.17.0"
7070
unicode-normalization = "0.1.24"
7171
url = { version = "2.5.0", features = ["serde"] }
7272
uuid = { version = "1.8.0", features = ["fast-rng", "serde", "v4"] }
73+
jsonwebtoken = { version = "9.3.1", default-features = false }

config.example.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f
144144
# - Dirk: a remote Dirk instance
145145
# - Local: a local Signer module
146146
# More details on the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration/#signer-module)
147+
# [signer]
147148
# Docker image to use for the Signer module.
148149
# OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest
149-
# [signer]
150150
# docker_image = "ghcr.io/commit-boost/signer:latest"
151151
# For Remote signer:
152152
# [signer.remote]

crates/cli/src/docker_init.rs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@ use std::{
66

77
use cb_common::{
88
config::{
9-
CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, BUILDER_PORT_ENV,
10-
BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT,
11-
DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT,
12-
DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT,
13-
LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV,
14-
PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT,
15-
PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT,
16-
SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT,
17-
SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV,
18-
SIGNER_URL_ENV,
9+
load_optional_env_var, CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig,
10+
SignerType, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV,
11+
DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV,
12+
DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV,
13+
LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV,
14+
PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV,
15+
PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT,
16+
PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV,
17+
SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_JWT_SECRET_ENV, SIGNER_KEYS_ENV,
18+
SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV,
1919
},
2020
pbs::{BUILDER_API_PATH, GET_STATUS_PATH},
2121
signer::{ProxyStore, SignerLoader},
2222
types::ModuleId,
23-
utils::random_jwt,
23+
utils::random_jwt_secret,
2424
};
2525
use docker_compose_types::{
2626
Compose, DependsCondition, DependsOnOptions, EnvFile, Environment, Healthcheck,
@@ -86,7 +86,10 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re
8686

8787
let mut warnings = Vec::new();
8888

89-
let mut needs_signer_module = cb_config.pbs.with_signer;
89+
let needs_signer_module = cb_config.pbs.with_signer ||
90+
cb_config.modules.as_ref().is_some_and(|modules| {
91+
modules.iter().any(|module| matches!(module.kind, ModuleKind::Commit))
92+
});
9093

9194
// setup modules
9295
if let Some(modules_config) = cb_config.modules {
@@ -97,9 +100,9 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re
97100
// a commit module needs a JWT and access to the signer network
98101
ModuleKind::Commit => {
99102
let mut ports = vec![];
100-
needs_signer_module = true;
101103

102-
let jwt = random_jwt();
104+
let jwt_secret = load_optional_env_var(SIGNER_JWT_SECRET_ENV)
105+
.unwrap_or_else(random_jwt_secret);
103106
let jwt_name = format!("CB_JWT_{}", module.id.to_uppercase());
104107

105108
// module ids are assumed unique, so envs dont override each other
@@ -146,8 +149,8 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re
146149
module_envs.insert(key, val);
147150
}
148151

149-
envs.insert(jwt_name.clone(), jwt.clone());
150-
jwts.insert(module.id.clone(), jwt);
152+
envs.insert(jwt_name.clone(), jwt_secret.clone());
153+
jwts.insert(module.id.clone(), jwt_secret);
151154

152155
// networks
153156
let module_networks = vec![SIGNER_NETWORK.to_owned()];

crates/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ tree_hash.workspace = true
4040
tree_hash_derive.workspace = true
4141
unicode-normalization.workspace = true
4242
url.workspace = true
43+
jsonwebtoken.workspace = true

crates/common/src/commit/client.rs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::sync::Arc;
1+
use std::time::{Duration, Instant};
22

33
use alloy::{primitives::Address, rpc::types::beacon::BlsSignature};
44
use eyre::WrapErr;
@@ -15,39 +15,76 @@ use super::{
1515
},
1616
};
1717
use crate::{
18+
constants::SIGNER_JWT_EXPIRATION,
1819
signer::{BlsPublicKey, EcdsaSignature},
20+
types::{Jwt, ModuleId},
21+
utils::create_jwt,
1922
DEFAULT_REQUEST_TIMEOUT,
2023
};
2124

2225
/// Client used by commit modules to request signatures via the Signer API
2326
#[derive(Debug, Clone)]
2427
pub struct SignerClient {
2528
/// Url endpoint of the Signer Module
26-
url: Arc<Url>,
29+
url: Url,
2730
client: reqwest::Client,
31+
last_jwt_refresh: Instant,
32+
module_id: ModuleId,
33+
jwt_secret: Jwt,
2834
}
2935

3036
impl SignerClient {
3137
/// Create a new SignerClient
32-
pub fn new(signer_server_url: Url, jwt: &str) -> eyre::Result<Self> {
33-
let mut headers = HeaderMap::new();
38+
pub fn new(signer_server_url: Url, jwt_secret: Jwt, module_id: ModuleId) -> eyre::Result<Self> {
39+
let jwt = create_jwt(&module_id, &jwt_secret)?;
3440

3541
let mut auth_value =
3642
HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?;
3743
auth_value.set_sensitive(true);
44+
45+
let mut headers = HeaderMap::new();
3846
headers.insert(AUTHORIZATION, auth_value);
47+
3948
let client = reqwest::Client::builder()
4049
.timeout(DEFAULT_REQUEST_TIMEOUT)
4150
.default_headers(headers)
4251
.build()?;
4352

44-
Ok(Self { url: signer_server_url.into(), client })
53+
Ok(Self {
54+
url: signer_server_url,
55+
client,
56+
last_jwt_refresh: Instant::now(),
57+
module_id,
58+
jwt_secret,
59+
})
60+
}
61+
62+
fn refresh_jwt(&mut self) -> Result<(), SignerClientError> {
63+
if self.last_jwt_refresh.elapsed() > Duration::from_secs(SIGNER_JWT_EXPIRATION) {
64+
let jwt = create_jwt(&self.module_id, &self.jwt_secret)?;
65+
66+
let mut auth_value =
67+
HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?;
68+
auth_value.set_sensitive(true);
69+
70+
let mut headers = HeaderMap::new();
71+
headers.insert(AUTHORIZATION, auth_value);
72+
73+
self.client = reqwest::Client::builder()
74+
.timeout(DEFAULT_REQUEST_TIMEOUT)
75+
.default_headers(headers)
76+
.build()?;
77+
}
78+
79+
Ok(())
4580
}
4681

4782
/// Request a list of validator pubkeys for which signatures can be
4883
/// requested.
4984
// TODO: add more docs on how proxy keys work
50-
pub async fn get_pubkeys(&self) -> Result<GetPubkeysResponse, SignerClientError> {
85+
pub async fn get_pubkeys(&mut self) -> Result<GetPubkeysResponse, SignerClientError> {
86+
self.refresh_jwt()?;
87+
5188
let url = self.url.join(GET_PUBKEYS_PATH)?;
5289
let res = self.client.get(url).send().await?;
5390

@@ -62,10 +99,12 @@ impl SignerClient {
6299
}
63100

64101
/// Send a signature request
65-
async fn request_signature<T>(&self, request: &SignRequest) -> Result<T, SignerClientError>
102+
async fn request_signature<T>(&mut self, request: &SignRequest) -> Result<T, SignerClientError>
66103
where
67104
T: for<'de> Deserialize<'de>,
68105
{
106+
self.refresh_jwt()?;
107+
69108
let url = self.url.join(REQUEST_SIGNATURE_PATH)?;
70109
let res = self.client.post(url).json(&request).send().await?;
71110

@@ -85,33 +124,35 @@ impl SignerClient {
85124
}
86125

87126
pub async fn request_consensus_signature(
88-
&self,
127+
&mut self,
89128
request: SignConsensusRequest,
90129
) -> Result<BlsSignature, SignerClientError> {
91130
self.request_signature(&request.into()).await
92131
}
93132

94133
pub async fn request_proxy_signature_ecdsa(
95-
&self,
134+
&mut self,
96135
request: SignProxyRequest<Address>,
97136
) -> Result<EcdsaSignature, SignerClientError> {
98137
self.request_signature(&request.into()).await
99138
}
100139

101140
pub async fn request_proxy_signature_bls(
102-
&self,
141+
&mut self,
103142
request: SignProxyRequest<BlsPublicKey>,
104143
) -> Result<BlsSignature, SignerClientError> {
105144
self.request_signature(&request.into()).await
106145
}
107146

108147
async fn generate_proxy_key<T>(
109-
&self,
148+
&mut self,
110149
request: &GenerateProxyRequest,
111150
) -> Result<SignedProxyDelegation<T>, SignerClientError>
112151
where
113152
T: ProxyId + for<'de> Deserialize<'de>,
114153
{
154+
self.refresh_jwt()?;
155+
115156
let url = self.url.join(GENERATE_PROXY_KEY_PATH)?;
116157
let res = self.client.post(url).json(&request).send().await?;
117158

@@ -131,7 +172,7 @@ impl SignerClient {
131172
}
132173

133174
pub async fn generate_proxy_key_bls(
134-
&self,
175+
&mut self,
135176
consensus_pubkey: BlsPublicKey,
136177
) -> Result<SignedProxyDelegation<BlsPublicKey>, SignerClientError> {
137178
let request = GenerateProxyRequest::new(consensus_pubkey, EncryptionScheme::Bls);
@@ -142,7 +183,7 @@ impl SignerClient {
142183
}
143184

144185
pub async fn generate_proxy_key_ecdsa(
145-
&self,
186+
&mut self,
146187
consensus_pubkey: BlsPublicKey,
147188
) -> Result<SignedProxyDelegation<Address>, SignerClientError> {
148189
let request = GenerateProxyRequest::new(consensus_pubkey, EncryptionScheme::Ecdsa);

crates/common/src/commit/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ pub enum SignerClientError {
1414

1515
#[error("url parse error: {0}")]
1616
ParseError(#[from] url::ParseError),
17+
18+
#[error("JWT error: {0}")]
19+
JWTError(#[from] eyre::Error),
1720
}

crates/common/src/config/constants.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub const SIGNER_PORT_ENV: &str = "CB_SIGNER_PORT";
3737

3838
/// Comma separated list module_id=jwt_secret
3939
pub const JWTS_ENV: &str = "CB_JWTS";
40+
/// The JWT secret for the signer to validate the modules requests
41+
pub const SIGNER_JWT_SECRET_ENV: &str = "CB_SIGNER_JWT_SECRET";
4042

4143
/// Path to json file with plaintext keys (testing only)
4244
pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_LOADER_FILE";

crates/common/src/config/module.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pub fn load_commit_module_config<T: DeserializeOwned>() -> Result<StartCommitMod
104104
.find(|m| m.static_config.id == module_id)
105105
.wrap_err(format!("failed to find module for {module_id}"))?;
106106

107-
let signer_client = SignerClient::new(signer_server_url, &module_jwt)?;
107+
let signer_client = SignerClient::new(signer_server_url, module_jwt, module_id)?;
108108

109109
Ok(StartCommitModuleConfig {
110110
id: module_config.static_config.id,

crates/common/src/config/pbs.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@ use super::{
2222
use crate::{
2323
commit::client::SignerClient,
2424
config::{
25-
load_env_var, load_file_from_env, PbsMuxes, CONFIG_ENV, MODULE_JWT_ENV, SIGNER_URL_ENV,
25+
load_env_var, load_file_from_env, PbsMuxes, CONFIG_ENV, MODULE_JWT_ENV, PBS_MODULE_NAME,
26+
SIGNER_URL_ENV,
2627
},
2728
pbs::{
2829
BuilderEventPublisher, DefaultTimeout, RelayClient, RelayEntry, DEFAULT_PBS_PORT,
2930
LATE_IN_SLOT_TIME_MS,
3031
},
31-
types::Chain,
32+
types::{Chain, Jwt, ModuleId},
3233
utils::{
3334
as_eth_str, default_bool, default_host, default_u16, default_u256, default_u64, WEI_PER_ETH,
3435
},
@@ -333,9 +334,13 @@ pub async fn load_pbs_custom_config<T: DeserializeOwned>() -> Result<(PbsModuleC
333334

334335
let signer_client = if cb_config.pbs.static_config.with_signer {
335336
// if custom pbs requires a signer client, load jwt
336-
let module_jwt = load_env_var(MODULE_JWT_ENV)?;
337+
let module_jwt = Jwt(load_env_var(MODULE_JWT_ENV)?);
337338
let signer_server_url = load_env_var(SIGNER_URL_ENV)?.parse()?;
338-
Some(SignerClient::new(signer_server_url, &module_jwt)?)
339+
Some(SignerClient::new(
340+
signer_server_url,
341+
module_jwt,
342+
ModuleId(PBS_MODULE_NAME.to_string()),
343+
)?)
339344
} else {
340345
None
341346
};

0 commit comments

Comments
 (0)