tatara_github_watcher/
verify.rs1use hmac::{Hmac, Mac};
4use sha2::Sha256;
5use subtle::ConstantTimeEq;
6
7type HmacSha256 = Hmac<Sha256>;
8
9#[derive(Debug, thiserror::Error, PartialEq, Eq)]
11pub enum VerifyError {
12 #[error("missing signature header")]
14 MissingHeader,
15 #[error("malformed signature header (expected sha256= prefix)")]
17 BadPrefix,
18 #[error("signature is not valid hex")]
20 BadHex,
21 #[error("signature mismatch")]
23 Mismatch,
24}
25
26pub fn verify_signature(header_value: &str, body: &[u8], secret: &[u8]) -> Result<(), VerifyError> {
34 if header_value.is_empty() {
35 return Err(VerifyError::MissingHeader);
36 }
37 let hex_part = header_value
38 .strip_prefix("sha256=")
39 .ok_or(VerifyError::BadPrefix)?;
40 let supplied = hex::decode(hex_part).map_err(|_| VerifyError::BadHex)?;
41
42 let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC accepts any key length");
43 mac.update(body);
44 let computed = mac.finalize().into_bytes();
45
46 if computed.as_slice().ct_eq(supplied.as_slice()).into() {
47 Ok(())
48 } else {
49 Err(VerifyError::Mismatch)
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 fn make_signature(body: &[u8], secret: &[u8]) -> String {
58 let mut mac = HmacSha256::new_from_slice(secret).unwrap();
59 mac.update(body);
60 let bytes = mac.finalize().into_bytes();
61 format!("sha256={}", hex::encode(bytes))
62 }
63
64 #[test]
65 fn round_trip_matches() {
66 let body = b"hello-world";
67 let secret = b"super-secret";
68 let sig = make_signature(body, secret);
69 assert!(verify_signature(&sig, body, secret).is_ok());
70 }
71
72 #[test]
73 fn empty_header_is_missing() {
74 assert_eq!(
75 verify_signature("", b"any", b"any"),
76 Err(VerifyError::MissingHeader)
77 );
78 }
79
80 #[test]
81 fn wrong_prefix_rejected() {
82 assert_eq!(
83 verify_signature("md5=abc", b"any", b"any"),
84 Err(VerifyError::BadPrefix)
85 );
86 }
87
88 #[test]
89 fn non_hex_rejected() {
90 assert_eq!(
91 verify_signature("sha256=not-hex!!", b"any", b"any"),
92 Err(VerifyError::BadHex)
93 );
94 }
95
96 #[test]
97 fn tampered_body_mismatches() {
98 let body = b"original";
99 let secret = b"k";
100 let sig = make_signature(body, secret);
101 assert_eq!(
102 verify_signature(&sig, b"tampered", secret),
103 Err(VerifyError::Mismatch)
104 );
105 }
106
107 #[test]
108 fn different_secret_mismatches() {
109 let body = b"x";
110 let sig = make_signature(body, b"secret-a");
111 assert_eq!(
112 verify_signature(&sig, body, b"secret-b"),
113 Err(VerifyError::Mismatch)
114 );
115 }
116
117 #[test]
118 fn empty_secret_still_works_for_known_pair() {
119 let body = b"x";
121 let sig = make_signature(body, b"");
122 assert!(verify_signature(&sig, body, b"").is_ok());
123 }
124}