Как работают подписи вебхуков
Вебхук — это просто HTTP POST с чужого сервера, и любой может отправить POST на ваш URL. Чтобы доказать, что запрос пришёл от провайдера, он вычисляет HMAC по телу с секретом, известным только вам двоим, и отправляет его в заголовке. Вы пересчитываете и сравниваете. Секрет никогда не передаётся; передаётся только хеш.
Почему возникает «неверная подпись»
- Вы хешировали разобранное тело, а не сырые байты. Фреймворки, разбирающие JSON до запуска вашего обработчика (Express с
express.json(), Rails, Django), передают вам объект; повторная сериализация меняет пробелы и порядок ключей, и хеш больше не совпадает. Захватывайте сырое тело. - Неверный секрет. У Stripe свой
whsec_для каждого эндпоинта и окружения; секрет GitHub задаётся на каждый вебхук; signing secret Slack — это не токен бота. - Провайдер подписывает не только тело. Stripe подписывает
timestamp.body; Slack —v0:timestamp:body; Svix —id.timestamp.body; Twilio — URL плюс отсортированные поля формы. Рецепт каждого показывается выше при выборе. - Кодировка. Одни провайдеры отправляют hex, другие Base64; некоторые добавляют префикс
sha256=. Сравните формы. - Прокси. Прокси или CDN, перекодирующие тело (gzip, charset), либо ограничение размера тела, обрезающее его, сломают хеш.
Безопасное сравнение
В продакшене сравнивайте функцией с постоянным временем выполнения (crypto.timingSafeEqual, hmac.compare_digest, hash_equals) и проверяйте, что временная метка свежая (Stripe и Slack допускают пять минут), чтобы остановить повторы. Эта страница — отладчик, а не эталонная реализация; она использует обычное сравнение, поскольку атаки по времени против вашего собственного браузера не представляют угрозы.
Вопросы
Мой секрет здесь в безопасности?
HMAC вычисляется в вашем браузере через WebCrypto, и страница не делает сетевых запросов. Тем не менее используйте секрет тестового эндпоинта и ротируйте всё, что вставляли в веб-страницу, если хотите быть строгими.
Почему GitHub отправляет и X-Hub-Signature, и X-Hub-Signature-256?
Первый — HMAC-SHA1, сохранён для старых интеграций; второй — HMAC-SHA256, именно его нужно проверять. Оба вычисляются по сырому телу с одним и тем же секретом.
Ожидаемое значение совпадает, кроме регистра. Это ошибка?
Hex нечувствителен к регистру в коде проверки любого провайдера, так что нет; этот инструмент тоже сравнивает hex без учёта регистра. Base64 чувствителен к регистру.
Stripe говорит, что временная метка слишком старая.
Библиотеки Stripe отклоняют подписи, у которых t= отличается от текущего времени более чем на 300 секунд, чтобы предотвратить повторы. Повторная отправка захваченного запроса позже провалится именно по этой причине, даже если HMAC верен; инструмент отмечает возраст.