fraiseql_webhooks/signature/mod.rs
1//! Webhook signature verification.
2//!
3//! Supports 15+ webhook providers with constant-time comparison for security.
4
5pub mod generic;
6pub mod github;
7pub mod registry;
8pub mod shopify;
9pub mod stripe;
10
11// Additional providers
12pub mod discord;
13pub mod gitlab;
14pub mod lemonsqueezy;
15pub mod paddle;
16pub mod postmark;
17pub mod sendgrid;
18pub mod slack;
19pub mod twilio;
20
21pub use registry::ProviderRegistry;
22
23/// Errors produced by low-level signature verification routines.
24#[derive(Debug, thiserror::Error)]
25#[non_exhaustive]
26pub enum SignatureError {
27 /// The signature header value could not be parsed according to the provider's expected format.
28 /// For example, a GitHub signature missing the `sha256=` prefix triggers this variant.
29 #[error("Invalid signature format")]
30 InvalidFormat,
31
32 /// The computed signature did not match the value supplied in the request header.
33 #[error("Signature mismatch")]
34 Mismatch,
35
36 /// The timestamp embedded in the request is older than the configured tolerance window,
37 /// indicating a potential replay attack.
38 #[error("Timestamp expired")]
39 TimestampExpired,
40
41 /// A timestamp is required for this provider's signing scheme but was not found in the request.
42 #[error("Missing timestamp")]
43 MissingTimestamp,
44
45 /// A cryptographic operation failed (e.g., an invalid key was supplied or key parsing failed).
46 /// The inner string contains the underlying error message.
47 #[error("Crypto error: {0}")]
48 Crypto(String),
49}
50
51/// Constant-time comparison to prevent timing attacks.
52///
53/// Uses the `subtle` crate for verified constant-time operations,
54/// including length-independent comparison.
55#[must_use]
56pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
57 use subtle::ConstantTimeEq;
58 a.ct_eq(b).into()
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn test_constant_time_eq_equal() {
67 assert!(constant_time_eq(b"test", b"test"));
68 assert!(constant_time_eq(b"", b""));
69 }
70
71 #[test]
72 fn test_constant_time_eq_not_equal() {
73 assert!(!constant_time_eq(b"test", b"fail"));
74 assert!(!constant_time_eq(b"test", b"tes"));
75 assert!(!constant_time_eq(b"test", b""));
76 }
77}