Validates the HMAC signature and timestamp of an incoming webhook request. Ensures the request was sent by Hume and has not been tampered with or replayed. Args: payload: The raw request payload as a string. headers: The headers from the incoming request. Raises:
(payload: str, headers: Headers)
| 51 | |
| 52 | |
| 53 | def validate_webhook_headers(payload: str, headers: Headers) -> None: |
| 54 | """ |
| 55 | Validates the HMAC signature and timestamp of an incoming webhook request. |
| 56 | Ensures the request was sent by Hume and has not been tampered with or replayed. |
| 57 | |
| 58 | Args: |
| 59 | payload: The raw request payload as a string. |
| 60 | headers: The headers from the incoming request. |
| 61 | |
| 62 | Raises: |
| 63 | ValueError: If headers are missing, the signature is invalid, or the timestamp is stale. |
| 64 | """ |
| 65 | timestamp = headers.get("X-Hume-AI-Webhook-Timestamp") |
| 66 | signature = headers.get("X-Hume-AI-Webhook-Signature") |
| 67 | |
| 68 | if not signature: |
| 69 | raise ValueError("Missing HMAC signature") |
| 70 | |
| 71 | if not timestamp: |
| 72 | raise ValueError("Missing timestamp") |
| 73 | |
| 74 | # Validate HMAC signature |
| 75 | signing_key = os.environ.get("HUME_WEBHOOK_SIGNING_KEY") |
| 76 | if not signing_key: |
| 77 | raise ValueError("HUME_WEBHOOK_SIGNING_KEY is not set in environment variables") |
| 78 | |
| 79 | message = (payload + "." + timestamp).encode("utf-8") |
| 80 | expected_sig = hmac.new( |
| 81 | key=signing_key.encode("utf-8"), |
| 82 | msg=message, |
| 83 | digestmod=hashlib.sha256, |
| 84 | ).hexdigest() |
| 85 | |
| 86 | if not hmac.compare_digest(signature, expected_sig): |
| 87 | raise ValueError("Invalid HMAC signature") |
| 88 | |
| 89 | # Validate timestamp to prevent replay attacks |
| 90 | try: |
| 91 | timestamp_int = int(timestamp) |
| 92 | except ValueError: |
| 93 | raise ValueError("Invalid timestamp format") |
| 94 | |
| 95 | current_time = int(time.time()) |
| 96 | TIMESTAMP_VALIDATION_WINDOW = 180 |
| 97 | if current_time - timestamp_int > TIMESTAMP_VALIDATION_WINDOW: |
| 98 | raise ValueError("The timestamp on the request is too old") |
| 99 | |
| 100 | |
| 101 | async def fetch_weather(parameters: str) -> str: |
no outgoing calls
no test coverage detected