Короткий ответ
Отправитель подписывает тело запроса общим секретом через HMAC и передаёт подпись в заголовке; приёмник независимо вычисляет HMAC от полученного тела с тем же секретом и сравнивает подписи константным по времени способом, отклоняя запрос при несовпадении.
Как это работает подробнее
HMAC (например, HMAC-SHA256) строится так, что вычислить корректную подпись без знания секрета вычислительно нецелесообразно; сравнение подписей нужно делать функцией, устойчивой к атакам по времени ответа (hmac.compare_digest в Python), а не обычным ==, который может завершаться раньше при первом несовпадающем байте.
Пример кода
example.py
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = b"shared-secret-for-example" # учебный пример, не реальный секрет
@app.post("/webhooks/partner")
async def receive_webhook(request: Request):
raw_body = await request.body()
signature = request.headers.get("X-Signature", "")
expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise HTTPException(status_code=401, detail="Invalid signature")
payload = await request.json()
process_webhook_event(payload)
return {"status": "ok"}Что использовать на практике
- Рекомендация. подпись обычно считается от сырого байтового тела запроса "как получено", а не от повторно сериализованного JSON — если фреймворк уже распарсил тело в объект и вы заново сериализуете его перед проверкой подписи, различия в форматировании (порядок ключей, пробелы) сломают проверку. Поэтому подпись нужно проверять до или независимо от парсинга JSON, на основе точных байт тела.
Подводные камни
- Риски. сравнивать подписи через обычный оператор сравнения строк вместо
hmac.compare_digest, что потенциально уязвимо к timing-атакам; вычислять подпись от пересериализованного объекта вместо исходных байт тела, из-за чего легитимные запросы иногда ложно отклоняются при отличиях в форматировании; хранить секрет в коде или логировать его при отладке.