discourse_webhooks/
signature.rs1use hex;
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum SignatureVerificationError {
8 #[error("Invalid hex encoding in signature: {0}")]
9 InvalidHex(#[from] hex::FromHexError),
10
11 #[error("Invalid HMAC key")]
12 InvalidKey,
13
14 #[error("Invalid signature format: {0}")]
15 InvalidFormat(String),
16
17 #[error("Signature verification failed")]
18 VerificationFailed,
19}
20
21pub fn verify_signature(
32 secret: &str,
33 payload: &str,
34 signature: &str,
35) -> Result<(), SignatureVerificationError> {
36 let signature = signature.strip_prefix("sha256=").ok_or_else(|| {
37 SignatureVerificationError::InvalidFormat("Signature must start with 'sha256='".to_string())
38 })?;
39
40 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
41 .map_err(|_| SignatureVerificationError::InvalidKey)?;
42
43 mac.update(payload.as_bytes());
44
45 let expected = mac.finalize().into_bytes();
46 let expected_hex = hex::encode(expected);
47
48 if signature.eq_ignore_ascii_case(&expected_hex) {
49 Ok(())
50 } else {
51 Err(SignatureVerificationError::VerificationFailed)
52 }
53}
54
55pub fn verify_json_signature(
59 secret: &str,
60 payload: &serde_json::Value,
61 signature: &str,
62) -> Result<(), SignatureVerificationError> {
63 let payload_str = serde_json::to_string(payload).map_err(|_| {
64 SignatureVerificationError::InvalidFormat("Failed to serialize JSON payload".to_string())
65 })?;
66
67 verify_signature(secret, &payload_str, signature)
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use serde_json::json;
74
75 #[test]
76 fn test_signature_verification() {
77 let secret = "test_secret";
78 let payload = r#"{"test":"data"}"#;
79
80 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
81 mac.update(payload.as_bytes());
82 let signature = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
83
84 assert!(verify_signature(secret, payload, &signature).is_ok());
85
86 assert!(verify_signature("wrong_secret", payload, &signature).is_err());
87
88 assert!(verify_signature(secret, payload, "invalid_format").is_err());
89 }
90
91 #[test]
92 fn test_json_signature_verification() {
93 let secret = "test_secret";
94 let payload = json!({"test": "data"});
95 let payload_str = serde_json::to_string(&payload).unwrap();
96
97 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
98 mac.update(payload_str.as_bytes());
99 let signature = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
100
101 assert!(verify_json_signature(secret, &payload, &signature).is_ok());
102 }
103}