1use std::time::{SystemTime, UNIX_EPOCH};
4
5use base64::Engine;
6use base64::engine::general_purpose::STANDARD as BASE64;
7use hmac::{Hmac, Mac};
8use sha2::Sha256;
9
10use crate::spec::webhooks::WebhookEvent;
11
12#[derive(Debug, thiserror::Error)]
14pub enum WebhookError {
15 #[error("invalid webhook signature")]
17 InvalidSignature,
18 #[error("invalid input ")]
20 Invalid(String),
21 #[error("failed to deserialize webhook payload: error:{0} content:{1}")]
23 Deserialization(serde_json::Error, String),
24}
25
26type HmacSha256 = Hmac<Sha256>;
27
28const DEFAULT_TOLERANCE_SECONDS: i64 = 300;
29
30pub struct Webhooks;
31
32impl Webhooks {
33 pub fn build_event(
34 body: &str,
35 signature: &str,
36 timestamp: &str,
37 webhook_id: &str,
38 secret: &str,
39 ) -> Result<WebhookEvent, WebhookError> {
40 Self::build_event_with_tolerance(
41 body,
42 signature,
43 timestamp,
44 webhook_id,
45 secret,
46 DEFAULT_TOLERANCE_SECONDS,
47 )
48 }
49
50 fn build_event_with_tolerance(
51 body: &str,
52 signature: &str,
53 timestamp: &str,
54 webhook_id: &str,
55 secret: &str,
56 tolerance_seconds: i64,
57 ) -> Result<WebhookEvent, WebhookError> {
58 Self::verify_signature_with_tolerance(
60 body,
61 signature,
62 timestamp,
63 webhook_id,
64 secret,
65 tolerance_seconds,
66 )?;
67
68 let event: WebhookEvent = serde_json::from_str(body)
70 .map_err(|e| WebhookError::Deserialization(e, body.to_string()))?;
71
72 Ok(event)
73 }
74
75 pub fn verify_signature(
76 body: &str,
77 signature: &str,
78 timestamp: &str,
79 webhook_id: &str,
80 secret: &str,
81 ) -> Result<(), WebhookError> {
82 Self::verify_signature_with_tolerance(
83 body,
84 signature,
85 timestamp,
86 webhook_id,
87 secret,
88 DEFAULT_TOLERANCE_SECONDS,
89 )
90 }
91
92 fn verify_signature_with_tolerance(
93 body: &str,
94 signature: &str,
95 timestamp: &str,
96 webhook_id: &str,
97 secret: &str,
98 tolerance_seconds: i64,
99 ) -> Result<(), WebhookError> {
100 let timestamp_seconds = timestamp
102 .parse::<i64>()
103 .map_err(|_| WebhookError::Invalid("invalid timestamp format".to_string()))?;
104
105 let now = SystemTime::now()
106 .duration_since(UNIX_EPOCH)
107 .unwrap()
108 .as_secs() as i64;
109
110 if now - timestamp_seconds > tolerance_seconds {
111 return Err(WebhookError::Invalid(
112 "webhook timestamp is too old".to_string(),
113 ));
114 }
115
116 if timestamp_seconds > now + tolerance_seconds {
117 return Err(WebhookError::Invalid(
118 "webhook timestamp is too new".to_string(),
119 ));
120 }
121
122 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
124
125 let secret_key = secret.strip_prefix("whsec_").unwrap_or(secret);
127
128 let secret_bytes = BASE64.decode(secret_key).map_err(|_| {
130 WebhookError::Invalid("failed to decode secret from base64".to_string())
131 })?;
132
133 let mut mac = HmacSha256::new_from_slice(&secret_bytes)
135 .map_err(|_| WebhookError::Invalid("invalid secret key length".to_string()))?;
136 mac.update(signed_payload.as_bytes());
137
138 let expected_signature = BASE64.encode(mac.finalize().into_bytes());
140
141 let signature_to_verify = if signature.contains(',') {
144 signature
146 .split_whitespace()
147 .filter_map(|sig| {
148 let parts: Vec<&str> = sig.split(',').collect();
149 if parts.len() == 2 && parts[0] == "v1" {
150 Some(parts[1])
151 } else {
152 None
153 }
154 })
155 .collect::<Vec<&str>>()
156 } else {
157 vec![signature]
158 };
159
160 for sig in signature_to_verify {
162 if constant_time_eq(sig.as_bytes(), expected_signature.as_bytes()) {
163 return Ok(());
164 }
165 }
166
167 Err(WebhookError::InvalidSignature)
168 }
169}
170
171fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
172 if a.len() != b.len() {
173 return false;
174 }
175
176 let mut result = 0u8;
177 for (a_byte, b_byte) in a.iter().zip(b.iter()) {
178 result |= a_byte ^ b_byte;
179 }
180
181 result == 0
182}
183
184#[cfg(all(test, feature = "webhook"))]
185mod tests {
186 use super::*;
187
188 fn current_timestamp() -> String {
189 SystemTime::now()
190 .duration_since(UNIX_EPOCH)
191 .unwrap()
192 .as_secs()
193 .to_string()
194 }
195
196 #[test]
197 fn test_constant_time_eq() {
198 assert!(constant_time_eq(b"hello", b"hello"));
199 assert!(!constant_time_eq(b"hello", b"world"));
200 assert!(!constant_time_eq(b"hello", b"hell"));
201 assert!(!constant_time_eq(b"hello", b"helloo"));
202 }
203
204 #[test]
205 fn test_verify_signature_invalid() {
206 let body = r#"{"test":"data"}"#;
207 let signature = "invalid_signature";
208 let timestamp = current_timestamp();
209 let webhook_id = "webhook_test";
210 let secret = BASE64.encode(b"test_secret");
211
212 let result = Webhooks::verify_signature(body, &signature, ×tamp, webhook_id, &secret);
213 assert!(result.is_err());
214 }
216
217 #[test]
218 fn test_verify_signature_valid() {
219 let body = r#"{"test":"data"}"#;
220 let timestamp = current_timestamp();
221 let webhook_id = "webhook_test";
222 let secret = BASE64.encode(b"test_secret");
224
225 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
227 let secret_bytes = BASE64.decode(&secret).unwrap();
228 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
229 mac.update(signed_payload.as_bytes());
230 let signature = BASE64.encode(mac.finalize().into_bytes());
231
232 let result = Webhooks::verify_signature(body, &signature, ×tamp, webhook_id, &secret);
233 assert!(result.is_ok());
234 }
235
236 #[test]
237 fn test_verify_signature_with_prefix() {
238 let body = r#"{"test":"data"}"#;
239 let timestamp = current_timestamp();
240 let webhook_id = "webhook_test";
241 let secret = BASE64.encode(b"test_secret");
242 let prefixed_secret = format!("whsec_{}", secret);
243
244 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
246 let secret_bytes = BASE64.decode(&secret).unwrap();
247 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
248 mac.update(signed_payload.as_bytes());
249 let signature = BASE64.encode(mac.finalize().into_bytes());
250
251 let result =
253 Webhooks::verify_signature(body, &signature, ×tamp, webhook_id, &prefixed_secret);
254 assert!(result.is_ok());
255 }
256
257 #[test]
258 fn test_verify_signature_with_version() {
259 let body = r#"{"test":"data"}"#;
260 let timestamp = current_timestamp();
261 let webhook_id = "webhook_test";
262 let secret = BASE64.encode(b"test_secret");
263
264 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
266 let secret_bytes = BASE64.decode(&secret).unwrap();
267 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
268 mac.update(signed_payload.as_bytes());
269 let sig_b64 = BASE64.encode(mac.finalize().into_bytes());
270
271 let signature = format!("v1,{}", sig_b64);
273
274 let result = Webhooks::verify_signature(body, &signature, ×tamp, webhook_id, &secret);
275 assert!(result.is_ok());
276 }
277
278 #[test]
279 fn test_timestamp_too_old() {
280 let body = r#"{"test":"data"}"#;
281 let old_timestamp = "1234567890"; let webhook_id = "webhook_test";
283 let secret = BASE64.encode(b"test_secret");
284
285 let signed_payload = format!("{}.{}.{}", webhook_id, old_timestamp, body);
287 let secret_bytes = BASE64.decode(&secret).unwrap();
288 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
289 mac.update(signed_payload.as_bytes());
290 let signature = BASE64.encode(mac.finalize().into_bytes());
291
292 let result =
293 Webhooks::verify_signature(body, &signature, old_timestamp, webhook_id, &secret);
294 assert!(result.is_err());
295 match result.unwrap_err() {
296 WebhookError::Invalid(msg) => {
297 assert!(msg.contains("too old"));
298 }
299 _ => panic!("Expected InvalidSignature error"),
300 }
301 }
302
303 #[test]
304 fn test_timestamp_too_new() {
305 let body = r#"{"test":"data"}"#;
306 let future_timestamp = (SystemTime::now()
308 .duration_since(UNIX_EPOCH)
309 .unwrap()
310 .as_secs()
311 + 1000)
312 .to_string();
313 let webhook_id = "webhook_test";
314 let secret = BASE64.encode(b"test_secret");
315
316 let signed_payload = format!("{}.{}.{}", webhook_id, future_timestamp, body);
318 let secret_bytes = BASE64.decode(&secret).unwrap();
319 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
320 mac.update(signed_payload.as_bytes());
321 let signature = BASE64.encode(mac.finalize().into_bytes());
322
323 let result =
324 Webhooks::verify_signature(body, &signature, &future_timestamp, webhook_id, &secret);
325 assert!(result.is_err());
326 match result.unwrap_err() {
327 WebhookError::Invalid(msg) => {
328 assert!(msg.contains("too new"));
329 }
330 _ => panic!("Expected InvalidSignature error"),
331 }
332 }
333
334 #[test]
335 fn test_invalid_timestamp_format() {
336 let body = r#"{"test":"data"}"#;
337 let invalid_timestamp = "not_a_number";
338 let webhook_id = "webhook_test";
339 let secret = BASE64.encode(b"test_secret");
340
341 let result = Webhooks::verify_signature(
342 body,
343 "any_signature",
344 invalid_timestamp,
345 webhook_id,
346 &secret,
347 );
348 assert!(result.is_err());
349 match result.unwrap_err() {
350 WebhookError::Invalid(msg) => {
351 assert!(msg.contains("timestamp"));
352 }
353 _ => panic!("Expected InvalidSignature error"),
354 }
355 }
356
357 #[test]
358 fn test_construct_event_invalid_json() {
359 let body = r#"{"invalid json"#;
360 let timestamp = current_timestamp();
361 let webhook_id = "webhook_test";
362 let secret = BASE64.encode(b"test_secret");
363
364 let signed_payload = format!("{}.{}.{}", webhook_id, timestamp, body);
366 let secret_bytes = BASE64.decode(&secret).unwrap();
367 let mut mac = HmacSha256::new_from_slice(&secret_bytes).unwrap();
368 mac.update(signed_payload.as_bytes());
369 let signature = BASE64.encode(mac.finalize().into_bytes());
370
371 let result = Webhooks::build_event(body, &signature, ×tamp, webhook_id, &secret);
372 assert!(result.is_err());
373 assert!(matches!(
374 result.unwrap_err(),
375 WebhookError::Deserialization(..)
376 ));
377 }
378}