Skip to content

Products

Compliance Officer Service Expert-led compliance, end to end Compliance Portal Share security documents securely Open-source platform Deploy Probo on your own infrastructure

Resources

Probo stories How teams get compliant with Probo Blog Ideas and guidance from the Probo team Guides & tools Practical compliance guides and free tools Love from Customers What customers say about working with Probo Changelog Latest product updates Download Get the Probo Agent

Company

About The people and vision powering Probo Careers Join the team building Probo Brand assets Official logos and visual resources Security Review our security and compliance posture
Overview Understand Probo and its core concepts Product Explore Probo's GRC capabilities Developers Explore GraphQL, CLI, MCP, n8n, and webhooks Deployment Probo Cloud, self-hosting, and configuration

Explore

GitHub Explore our open-source compliance tools

Signature Verification

Verify that webhook requests are genuinely from Probo

View as Markdown

Every Probo webhook includes an HMAC signature. Verify it before parsing the body or performing side effects. Signature verification proves that the payload and timestamp were produced with your subscription’s signing secret; a timestamp freshness check limits replay attacks.

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

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

{timestamp}:{body}

Where:

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

Use the full signing secret string (including the whsec_ prefix) as the HMAC key. Do not strip the prefix or hex-decode the secret.

  1. Extract the headers

    Read X-Probo-Webhook-Timestamp and X-Probo-Webhook-Signature from the request.

  2. Build the signed message

    Concatenate the timestamp, a colon (:), and the raw request body.

  3. Compute the expected signature

    Calculate HMAC-SHA256 using your full signing secret (including the whsec_ prefix) as the key and the signed message as the input. Hex-encode the result.

  4. Compare signatures

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

  5. Check timestamp freshness

    After the signature matches, reject the request if its timestamp is more than 5 minutes in the past or future. The signed timestamp prevents an attacker from substituting a fresh value.

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
}
  • Verify the signature before parsing JSON, authorizing the organization, or queuing work.
  • Reject missing, malformed, stale, and future-dated timestamps. The examples use a 5-minute tolerance.
  • Compare decoded signature bytes in constant time. Check their length first where the comparison API requires equal-length inputs.
  • Keep a separate secret for each subscription and store it in a secret manager.
  • Return a generic 400 or 403 response. Do not reveal which verification check failed.
  • Record eventId after verification and process it only once. Timestamp validation limits replay time; idempotency prevents duplicate side effects.
Symptom Likely cause
Every signature fails The framework parsed or modified the body before verification
Only non-ASCII payloads fail The receiver decoded and re-encoded the body instead of hashing raw bytes
timingSafeEqual throws The received signature was not validated as 32-byte hexadecimal first
Valid deliveries are reported as stale The receiver clock is out of sync or the timestamp was treated as milliseconds
Verification works with one subscription only The endpoint is selecting the wrong subscription secret