use hmac::{Hmac, Mac};
use sha2::Sha256;
pub fn verify_webhook_signature(secret: &str, body: &[u8], signature_header: &str) -> bool {
let Some(hex_sig) = signature_header.strip_prefix("sha256=") else {
return false;
};
let Ok(expected) = hex::decode(hex_sig) else {
return false;
};
let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(secret.as_bytes()) else {
return false;
};
mac.update(body);
mac.verify_slice(&expected).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_signature(secret: &str, body: &[u8]) -> String {
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
mac.update(body);
format!("sha256={}", hex::encode(mac.finalize().into_bytes()))
}
#[test]
fn webhook_signature_accepts_valid() {
let secret = "test-hmac-key"; let body = br#"{"action":"review_requested"}"#;
let header = make_signature(secret, body);
assert!(verify_webhook_signature(secret, body, &header));
}
#[test]
fn webhook_signature_rejects_wrong_secret() {
let body = br#"{"action":"review_requested"}"#;
let header = make_signature("correct-secret", body); assert!(!verify_webhook_signature("wrong-secret", body, &header));
}
#[test]
fn webhook_signature_rejects_missing_prefix() {
let secret = "s"; let body = b"payload";
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
mac.update(body);
let raw_hex = hex::encode(mac.finalize().into_bytes());
assert!(!verify_webhook_signature(secret, body, &raw_hex));
}
#[test]
fn webhook_signature_rejects_bad_hex() {
let secret = "s"; let body = b"payload";
assert!(!verify_webhook_signature(
secret,
body,
"sha256=not-valid-hex!!"
));
}
#[test]
fn webhook_signature_rejects_empty_header() {
assert!(!verify_webhook_signature("secret", b"body", ""));
}
#[test]
fn webhook_signature_rejects_tampered_body() {
let secret = "my-secret"; let original = br#"{"action":"review_requested"}"#;
let tampered = br#"{"action":"force_push"}"#;
let header = make_signature(secret, original);
assert!(!verify_webhook_signature(secret, tampered, &header));
}
}