Bash and openssl script to verify a Sume webhook signature

A tested bash script that checks a Sume webhook signature with openssl, enforces the 300 s window and exits on an empty secret. Plus what it cannot do.

3 min readSume
All posts

You can verify a Sume webhook from a shell with openssl dgst: build the string timestamp, dot, raw body, HMAC it with SHA-256 using your signing secret and compare it to the sume-v1= value in x-sume-webhook-signature. The script below does that and exits non-zero on a stale timestamp, a bad signature or an empty secret.

It is useful for replaying a saved delivery while you debug, and for small hooks behind a web server that hands you the body as a file. It is not a good production verifier, for a reason covered below.

The script

Usage is verify.sh BODY_FILE TIMESTAMP SIGNATURE_HEADER with the secret in SUME_WEBHOOK_SECRET. It was tested with a good signature (prints ok), a stale timestamp (exit 1), a bad signature (exit 1) and an empty secret (exit 2).

The awk step takes the last field of the openssl output because different openssl versions print the digest with or without a leading label, such as an algorithm name and an equals sign. Taking the last field works for both. The script checks the timestamp before computing anything, so a stale replay costs nothing to reject. Where you get the three inputs depends on your web server: save the body to a file exactly as received, and copy the two headers x-sume-webhook-timestamp and x-sume-webhook-signature as they came.

#!/usr/bin/env bash
# usage: verify.sh BODY_FILE TIMESTAMP SIGNATURE_HEADER   (secret in SUME_WEBHOOK_SECRET)
set -euo pipefail
[ -n "${SUME_WEBHOOK_SECRET:-}" ] || { echo "empty secret" >&2; exit 2; }
body_file=$1 ts=$2 header=$3
now=$(date +%s)
skew=$(( ts > now ? ts - now : now - ts ))
(( skew <= 300 )) || { echo "stale timestamp" >&2; exit 1; }
digest=$({ printf '%s.' "$ts"; cat "$body_file"; } \
  | openssl dgst -sha256 -hmac "$SUME_WEBHOOK_SECRET" -hex | awk '{print $NF}')
IFS=',' read -ra entries <<< "$header"
for e in "${entries[@]}"; do
  [ "${e// /}" = "sume-v1=$digest" ] && { echo ok; exit 0; }
done
echo "bad signature" >&2; exit 1

Exit codes

The codes let a caller tell a configuration error from a rejected request.

Script exit codes (read 2026-10-06, from the tested script)
ExitMeaning
0Signature matches, timestamp within 300 s
1Stale timestamp or no matching entry
2SUME_WEBHOOK_SECRET is empty

Tradeoffs

The string comparison in bash is not constant time, so do not expose this to untrusted network timing. Use it for offline checks and CI fixtures, and use your language's HMAC compare in the real receiver. Also keep the body byte for byte: command substitution strips trailing newlines, which is why the script streams the file with cat instead of capturing it.

For a quick local test, save a delivery body to body.json, compute its signature with the same openssl command, and pass both to the script. Changing one byte of the file should flip the result to bad signature, which is a fast way to confirm that you are hashing the bytes you think you are.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume