windmill-labs/windmill · error

signature mismatch

Error message

signature mismatch

What it means

`AuthedClient::verify` computes an HMAC-SHA256 over `v0:<timestamp>:<body>` with the shared secret and compares it hex-encoded (`v0=<hex>`) against the provided signature. A mismatch means either the body bytes differ from what was signed, the timestamp differs, or the wrong secret/algorithm was used. Thrown from `validate_view_token` when verifying a signed view-token payload.

Source

Thrown at backend/windmill-api/src/oauth2_oss.rs:193

#[cfg(not(feature = "private"))]
pub struct SlackVerifier {
    mac: HmacSha256,
}
#[cfg(not(feature = "private"))]
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(())
    }
}

View on GitHub (pinned to e474e8803c)

Solutions

  1. Recompute the signature over the EXACT byte string that will be verified: sign "v0:{ts}:{body}" with the same secret and hex-encode as v0=<hex>.
  2. Confirm both sides use the same secret and HMAC-SHA256.
  3. Ensure the ts string is byte-identical (no reformatting of timestamps).
  4. Regenerate the view token rather than hand-editing its body; re-check any intermediary that could rewrite the body.

Example fix

// before
const sig = 'v0=' + hmacSha256(otherSecret, ts + body); // wrong format & secret
// after
const base = `v0:${ts}:${JSON.stringify(body)}`; // exact serialized body sent
const sig = 'v0=' + crypto.createHmac('sha256', sharedSecret).update(base).digest('hex');
Defensive patterns

Strategy: try-catch

Validate before calling

const base = `v0:${ts}:${bodyString}`;
const expected = 'v0=' + crypto.createHmac('sha256', secret).update(base).digest('hex');
if (expected !== expSig) throw new Error('local signature check failed before sending');

Type guard

function looksLikeSig(s) { return typeof s === 'string' && /^v0=[0-9a-f]{64}$/.test(s); }

Try / catch

try {
  await validateViewToken(token);
} catch (e) {
  if (String(e).includes('signature mismatch')) {
    // regenerate token with the current secret and exact body bytes
  } else throw e;
}

Prevention

When it happens

Trigger: Calling validate_view_token (which invokes verify at backend/windmill-api/src/oauth2_oss.rs:193) with an exp_sig that does not equal HMAC(secret, "v0:{ts}:{body}") — altered body, different ts, wrong secret, or re-signed payload with a different key.

Common situations: Payload mutated after signing (proxy adding/removing fields, whitespace, key reordering); clock/timestamp string mismatch between signer and verifier; rotating the secret on one side only; base64-vs-hex confusion on the client; encoding differences (JSON serialization order).

Related errors


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