Python compare_digest TypeError on a Sume signature header
compare_digest raises TypeError on non-ASCII str. A tested Python verifier for the Sume webhook signature that compares bytes and rejects an empty secret.

If you pass a str containing a non-ASCII character to hmac.compare_digest, Python raises TypeError, so an attacker-controlled signature header can turn your verifier into a 500. Encode both sides to bytes first and the comparison simply returns False.
Sume signs webhooks with HMAC-SHA256 over the timestamp, a dot and the raw body, and sends sume-v1=<hex> in x-sume-webhook-signature. The header can hold several comma-separated entries during secret rotation, newest first, so the verifier has to check each.
A verifier that does not crash
This version rejects an empty secret, rejects timestamps more than 300 seconds from now, compares bytes, and accepts a match against any entry. It was run with three cases and prints True, False, False.
The loop uses a bitwise-or accumulator instead of returning on the first match, so the time spent does not depend on which entry matched. During a rotation the header can hold the signature made with the new secret first and the old one after it, and your receiver may still only know one of them, so you want to accept if any entry matches. Outside a rotation there is just one entry and the loop runs once. Notice also that the timestamp must be all digits before it is parsed, which stops a malformed header from raising ValueError inside the verifier.
import hmac
from hashlib import sha256
def verify(body: bytes, ts: str, header: str, secret: str, now: int) -> bool:
if not secret or not ts.isdigit() or abs(now - int(ts)) > 300:
return False
digest = hmac.new(secret.encode(), ts.encode() + b"." + body, sha256).hexdigest()
expected = f"sume-v1={digest}".encode()
ok = False
for entry in header.split(","):
# bytes, not str: compare_digest raises TypeError on non-ASCII str
ok |= hmac.compare_digest(entry.strip().encode(), expected)
return ok
body, now = b'{"event":"job.completed"}', 1_780_000_000
sig = "sume-v1=" + hmac.new(b"s3cret", b"1780000000." + body, sha256).hexdigest()
print(verify(body, "1780000000", sig, "s3cret", now)) # True
print(verify(body, "1780000000", "sume-v1=é" + "a" * 63, "s3cret", now)) # False, no crash
print(verify(body, "1780000000", sig, "", now)) # False: empty secretWhy each line is there
The bytes comparison is the fix for the TypeError. The empty-secret check matters because HMAC with an empty key still produces a valid-looking digest, so a missing environment variable would otherwise verify forged requests built with the same empty key. The 300 second window is the documented default replay tolerance.
| Check | Guards against |
|---|---|
| Empty secret rejected | Unset environment variable accepting forgeries |
| abs(now - ts) > 300 rejected | Replay of a captured delivery |
| Bytes passed to compare_digest | TypeError on non-ASCII header |
| Loop over comma-separated entries | Secret rotation, where two signatures are sent |
Tradeoffs
Verify against the raw request bytes, not a re-serialized JSON object, or the digest will not match. Frameworks that parse the body first must give you the original bytes. The secret comes from GET /v1/webhooks/signing-secret and is per workspace.
To reproduce the original problem, call hmac.compare_digest with two str values where one contains an accented character; Python raises TypeError rather than returning False. Encoding to UTF-8 bytes first, as the sample does, makes both sides bytes, which compare_digest accepts for any content.
Sources
Related posts
More in Developers
- Python pre-flight for a Short: catch Timeline schema errors offline
A Python check for a Sume Timeline body: even width and height, allowed fps, 1 to 1800 seconds, fade limits and start order. Run it before the plan call.
- Python: run one prompt on four AI video models and save the clips
A Python script that sends one prompt to Wan 3.0, Seedance 2.5, Kling 3 and MiniMax H3 on Sume, polls all four jobs and saves each MP4, with costs.
- Python TTS cost calculator: Sume job rounding vs per-character rates
A runnable Python function that prices narration lines on Sume (cent rounding, 1-cent minimum) and at flat per-million rates for MAI-Voice-2.1 and Flash.
- Python urllib timeout vs Sume's 30 second sync wait
A client read timeout shorter than Sume's sync wait fails before the server answers. A tested demo of the timeout behavior and how to pick a margin above 30 s.
Written by Sume