windmill-labs/windmill · error

Invalid signature

Error message

Invalid signature

What it means

Resume secrets (approval/approver links) are HMAC-signed; this error is thrown when mac.verify_slice fails, i.e. the provided signature does not match a freshly computed HMAC of job_id, resume_id and optional approver with the server's secret. It means the resume link or token is forged, corrupted, or signed with a different secret.

Source

Thrown at backend/windmill-api/src/jobs.rs:5239

}

async fn verify_suspended_secret(
    w_id: &String,
    db: &DB,
    job_id: Uuid,
    resume_id: u32,
    approver: &QueryApprover,
    secret: String,
) -> Result<(), Error> {
    let key = get_workspace_key(w_id, db).await?;
    let mut mac = HmacSha256::new_from_slice(key.as_bytes()).map_err(to_anyhow)?;
    mac.update(job_id.as_bytes());
    mac.update(resume_id.to_be_bytes().as_ref());
    if let Some(approver) = approver.approver.clone() {
        mac.update(approver.as_bytes());
    }
    mac.verify_slice(hex::decode(secret)?.as_ref())
        .map_err(|_| anyhow::anyhow!("Invalid signature"))?;
    Ok(())
}

/* If the flow is currently waiting to be resumed (`FlowStatusModule::WaitingForEvents`)
 * the suspend column must be set to the number of resume messages waited on.
 *
 * The flow's queue row is locked in this transaction because to avoid race conditions around
 * the suspend column.
 * That is, a job needs one event but it hasn't arrived, a worker counts zero events before
 * entering WaitingForEvents.  Then this message arrives but the job isn't in WaitingForEvents
 * yet so the suspend counter isn't updated.  Then the job enters WaitingForEvents expecting
 * one event to arrive based on the count that is no longer correct. */
async fn resume_immediately_if_relevant<'c>(
    flow: FlowInfo,
    job_id: Uuid,
    tx: &mut Transaction<'c, Postgres>,
) -> error::Result<()> {
    Ok(

View on GitHub (pinned to e474e8803c)

Solutions

  1. Request a fresh approval/resume link (re-trigger the resume request)
  2. Verify the URL's signature parameter was not truncated or escaped by the email client
  3. Check that the instance's HMAC secret was not changed after the link was generated
  4. Never hand-craft the URL; always use the link emitted by the server

Example fix

// before
const sig = url.searchParams.get('s').slice(0, 32); // corrupted
// after
const sig = url.searchParams.get('s'); // pass full hex signature verbatim
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure the URL carries a complete hex signature before calling
const sig = new URL(link).searchParams.get('signature');
if (!sig || !/^[0-9a-f]+$/i.test(sig)) throw new Error('malformed resume link');

Type guard

function hasValidSignature(params) {
  return typeof params.signature === 'string' && /^[0-9a-f]{40,}$/i.test(params.signature);
}

Try / catch

try {
  await client.resume(flowId, resumeId, payload);
} catch (e) {
  if (/Invalid signature/.test(e.message)) {
    // link tampered/stale: request a fresh one, do not retry with the same link
    await requestNewApprovalLink(flowId);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the anonymous approval/resume endpoint with a tampered signature parameter; using a resume link generated before the server secret was rotated; truncation/corruption of the hex-encoded signature in an email client.

Common situations: Email clients line-wrapping or HTML-escaping the signed link; rotating WINDMILL secrets in a running deployment so old links invalidate; manually constructing approval URLs.

Related errors


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