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
49 changes: 49 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ cb-pbs = { path = "crates/pbs" }
cb-signer = { path = "crates/signer" }
cipher = "0.4"
clap = { version = "4.5.4", features = ["derive", "env"] }
client-ip = { version = "0.1.1", features = [ "forwarded-header" ] }
color-eyre = "0.6.3"
const_format = "0.2.34"
ctr = "0.9.2"
Expand Down Expand Up @@ -74,6 +75,7 @@ tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
tracing-test = { version = "0.2.5", features = ["no-env-filter"] }
tree_hash = "0.9"
tree_hash_derive = "0.9"
typenum = "1.17.0"
Expand Down
5 changes: 5 additions & 0 deletions api/signer-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ paths:

The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `route` (string): The route being requested (must be `/signer/v1/get_pubkeys` for this endpoint).
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
tags:
- Signer
Expand Down Expand Up @@ -73,6 +74,7 @@ paths:
The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
- `route` (string): The route being requested (must be `/signer/v1/request_signature/bls` for this endpoint).
- `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks.
tags:
- Signer
Expand Down Expand Up @@ -220,6 +222,7 @@ paths:
The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
- `route` (string): The route being requested (must be `/signer/v1/request_signature/proxy-bls` for this endpoint).
- `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks.
tags:
- Signer
Expand Down Expand Up @@ -367,6 +370,7 @@ paths:
The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
- `route` (string): The route being requested (must be `/signer/v1/request_signature/proxy-ecdsa` for this endpoint).
- `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks.
tags:
- Signer
Expand Down Expand Up @@ -514,6 +518,7 @@ paths:
The token **must include** the following claims:
- `exp` (integer): Expiration timestamp
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
- `route` (string): The route being requested (must be `/signer/v1/generate_proxy_key` for this endpoint).
- `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks.
tags:
- Signer
Expand Down
3 changes: 2 additions & 1 deletion config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ port = 20000
# Number of JWT authentication attempts a client can fail before blocking that client temporarily from Signer access
# OPTIONAL, DEFAULT: 3
jwt_auth_fail_limit = 3
# How long to block a client from Signer access, in seconds, if it failed JWT authentication too many times
# How long to block a client from Signer access, in seconds, if it failed JWT authentication too many times.
# This also defines the interval at which failed attempts are regularly checked and expired ones are cleaned up.
# OPTIONAL, DEFAULT: 300
jwt_auth_fail_timeout_seconds = 300

Expand Down
36 changes: 9 additions & 27 deletions crates/common/src/commit/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ use std::path::PathBuf;

use alloy::primitives::Address;
use eyre::WrapErr;
use reqwest::{
Certificate,
header::{AUTHORIZATION, HeaderMap, HeaderValue},
};
use reqwest::Certificate;
use serde::{Deserialize, Serialize};
use url::Url;

Expand Down Expand Up @@ -60,30 +57,13 @@ impl SignerClient {
Ok(Self { url: signer_server_url, client: builder.build()?, module_id, jwt_secret })
}

fn refresh_jwt(&mut self) -> Result<(), SignerClientError> {
let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?;

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(())
}

fn create_jwt_for_payload<T: Serialize>(
&mut self,
route: &str,
payload: &T,
) -> Result<Jwt, SignerClientError> {
let payload_vec = serde_json::to_vec(payload)?;
create_jwt(&self.module_id, &self.jwt_secret, Some(&payload_vec))
create_jwt(&self.module_id, &self.jwt_secret, route, Some(&payload_vec))
.wrap_err("failed to create JWT for payload")
.map_err(SignerClientError::JWTError)
}
Expand All @@ -92,10 +72,12 @@ impl SignerClient {
/// requested.
// TODO: add more docs on how proxy keys work
pub async fn get_pubkeys(&mut self) -> Result<GetPubkeysResponse, SignerClientError> {
self.refresh_jwt()?;
let jwt = create_jwt(&self.module_id, &self.jwt_secret, GET_PUBKEYS_PATH, None)
.wrap_err("failed to create JWT for payload")
.map_err(SignerClientError::JWTError)?;

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

if !res.status().is_success() {
return Err(SignerClientError::FailedRequest {
Expand All @@ -117,7 +99,7 @@ impl SignerClient {
Q: Serialize,
T: for<'de> Deserialize<'de>,
{
let jwt = self.create_jwt_for_payload(request)?;
let jwt = self.create_jwt_for_payload(route, request)?;

let url = self.url.join(route)?;
let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?;
Expand Down Expand Up @@ -165,7 +147,7 @@ impl SignerClient {
where
T: ProxyId + for<'de> Deserialize<'de>,
{
let jwt = self.create_jwt_for_payload(request)?;
let jwt = self.create_jwt_for_payload(GENERATE_PROXY_KEY_PATH, request)?;

let url = self.url.join(GENERATE_PROXY_KEY_PATH)?;
let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?;
Expand Down
3 changes: 2 additions & 1 deletion crates/common/src/config/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ pub struct SignerConfig {
pub jwt_auth_fail_limit: u32,

/// Duration in seconds to rate limit an endpoint after the JWT auth failure
/// limit has been reached
/// limit has been reached. This also defines the interval at which failed
/// attempts are regularly checked and expired ones are cleaned up.
#[serde(default = "default_u32::<SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT>")]
pub jwt_auth_fail_timeout_seconds: u32,

Expand Down
6 changes: 3 additions & 3 deletions crates/common/src/signer/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,14 @@ impl ProxyStore {
serde_json::from_str(&file_content)?;
let signer =
EcdsaSigner::new_from_bytes(&key_and_delegation.secret)?;
let pubkey = signer.address();
let address = signer.address();
let proxy_signer = EcdsaProxySigner {
signer,
delegation: key_and_delegation.delegation,
};

proxy_signers.ecdsa_signers.insert(pubkey, proxy_signer);
ecdsa_map.entry(module_id.clone()).or_default().push(pubkey);
proxy_signers.ecdsa_signers.insert(address, proxy_signer);
ecdsa_map.entry(module_id.clone()).or_default().push(address);
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/common/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ pub struct Jwt(pub String);
pub struct JwtClaims {
pub exp: u64,
pub module: ModuleId,
pub route: String,
pub payload_hash: Option<B256>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct JwtAdminClaims {
pub exp: u64,
pub admin: bool,
pub route: String,
pub payload_hash: Option<B256>,
}

Expand Down
Loading
Loading