"""Verify a CallQA webhook signature (Python 3.9+, stdlib only). CallQA signs every delivery with the endpoint secret (``whsec_...``):: CallQA-Signature: t=,v1=.")> Rules: use the RAW request body (bytes, before any JSON parsing), compare in constant time, reject timestamps older than 5 minutes. Run a tiny receiver locally:: CALLQA_WEBHOOK_SECRET=whsec_... python verify_webhook.py # listens on :8080 """ from __future__ import annotations import hashlib import hmac import json import os import time from http.server import BaseHTTPRequestHandler, HTTPServer TOLERANCE_S = 300 class InvalidSignature(Exception): pass def verify(secret: str, header: str | None, body: bytes, tolerance_s: int = TOLERANCE_S, now: float | None = None) -> dict: """Return the parsed event, or raise InvalidSignature.""" if not header: raise InvalidSignature("missing CallQA-Signature header") ts, signatures = None, [] for part in header.split(","): key, _, value = part.strip().partition("=") if key == "t": ts = value elif key == "v1": signatures.append(value) if not ts or not ts.isdigit() or not signatures: raise InvalidSignature("malformed CallQA-Signature header") now = time.time() if now is None else now if abs(now - int(ts)) > tolerance_s: raise InvalidSignature("timestamp outside the tolerance window") expected = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest() if not any(hmac.compare_digest(expected, s) for s in signatures): raise InvalidSignature("signature mismatch") return json.loads(body) class Receiver(BaseHTTPRequestHandler): secret = os.environ.get("CALLQA_WEBHOOK_SECRET", "") seen: set[str] = set() def do_POST(self): # noqa: N802 body = self.rfile.read(int(self.headers.get("Content-Length") or 0)) try: event = verify(self.secret, self.headers.get("CallQA-Signature"), body) except (InvalidSignature, ValueError) as exc: self.send_response(400) self.end_headers() self.wfile.write(str(exc).encode()) return # Deliveries are retried: deduplicate on the event id. if event["id"] not in self.seen: self.seen.add(event["id"]) print(f"{event['type']}: {json.dumps(event['data'])}") self.send_response(204) # answer 2xx fast, do slow work asynchronously self.end_headers() if __name__ == "__main__": if not Receiver.secret: raise SystemExit("set CALLQA_WEBHOOK_SECRET") HTTPServer(("0.0.0.0", 8080), Receiver).serve_forever() # noqa: S104