Verifying X-Device-Signature by hand with CyberChef
When a webhook signature doesn't match, the first thing you need is to isolate which side of the comparison is wrong — yours or ours. CyberChef lets you run UnifyPort's exact signing algorithm in a browser, with the same inputs your handler is seeing, in about thirty seconds.
What UnifyPort actually signs
Every delivery that has a signing secret configured includes three headers:
The signature is computed as:
Notes worth pinning to a Post-it before you debug:
- The literal byte between timestamp and body is an ASCII dot (
.), not a newline. raw_bodymeans the bytes we sent — before any JSON parsing, whitespace stripping, or unicode normalization on your side.- The hex output is lowercase. If you compare against an uppercase digest, you'll false-negative.
The CyberChef recipe
In CyberChef, drop these two operations into the recipe pane, in order:
- 1. HMAC — set Hashing function to
SHA256. Paste your signing secret into the Key field, with Type set toUTF8. - 2. To Hex — leave Delimiter empty and Bytes per line at
0. This produces an unspaced lowercase hex string, matching the header.
In the Input pane, paste exactly <timestamp>.<raw_body> — for example:
The Output pane should now match the X-Device-Signature from the delivery. If it doesn't, you've isolated the bug to one of three places: a different secret, a body that's been mutated on the way in, or a timestamp you read from the wrong header.
When this manual check actually earns its keep
- After rotating a signing secret. Confirms the new secret reached your handler before you blame DNS or a stale deploy.
- After upgrading a JSON library. Some parsers re-emit the body before handing it to your verifier — easy to miss until signatures stop matching.
- When a teammate says "it works on my machine". Both of you run the same recipe with the same inputs; whoever gets a different digest is holding the wrong secret.
A word on pasting secrets into a webpage
CyberChef is a static client-side bundle — it runs entirely in your browser and the secret never leaves your machine in the official hosted version. That said, if you're nervous about browser extensions or shared sessions, run CyberChef locally (clone the repo, open index.html) or use DevToys — a fully offline desktop equivalent. For production signing-secret rotation, drive the comparison from code with constant-time compare, not by eyeballing CyberChef output.
Common questions
- How do I verify a UnifyPort webhook signature manually?
-
In CyberChef, add HMAC (Hashing function SHA256, your signing secret as Key with UTF8 encoding) followed by To Hex (delimiter empty, bytes per line 0). Paste
<timestamp>.<raw_body>exactly into the Input pane. The Output should match theX-Device-Signatureheader byte-for-byte. - Why doesn't my signature match what UnifyPort sends?
-
Three common causes: (1) you're using the wrong signing secret — confirm the value matches what you see in the dashboard; (2) the body was modified before verification — re-emitted JSON, whitespace stripping, or unicode normalization will all break the digest; (3) the timestamp was read from the wrong header — only
X-Device-Timestampis signed, not the framework's Date header. - Is it safe to paste my signing secret into CyberChef?
-
The official hosted CyberChef is a static client-side bundle — the secret never leaves your browser, and there's no server-side logging. If you still want zero exposure, clone the repository and open
index.htmllocally, or use DevToys — a fully offline desktop equivalent.
Once you've verified by hand
Drop the equivalent into your handler. Most languages ship the primitive in the standard library: crypto.createHmac in Node, hmac.new in Python, hmac.New in Go. Always compare with a constant-time function (timingSafeEqual, hmac.compare_digest, hmac.Equal) — never ==.