Skip to content
DaksCoder23Public

About

Signed webhooks for Python: HMAC-SHA256, replay protection, secret rotation, safe retries and receiver dedupe.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

hooksign

Signed webhooks for Python, done the boring, correct way: HMAC-SHA256 signatures, replay protection, zero-downtime secret rotation, retries that are safe to repeat, and receiver-side duplicate suppression. One dependency (httpx).

receiver: simulating an outage, answering 503
receiver: verified evt_demo_1, processing
sender: delivered=True attempts=2 history=[503, 200]
sender: replaying the same event (as a retry after a lost ack would)
receiver: event evt_demo_1 already processed, ignoring

That is the real output of examples/demo.py.

Why

Most webhook bugs are not about the HMAC. They are about everything around it:

Problem What hooksign does
Receiver re-serialises JSON before checking, signature never matches Verifies the raw body bytes and says so in the API
Captured request is replayed later Timestamp is inside the signed string; stale or future timestamps are rejected (default 300s)
Rotating a secret breaks every receiver at once Sender can sign with several secrets; receiver can accept several. Rotate in two steps, no cutover
Sender retries, receiver processes the event twice Same event id and body on every attempt, plus an idempotency store on the receiver
Retrying a 400 forever Only 429, 5xx and network errors are retried, with capped exponential backoff and jitter; Retry-After is honoured
Forged requests fill the dedupe store The duplicate check runs only after the signature is valid
Timing attacks on comparison hmac.compare_digest

Install

pip install git+https://github.com/DaksCoder23/hooksign

Sender

from hooksign import build_event, deliver

event = build_event("invoice.paid", {"amount": 4200})
result = deliver("https://customer.example.com/hooks", event, "whsec_...")
print(result.delivered, result.attempts, result.history)

Headers sent:

Webhook-Id: evt_...
Webhook-Signature: t=<unix seconds>,v1=<64 hex chars>

The signature is HMAC_SHA256(secret, "<t>.<raw body>") as lowercase hex, so a receiver in any language can verify it in a few lines.

Receiver

from hooksign import DuplicateEventError, MemoryIdempotencyStore, VerificationError, verify_request

store = MemoryIdempotencyStore()

def handle(raw_body: bytes, headers: dict) -> int:
    try:
        event_id = verify_request(raw_body, headers, ["whsec_new", "whsec_old"], store=store)
    except DuplicateEventError:
        return 200   # already processed; 2xx stops the sender retrying
    except VerificationError:
        return 400
    enqueue(event_id, raw_body)  # return fast, do the work elsewhere
    return 200

A FastAPI version is in examples/fastapi_receiver.py. MemoryIdempotencyStore is per process; with several workers, back the same check_and_remember method with Redis SET key 1 NX EX ttl or a unique database index.

Rotating a secret

  1. Receiver: accept [new, old].
  2. Sender: sign with [new, old] (the header carries two v1 values).
  3. Sender: sign with new only.
  4. Receiver: drop old.

At no point does a correctly configured side reject a valid delivery.

CLI

echo '{"x":1}' | hooksign sign --secret whsec_demo
echo '{"x":1}' | hooksign verify --secret whsec_demo --header "t=...,v1=..."
hooksign test-fire --url https://staging.example.com/hooks --secret whsec_demo

test-fire sends a signed test.ping event, which is the quickest way for a customer to check their endpoint before going live.

Development

python -m venv .venv && .venv/Scripts/python -m pip install -e ".[dev]"   # bin/ on macOS/Linux
python -m pytest        # 28 tests, all offline (httpx.MockTransport)
python -m ruff check .
python examples/demo.py

Limitations

  • The bundled idempotency store is in-memory and per process.
  • Delivery is synchronous; for many endpoints, run deliver from a worker queue.
  • No asymmetric (Ed25519) signatures yet; every receiver shares a secret with the sender.

License

MIT

About

Signed webhooks for Python: HMAC-SHA256, replay protection, secret rotation, safe retries and receiver dedupe.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages