use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
pub fn verify_hmac(secret: &[u8], body: &[u8], signature_hex: &str) -> bool {
let hex = signature_hex
.strip_prefix("sha256=")
.unwrap_or(signature_hex);
let Ok(sig_bytes) = hex::decode(hex) else {
return false;
};
let Ok(mut mac) = HmacSha256::new_from_slice(secret) else {
return false;
};
mac.update(body);
mac.verify_slice(&sig_bytes).is_ok()
}
mod hex {
pub fn decode(s: &str) -> Result<Vec<u8>, ()> {
if s.len() % 2 != 0 {
return Err(());
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| ()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_signature_accepted() {
use hmac::{Hmac, Mac};
use sha2::Sha256;
type H = Hmac<Sha256>;
let secret = b"test-secret";
let body = b"hello world";
let mut mac = H::new_from_slice(secret).unwrap();
mac.update(body);
let result = mac.finalize().into_bytes();
let hex_sig = result
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
let prefixed = format!("sha256={}", hex_sig);
assert!(verify_hmac(secret, body, &prefixed));
}
#[test]
fn invalid_signature_rejected() {
assert!(!verify_hmac(b"secret", b"body", "sha256=deadbeef"));
}
}