Skip to content

Commit 52aec57

Browse files
jclapisltitanbManuelBilbao
authored
Add payload hash to signer JWT claims (#356)
Co-authored-by: eltitanb <[email protected]> Co-authored-by: ltitanb <[email protected]> Co-authored-by: Manuel Iñaki Bilbao <[email protected]>
1 parent 25bd5a6 commit 52aec57

File tree

8 files changed

+332
-115
lines changed

8 files changed

+332
-115
lines changed

api/signer-api.yml

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ paths:
1010
/signer/v1/get_pubkeys:
1111
get:
1212
summary: Get a list of public keys for which signatures may be requested
13+
description: >
14+
This endpoint requires a valid JWT Bearer token.
15+
16+
The token **must include** the following claims:
17+
- `exp` (integer): Expiration timestamp
18+
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
1319
tags:
1420
- Signer
1521
security:
@@ -61,6 +67,13 @@ paths:
6167
/signer/v1/request_signature/bls:
6268
post:
6369
summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the BLS private key for the requested public key.
70+
description: >
71+
This endpoint requires a valid JWT Bearer token.
72+
73+
The token **must include** the following claims:
74+
- `exp` (integer): Expiration timestamp
75+
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
76+
- `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.
6477
tags:
6578
- Signer
6679
security:
@@ -201,6 +214,13 @@ paths:
201214
/signer/v1/request_signature/proxy-bls:
202215
post:
203216
summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the BLS private key for the requested proxy public key.
217+
description: >
218+
This endpoint requires a valid JWT Bearer token.
219+
220+
The token **must include** the following claims:
221+
- `exp` (integer): Expiration timestamp
222+
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
223+
- `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.
204224
tags:
205225
- Signer
206226
security:
@@ -341,6 +361,13 @@ paths:
341361
/signer/v1/request_signature/proxy-ecdsa:
342362
post:
343363
summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the ECDSA private key for the requested proxy Ethereum address.
364+
description: >
365+
This endpoint requires a valid JWT Bearer token.
366+
367+
The token **must include** the following claims:
368+
- `exp` (integer): Expiration timestamp
369+
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
370+
- `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.
344371
tags:
345372
- Signer
346373
security:
@@ -481,6 +508,13 @@ paths:
481508
/signer/v1/generate_proxy_key:
482509
post:
483510
summary: Request a proxy key be generated for a specific consensus pubkey
511+
description: >
512+
This endpoint requires a valid JWT Bearer token.
513+
514+
The token **must include** the following claims:
515+
- `exp` (integer): Expiration timestamp
516+
- `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file.
517+
- `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.
484518
tags:
485519
- Signer
486520
security:
@@ -580,20 +614,6 @@ paths:
580614
type: string
581615
example: "Internal error"
582616

583-
/status:
584-
get:
585-
summary: Get the status of the Signer API module
586-
tags:
587-
- Management
588-
responses:
589-
"200":
590-
description: Success
591-
content:
592-
text/plain:
593-
schema:
594-
type: string
595-
example: "OK"
596-
597617
components:
598618
securitySchemes:
599619
BearerAuth:

crates/common/src/commit/client.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub struct SignerClient {
4242
impl SignerClient {
4343
/// Create a new SignerClient
4444
pub fn new(signer_server_url: Url, jwt_secret: Jwt, module_id: ModuleId) -> eyre::Result<Self> {
45-
let jwt = create_jwt(&module_id, &jwt_secret)?;
45+
let jwt = create_jwt(&module_id, &jwt_secret, None)?;
4646

4747
let mut auth_value =
4848
HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?;
@@ -67,7 +67,7 @@ impl SignerClient {
6767

6868
fn refresh_jwt(&mut self) -> Result<(), SignerClientError> {
6969
if self.last_jwt_refresh.elapsed() > Duration::from_secs(SIGNER_JWT_EXPIRATION) {
70-
let jwt = create_jwt(&self.module_id, &self.jwt_secret)?;
70+
let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?;
7171

7272
let mut auth_value =
7373
HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?;
@@ -85,6 +85,16 @@ impl SignerClient {
8585
Ok(())
8686
}
8787

88+
fn create_jwt_for_payload<T: Serialize>(
89+
&mut self,
90+
payload: &T,
91+
) -> Result<Jwt, SignerClientError> {
92+
let payload_vec = serde_json::to_vec(payload)?;
93+
create_jwt(&self.module_id, &self.jwt_secret, Some(&payload_vec))
94+
.wrap_err("failed to create JWT for payload")
95+
.map_err(SignerClientError::JWTError)
96+
}
97+
8898
/// Request a list of validator pubkeys for which signatures can be
8999
/// requested.
90100
// TODO: add more docs on how proxy keys work
@@ -114,10 +124,10 @@ impl SignerClient {
114124
Q: Serialize,
115125
T: for<'de> Deserialize<'de>,
116126
{
117-
self.refresh_jwt()?;
127+
let jwt = self.create_jwt_for_payload(request)?;
118128

119129
let url = self.url.join(route)?;
120-
let res = self.client.post(url).json(&request).send().await?;
130+
let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?;
121131

122132
let status = res.status();
123133
let response_bytes = res.bytes().await?;
@@ -162,10 +172,10 @@ impl SignerClient {
162172
where
163173
T: ProxyId + for<'de> Deserialize<'de>,
164174
{
165-
self.refresh_jwt()?;
175+
let jwt = self.create_jwt_for_payload(request)?;
166176

167177
let url = self.url.join(GENERATE_PROXY_KEY_PATH)?;
168-
let res = self.client.post(url).json(&request).send().await?;
178+
let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?;
169179

170180
let status = res.status();
171181
let response_bytes = res.bytes().await?;

crates/common/src/types.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ pub struct Jwt(pub String);
2525
#[derive(Debug, Serialize, Deserialize)]
2626
pub struct JwtClaims {
2727
pub exp: u64,
28-
pub module: String,
28+
pub module: ModuleId,
29+
pub payload_hash: Option<B256>,
2930
}
3031

3132
#[derive(Debug, Serialize, Deserialize)]
32-
pub struct JwtAdmin {
33+
pub struct JwtAdminClaims {
3334
pub exp: u64,
3435
pub admin: bool,
36+
pub payload_hash: Option<B256>,
3537
}
3638

3739
#[derive(Clone, Copy, PartialEq, Eq)]

0 commit comments

Comments
 (0)