use sha2::{Digest, Sha256};
const BLOCK_SIZE: usize = 64;
pub const DEFAULT_MAX_SKEW_SECS: i64 = 300;
#[derive(Debug, PartialEq, Eq)]
pub enum SignatureError {
StaleTimestamp,
Mismatch,
}
impl std::fmt::Display for SignatureError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::StaleTimestamp => write!(f, "timestamp fora da janela (possível replay)"),
Self::Mismatch => write!(f, "assinatura Slack inválida"),
}
}
}
impl std::error::Error for SignatureError {}
#[allow(dead_code)]
pub fn verify_slack_signature(
signing_secret: &str,
timestamp: i64,
body: &str,
signature_header: &str,
now: i64,
max_skew_secs: i64,
) -> Result<(), SignatureError> {
if (now - timestamp).abs() > max_skew_secs {
return Err(SignatureError::StaleTimestamp);
}
let basestring = format!("v0:{timestamp}:{body}");
let expected = format!(
"v0={}",
to_hex(&hmac_sha256(
signing_secret.as_bytes(),
basestring.as_bytes()
))
);
if constant_time_eq(expected.as_bytes(), signature_header.as_bytes()) {
Ok(())
} else {
Err(SignatureError::Mismatch)
}
}
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
let mut block_key = [0u8; BLOCK_SIZE];
if key.len() > BLOCK_SIZE {
let digest = Sha256::digest(key);
block_key[..digest.len()].copy_from_slice(&digest);
} else {
block_key[..key.len()].copy_from_slice(key);
}
let mut ipad = [0x36u8; BLOCK_SIZE];
let mut opad = [0x5cu8; BLOCK_SIZE];
for i in 0..BLOCK_SIZE {
ipad[i] ^= block_key[i];
opad[i] ^= block_key[i];
}
let mut inner = Sha256::new();
inner.update(ipad);
inner.update(message);
let inner_digest = inner.finalize();
let mut outer = Sha256::new();
outer.update(opad);
outer.update(inner_digest);
let outer_digest = outer.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&outer_digest);
out
}
fn to_hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hmac_matches_rfc4231_test_case_2() {
let mac = hmac_sha256(b"Jefe", b"what do ya want for nothing?");
assert_eq!(
to_hex(&mac),
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
);
}
fn sign(secret: &str, ts: i64, body: &str) -> String {
format!(
"v0={}",
to_hex(&hmac_sha256(
secret.as_bytes(),
format!("v0:{ts}:{body}").as_bytes()
))
)
}
#[test]
fn valid_signature_within_window_passes() {
let secret = "8f742231b10e8888abcd99yyyzzz85a5";
let body = "payload=approve&id=DEM-1";
let sig = sign(secret, 1000, body);
assert!(
verify_slack_signature(secret, 1000, body, &sig, 1100, DEFAULT_MAX_SKEW_SECS).is_ok()
);
}
#[test]
fn tampered_body_is_rejected() {
let secret = "secret";
let sig = sign(secret, 1000, "original");
let err =
verify_slack_signature(secret, 1000, "tampered", &sig, 1000, DEFAULT_MAX_SKEW_SECS)
.unwrap_err();
assert_eq!(err, SignatureError::Mismatch);
}
#[test]
fn wrong_secret_is_rejected() {
let sig = sign("secret-a", 1000, "body");
assert_eq!(
verify_slack_signature("secret-b", 1000, "body", &sig, 1000, DEFAULT_MAX_SKEW_SECS),
Err(SignatureError::Mismatch)
);
}
#[test]
fn stale_timestamp_is_rejected_anti_replay() {
let secret = "secret";
let body = "body";
let sig = sign(secret, 1000, body);
let err =
verify_slack_signature(secret, 1000, body, &sig, 1000 + 999, DEFAULT_MAX_SKEW_SECS)
.unwrap_err();
assert_eq!(err, SignatureError::StaleTimestamp);
}
#[test]
fn length_mismatch_is_constant_time_false() {
assert!(!constant_time_eq(b"abc", b"abcd"));
assert!(constant_time_eq(b"abc", b"abc"));
}
}