Skip to content

Commit 5935659

Browse files
authored
CBST2-04: Update JWT secrets on reload and revoke module endpoint (#295)
1 parent 2f785d1 commit 5935659

File tree

16 files changed

+250
-34
lines changed

16 files changed

+250
-34
lines changed

Cargo.lock

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

crates/cli/src/docker_init.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ 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_ENDPOINT_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME,
18-
SIGNER_URL_ENV,
9+
CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, ADMIN_JWT_ENV,
10+
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_ENDPOINT_ENV, SIGNER_KEYS_ENV,
18+
SIGNER_MODULE_NAME, SIGNER_URL_ENV,
1919
},
2020
pbs::{BUILDER_API_PATH, GET_STATUS_PATH},
2121
signer::{ProxyStore, SignerLoader, DEFAULT_SIGNER_PORT},
@@ -333,6 +333,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re
333333
let mut signer_envs = IndexMap::from([
334334
get_env_val(CONFIG_ENV, CONFIG_DEFAULT),
335335
get_env_same(JWTS_ENV),
336+
get_env_same(ADMIN_JWT_ENV),
336337
]);
337338

338339
// Bind the signer API to 0.0.0.0
@@ -366,6 +367,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re
366367

367368
// write jwts to env
368369
envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts));
370+
envs.insert(ADMIN_JWT_ENV.into(), random_jwt_secret());
369371

370372
// volumes
371373
let mut volumes = vec![config_volume.clone()];

crates/common/src/commit/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub const REQUEST_SIGNATURE_PATH: &str = "/signer/v1/request_signature";
33
pub const GENERATE_PROXY_KEY_PATH: &str = "/signer/v1/generate_proxy_key";
44
pub const STATUS_PATH: &str = "/status";
55
pub const RELOAD_PATH: &str = "/reload";
6+
pub const REVOKE_MODULE_PATH: &str = "/revoke_jwt";

crates/common/src/commit/request.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
collections::HashMap,
23
fmt::{self, Debug, Display},
34
str::FromStr,
45
};
@@ -9,13 +10,17 @@ use alloy::{
910
rpc::types::beacon::BlsSignature,
1011
};
1112
use derive_more::derive::From;
12-
use serde::{Deserialize, Serialize};
13+
use serde::{Deserialize, Deserializer, Serialize};
1314
use tree_hash::TreeHash;
1415
use tree_hash_derive::TreeHash;
1516

1617
use crate::{
17-
constants::COMMIT_BOOST_DOMAIN, error::BlstErrorWrapper, signature::verify_signed_message,
18-
signer::BlsPublicKey, types::Chain,
18+
config::decode_string_to_map,
19+
constants::COMMIT_BOOST_DOMAIN,
20+
error::BlstErrorWrapper,
21+
signature::verify_signed_message,
22+
signer::BlsPublicKey,
23+
types::{Chain, ModuleId},
1924
};
2025

2126
pub trait ProxyId: AsRef<[u8]> + Debug + Clone + Copy + TreeHash + Display {}
@@ -198,6 +203,31 @@ pub struct GetPubkeysResponse {
198203
pub keys: Vec<ConsensusProxyMap>,
199204
}
200205

206+
#[derive(Debug, Clone, Serialize, Deserialize)]
207+
pub struct ReloadRequest {
208+
#[serde(default, deserialize_with = "deserialize_jwt_secrets")]
209+
pub jwt_secrets: Option<HashMap<ModuleId, String>>,
210+
pub admin_secret: Option<String>,
211+
}
212+
213+
pub fn deserialize_jwt_secrets<'de, D>(
214+
deserializer: D,
215+
) -> Result<Option<HashMap<ModuleId, String>>, D::Error>
216+
where
217+
D: Deserializer<'de>,
218+
{
219+
let raw: String = Deserialize::deserialize(deserializer)?;
220+
221+
decode_string_to_map(&raw)
222+
.map(Some)
223+
.map_err(|_| serde::de::Error::custom("Invalid format".to_string()))
224+
}
225+
226+
#[derive(Debug, Clone, Serialize, Deserialize)]
227+
pub struct RevokeModuleRequest {
228+
pub module_id: ModuleId,
229+
}
230+
201231
/// Map of consensus pubkeys to proxies
202232
#[derive(Debug, Clone, Deserialize, Serialize)]
203233
pub struct ConsensusProxyMap {
@@ -288,7 +318,7 @@ mod tests {
288318

289319
let _: SignedProxyDelegationBls = serde_json::from_str(data).unwrap();
290320

291-
let data = r#"{
321+
let data = r#"{
292322
"message": {
293323
"delegator": "0xa3366b54f28e4bf1461926a3c70cdb0ec432b5c92554ecaae3742d33fb33873990cbed1761c68020e6d3c14d30a22050",
294324
"proxy": "0x4ca9939a8311a7cab3dde201b70157285fa81a9d"

crates/common/src/config/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub const SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV: &str =
4242

4343
/// Comma separated list module_id=jwt_secret
4444
pub const JWTS_ENV: &str = "CB_JWTS";
45+
pub const ADMIN_JWT_ENV: &str = "CB_SIGNER_ADMIN_JWT";
4546

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

crates/common/src/config/signer.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ pub struct StartSignerConfig {
133133
pub store: Option<ProxyStore>,
134134
pub endpoint: SocketAddr,
135135
pub jwts: HashMap<ModuleId, String>,
136+
pub admin_secret: String,
136137
pub jwt_auth_fail_limit: u32,
137138
pub jwt_auth_fail_timeout_seconds: u32,
138139
pub dirk: Option<DirkConfig>,
@@ -142,7 +143,7 @@ impl StartSignerConfig {
142143
pub fn load_from_env() -> Result<Self> {
143144
let config = CommitBoostConfig::from_env_path()?;
144145

145-
let jwts = load_jwt_secrets()?;
146+
let (admin_secret, jwts) = load_jwt_secrets()?;
146147

147148
let signer_config = config.signer.ok_or_eyre("Signer config is missing")?;
148149

@@ -177,6 +178,7 @@ impl StartSignerConfig {
177178
loader: Some(loader),
178179
endpoint,
179180
jwts,
181+
admin_secret,
180182
jwt_auth_fail_limit,
181183
jwt_auth_fail_timeout_seconds,
182184
store,
@@ -207,6 +209,7 @@ impl StartSignerConfig {
207209
chain: config.chain,
208210
endpoint,
209211
jwts,
212+
admin_secret,
210213
jwt_auth_fail_limit,
211214
jwt_auth_fail_timeout_seconds,
212215
loader: None,

crates/common/src/config/utils.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use alloy::rpc::types::beacon::BlsPublicKey;
44
use eyre::{bail, Context, Result};
55
use serde::de::DeserializeOwned;
66

7-
use super::JWTS_ENV;
7+
use super::{ADMIN_JWT_ENV, JWTS_ENV};
88
use crate::{config::MUXER_HTTP_MAX_LENGTH, types::ModuleId, utils::read_chunked_body_with_max};
99

1010
pub fn load_env_var(env: &str) -> Result<String> {
@@ -26,9 +26,10 @@ pub fn load_file_from_env<T: DeserializeOwned>(env: &str) -> Result<T> {
2626
}
2727

2828
/// Loads a map of module id -> jwt secret from a json env
29-
pub fn load_jwt_secrets() -> Result<HashMap<ModuleId, String>> {
29+
pub fn load_jwt_secrets() -> Result<(String, HashMap<ModuleId, String>)> {
30+
let admin_jwt = std::env::var(ADMIN_JWT_ENV).wrap_err(format!("{ADMIN_JWT_ENV} is not set"))?;
3031
let jwt_secrets = std::env::var(JWTS_ENV).wrap_err(format!("{JWTS_ENV} is not set"))?;
31-
decode_string_to_map(&jwt_secrets)
32+
decode_string_to_map(&jwt_secrets).map(|secrets| (admin_jwt, secrets))
3233
}
3334

3435
/// Reads an HTTP response safely, erroring out if it failed or if the body is
@@ -71,7 +72,7 @@ pub fn remove_duplicate_keys(keys: Vec<BlsPublicKey>) -> Vec<BlsPublicKey> {
7172
unique_keys
7273
}
7374

74-
fn decode_string_to_map(raw: &str) -> Result<HashMap<ModuleId, String>> {
75+
pub fn decode_string_to_map(raw: &str) -> Result<HashMap<ModuleId, String>> {
7576
// trim the string and split for comma
7677
raw.trim()
7778
.split(',')

crates/common/src/types.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ pub struct JwtClaims {
2323
pub module: String,
2424
}
2525

26+
#[derive(Debug, Serialize, Deserialize)]
27+
pub struct JwtAdmin {
28+
pub exp: u64,
29+
pub admin: bool,
30+
}
31+
2632
#[derive(Clone, Copy, PartialEq, Eq)]
2733
pub enum Chain {
2834
Mainnet,

crates/common/src/utils.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use crate::{
3030
config::LogsSettings,
3131
constants::SIGNER_JWT_EXPIRATION,
3232
pbs::HEADER_VERSION_VALUE,
33-
types::{Chain, Jwt, JwtClaims, ModuleId},
33+
types::{Chain, Jwt, JwtAdmin, JwtClaims, ModuleId},
3434
};
3535

3636
const MILLIS_PER_SECOND: u64 = 1_000;
@@ -405,6 +405,24 @@ pub fn validate_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> {
405405
.map_err(From::from)
406406
}
407407

408+
/// Validate an admin JWT with the given secret
409+
pub fn validate_admin_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> {
410+
let mut validation = jsonwebtoken::Validation::default();
411+
validation.leeway = 10;
412+
413+
let token = jsonwebtoken::decode::<JwtAdmin>(
414+
jwt.as_str(),
415+
&jsonwebtoken::DecodingKey::from_secret(secret.as_ref()),
416+
&validation,
417+
)?;
418+
419+
if token.claims.admin {
420+
Ok(())
421+
} else {
422+
eyre::bail!("Token is not admin")
423+
}
424+
}
425+
408426
/// Generates a random string
409427
pub fn random_jwt_secret() -> String {
410428
rand::rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect()

crates/signer/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ pub enum SignerModuleError {
2525
#[error("Dirk signer does not support this operation")]
2626
DirkNotSupported,
2727

28+
#[error("module id not found")]
29+
ModuleIdNotFound,
30+
2831
#[error("internal error: {0}")]
2932
Internal(String),
3033

@@ -48,6 +51,7 @@ impl IntoResponse for SignerModuleError {
4851
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".to_string())
4952
}
5053
SignerModuleError::SignerError(err) => (StatusCode::BAD_REQUEST, err.to_string()),
54+
SignerModuleError::ModuleIdNotFound => (StatusCode::NOT_FOUND, self.to_string()),
5155
SignerModuleError::RateLimited(duration) => {
5256
(StatusCode::TOO_MANY_REQUESTS, format!("rate limited for {duration:?}"))
5357
}

0 commit comments

Comments
 (0)