# Signature Verification

Fiecare platformă webhook include o semnătură HMAC. Verificați-o înainte de a analiza corpul sau de a efectua efecte secundare. Verificarea semnăturilor dovedește că sarcina de utilizare și timestamp-ul au fost produse cu secretul de semnătură al abonamentului; un timestamp verifică prospețimea limitează atacurile de replay.

:::pericol[Verificați corpul cererii brute] Utilizați byte-urile exacte primite de la rețea. Parsarea și re-serializarea JSON poate schimba spatiul alb sau ordinea cheii și va produce o semnătură diferită. Configurați-vă cadrul pentru a expune corpul brut înainte de a rula middleware-ul său JSON. :::

## How it works

the platform 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.

Mesajul semnat este concatenarea timestamp-ului și a corpului de cerere brut, separat de un 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.

## Verification steps

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**

   După ce semnătura se potrivește, respingeți solicitarea dacă timestamp-ul său este mai mare de 5 minute în trecut sau în viitor.

## Examples

  

```go
package main

	"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
}
```

  
  

```python

def verify_webhook(
    body: bytes,
    timestamp: str | None,
    signature: str | None,
    signing_secret: str,
) -> bool:
    if (
        timestamp is None
        or signature is None
        or re.fullmatch(r"[0-9]+", timestamp) is None
    ):
        return False

    expected = hmac.new(
        signing_secret.encode(),
        timestamp.encode("ascii") + b":" + body,
        hashlib.sha256,
    ).digest()

    if re.fullmatch(r"[0-9a-fA-F]{64}", signature) is None:
        return False
    if not hmac.compare_digest(expected, bytes.fromhex(signature)):
        return False

    signed_at = int(timestamp)
    return abs(time.time() - signed_at) <= 300
```

  
  

```javascript

function verifyWebhook(rawBody, timestamp, signature, signingSecret) {
  if (
    !Buffer.isBuffer(rawBody) ||
    typeof timestamp !== "string" ||
    typeof signature !== "string" ||
    !/^[0-9]+$/.test(timestamp) ||
    !/^[0-9a-fA-F]{64}$/.test(signature)
  ) {
    return false;
  }

  const expected = createHmac("sha256", signingSecret)
    .update(timestamp)
    .update(":")
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature, "hex");

  if (
    expected.length !== received.length ||
    !timingSafeEqual(expected, received)
  ) {
    return false;
  }

  const signedAt = Number(timestamp);
  return (
    Number.isFinite(signedAt) && Math.abs(Date.now() / 1000 - signedAt) <= 300
  );
}
```

  

## Security recommendations

- 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.

## Troubleshooting

| 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                        |
