HMAC and Constant-Time Compare in Crypto Webhooks Done Right

HMAC and Constant-Time Compare in Crypto Webhooks Done Right
Carolyn Lowe 23 July 2026 0 Comments

Picture this: your crypto payment gateway detects a transaction on-chain. It fires off a webhook to your server telling you that an invoice is paid. Your backend receives the request, parses the JSON, updates the user's account, and sends out a confirmation email. Everything looks fine.

But what if that webhook didn't come from your payment provider? What if it came from a script kiddie who found your endpoint URL in a public GitHub repo? Without proper verification, your server just handed them admin access or credited their account with free premium features.

This is why HMAC (Hash-based Message Authentication Code) combined with constant-time comparison is the gold standard for securing webhooks in 2026. It’s not just about checking a signature; it’s about doing so in a way that doesn’t leak information to attackers listening on the network.

The Anatomy of a Secure Webhook Signature

At its core, an HMAC is a cryptographic checksum. It takes two inputs: the message payload (the raw HTTP body) and a secret key known only to the sender and the receiver. The output is a fixed-length string of characters-the signature.

If anyone changes even a single character in the payload-adding a space, changing a price from $10 to $100-the resulting HMAC will be completely different. This ensures integrity. But how do you know the sender is who they say they are? That’s where authentication comes in. Since the attacker doesn’t have the secret key, they can’t generate a valid HMAC for their forged payload.

Most modern crypto payment providers, including platforms like TxNod which uses HMAC-signed webhooks for reliable payment notifications, Stripe, and GitHub, use SHA-256 as the hashing algorithm. This produces a 256-bit digest that is currently considered computationally infeasible to reverse-engineer.

  • Algorithm: HMAC-SHA256 is the industry standard. Avoid MD5 or SHA-1 due to collision vulnerabilities.
  • Secret Key: A random string generated by the provider (e.g., starting with `whsec_` for Stripe). Keep this safe.
  • Payload: The exact raw bytes of the HTTP request body before any parsing.

Why Naive String Comparison Is Dangerous

Here is the part most developers get wrong. You compute the expected HMAC on your server. You extract the received HMAC from the HTTP header. Now you need to compare them.

The instinctive move is to use a standard equality operator like `==`, `===`, or `.equals()`. In almost every programming language, these operators short-circuit. They stop comparing as soon as they find the first mismatched byte.

This creates a timing side-channel. If the first byte matches, the comparison takes slightly longer than if it doesn’t. An attacker can send millions of requests with different signatures, measuring the response time down to the microsecond. By analyzing these tiny delays, they can deduce the correct signature one byte at a time. This is called a timing attack.

In a crypto billing context, where high-value transactions happen instantly, this isn't theoretical. If an attacker can forge a valid webhook signature, they can trigger arbitrary actions on your backend-refunding payments, upgrading accounts, or draining wallets.

Implementing Constant-Time Comparison

To fix this, you must use a constant-time comparison function. These functions iterate through every byte of both strings regardless of whether they match early on. The execution time remains identical whether the signatures are identical or completely different.

Here is how you do it in the most common languages used for crypto backends:

Constant-Time Comparison Functions by Language
Language / Runtime Function Name Important Notes
Node.js crypto.timingSafeEqual() Requires inputs to be Buffers of equal length. Throws error if lengths differ.
Python hmac.compare_digest() Works with strings or bytes. Handles variable lengths safely.
Ruby (Rails) ActiveSupport::SecurityUtils.secure_compare Designed specifically for secrets and digests.
Go crypto/subtle.ConstantTimeCompare Returns boolean. Ensure slices are same length before calling.
.NET / C# CryptographicOperations.FixedTimeEquals Part of System.Security.Cryptography.
Cryptographic key under magnifying glass showing timing gaps

The Verification Flow Step-by-Step

Getting HMAC right involves more than just the comparison function. You need to handle the raw data correctly. Here is the robust flow you should implement in your webhook handler:

  1. Capture Raw Body: Before your framework parses the JSON, capture the raw HTTP body bytes. Many frameworks transform the body (changing whitespace or key order), which breaks the HMAC calculation.
  2. Extract Headers: Pull the signature header (e.g., X-Hub-Signature-256) and the timestamp header (if provided).
  3. Reconstruct Signed String: Some providers sign a concatenation of the timestamp and the body (e.g., `.`). Check your provider's documentation. TxNod, for instance, follows standard practices where the signature covers the payload integrity.
  4. Compute Expected HMAC: Use your stored secret key and the reconstructed string to generate the expected HMAC using SHA-256.
  5. Validate Timestamp: If the provider includes a timestamp, check if it falls within a reasonable window (e.g., +/- 5 minutes). This prevents replay attacks where an old valid webhook is resent to trick your system.
  6. Constant-Time Compare: Use the language-specific secure comparison function to check the expected HMAC against the received HMAC.
  7. Process Payload: Only if all checks pass, parse the JSON and update your database. Return a 200 OK status immediately to acknowledge receipt, then process the logic asynchronously if needed.

Common Pitfalls to Avoid

Even experienced developers stumble here. Watch out for these specific issues:

Using the Wrong Secret: Providers often have multiple keys. Don't confuse your API key with your webhook signing secret. For example, Stripe uses keys prefixed with sk_live_ for APIs but whsec_ for webhooks. Using the wrong one will always result in a signature mismatch.

Encoding Mismatches: HMAC outputs are binary. Providers usually encode them as hexadecimal strings. Ensure you are comparing hex strings to hex strings, or base64 to base64. Mixing encodings will cause false negatives.

Ignoring Replay Attacks: An attacker might record a legitimate webhook and resend it later. Without timestamp validation, your server would accept it as new. Always enforce a strict time window for acceptance.

Logging Secrets: Never log the raw body or the secret key in production logs. If you need to debug signature mismatches, log the length of the strings or the first few characters, but mask the rest.

Secure data block passing HMAC verification gate

Idempotency Is Your Safety Net

Webhooks are unreliable by nature. Networks drop packets. Servers crash. Providers like TxNod will retry failed webhooks exponentially over time. If your code processes the same webhook twice, you might double-charge a user or grant duplicate rewards.

You must make your webhook handlers idempotent. This means processing the same event multiple times has the same effect as processing it once. The easiest way to do this is to store the unique webhook ID or event ID in your database before processing. If you see that ID again, skip the logic and return success.

FAQ

What happens if I use == instead of constant-time comparison?

Your application becomes vulnerable to timing attacks. Attackers can measure the time it takes for your server to respond to different signatures. Because standard equality checks stop at the first mismatch, responses with more matching bytes take slightly longer. Over many requests, an attacker can reconstruct your secret signature byte-by-byte.

Do I need to parse the JSON before verifying the HMAC?

No. In fact, you should verify the HMAC before parsing the JSON. Parsing libraries often normalize whitespace or reorder keys, which changes the raw bytes. Since the HMAC is calculated on the exact raw bytes sent by the provider, any transformation will cause the verification to fail.

How long should my HMAC secret key be?

A minimum of 32 bytes (256 bits) is recommended for SHA-256. Most providers generate these keys for you automatically when you set up a webhook endpoint. Store this key securely in environment variables, never in your source code.

Why is timestamp validation important for webhooks?

Timestamp validation prevents replay attacks. If an attacker intercepts a valid webhook request, they could resend it later to trigger the same action again. By rejecting requests older than a certain window (e.g., 5 minutes), you ensure that only fresh events are processed.

Can I use MD5 for webhook signatures?

It is highly discouraged. MD5 is cryptographically broken and susceptible to collision attacks. SHA-256 is the current industry standard for HMACs because it provides a much higher level of security assurance against brute-force and collision attacks.

Similar Posts

HMAC and Constant-Time Compare in Crypto Webhooks Done Right

Learn how to secure crypto webhooks using HMAC-SHA256 and constant-time comparison to prevent timing attacks and replay exploits.