Skip to main content

autumn_plugin_slack/
verify.rs

1//! Slack request signature check (`v0`).
2//!
3//! Base string: `v0:{timestamp}:{raw body}`. Key: the signing secret.
4//! Signature header: `X-Slack-Signature: v0=<hex HMAC-SHA256>`.
5
6use hmac::{Hmac, Mac};
7use sha2::Sha256;
8
9use crate::policy::timestamp_fresh;
10
11/// Header with the request timestamp (Unix seconds).
12pub const TIMESTAMP_HEADER: &str = "x-slack-request-timestamp";
13/// Header with the signature.
14pub const SIGNATURE_HEADER: &str = "x-slack-signature";
15
16/// Why a request failed verification.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
18#[non_exhaustive]
19pub enum VerifyError {
20    /// No timestamp header.
21    #[error("missing timestamp header")]
22    MissingTimestamp,
23    /// No signature header.
24    #[error("missing signature header")]
25    MissingSignature,
26    /// The timestamp is not a number.
27    #[error("bad timestamp header")]
28    BadTimestamp,
29    /// The signature is not `v0=` and 64 hex digits.
30    #[error("bad signature header")]
31    BadSignatureFormat,
32    /// The timestamp is outside the tolerance.
33    #[error("stale timestamp")]
34    Stale,
35    /// No secret gives this signature.
36    #[error("signature mismatch")]
37    Mismatch,
38}
39
40impl VerifyError {
41    /// HTTP status for this error: 400 for bad input, 401 for a failed check.
42    #[must_use]
43    pub const fn status(self) -> u16 {
44        match self {
45            Self::Stale | Self::Mismatch => 401,
46            _ => 400,
47        }
48    }
49
50    /// Short label for metrics.
51    #[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/// Checks Slack signatures. Holds the current and the previous secrets.
62#[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    /// Makes a verifier. `previous` holds old secrets during rotation.
79    ///
80    /// It ignores an empty or blank secret. HMAC accepts an empty key, so an
81    /// empty secret would let anyone sign. With no secret left, all checks fail.
82    #[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    /// Checks one request. Call it before any parse of `body`.
96    ///
97    /// # Errors
98    /// Returns the first [`VerifyError`] found.
99    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        // Digits only: no sign, no space.
109        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            // `verify_slice` compares in constant time.
123            if mac(secret, ts, body).verify_slice(&expected).is_ok() {
124                return Ok(());
125            }
126        }
127        Err(VerifyError::Mismatch)
128    }
129}
130
131/// Returns `v0=<hex>` for this secret, timestamp, and body.
132pub(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
139/// HMAC-SHA256 over `v0:{ts}:{body}`.
140fn mac(secret: &[u8], ts: &str, body: &[u8]) -> Hmac<Sha256> {
141    // HMAC accepts a key of any length. The error case cannot happen.
142    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    // Example from the Slack docs ("Verifying requests from Slack").
157    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        // Regression: an empty previous secret let anyone sign with an empty key.
255        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}