Проверка HMAC-подписи webhook: как сделать правильно

Любой, кто знает — или угадал — URL вашей точки приёма вебхуков, может отправить POST-запрос с JSON, который выглядит в точности как настоящее событие: та же структура, те же названия полей, полностью поддельное содержимое. Подпись закрывает этот пробел: она доказывает, что данные пришли от обладателя общего секрета и что в пути ничего не изменилось. Stripe, GitHub и большинство серьёзных провайдеров подписывают запросы той или иной вариацией одной и той же схемы HMAC, поэтому механика здесь почти универсальна — конкретный пример взят из реальной, действующей схемы самой Olmira, вплоть до названий заголовков.

Что на самом деле доказывает HMAC

HMAC (Hash-based Message Authentication Code) отталкивается от секрета, который знают только обе стороны и никто больше. Отправитель хэширует данные вместе с этим секретом и прикладывает результат; вы, владея тем же секретом, пересчитываете хэш и сравниваете. Совпадение доказывает сразу две вещи: сообщение действительно пришло от того, кто владеет секретом, — никакое знание содержимого не позволяет злоумышленнику подделать хэш, — и оно не было изменено после подписи, потому что изменение даже одного символа даёт совершенно другой хэш.

Точная схема, которую использует Olmira

Каждая исходящая доставка вебхука несёт три заголовка: X-Aether-Signature, X-Aether-Webhook-Id и X-Aether-Event. (Префикс заголовка — Aether, а не Olmira: это наследие внутреннего кодового названия платформы; на работу схемы это никак не влияет, меняется только буквальное имя заголовка.)

Проверка доставки простыми словами

Возьмите тело запроса в исходном виде — те самые байты, что были доставлены, до какого-либо разбора JSON, — добавьте перед ним временную метку из заголовка подписи и точку, вычислите HMAC этой строки с секретом вашей точки приёма и сравните результат с полученной подписью, используя сравнение за постоянное время. Встроенный в Node модуль crypto покрывает всё это без единой зависимости; именно в предварительных проверках ниже корректные реализации на самом деле и отличаются от сломанных.

Ошибки, из-за которых проверка не проходит

Почти каждое обращение «моя подпись никогда не совпадает» сводится к одной из небольшого числа причин. Хеширование заново разобранного или заново сериализованного тела вместо точных сырых байтов — с большим отрывом самая частая: пробелы, порядок ключей или формат чисел могут молча измениться, когда тело проходит через JSON-парсер и обратную сборку в строку, а любое изменение вообще даёт другой хеш. Забыть, что метка времени — часть того, что подписывается, то есть хешировать одно только тело, без префикса ., — вторая по частоте, и её легко допустить, потому что тело — более «очевидное», что хочется хешировать.

Прежде чем доверять этому в продакшене

1

Сначала проверьте окно временных меток

Отклоняйте всё, что выходит за небольшой допуск (Olmira публикует 300 секунд, то есть пять минут, как собственное ориентировочное значение), прежде чем тратить время процессора на хеширование: устаревшая подпись не заслуживает сравнения.

2

Сравнивайте за постоянное время

Используйте примитив сравнения за постоянное время из вашего языка, а не обычную проверку на равенство, чтобы несовпадение никогда не выдавало по времени, насколько близкой была догадка.

3

Убедитесь, что вы хешируете сырое тело запроса

Хотя бы один раз во время настройки запишите в лог точную строку, которую ваш код собирается хешировать, и сравните её побайтово с тем, что реально прислала тестовая доставка.

4

Учтите смену секретного ключа

Где бы вы ни хранили секрет, сделайте его замену однострочным изменением конфигурации: если ротировать его в панели управления и не обновить в тот же момент, все доставки начнут падать, пока вы не догоните.

Как это устроено в Olmira

Каждое из 22 webhook-событий Olmira — заказы, бронирования, платежи, счета, обращения в поддержку и другие — подписывается ровно так, как описано выше, без исключений и без неподписанных типов событий. Каждый регистрируемый вами endpoint получает собственный независимый секрет, поэтому утечка в одной интеграции никогда не компрометирует другую. Перед запуском действие «Отправить тест» в настройках endpoint отправляет реальную, полностью подписанную пробную доставку по тому же самому пути в коде, что и боевое событие, — с теми же заголовками и той же структурой конверта, — так что вы можете проверить свой верификатор на настоящей схеме, не дожидаясь, пока её вызовет реальный заказ или бронирование. Полная платформа коннекторов, частью которой это является, — импорт CSV, запланированные выгрузки и входящие отправки наряду с исходящими webhooks — это Интеграции.

Соберите интеграцию один раз — и доверяйте ей

Бесплатный пробный период 30 дней. Зарегистрируйте эндпоинт и отправьте себе настоящую подписанную тестовую доставку за считаные минуты.