Skip to content

Verify webhook signatures

Check that a request comes from BIRP and is recent, with the secret of the webhook endpoint.

Version v1.2, updated

On this page

Every request BIRP sends carries a BIRP-Signature header, computed with the secret of the webhook endpoint. Check it before you trust the body, and refuse the request when it does not match.

The header

BIRP-Signature holds a timestamp and one or two signatures, such as t=1790604312,v1=.... t is the time of sending, in Unix seconds.

Each v1 value is the HMAC-SHA256 of the text <t>.<raw body>, keyed with the secret, written in lowercase hex.

Use the secret exactly as BIRP shows it, as text, with its birp_whsec_ prefix: do not strip or decode any part of it.

Steps

  1. Refuse every request when your secret is missing or empty: never compute a signature with an empty key.

  2. Read the raw body as received, before any JSON parsing. Parsing it and writing it again changes the bytes.

  3. Split the header on commas, and take t and every v1 value.

  4. Refuse the request when t is more than 5 minutes away from your clock. An old request sent again then fails the check.

  5. Compute the HMAC-SHA256 of <t>.<raw body> with the secret, in hex.

  6. Accept the request when one of the v1 values equals it. Compare in constant time.

Code

Each sample is a function, then a short use of it. The shell sample is for a check by hand, with a body saved in a file.

Verify a signature
# header: the value of BIRP-Signature; body.json: the body exactly as received.
if [ -z "$BIRP_WEBHOOK_SECRET" ]; then
  echo "invalid signature" # never check with an empty key
else
  t=$(printf '%s' "$header" | tr ',' '\n' | sed -n 's/^t=//p')
  now=$(date +%s)
  expected=$(printf '%s.' "$t" | cat - body.json | openssl dgst -sha256 -hmac "$BIRP_WEBHOOK_SECRET" | sed 's/^.*= //')
  if [ $((now - ${t:-0})) -le 300 ] && [ $((${t:-0} - now)) -le 300 ] && printf '%s' "$header" | tr ',' '\n' | grep -qx "v1=$expected"; then
    echo "valid signature"
  else
    echo "invalid signature"
  fi
fi

During a secret rotation

For 24 hours after a rotation the header carries two v1 values: the first is signed with the new secret, the second with the previous one. Code that accepts any matching v1 value keeps working with either secret.

When the check fails

  • Answer with a 400 status and leave the body unprocessed.

  • Check that the secret is the one of this webhook endpoint, from its last rotation.

  • Check that your framework gives you the raw body, not a body parsed and written again.

  • Check that the clock of your server is right.

  • webhooks-overview
  • register-webhook-endpoint
  • handle-webhooks-once
  • keep-your-key-safe