> ## Documentation Index
> Fetch the complete documentation index at: https://try.materi.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify webhook signatures (HMAC-SHA256)

> How to validate X-Hub-Signature-256 in webhook receivers (matches Shield)

Materi uses GitHub-style webhook signatures in several places: a request body is signed with
HMAC-SHA256, and the signature is carried in the `X-Hub-Signature-256` header as:

* `sha256=<hex>`

This recipe is aligned with the Shield implementation in `domain/shield/apps/cicd/webhooks.py`.

## Inputs

You need three pieces of data:

* `payload_bytes`: the raw HTTP request body bytes (before JSON parsing)
* `signature_header`: the header value from `X-Hub-Signature-256`
* `secret`: the shared secret configured on both sides

## Python

This matches the Shield implementation in `domain/shield/apps/cicd/webhooks.py`.

```python theme={null}
import hashlib
import hmac

def verify_signature(payload_bytes: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not secret:
        return False

    if "=" not in signature_header:
        return False

    algo, expected = signature_header.split("=", 1)
    if algo != "sha256":
        return False

    computed = hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, expected)
```

Common pitfalls:

* Verify against the **raw request body bytes** (before JSON parsing).
* Use constant-time comparison (`hmac.compare_digest`).

## Node.js

This matches the Shield implementation in `domain/shield/apps/cicd/webhooks.py`.

```js theme={null}
import crypto from 'node:crypto';

export function verifySignature(payloadBytes, signatureHeader, secret) {
    if (!signatureHeader || !secret) return false;
    const parts = signatureHeader.split('=');
    if (parts.length < 2) return false;

    const algo = parts[0];
    const expected = parts.slice(1).join('=');
    if (algo !== 'sha256') return false;

    const computed = crypto
        .createHmac('sha256', Buffer.from(secret, 'utf8'))
        .update(payloadBytes)
        .digest('hex');

    return crypto.timingSafeEqual(Buffer.from(computed, 'utf8'), Buffer.from(expected, 'utf8'));
}
```

Notes:

* Pass the **raw bytes** (e.g., `Buffer`), not a parsed JSON object.
* `timingSafeEqual` requires buffers of equal length; using the computed/expected hex strings avoids
  length mismatches unless the header is malformed.

## Receiver checklist

* Reject missing/invalid signatures with `401`.
* Verify against raw bytes.
* Use constant-time comparison.
* Log only high-level info (avoid logging secrets or full payloads in production).
