1use hmac::{Hmac, Mac};
7use sha2::Sha256;
8
9type HmacSha256 = Hmac<Sha256>;
10
11pub fn verify_webhook_signature(payload_body: &[u8], signature_header: &str, secret: &str) -> bool {
23 if signature_header.is_empty() {
24 return false;
25 }
26
27 let hex_signature = match signature_header.strip_prefix("sha256=") {
28 Some(hex) => hex,
29 None => return false,
30 };
31
32 let signature_bytes = match hex::decode(hex_signature) {
33 Ok(bytes) => bytes,
34 Err(_) => return false,
35 };
36
37 let mut mac =
38 HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
39 mac.update(payload_body);
40
41 mac.verify_slice(&signature_bytes).is_ok()
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use hmac::Mac;
49
50 fn compute_signature(payload: &[u8], secret: &str) -> String {
52 let mut mac =
53 HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
54 mac.update(payload);
55 let result = mac.finalize();
56 format!("sha256={}", hex::encode(result.into_bytes()))
57 }
58
59 #[test]
60 fn valid_signature_returns_true() {
61 let payload = b"hello world";
62 let secret = "my-secret";
63 let sig = compute_signature(payload, secret);
64 assert!(verify_webhook_signature(payload, &sig, secret));
65 }
66
67 #[test]
68 fn invalid_signature_returns_false() {
69 let payload = b"hello world";
70 let secret = "my-secret";
71 let bad_sig = "sha256=0000000000000000000000000000000000000000000000000000000000000000";
72 assert!(!verify_webhook_signature(payload, bad_sig, secret));
73 }
74
75 #[test]
76 fn empty_signature_header_returns_false() {
77 assert!(!verify_webhook_signature(b"payload", "", "secret"));
78 }
79
80 #[test]
81 fn missing_sha256_prefix_returns_false() {
82 let payload = b"hello world";
83 let secret = "my-secret";
84 let sig = compute_signature(payload, secret);
85 let hex_only = sig.strip_prefix("sha256=").unwrap();
87 assert!(!verify_webhook_signature(payload, hex_only, secret));
88 }
89
90 #[test]
91 fn invalid_hex_in_signature_returns_false() {
92 assert!(!verify_webhook_signature(
93 b"payload",
94 "sha256=not-valid-hex!!!",
95 "secret"
96 ));
97 }
98
99 #[test]
100 fn different_payload_produces_different_signature() {
101 let secret = "shared-secret";
102 let sig_a = compute_signature(b"payload-a", secret);
103 let sig_b = compute_signature(b"payload-b", secret);
104 assert_ne!(sig_a, sig_b);
105 assert!(!verify_webhook_signature(b"payload-a", &sig_b, secret));
106 }
107
108 #[test]
109 fn different_secret_produces_different_signature() {
110 let payload = b"same payload";
111 let sig_a = compute_signature(payload, "secret-a");
112 let sig_b = compute_signature(payload, "secret-b");
113 assert_ne!(sig_a, sig_b);
114 assert!(!verify_webhook_signature(payload, &sig_a, "secret-b"));
115 }
116}