fraiseql_webhooks/signature/
github.rs1use hmac::{Hmac, Mac};
7use sha2::Sha256;
8
9use crate::{
10 signature::{SignatureError, constant_time_eq},
11 traits::SignatureVerifier,
12};
13
14pub struct GitHubVerifier;
19
20impl SignatureVerifier for GitHubVerifier {
21 fn name(&self) -> &'static str {
22 "github"
23 }
24
25 fn signature_header(&self) -> &'static str {
26 "X-Hub-Signature-256"
27 }
28
29 fn verify(
30 &self,
31 payload: &[u8],
32 signature: &str,
33 secret: &str,
34 _timestamp: Option<&str>,
35 _url: Option<&str>,
36 ) -> Result<bool, SignatureError> {
37 if secret.is_empty() {
38 return Err(SignatureError::Crypto(
39 "GitHub webhook secret must not be empty".to_string(),
40 ));
41 }
42 let sig_hex = signature.strip_prefix("sha256=").ok_or(SignatureError::InvalidFormat)?;
44
45 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
46 .map_err(|e| SignatureError::Crypto(e.to_string()))?;
47 mac.update(payload);
48
49 let expected = hex::encode(mac.finalize().into_bytes());
50
51 Ok(constant_time_eq(sig_hex.as_bytes(), expected.as_bytes()))
52 }
53}
54
55#[allow(clippy::unwrap_used)] #[cfg(test)]
57mod tests {
58 use super::*;
59
60 fn generate_signature(payload: &[u8], secret: &str) -> String {
61 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
62 mac.update(payload);
63 format!("sha256={}", hex::encode(mac.finalize().into_bytes()))
64 }
65
66 #[test]
67 fn test_valid_signature() {
68 let verifier = GitHubVerifier;
69 let payload = b"test payload";
70 let secret = "secret";
71 let signature = generate_signature(payload, secret);
72
73 assert!(verifier.verify(payload, &signature, secret, None, None).unwrap());
74 }
75
76 #[test]
77 fn test_invalid_signature() {
78 let verifier = GitHubVerifier;
79 let signature = "sha256=invalid";
80
81 assert!(!verifier.verify(b"test", signature, "secret", None, None).unwrap());
82 }
83
84 #[test]
85 fn test_missing_prefix() {
86 let verifier = GitHubVerifier;
87 let result = verifier.verify(b"test", "abc123", "secret", None, None);
88 assert!(matches!(result, Err(SignatureError::InvalidFormat)));
89 }
90}