# Verifying Webhooks

When receiving webhooks, the signing operation has already been performed by the platform and you must validate the
signature we provide to ensure the webhook originated from us.

When creating a subscription (using the subscription API), you are provided a `signature_verification_key`. This value
is a base64‑encoded symmetric secret used to compute an HMAC-SHA256 signature for each webhook payload. You must persist
this secret securely at subscription creation time.

**NOTE**: The returned `signature_verification_key` is base64-encoded binary. In the code sample below we base64‑decode it
to raw bytes (you do NOT re‑encode it). You may display it in hex for debugging, but hex is not required for
verification.

When a webhook is dispatched:

- We include `X-Webhook-Timestamp` (epoch milliseconds).
- We include `X-Webhook-Signature` in the form: `t=<timestamp>,v1=<hex_hmac_sha256>`.
- You reconstruct the exact signing string and verify.


### Verification is a 2 step process:

| Step | Action | Description |
|  --- | --- | --- |
| 1 | Reconstruct | Concatenate the timestamp, a dot (`.`), and the SHA256 hex digest of the raw request body: `<timestamp>.<sha256(body)>` |
| 2 | Verify | HMAC-SHA256 the signing string with the decoded secret; compare the resulting hex digest to `v1` (constant-time). |


#### Fields Used in the Signature

| Field | Description |
|  --- | --- |
| Timestamp | Raw value from `X-Webhook-Timestamp` (epoch millis as a string). |
| Body | The exact raw HTTP request body bytes (no parsing / reformatting). |
| body_hash | `SHA256` hex digest of the raw body. |
| signing_string | `<timestamp>.<body_hash>` |
| Secret | Base64-decoded `signature_verification_key`. |
| Signature | Hex digest of `HMAC_SHA256(secret, signing_string)` placed in `v1`. |


If any of these values are transformed (e.g. JSON pretty-printed, numeric formatting changed, timestamp altered) the
verification will fail.

#### Verification Example


```python
import base64
import hashlib
import hmac
import time
from typing import Optional

def verify_webhook(raw_body: bytes,
                   timestamp: str,
                   signature_header: str,
                   base64_secret: str,
                   max_age_seconds: int = 300) -> bool:
    """
    Verify an incoming webhook.

    Parameters:
      raw_body:        Exact raw request body bytes (no mutation).
      timestamp:       X-Webhook-Timestamp header (epoch millis as string).
      signature_header:X-Webhook-Signature header in form "t=<timestamp>,v1=<hex>".
      base64_secret:   subscription signature_verification_key (base64).
      max_age_seconds: Allowed clock skew / replay window (0 disables freshness check).

    Returns:
      True if signature is valid, else False.
    """
    if not (raw_body and timestamp and signature_header and base64_secret):
        return False

    # Parse signature header
    parts = {}
    try:
        for seg in signature_header.split(","):
            k, v = seg.split("=", 1)
            parts[k.strip()] = v.strip()
    except Exception:
        return False

    if parts.get("t") != timestamp or "v1" not in parts:
        return False

    # Freshness (optional)
    if max_age_seconds > 0 and timestamp.isdigit():
        try:
            ts_int = int(timestamp)
            # Detect millis
            if ts_int > 1_000_000_000_000:
                ts_int //= 1000
            if abs(int(time.time()) - ts_int) > max_age_seconds:
                return False
        except ValueError:
            return False

    # SHA256 hex of body
    body_hash = hashlib.sha256(raw_body).hexdigest()
    signing_string = f"{timestamp}.{body_hash}".encode("utf-8")

    # Decode secret (single base64 decode only)
    try:
        secret = base64.b64decode(base64_secret, validate=True)
    except Exception:
        return False

    expected = hmac.new(secret, signing_string, hashlib.sha256).hexdigest()
    provided = parts["v1"]
    # Constant-time compare
    return hmac.compare_digest(expected, provided)
```

#### Flask Usage Example


```python
from flask import Flask, request, Response
import os

from verifier import verify_webhook  # assuming the function above saved as verifier.py

app = Flask(__name__)
SECRET = os.environ.get("WEBHOOK_SECRET_VERIFICATION_KEY")  # set from secure config

@app.post("/webhook")
def webhook():
    raw_body = request.get_data()  # exact bytes
    ts = request.headers.get("X-Webhook-Timestamp", "")
    sig = request.headers.get("X-Webhook-Signature", "")
    if not verify_webhook(raw_body, ts, sig, SECRET, max_age_seconds=300):
        return Response("invalid signature", status=400)
    # Safe to parse AFTER verification
    return Response("ok", status=200)
```

#### Key Points

- Always read raw bytes first; only parse JSON after verification.
- Do not pretty-print or modify the JSON before hashing.
- Reject if age window exceeded or headers malformed.
- Use `hmac.compare_digest` for constant-time comparison.


#### Common Pitfalls

| Issue | Cause | Resolution |
|  --- | --- | --- |
| signature_mismatch | Secret double-base64 encoded | Use original subscription value directly |
| signature_mismatch | Body re-serialized / pretty-printed | Hash exact raw bytes |
| signature_mismatch | Timestamp mismatch between `t` and header | Ensure they match verbatim |
| Stale timestamp | Clock skew or delayed processing | Check system clock; adjust max age |
| Invalid header format | Missing `t=` or `v1=` segment | Ensure exact format `t=...,v1=...` |


#### Checklist

- [ ] Captured raw body (not parsed then re-stringified)
- [ ] Extracted `X-Webhook-Timestamp`
- [ ] Parsed `X-Webhook-Signature` → t & v1
- [ ] Computed SHA256 hex of raw body
- [ ] Built signing string `<timestamp>.<body_hash>`
- [ ] Base64-decoded stored secret
- [ ] HMAC-SHA256 computed & constant-time compared
- [ ] (Optional) Freshness window passed


If every box is checked and comparison succeeds, the webhook is authentic.