webhook-verify
# webhook-verify
Timing-safe, replay-resistant webhook signature verification for **Stripe, GitHub, Slack, Shopify and Twilio**.
```
29 passed
```
Standard library only. No dependencies, no network, no telemetry.
## Why
Webhook verification is the one piece of security code almost every backend writes itself, and it goes
wrong in four predictable ways:
| Mistake | Consequence |
|---|---|
| `signature == expected` | Byte-by-byte comparison leaks the position of the first wrong byte through timing. Forgeable given enough attempts. |
| No timestamp check | A captured valid request stays valid forever. Replay it tomorrow and it still passes. |
| Verifying the parsed body | You re-serialise, a key order or whitespace changes, and the HMAC no longer matches — so people "fix" it by skipping verification. |
| Returning `False` on failure | A caller writes `if verify(...)` and a truthy string or `None` slips through. |
This library does the opposite of each: `hmac.compare_digest` everywhere, replay windows on every
provider that signs a timestamp, verification against the **raw** body, and a specific exception on
every failure so nothing can be accidentally treated as success.
## Use
```python
from webhookverify import verify_stripe, SignatureMismatch, TimestampOutOfRange
try:
verify_stripe(secret, raw_body, request.headers["Stripe-Signature"])
except TimestampOutOfRange:
return 400, "replayed or clock-skewed"
except SignatureMismatch:
return 401, "not from Stripe"
# only now is it safe to parse raw_body
```
Supported: `verify_stripe`, `verify_github`, `verify_slack`, `verify_shopify`, `verify_twilio`,
and `verify_hmac` for anything else.
Stripe secret rotation is handled: a header carrying several `v1=` signatures passes if any one matches.
## MCP server
`mcp/server.py` exposes one tool, `verify_webhook`, over JSON-RPC 2.0 stdio (protocol `2024-11-05`).
It returns `valid:true`, or `valid:false` with a precise reason (`signature_mismatch`,
`timestamp_out_of_range`, `malformed_signature`) and the instruction not to parse the payload.
```json
{ "mcpServers": { "webhook-verify": { "command": "python3", "args": ["/path/to/webhook-verify/mcp/server.py"] } } }
```
## Tested behaviour
- Valid signatures pass for all five providers
- Tampered bodies, wrong secrets and truncated signatures raise `SignatureMismatch`
- Replayed and future-dated timestamps raise `TimestampOutOfRange` (the window is symmetric)
- Malformed headers raise `MalformedSignature` — missing prefix, bad base64, non-hex, non-numeric timestamp
- Stripe multi-`v1` rotation passes when any signature matches
- A source-level guard asserts the module never regresses to `==` on digests and always length-checks
before `compare_digest`
```bash
python -m pytest tests -q
```
## License
MIT
TDQS
Scored across 1 tool
With only a single tool, there is no possibility of confusing it with another. The tool's purpose is fully specified and self-contained, leaving no ambiguity about when to invoke it.
The tool name follows a clear verb_noun convention with snake_case, which is consistent and predictable. Even though there is only one tool, the pattern aligns with common standards and leaves no room for stylistic mismatch.
The server is purpose-built for webhook signature verification, and one focused tool is exactly the right scope. There is no unnecessary bloat, and the single tool fully addresses the server's intended functionality without feeling thin.
The tool covers the entire lifecycle of webhook verification: timing-safe comparison, replay protection, and detailed failure reasons. No additional tools appear needed for the stated purpose, making the surface complete for its domain.