use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::{SystemTime, UNIX_EPOCH};
type HmacSha256 = Hmac<Sha256>;
use super::types::MIN_WEBHOOK_SECRET_LEN;
pub fn verify_signature(payload: &str, signature: &str, secret: &str) -> bool {
if secret.is_empty() {
tracing::warn!("Webhook signature verification rejected: secret is empty");
return false;
}
if secret.len() < MIN_WEBHOOK_SECRET_LEN {
tracing::warn!(
secret_len = secret.len(),
min_len = MIN_WEBHOOK_SECRET_LEN,
"Webhook signature verification rejected: secret too short"
);
return false;
}
let Ok(mut mac) = HmacSha256::new_from_slice(secret.as_bytes()) else {
return false;
};
mac.update(payload.as_bytes());
let expected = hex::encode(mac.finalize().into_bytes());
constant_time_eq(signature.as_bytes(), expected.as_bytes())
}
#[allow(dead_code)]
pub fn verify_signature_with_timestamp(
payload: &str,
signature: &str,
secret: &str,
timestamp: &str,
max_age_secs: u64,
) -> bool {
let Ok(ts) = timestamp.parse::<u64>() else {
return false;
};
let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) else {
return false;
};
if now.as_secs().saturating_sub(ts) > max_age_secs {
return false;
}
let signed_payload = format!("{}.{}", timestamp, payload);
verify_signature(&signed_payload, signature, secret)
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::callback::webhook::{WebhookCallback, WebhookConfig};
#[test]
fn test_sign_and_verify() {
let secret = "test-webhook-secret-that-is-long-enough-for-security";
let payload = r#"{"event":"user_authenticated","timestamp":1234567890}"#;
let timestamp = "1234567890";
let signed_payload = format!("{}.{}", timestamp, payload);
let config = WebhookConfig {
url: "http://localhost:8080/webhook".to_string(),
secret: secret.to_string(),
timeout_secs: 10,
retry_attempts: 2,
};
let callback = WebhookCallback::try_new(config).unwrap();
let signature = callback.sign_payload(&signed_payload);
assert!(verify_signature(&signed_payload, &signature, secret));
assert!(!verify_signature(
&signed_payload,
"invalid-signature",
secret
));
assert!(!verify_signature("different-payload", &signature, secret));
}
#[test]
fn test_constant_time_eq() {
assert!(constant_time_eq(b"hello", b"hello"));
assert!(!constant_time_eq(b"hello", b"world"));
assert!(!constant_time_eq(b"hello", b"hello!"));
}
#[test]
fn test_constant_time_eq_empty() {
assert!(constant_time_eq(b"", b""));
assert!(!constant_time_eq(b"", b"a"));
assert!(!constant_time_eq(b"a", b""));
}
#[test]
fn test_verify_signature_rejects_empty_secret() {
let payload = r#"{"event":"test"}"#;
let secret = "";
let timestamp = "123";
let signed_payload = format!("{}.{}", timestamp, payload);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(signed_payload.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
assert!(!verify_signature(&signed_payload, &signature, secret));
}
#[test]
fn test_verify_signature_rejects_short_secret() {
let payload = r#"{"event":"test"}"#;
let secret = "too-short"; let timestamp = "123";
let signed_payload = format!("{}.{}", timestamp, payload);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(signed_payload.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
assert!(!verify_signature(&signed_payload, &signature, secret));
}
#[test]
fn test_verify_signature_different_secrets() {
let payload = r#"{"event":"test"}"#;
let secret1 = "secret1-that-is-long-enough-for-security-testing";
let secret2 = "secret2-that-is-also-long-enough-for-testing";
let timestamp = "456";
let signed_payload = format!("{}.{}", timestamp, payload);
let mut mac = HmacSha256::new_from_slice(secret1.as_bytes()).unwrap();
mac.update(signed_payload.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
assert!(verify_signature(&signed_payload, &signature, secret1));
assert!(!verify_signature(&signed_payload, &signature, secret2));
}
#[test]
fn test_verify_signature_with_timestamp_valid() {
let payload = r#"{"event":"test"}"#;
let secret = "secret-that-is-long-enough-for-security-testing";
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let signed_payload = format!("{}.{}", now, payload);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(signed_payload.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
assert!(verify_signature_with_timestamp(
payload,
&signature,
secret,
&now.to_string(),
60
));
}
#[test]
fn test_verify_signature_with_timestamp_stale() {
let payload = r#"{"event":"test"}"#;
let secret = "secret-that-is-long-enough-for-security-testing";
let stale = 1u64;
let signed_payload = format!("{}.{}", stale, payload);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(signed_payload.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
assert!(!verify_signature_with_timestamp(
payload,
&signature,
secret,
&stale.to_string(),
60
));
}
}