HMAC signature
An HMAC signature is a keyed hash attached to a message, computed from the message contents and a secret shared between sender and receiver. Because only holders of the secret can produce a matching value, a correct signature demonstrates both that the message came from a holder of the key and that it has not been modified in transit.
| Mistake | Why it happens | Consequence |
|---|---|---|
| Verifying against re-serialised JSON | The web framework parses the body before your handler sees it | Any difference in key order, whitespace or number formatting breaks valid signatures — and teams then "fix" it by disabling verification |
Comparing with == | String comparison is the obvious thing to write | Early-exit comparison leaks byte-by-byte timing, allowing a forged signature to be discovered incrementally over many requests |
| No timestamp tolerance check | The signature verified, so the request looks fine | A captured request can be replayed indefinitely — every replay carries a genuine signature |
| Accepting unsigned requests as a fallback | Added during integration testing and never removed | The entire control is optional at the attacker's discretion |
| Logging the secret or the raw header at debug level | Debugging a failing integration | The key ends up in log aggregation, in a support ticket, or in a screenshot |
Anatomy of a signing scheme
The common construction, used by Stripe and adopted by Kastr, signs a composite string rather than the body alone:
signature = HMAC-SHA256(secret, "${timestamp}.${rawBody}"), transmitted as X-Kastr-Signature: sha256=<hex> alongside the timestamp.
Each part earns its place. The raw body — the exact bytes received — is signed because any re-serialisation changes the bytes and therefore the hash. The timestamp is inside the signed material rather than sent alongside it, because a timestamp the receiver checks but does not verify can simply be rewritten by an attacker replaying a captured request. The delimiter prevents an ambiguity attack in which different timestamp and body pairs concatenate to the same string. The algorithm prefix in the header allows a future migration without a flag day.
Verification, in order: read the raw body before any parsing middleware touches it; check the timestamp is within tolerance (five minutes is conventional); recompute the HMAC over timestamp.rawBody; compare using a constant-time function such as Node's timingSafeEqual; only then parse the JSON.
Why it matters to a district
Any system that accepts callbacks from a vendor is accepting instructions from the public internet. If your student information system, data warehouse or notification middleware exposes an endpoint that a communications platform posts delivery results to, then without signature verification anyone who learns that URL can post fabricated delivery results, fabricated opt-outs, or fabricated roster events into your systems.
This is the question worth putting into a security questionnaire in a specific form: not "are webhooks secure" but "are outbound webhooks signed with an HMAC over the raw body and a timestamp, and is the signing secret rotatable without missed deliveries?" A vendor whose answer is "we use HTTPS" has confused transport encryption with origin authentication. TLS proves you are talking to the host in the certificate; it says nothing about whether the payload was produced by the party you have a contract with.
Rotating a signing secret without losing deliveries
Rotation is where most implementations fail, because the naive procedure — change the secret on both sides at the same moment — guarantees a window of rejected deliveries.
- The receiver begins accepting either the old or the new secret. Verify against both; accept if either matches.
- The sender switches to signing with the new secret only.
- After a period comfortably longer than the maximum retry window, the receiver stops accepting the old secret.
Note the ordering constraint: the receiver must accept both before the sender switches, and the retry schedule sets the minimum length of step 3. With retries spread across roughly six hours, retiring the old secret after one hour would reject a delivery that was first attempted before the switch.
The common misconception
"An HMAC signature is the same as a digital signature." It is not. HMAC uses a symmetric secret: both parties hold the same key, and either can produce a valid signature. That means a correct HMAC proves the message came from someone with the key, but it cannot prove to a third party which of the two produced it — there is no non-repudiation. A digital signature using asymmetric keys can prove that, at higher cost and complexity. For webhook authentication between two parties who already have a contract, HMAC is the right tool; for a claim you intend to prove to a court, it is not.
Kastr signs every outbound webhook this way, pairs it with a DNS-resolving SSRF guard on the destination, retries on a [1, 2, 5, 15, 60, 360]-minute backoff and disables an endpoint after ten consecutive failures. The MIT-licensed CLI includes the verification logic, so you can read exactly what we compute rather than take our description of it.
Questions people actually ask
Why sign the raw body instead of the parsed JSON?
Because parsing and re-serialising changes bytes — key order, whitespace, numeric formatting, unicode escaping — and the hash changes with them. Signing the raw bytes is the only construction where sender and receiver reliably compute the same value. In practice this means capturing the body before your framework's JSON middleware consumes it.
What is a timing-safe comparison and why does it matter?
An ordinary string comparison returns as soon as it finds a differing byte, so a wrong signature that shares a longer prefix takes measurably longer to reject. Over enough requests an attacker can use that timing difference to construct a valid signature byte by byte. A constant-time comparison always examines the full length, removing the signal.
How long should a signature timestamp be considered valid?
Five minutes is the usual tolerance: long enough to survive clock skew and network delay, short enough that a captured request cannot be replayed later. The timestamp must be part of the signed material, otherwise an attacker replaying a captured request simply rewrites it.
How do you rotate a webhook signing secret safely?
Accept both secrets on the receiver first, then switch the sender to the new one, then retire the old one after a period longer than the full retry window. Reversing the first two steps guarantees rejected deliveries; retiring too early rejects retries of messages first attempted before the switch.
One price. Every feature. Locked for three years.
$3.50 per student per year under 5,000 students. No tiers, no add-on modules, no per-message fees. Published on the site because you should not have to book a call to learn a price.