BETA
Skip to content

Verifying Signatures ​

Every request Krafter makes to your endpoint can be signed: webhook deliveries for every service, forwarded events, and cron jobs. Signing is on whenever the thing making the call has a secret — a webhook's secret, or a cron job's signing_secret.

The scheme is the same everywhere, so you write the verification once.

The header ​

A signed request carries one header:

x-krafter-signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>

t is the moment we sent it. v1 is the scheme version, so a future change has somewhere to go.

The timestamp is inside the signed payload. That is what makes the signature expire: sign the body alone and the value never changes, so a request captured once stays valid forever.

Verify it ​

Three steps, in this order:

  1. Split the header into t and v1.
  2. Check that t is recent — we recommend a tolerance of 300 seconds. Reject anything older.
  3. Compute HMAC-SHA256(secret, "<t>." + rawBody) and compare it to v1 in constant time.

Step 2 is the one that does the work. Skipping it leaves you exactly where x-krafter-signature left you, because a signature with no expiry is a signature a replayer can reuse.

A replayer cannot buy time by editing t: it is inside the signed payload, so changing it invalidates v1.

Node.js ​

js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyKrafterSignature(req, secret, toleranceSeconds = 300) {
  const header = req.headers["x-krafter-signature"] ?? "";

  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=", 2)),
  );
  const t = Number(parts.t);
  const provided = parts.v1;
  if (!Number.isInteger(t) || !provided) return false;

  // 2. Freshness first — a valid signature on a stale request is still a replay.
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.${req.rawBody}`)
    .digest("hex");

  const a = Buffer.from(provided);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

Elixir ​

elixir
def verify(header, raw_body, secret, tolerance \\ 300) do
  with %{"t" => t_str, "v1" => provided} <-
         header |> String.split(",") |> Map.new(&(&1 |> String.trim() |> String.split("=", parts: 2) |> List.to_tuple())),
       {t, ""} <- Integer.parse(t_str),
       true <- abs(System.system_time(:second) - t) <= tolerance do
    expected =
      :crypto.mac(:hmac, :sha256, secret, "#{t}.#{raw_body}")
      |> Base.encode16(case: :lower)

    Plug.Crypto.secure_compare(provided, expected)
  else
    _ -> false
  end
end

Python ​

python
import hashlib, hmac, time

def verify_krafter_signature(header: str, raw_body: bytes, secret: str, tolerance: int = 300) -> bool:
    parts = dict(
        p.split("=", 1) for p in (x.strip() for x in header.split(",")) if "=" in p
    )
    t, provided = parts.get("t"), parts.get("v1")
    if not t or not provided or not t.isdigit():
        return False

    # Freshness first — a valid signature on a stale request is still a replay.
    if abs(int(time.time()) - int(t)) > tolerance:
        return False

    expected = hmac.new(
        secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(provided, expected)

Use the raw body

rawBody must be the exact bytes Krafter sent. Re-serialising parsed JSON will not match — key order and whitespace change the signature. Capture the raw body before any body-parser middleware runs.

Retries and duplicates ​

Each delivery attempt is signed afresh with its own t, so a retry is not a replay and passes the freshness check normally. Retries can still mean your endpoint sees the same logical event twice — that is what the per-delivery id header is for. Deduplicate on it:

ServiceDelivery id header
Webhooks, and every service's own webhooksx-krafter-webhook-id
Event forwardingx-krafter-event-id
Cronx-krafter-job-id (identifies the job, not the run)

Built by Krafter Studio