windmill-labs/windmill · error

signature mismatch

Error message

signature mismatch

What it means

SlackVerifier::verify recomputes the Slack 'v0' HMAC-SHA256 signature over "v0:{timestamp}:{body}" using the signing secret and compares it to the X-Slack-Signature header. Any mismatch means the request was not signed with the expected secret or the payload changed in transit.

Source

Thrown at backend/windmill-oauth/src/lib.rs:1124

pub struct SlackVerifier {
    mac: HmacSha256,
}

impl SlackVerifier {
    pub fn new<S: AsRef<[u8]>>(secret: S) -> anyhow::Result<SlackVerifier> {
        HmacSha256::new_from_slice(secret.as_ref())
            .map(|mac| SlackVerifier { mac })
            .map_err(|_| anyhow::anyhow!("invalid secret"))
    }

    pub fn verify(&self, ts: &str, body: &str, exp_sig: &str) -> anyhow::Result<()> {
        let basestring = format!("v0:{}:{}", ts, body);
        let mut mac = self.mac.clone();

        mac.update(basestring.as_bytes());
        let sig = format!("v0={}", hex::encode(mac.finalize().into_bytes()));
        if sig != exp_sig {
            Err(anyhow::anyhow!("signature mismatch"))?;
        }
        Ok(())
    }
}

/// Fetch user info from OAuth provider
pub async fn http_get_user_info<T: DeserializeOwned>(
    http_client: &reqwest::Client,
    url: &str,
    token: &str,
) -> error::Result<T> {
    let res = http_client
        .get(url)
        .bearer_auth(token)
        .send()
        .await
        .map_err(to_anyhow)
        .map_err(|e| error::Error::InternalErr(format!("failed to fetch user info: {}", e)))?;

View on GitHub (pinned to e474e8803c)

Solutions

  1. Verify the exact raw request body bytes are passed to verify — never a re-serialized or pretty-printed JSON
  2. Confirm the signing secret matches the Slack app that sent the webhook (reset/re-copy from Slack app settings)
  3. Ensure the timestamp header (X-Slack-Request-Timestamp) is forwarded unmodified
  4. Check no reverse proxy (nginx, Cloudflare) is rewriting the body (e.g. gzip decompression with re-encoding, WAF modification)

Example fix

// before (body re-serialized by handler framework)
let body = serde_json::to_string(&payload)?; // alters key order/spacing
verify(&ts, &body, &sig)?;
// after
let body = raw_body_bytes; // exact bytes Slack sent
verify(&ts, &body, &sig)?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Before verifying, ensure you have raw bytes and a plausible v0 signature
if !exp_sig.starts_with("v0=") { anyhow::bail!("not a Slack v0 signature"); }
if ts.parse::<i64>().map(|t| now - t > 300).unwrap_or(true) { anyhow::bail!("stale or bad timestamp"); }

Type guard

fn is_v0_signature(s: &str) -> bool { s.starts_with("v0=") && s[3..].len() == 64 && s[3..].bytes().all(|b| b.is_ascii_hexdigit()) }

Try / catch

if let Err(e) = verifier.verify(&ts, &raw_body, &sig) {
    if e.to_string() == "signature mismatch" {
        // reject the webhook: wrong secret or mutated body
        return HttpResponse::Unauthorized().finish();
    }
    return HttpResponse::BadRequest().finish();
}

Prevention

When it happens

Trigger: verify(ts, body, expected_sig) called with a timestamp/body that differs from what Slack signed, or an expected signature computed with a different signing secret.

Common situations: A proxy or framework re-serializing/reordering the JSON body before verification; verifying against the wrong Slack app's signing secret (multiple Slack apps / workspaces); replayed or modified webhooks; comparing against a truncated or URL-decoded signature.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/a3835db2212c1645. Report an issue: GitHub.