Skip to content

Commit ce9a41d

Browse files
committed
fix verifier: add body hash to auth payload
1 parent e5dc2dc commit ce9a41d

File tree

4 files changed

+94
-13
lines changed

4 files changed

+94
-13
lines changed

via_verifier/node/via_verifier_coordinator/src/auth.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,35 @@ mod tests {
7777
assert!(!verify_signature(&payload, &signature, &wrong_public_key)
7878
.expect("Verification with wrong key unexpectedly succeeded"));
7979
}
80+
81+
#[test]
82+
fn test_body_hash_in_signature() {
83+
let secp = Secp256k1::new();
84+
let (secret_key, public_key) = secp.generate_keypair(&mut OsRng);
85+
86+
let body = b"test body content";
87+
let body_hash = hex::encode(Sha256::digest(body));
88+
89+
let payload = json!({
90+
"timestamp": 1234567890,
91+
"verifier_index": "0",
92+
"sequencer_version": "0.1.0",
93+
"body_hash": body_hash
94+
});
95+
96+
let signature = sign_request(&payload, &secret_key).unwrap();
97+
assert!(verify_signature(&payload, &signature, &public_key).unwrap());
98+
99+
// Verify different body produces different hash
100+
let different_body = b"different body";
101+
let different_hash = hex::encode(Sha256::digest(different_body));
102+
let different_payload = json!({
103+
"timestamp": 1234567890,
104+
"verifier_index": "0",
105+
"sequencer_version": "0.1.0",
106+
"body_hash": different_hash
107+
});
108+
109+
assert!(!verify_signature(&different_payload, &signature, &public_key).unwrap());
110+
}
80111
}

via_verifier/node/via_verifier_coordinator/src/coordinator/api_decl.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ impl RestApi {
9292
)
9393
.route("/nonce", axum::routing::post(Self::submit_nonce))
9494
.route("/nonce", axum::routing::get(Self::get_nonces))
95-
.route_layer(body_mw)
95+
// body_mw (outer) extracts body bytes first
96+
// auth_mw (inner) verifies signature with body hash second
9697
.route_layer(auth_mw)
98+
.route_layer(body_mw)
9799
.with_state(shared_state.clone())
98100
.layer(
99101
ServiceBuilder::new()

via_verifier/node/via_verifier_coordinator/src/coordinator/auth_middleware.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use std::{str::FromStr, sync::Arc};
22

33
use axum::{
4-
body::{self, Body},
4+
body::{self, Body, Bytes},
55
extract::{Request, State},
66
middleware::Next,
77
response::Response,
88
};
9+
use sha2::{Digest, Sha256};
910
use via_verifier_dal::VerifierDal;
1011
use zksync_types::protocol_version::ProtocolSemanticVersion;
1112

@@ -35,6 +36,11 @@ pub async fn auth_middleware(
3536
.and_then(|h| h.to_str().ok())
3637
.ok_or_else(|| ApiError::Unauthorized("Missing signature header".into()))?;
3738

39+
let body_hash = headers
40+
.get("X-Body-Hash")
41+
.and_then(|h| h.to_str().ok())
42+
.ok_or_else(|| ApiError::Unauthorized("Missing body hash header".into()))?;
43+
3844
let sequencer_version = headers
3945
.get("X-Sequencer-Version")
4046
.and_then(|h| h.to_str().ok())
@@ -46,7 +52,8 @@ pub async fn auth_middleware(
4652
}
4753

4854
let timestamp_now = chrono::Utc::now().timestamp();
49-
let timestamp_diff = timestamp_now - timestamp.parse::<i64>().unwrap();
55+
let timestamp_diff = timestamp_now - timestamp.parse::<i64>()
56+
.map_err(|_| ApiError::Unauthorized("Invalid timestamp".into()))?;
5057

5158
if timestamp_diff > state.state.verifier_request_timeout.into() {
5259
return Err(ApiError::Unauthorized("Timestamp is too old".into()));
@@ -55,11 +62,25 @@ pub async fn auth_middleware(
5562
// Get the public key for this verifier
5663
let public_key = &state.state.verifiers_pub_keys[verifier_index];
5764

58-
// verify timestamp + verifier_index
65+
// Get the request body bytes (stored by extract_body middleware)
66+
let body_bytes = request
67+
.extensions()
68+
.get::<Bytes>()
69+
.map(|b| b.to_vec())
70+
.unwrap_or_default();
71+
72+
// Verify body hash matches actual body content
73+
let computed_body_hash = hex::encode(Sha256::digest(&body_bytes));
74+
if computed_body_hash != body_hash {
75+
return Err(ApiError::Unauthorized("Body hash mismatch ".into()));
76+
}
77+
78+
// Verify the signature over timestamp + verifier_index + sequencer_version + body_hash
5979
let payload = serde_json::json!({
6080
"timestamp": timestamp,
6181
"verifier_index": verifier_index.to_string(),
62-
"sequencer_version": sequencer_version
82+
"sequencer_version": sequencer_version,
83+
"body_hash": body_hash
6384
});
6485

6586
// Verify the signature

via_verifier/node/via_verifier_coordinator/src/verifier/mod.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use anyhow::Context;
77
use bitcoin::{TapSighashType, Witness};
88
use musig2::{CompactSignature, PartialSignature};
99
use reqwest::{header, Client, StatusCode};
10+
use sha2::{Digest, Sha256};
1011
use tokio::sync::watch;
1112
use via_btc_client::traits::{BitcoinOps, Serializable};
1213
use via_musig2::{
@@ -234,7 +235,9 @@ impl ViaWithdrawalVerifier {
234235
Ok(())
235236
}
236237

237-
fn create_request_headers(&self) -> anyhow::Result<header::HeaderMap> {
238+
/// Creates authenticated request headers with body hash for replay protection
239+
/// The body hash is included in the signed payload
240+
fn create_request_headers_with_body(&self, body: &[u8]) -> anyhow::Result<header::HeaderMap> {
238241
let mut headers = header::HeaderMap::new();
239242
let timestamp = chrono::Utc::now().timestamp().to_string();
240243
let signer = get_signer_with_merkle_root(
@@ -248,11 +251,15 @@ impl ViaWithdrawalVerifier {
248251
let private_key = bitcoin::PrivateKey::from_wif(&self.wallet.private_key)?;
249252
let secret_key = private_key.inner;
250253

251-
// Sign timestamp + verifier_index + sequencer_version as a JSON object
254+
// Compute SHA-256 hash of the request body for replay protection
255+
let body_hash = hex::encode(Sha256::digest(body));
256+
257+
// Sign timestamp + verifier_index + sequencer_version + body_hash
252258
let payload = serde_json::json!({
253259
"timestamp": timestamp,
254260
"verifier_index": verifier_index,
255-
"sequencer_version": sequencer_version
261+
"sequencer_version": sequencer_version,
262+
"body_hash": body_hash
256263
});
257264
let signature = crate::auth::sign_request(&payload, &secret_key)?;
258265

@@ -266,10 +273,20 @@ impl ViaWithdrawalVerifier {
266273
"X-Sequencer-Version",
267274
header::HeaderValue::from_str(&sequencer_version)?,
268275
);
276+
headers.insert(
277+
"X-Body-Hash",
278+
header::HeaderValue::from_str(&body_hash)?,
279+
);
269280

270281
Ok(headers)
271282
}
272283

284+
/// Creates authenticated request headers GET requests (empty body)
285+
/// Get requests that have no body to use as an empty body hash
286+
fn create_request_headers(&self) -> anyhow::Result<header::HeaderMap> {
287+
self.create_request_headers_with_body(&[])
288+
}
289+
273290
async fn get_session(&self) -> anyhow::Result<SigningSessionResponse> {
274291
let url = format!("{}/session", self.verifier_config.coordinator_http_url);
275292
let headers = self.create_request_headers()?;
@@ -336,17 +353,20 @@ impl ViaWithdrawalVerifier {
336353
nonce_map.insert(*input_index, nonce_pair);
337354
}
338355

356+
let body = serde_json::to_vec(&nonce_map)?;
357+
339358
let url = format!(
340359
"{}/session/nonce",
341360
self.verifier_config.coordinator_http_url
342361
);
343-
let headers = self.create_request_headers()?;
362+
let headers = self.create_request_headers_with_body(&body)?;
344363

345364
let res = self
346365
.client
347366
.post(&url)
348367
.headers(headers.clone())
349-
.json(&nonce_map)
368+
.body(body)
369+
.header(header::CONTENT_TYPE, "application/json")
350370
.send()
351371
.await?;
352372

@@ -473,19 +493,23 @@ impl ViaWithdrawalVerifier {
473493
sig_pair_per_input.insert(input_index, encoded);
474494
}
475495

496+
// Serialize body first for body hash computation
497+
let body = serde_json::to_vec(&sig_pair_per_input)?;
498+
476499
let url = format!(
477500
"{}/session/signature",
478501
self.verifier_config.coordinator_http_url
479502
);
480-
let headers = self.create_request_headers()?;
503+
let headers = self.create_request_headers_with_body(&body)?;
481504

482505
tracing::debug!("Submitting all partial signatures to {}", url);
483506

484507
let response = self
485508
.client
486509
.post(&url)
487510
.headers(headers.clone())
488-
.json(&sig_pair_per_input)
511+
.body(body)
512+
.header(header::CONTENT_TYPE, "application/json")
489513
.send()
490514
.await?;
491515

@@ -535,13 +559,16 @@ impl ViaWithdrawalVerifier {
535559
}
536560

537561
async fn create_new_session(&mut self) -> anyhow::Result<()> {
562+
let body = Vec::new();
563+
538564
let url = format!("{}/session/new", self.verifier_config.coordinator_http_url);
539-
let headers = self.create_request_headers()?;
565+
let headers = self.create_request_headers_with_body(&body)?;
540566
let resp = self
541567
.client
542568
.post(&url)
543569
.headers(headers.clone())
544570
.header(header::CONTENT_TYPE, "application/json")
571+
.body(body)
545572
.send()
546573
.await?;
547574

0 commit comments

Comments
 (0)