jump to content

Signature Verification

How to check webhook platform signatures with HMAC-SHA256 over the raw body and timestamp, with examples of working in Go, Python, and JavaScript.

Show as Markdown

Each webhook platform includes a HMAC signature. Check it before you analyze the body or perform side effects. Signature verification proves that the usage load and timestamp were produced with the subscription signature secret; a timestamp checks freshness limits replay attacks.

the platform signs each webhook payload using HMAC-SHA256 with the signature secret from your webhook subscription. The signature is sent to the header X-Probo-Webhook-Signature.

The signed message is the concatenation of the timestamp and the gross application body, separated by a colon:

{timestamp}:{body}

Where:

  • timestamp is the value in the header X-Probo-Webhook-Timestamp (Unix second)
  • body is the raw JSON request body

Use the full signing secret string (including the prefix whsec_) as the HMAC key.

  1. Extract the headers

    Read X-Probo-Webhook-Timestamp and X-Probo-Webhook-Signature in the application.

  2. Build the signed message

    Connect the timestamp, a colon (:) and the gross request body.

  3. Compute the expected signature

    Calculate HMAC-SHA256 using the full signature secret (including the prefix whsec_) as the key and the signed message as the input.

  4. Compare signatures

    Use a constant time comparison to check if the calculated signature matches the X-Probo-Webhook-Signature header.

  5. Check timestamp freshness

    Once the signature matches, reject the request if its timestamp is more than 5 minutes in the past or in the future.

package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
func verifyWebhook(r *http.Request, signingSecret string) ([]byte, error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
timestamp := r.Header.Get("X-Probo-Webhook-Timestamp")
signature := r.Header.Get("X-Probo-Webhook-Signature")
if timestamp == "" || signature == "" {
return nil, fmt.Errorf("missing signature headers")
}
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write([]byte(timestamp))
mac.Write([]byte(":"))
mac.Write(body)
received, err := hex.DecodeString(signature)
if err != nil || !hmac.Equal(mac.Sum(nil), received) {
return nil, fmt.Errorf("invalid signature")
}
signedAt, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid timestamp")
}
delta := time.Now().Unix() - signedAt
if delta > 300 || delta < -300 {
return nil, fmt.Errorf("stale timestamp")
}
return body, nil
}
  • Check your signature before you analyze JSON, authorize your organization, or sequence work.
  • Reject missing, deformed, outdated and dated timestamps in the future. Examples use a 5-minute tolerance.
  • First check their length, where the comparison requires entries of equal length.
  • Keep a separate secret for each subscription and store it in a secret manager.
  • Return a general answer 400 or 403. Do not disclose which verification failed.
  • It records eventId after checking and processes it once. timestamp validation limits playtime; idempotency prevents duplicate side effects.
Symptom Likely cause
Every signature fails The frame breaks or changes the body before checking
Only non-ASCII payloads fail The receiver decoded and re-coded the body instead of hashing raw bytes
timingSafeEqual throws The received signature was not first validated as 32-byte hexadecimal
Valid deliveries are almost as stands The receiver clock is not synchronized or the timestamp has been treated as milliseconds
Verification works with only one subscription The end point is selecting the wrong subscription secret

Ultima actualizare: