use sha2::{Digest, Sha256};
pub const SIGNATURE_HEADER: &str = "X-Webhook-Signature";
const BLOCK_SIZE: usize = 64;
pub fn verify_signature(payload: &[u8], signature: &str, secret: &str) -> bool {
if secret.is_empty() || signature.is_empty() {
return false;
}
constant_time_eq(
compute_signature(payload, secret).as_bytes(),
signature.trim().as_bytes(),
)
}
pub fn compute_signature(payload: &[u8], secret: &str) -> String {
hex(&hmac_sha256(secret.as_bytes(), payload))
}
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
let mut block = [0u8; BLOCK_SIZE];
if key.len() > BLOCK_SIZE {
block[..32].copy_from_slice(&Sha256::digest(key));
} else {
block[..key.len()].copy_from_slice(key);
}
let mut inner = Sha256::new();
inner.update(block.map(|byte| byte ^ 0x36));
inner.update(message);
let inner = inner.finalize();
let mut outer = Sha256::new();
outer.update(block.map(|byte| byte ^ 0x5c));
outer.update(inner);
outer.finalize().into()
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
let mut difference = a.len() ^ b.len();
for index in 0..a.len().max(b.len()) {
let left = a.get(index).copied().unwrap_or(0);
let right = b.get(index).copied().unwrap_or(0);
difference |= usize::from(left ^ right);
}
difference == 0
}
fn hex(bytes: &[u8]) -> String {
use std::fmt::Write;
bytes.iter().fold(String::new(), |mut out, byte| {
let _ = write!(out, "{byte:02x}");
out
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hmac_matches_the_rfc_4231_vector() {
assert_eq!(
compute_signature(b"what do ya want for nothing?", "Jefe"),
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
);
}
#[test]
fn a_long_key_is_hashed_before_use() {
let key = "\u{aa}".repeat(131).into_bytes();
let key: Vec<u8> = key.iter().filter(|byte| **byte == 0xaa).copied().collect();
assert_eq!(key.len(), 131);
let message = b"Test Using Larger Than Block-Size Key - Hash Key First";
assert_eq!(
hex(&hmac_sha256(&key, message)),
"60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
);
}
#[test]
fn verification_needs_a_secret_and_a_signature_and_the_same_bytes() {
let signature = compute_signature(b"payload", "secret");
assert!(verify_signature(b"payload", &signature, "secret"));
assert!(verify_signature(
b"payload",
&format!(" {signature}\n"),
"secret"
));
assert!(!verify_signature(b"payload", &signature, "other"));
assert!(!verify_signature(b"other", &signature, "secret"));
assert!(!verify_signature(b"payload", "", "secret"));
assert!(!verify_signature(b"payload", &signature, ""));
assert!(!verify_signature(b"payload", &signature[..10], "secret"));
}
#[test]
fn constant_time_comparison_is_exact() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"ab"));
assert!(!constant_time_eq(b"", b"a"));
assert!(constant_time_eq(b"", b""));
}
}