1use hmac::{Hmac, Mac};
7use sha2::Sha256;
8
9use crate::policy::timestamp_fresh;
10
11pub const TIMESTAMP_HEADER: &str = "x-slack-request-timestamp";
13pub const SIGNATURE_HEADER: &str = "x-slack-signature";
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
18#[non_exhaustive]
19pub enum VerifyError {
20 #[error("missing timestamp header")]
22 MissingTimestamp,
23 #[error("missing signature header")]
25 MissingSignature,
26 #[error("bad timestamp header")]
28 BadTimestamp,
29 #[error("bad signature header")]
31 BadSignatureFormat,
32 #[error("stale timestamp")]
34 Stale,
35 #[error("signature mismatch")]
37 Mismatch,
38}
39
40impl VerifyError {
41 #[must_use]
43 pub const fn status(self) -> u16 {
44 match self {
45 Self::Stale | Self::Mismatch => 401,
46 _ => 400,
47 }
48 }
49
50 #[must_use]
52 pub const fn label(self) -> &'static str {
53 match self {
54 Self::Stale => "stale",
55 Self::Mismatch => "bad_signature",
56 _ => "bad_request",
57 }
58 }
59}
60
61#[derive(Clone)]
63pub struct Verifier {
64 secrets: Vec<Vec<u8>>,
65 tolerance_secs: u64,
66}
67
68impl std::fmt::Debug for Verifier {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("Verifier")
71 .field("secrets", &self.secrets.len())
72 .field("tolerance_secs", &self.tolerance_secs)
73 .finish()
74 }
75}
76
77impl Verifier {
78 #[must_use]
83 pub fn new(secret: &str, previous: &[String], tolerance_secs: u64) -> Self {
84 let secrets = std::iter::once(secret)
85 .chain(previous.iter().map(String::as_str))
86 .filter(|s| !s.trim().is_empty())
87 .map(|s| s.as_bytes().to_vec())
88 .collect();
89 Self {
90 secrets,
91 tolerance_secs,
92 }
93 }
94
95 pub fn verify(
100 &self,
101 timestamp: Option<&str>,
102 signature: Option<&str>,
103 body: &[u8],
104 now_secs: u64,
105 ) -> Result<(), VerifyError> {
106 let ts = timestamp.ok_or(VerifyError::MissingTimestamp)?;
107 let sig = signature.ok_or(VerifyError::MissingSignature)?;
108 if ts.is_empty() || !ts.bytes().all(|b| b.is_ascii_digit()) {
110 return Err(VerifyError::BadTimestamp);
111 }
112 let ts_secs: u64 = ts.parse().map_err(|_| VerifyError::BadTimestamp)?;
113 let expected = sig
114 .strip_prefix("v0=")
115 .filter(|h| h.len() == 64)
116 .and_then(|h| hex::decode(h).ok())
117 .ok_or(VerifyError::BadSignatureFormat)?;
118 if !timestamp_fresh(now_secs, ts_secs, self.tolerance_secs) {
119 return Err(VerifyError::Stale);
120 }
121 for secret in &self.secrets {
122 if mac(secret, ts, body).verify_slice(&expected).is_ok() {
124 return Ok(());
125 }
126 }
127 Err(VerifyError::Mismatch)
128 }
129}
130
131pub(crate) fn signature(secret: &[u8], ts: &str, body: &[u8]) -> String {
133 format!(
134 "v0={}",
135 hex::encode(mac(secret, ts, body).finalize().into_bytes())
136 )
137}
138
139fn mac(secret: &[u8], ts: &str, body: &[u8]) -> Hmac<Sha256> {
141 let mut m = <Hmac<Sha256> as Mac>::new_from_slice(secret)
143 .unwrap_or_else(|_| unreachable!("HMAC takes any key length"));
144 m.update(b"v0:");
145 m.update(ts.as_bytes());
146 m.update(b":");
147 m.update(body);
148 m
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use proptest::prelude::*;
155
156 const DOC_SECRET: &str = "8f742231b10e8888abcd99yyyzzz85a5";
158 const DOC_TS: &str = "1531420618";
159 const DOC_BODY: &str = "token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c";
160 const DOC_SIG: &str = "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503";
161
162 fn now() -> u64 {
163 DOC_TS.parse().unwrap()
164 }
165
166 #[test]
167 fn slack_doc_example_verifies() {
168 assert_eq!(
169 signature(DOC_SECRET.as_bytes(), DOC_TS, DOC_BODY.as_bytes()),
170 DOC_SIG
171 );
172 let v = Verifier::new(DOC_SECRET, &[], 300);
173 assert_eq!(
174 v.verify(Some(DOC_TS), Some(DOC_SIG), DOC_BODY.as_bytes(), now()),
175 Ok(())
176 );
177 }
178
179 #[test]
180 fn errors_in_order() {
181 let v = Verifier::new(DOC_SECRET, &[], 300);
182 let b = DOC_BODY.as_bytes();
183 assert_eq!(
184 v.verify(None, Some(DOC_SIG), b, now()),
185 Err(VerifyError::MissingTimestamp)
186 );
187 assert_eq!(
188 v.verify(Some(DOC_TS), None, b, now()),
189 Err(VerifyError::MissingSignature)
190 );
191 assert_eq!(
192 v.verify(Some("-5"), Some(DOC_SIG), b, now()),
193 Err(VerifyError::BadTimestamp)
194 );
195 assert_eq!(
196 v.verify(Some(DOC_TS), Some("v1=00"), b, now()),
197 Err(VerifyError::BadSignatureFormat)
198 );
199 assert_eq!(
200 v.verify(Some(DOC_TS), Some("v0=zz"), b, now()),
201 Err(VerifyError::BadSignatureFormat)
202 );
203 assert_eq!(
204 v.verify(Some(DOC_TS), Some(DOC_SIG), b, now() + 301),
205 Err(VerifyError::Stale)
206 );
207 let mut bad = DOC_SIG.to_owned();
208 bad.replace_range(bad.len() - 1.., "4");
209 assert_eq!(
210 v.verify(Some(DOC_TS), Some(&bad), b, now()),
211 Err(VerifyError::Mismatch)
212 );
213 assert_eq!(
214 v.verify(Some(DOC_TS), Some(DOC_SIG), b"x", now()),
215 Err(VerifyError::Mismatch)
216 );
217 }
218
219 #[test]
220 fn upper_case_hex_and_space_in_timestamp() {
221 let v = Verifier::new(DOC_SECRET, &[], 300);
222 let upper = format!("v0={}", DOC_SIG[3..].to_ascii_uppercase());
223 assert_eq!(
224 v.verify(Some(DOC_TS), Some(&upper), DOC_BODY.as_bytes(), now()),
225 Ok(())
226 );
227 assert_eq!(
228 v.verify(
229 Some(" 1531420618"),
230 Some(DOC_SIG),
231 DOC_BODY.as_bytes(),
232 now()
233 ),
234 Err(VerifyError::BadTimestamp)
235 );
236 }
237
238 #[test]
239 fn previous_secret_verifies() {
240 let v = Verifier::new("new-secret", &[DOC_SECRET.to_owned()], 300);
241 assert_eq!(
242 v.verify(Some(DOC_TS), Some(DOC_SIG), DOC_BODY.as_bytes(), now()),
243 Ok(())
244 );
245 let v = Verifier::new("new-secret", &[], 300);
246 assert_eq!(
247 v.verify(Some(DOC_TS), Some(DOC_SIG), DOC_BODY.as_bytes(), now()),
248 Err(VerifyError::Mismatch)
249 );
250 }
251
252 #[test]
253 fn empty_secrets_never_verify() {
254 let body = b"command=%2Fx";
256 let forged = signature(b"", DOC_TS, body);
257 let v = Verifier::new(DOC_SECRET, &[String::new(), " ".to_owned()], 300);
258 assert_eq!(
259 v.verify(Some(DOC_TS), Some(&forged), body, now()),
260 Err(VerifyError::Mismatch)
261 );
262 let v = Verifier::new("", &[], 300);
263 assert_eq!(
264 v.verify(Some(DOC_TS), Some(&forged), body, now()),
265 Err(VerifyError::Mismatch)
266 );
267 assert!(format!("{v:?}").contains("secrets: 0"));
268 }
269
270 #[test]
271 fn debug_hides_secrets() {
272 let v = Verifier::new(DOC_SECRET, &["older".to_owned()], 300);
273 let d = format!("{v:?}");
274 assert!(!d.contains(DOC_SECRET) && !d.contains("older"), "{d}");
275 }
276
277 #[test]
278 fn status_and_labels() {
279 assert_eq!(VerifyError::Mismatch.status(), 401);
280 assert_eq!(VerifyError::Stale.status(), 401);
281 assert_eq!(VerifyError::BadTimestamp.status(), 400);
282 assert_eq!(VerifyError::MissingSignature.label(), "bad_request");
283 assert_eq!(VerifyError::Mismatch.label(), "bad_signature");
284 assert_eq!(VerifyError::Stale.label(), "stale");
285 }
286
287 proptest! {
288 #[test]
289 fn any_signed_body_verifies_and_any_flip_fails(
290 body in proptest::collection::vec(any::<u8>(), 0..512),
291 flip in any::<prop::sample::Index>(),
292 ) {
293 let v = Verifier::new("s3cret", &[], 300);
294 let ts = "1700000000";
295 let sig = signature(b"s3cret", ts, &body);
296 prop_assert_eq!(v.verify(Some(ts), Some(&sig), &body, 1_700_000_000), Ok(()));
297 if !body.is_empty() {
298 let mut tampered = body;
299 let i = flip.index(tampered.len());
300 tampered[i] ^= 0x01;
301 prop_assert_eq!(v.verify(Some(ts), Some(&sig), &tampered, 1_700_000_000), Err(VerifyError::Mismatch));
302 }
303 }
304 }
305}