diff --git a/server/src/chat/transport/slack/verify.rs b/server/src/chat/transport/slack/verify.rs index 9855c4dc..0a8c60dd 100644 --- a/server/src/chat/transport/slack/verify.rs +++ b/server/src/chat/transport/slack/verify.rs @@ -2,6 +2,12 @@ use std::fmt::Write as FmtWrite; +use hmac::{Hmac, KeyInit, Mac}; +use sha2::Sha256; +use subtle::ConstantTimeEq; + +type HmacSha256 = Hmac; + /// Verify the Slack request signature using HMAC-SHA256. /// /// Slack sends `X-Slack-Signature` and `X-Slack-Request-Timestamp` headers. @@ -15,163 +21,22 @@ pub(super) fn verify_slack_signature( body: &[u8], signature: &str, ) -> bool { - // Compute HMAC-SHA256 manually using the signing secret. - // Slack signature format: v0={hex(HMAC-SHA256(secret, "v0:{ts}:{body}"))} - let base_string = format!("v0:{timestamp}:"); - - // Simple HMAC-SHA256 implementation using ring-style approach. - // We use the hmac crate pattern with SHA-256. - // Since we don't want to add a dependency, we'll use a manual approach: - // HMAC(K, m) = H((K' ^ opad) || H((K' ^ ipad) || m)) - // where K' is the key padded/hashed to block size. - - let key = signing_secret.as_bytes(); - let block_size = 64; // SHA-256 block size - - // If key is longer than block size, hash it first. - let key_block = if key.len() > block_size { - let digest = sha256(key); - let mut k = vec![0u8; block_size]; - k[..32].copy_from_slice(&digest); - k - } else { - let mut k = vec![0u8; block_size]; - k[..key.len()].copy_from_slice(key); - k - }; - - // Inner and outer padded keys. - let mut ipad = vec![0x36u8; block_size]; - let mut opad = vec![0x5cu8; block_size]; - for i in 0..block_size { - ipad[i] ^= key_block[i]; - opad[i] ^= key_block[i]; - } - - // Inner hash: H(ipad || message) - let mut inner_data = ipad; - inner_data.extend_from_slice(base_string.as_bytes()); - inner_data.extend_from_slice(body); - let inner_hash = sha256(&inner_data); - - // Outer hash: H(opad || inner_hash) - let mut outer_data = opad; - outer_data.extend_from_slice(&inner_hash); - let hmac_result = sha256(&outer_data); + let mut mac = HmacSha256::new_from_slice(signing_secret.as_bytes()) + .expect("HMAC can take key of any size"); + mac.update(b"v0:"); + mac.update(timestamp.as_bytes()); + mac.update(b":"); + mac.update(body); + let result = mac.finalize().into_bytes(); // Format as "v0={hex}" let mut expected = String::from("v0="); - for byte in &hmac_result { + for byte in &result { write!(expected, "{byte:02x}").unwrap(); } - // Constant-time comparison. - constant_time_eq(expected.as_bytes(), signature.as_bytes()) -} - -/// Minimal SHA-256 implementation (no external dependency). -/// -/// This follows FIPS 180-4. Only used for HMAC signature verification, -/// not for any security-critical path beyond webhook authentication. -fn sha256(data: &[u8]) -> [u8; 32] { - let mut h: [u32; 8] = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, - 0x5be0cd19, - ]; - - let k: [u32; 64] = [ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, - 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, - 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, - 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, - 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, - 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, - 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, - 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, - 0xc67178f2, - ]; - - // Pre-processing: padding - let bit_len = (data.len() as u64) * 8; - let mut padded = data.to_vec(); - padded.push(0x80); - while (padded.len() % 64) != 56 { - padded.push(0); - } - padded.extend_from_slice(&bit_len.to_be_bytes()); - - // Process each 512-bit block - for chunk in padded.chunks_exact(64) { - let mut w = [0u32; 64]; - for i in 0..16 { - w[i] = u32::from_be_bytes([ - chunk[4 * i], - chunk[4 * i + 1], - chunk[4 * i + 2], - chunk[4 * i + 3], - ]); - } - for i in 16..64 { - let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); - let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10); - w[i] = w[i - 16] - .wrapping_add(s0) - .wrapping_add(w[i - 7]) - .wrapping_add(s1); - } - - let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; - - for i in 0..64 { - let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); - let ch = (e & f) ^ ((!e) & g); - let temp1 = hh - .wrapping_add(s1) - .wrapping_add(ch) - .wrapping_add(k[i]) - .wrapping_add(w[i]); - let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); - let maj = (a & b) ^ (a & c) ^ (b & c); - let temp2 = s0.wrapping_add(maj); - - hh = g; - g = f; - f = e; - e = d.wrapping_add(temp1); - d = c; - c = b; - b = a; - a = temp1.wrapping_add(temp2); - } - - h[0] = h[0].wrapping_add(a); - h[1] = h[1].wrapping_add(b); - h[2] = h[2].wrapping_add(c); - h[3] = h[3].wrapping_add(d); - h[4] = h[4].wrapping_add(e); - h[5] = h[5].wrapping_add(f); - h[6] = h[6].wrapping_add(g); - h[7] = h[7].wrapping_add(hh); - } - - let mut result = [0u8; 32]; - for (i, val) in h.iter().enumerate() { - result[4 * i..4 * i + 4].copy_from_slice(&val.to_be_bytes()); - } - result -} - -/// Constant-time byte comparison. -fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { - if a.len() != b.len() { - return false; - } - let mut diff = 0u8; - for (x, y) in a.iter().zip(b.iter()) { - diff |= x ^ y; - } - diff == 0 + // Constant-time comparison to prevent timing attacks. + expected.as_bytes().ct_eq(signature.as_bytes()).into() } // ── Tests ─────────────────────────────────────────────────────────────── @@ -189,7 +54,6 @@ mod tests { let timestamp = "1531420618"; let body = b"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.803a0bc887a14d10d2c659f"; - // Compute expected signature for this test case. let sig = compute_test_signature(secret, timestamp, body); assert!(verify_slack_signature(secret, timestamp, body, &sig)); @@ -223,83 +87,22 @@ mod tests { )); } - /// Helper to compute a test signature using our sha256 + HMAC implementation. + /// Helper to compute a test signature using `Hmac`. fn compute_test_signature(secret: &str, timestamp: &str, body: &[u8]) -> String { use std::fmt::Write; - let key = secret.as_bytes(); - let block_size = 64; - let key_block = if key.len() > block_size { - let digest = sha256(key); - let mut k = vec![0u8; block_size]; - k[..32].copy_from_slice(&digest); - k - } else { - let mut k = vec![0u8; block_size]; - k[..key.len()].copy_from_slice(key); - k - }; + let mut mac = + HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size"); + mac.update(b"v0:"); + mac.update(timestamp.as_bytes()); + mac.update(b":"); + mac.update(body); + let result = mac.finalize().into_bytes(); - let mut ipad = vec![0x36u8; block_size]; - let mut opad = vec![0x5cu8; block_size]; - for i in 0..block_size { - ipad[i] ^= key_block[i]; - opad[i] ^= key_block[i]; + let mut sig = String::from("v0="); + for byte in &result { + write!(sig, "{byte:02x}").unwrap(); } - - let base_string = format!("v0:{timestamp}:"); - let mut inner_data = ipad; - inner_data.extend_from_slice(base_string.as_bytes()); - inner_data.extend_from_slice(body); - let inner_hash = sha256(&inner_data); - - let mut outer_data = opad; - outer_data.extend_from_slice(&inner_hash); - let hmac_result = sha256(&outer_data); - - let mut expected = String::from("v0="); - for byte in &hmac_result { - write!(expected, "{byte:02x}").unwrap(); - } - expected - } - - // ── SHA-256 implementation ────────────────────────────────────────── - - #[test] - fn sha256_empty_string() { - let result = sha256(b""); - let hex: String = result.iter().map(|b| format!("{b:02x}")).collect(); - assert_eq!( - hex, - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - ); - } - - #[test] - fn sha256_hello_world() { - let result = sha256(b"hello world"); - let hex: String = result.iter().map(|b| format!("{b:02x}")).collect(); - assert_eq!( - hex, - "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" - ); - } - - // ── Constant-time comparison ──────────────────────────────────────── - - #[test] - fn constant_time_eq_same_values() { - assert!(constant_time_eq(b"hello", b"hello")); - } - - #[test] - fn constant_time_eq_different_values() { - assert!(!constant_time_eq(b"hello", b"world")); - } - - #[test] - fn constant_time_eq_different_lengths() { - assert!(!constant_time_eq(b"hello", b"hi")); + sig } }