Memvalidasi tanda tangan kriptografis (HMAC signature) adalah standar industri untuk memverifikasi keaslian webhook dari penyedia layanan seperti Stripe, GitHub, atau Shopify. Pada Actix Web, implementasi verifikasi webhook sering kali memicu kendala teknis: body request tidak dapat dibaca lagi di dalam handler, atau signature selalu tidak cocok meskipun secret key sudah benar.
Akar Masalah: Single-Consumption Body Stream
Penyebab utama kegagalan verifikasi webhook di Actix Web berakar pada arsitektur I/O asinkron:
- Stream bersifat single-consumption:
HttpRequestpada Actix Web membaca body sebagai asynchronous byte stream. Sekali payload stream dikonsumsi (misalnya oleh middleware logging atau extractor standar), buffer tersebut kosong dan tidak dapat dibaca ulang oleh extractor berikutnya. - Ketidaksesuaian representasi data: Jika Anda menggunakan extractor
web::Json<T>, Actix Web langsung mendeserialisasi stream menjadi Rust struct. Jika struct tersebut di-serialize kembali ke format string JSON untuk verifikasi HMAC, susunan key, whitespace, atau encoding kemungkinan besar berbeda dari raw bytes asli yang dikirim client. Perbedaan satu byte saja akan menghasilkan hash digest yang sama sekali berbeda.
Solusi yang benar: konsumsi payload sebagai raw bytes (web::Bytes), jalankan verifikasi HMAC secara langsung pada buffer bytes tersebut, lalu lakukan deserialisasi JSON secara manual menggunakan serde_json::from_slice.
Dependensi yang Diperlukan
Tambahkan crate berikut ke dalam Cargo.toml Anda:
[dependencies]
actix-web = "4.9"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
hmac = "0.12"
sha2 = "0.10"
hex = "0.4"
subtle = "2.6"
Implementasi Verifikasi HMAC dan Deserialisasi
Kode berikut mendemonstrasikan verifikasi HMAC-SHA256 menggunakan web::Bytes. Perbandingan hash wajib menggunakan subtle::ConstantTimeEq untuk mencegah kerentanan timing attack.
use actix_web::{post, web, HttpRequest, HttpResponse, Responder};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use serde::Deserialize;
type HmacSha256 = Hmac<Sha256>;
#[derive(Deserialize, Debug)]
pub struct WebhookEvent {
pub id: String,
pub event_type: String,
}
fn verify_signature(secret: &[u8], raw_body: &[u8], signature_hex: &str) -> bool {
let Ok(expected_sig) = hex::decode(signature_hex) else {
return false;
};
let Ok(mut mac) = HmacSha256::new_from_slice(secret) else {
return false;
};
mac.update(raw_body);
let actual_sig = mac.finalize().into_bytes();
actual_sig.as_slice().ct_eq(&expected_sig).into()
}
#[post("/webhook")]
pub async fn handle_webhook(
req: HttpRequest,
body: web::Bytes,
) -> impl Responder {
let secret = b"whsec_test_secret_key_123";
let signature = match req.headers().get("X-Hub-Signature-256") {
Some(val) => match val.to_str() {
Ok(s) => s.trim_start_matches("sha256="),
Err(_) => return HttpResponse::BadRequest().body("Header signature bukan UTF-8 valid"),
},
None => return HttpResponse::Unauthorized().body("Header signature tidak ditemukan"),
};
if !verify_signature(secret, &body, signature) {
return HttpResponse::Unauthorized().body("Signature webhook tidak valid");
}
let event: WebhookEvent = match serde_json::from_slice(&body) {
Ok(parsed) => parsed,
Err(err) => {
return HttpResponse::UnprocessableEntity()
.body(format!("Gagal parsing payload JSON: {err}"));
}
};
// ponytail: proses event langsung di handler. Pindahkan ke job queue jika task memakan waktu.
HttpResponse::Ok().json(serde_json::json!({
"status": "success",
"processed_id": event.id
}))
}
Mitigasi DoS: Batasi Ukuran Payload
Membaca seluruh raw body ke dalam memori via web::Bytes membuka risiko Memory Exhaustion (Denial of Service) jika penyerang mengirim request berukuran gigabyte. Pasang batasan ukuran payload pada konfigurasi Actix Web menggunakan PayloadConfig.
use actix_web::{web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Batasi payload maksimal 256 KB
let max_payload_size = 256 * 1024;
HttpServer::new(move || {
App::new()
.app_data(web::PayloadConfig::new(max_payload_size))
.service(handle_webhook)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Catatan: Jika ukuran request melewati batas konfigurasi, Actix Web akan otomatis menghentikan pembacaan stream dan mengembalikan response 413 Payload Too Large tanpa membebani heap memory server.Checklist Implementasi Production
- Jangan gunakan middleware logging body mentah: Middleware yang mengonsumsi stream tanpa memulihkannya akan membuat handler menerima stream kosong.
- Validasi format signature header: Sebagian provider menyertakan prefix seperti
sha256=atau format timestamp (contoh: headerStripe-Signatureformatt=...,v1=...). Pisahkan prefix tersebut sebelum hex decoding. - Selalu gunakan perbandingan constant-time: Hindari operator perbandingan standar (
==) pada cryptographic digest untuk memitigasi side-channel timing attacks.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!