Подпись webhook SMS: как проверять подлинность DLR

Webhook со статусом доставки меняет данные и может запускать бизнес-логику. Если endpoint принимает любой POST без проверки, посторонний отправитель способен подделать delivered, создать поток ошибок или перегрузить систему. HTTPS защищает канал передачи, но не доказывает, что запрос сформировал именно SMS-провайдер.
Подпись webhook связывает тело запроса с секретом, известным двум сторонам. Система вычисляет контрольное значение и сравнивает его с заголовком провайдера до разбора и изменения статуса.
Следовать контракту провайдера
Не существует единого формата подписи. Провайдер определяет алгоритм, набор подписываемых полей, порядок, кодировку, заголовки и формат результата. Нельзя заменить документированный алгоритм собственной похожей схемой.
Часто используется HMAC-SHA256 по сырому телу и timestamp, но возможны другие варианты. Сохраните официальный пример запроса как тестовый fixture и проверяйте его при обновлении SDK или фреймворка.
Сырое тело запроса
Подпись обычно вычисляется до JSON-парсинга. Повторная сериализация меняет пробелы, порядок полей или Unicode и приводит к другому хешу. Поэтому middleware должен сохранить точные байты входящего тела.
Сначала проверяются лимит размера, обязательные заголовки и подпись. Только после успешной аутентификации JSON разбирается и проходит валидацию схемы.
Constant-time comparison
Сравнивать подписи следует функцией постоянного времени, доступной в языке или библиотеке. Обычное == может завершаться на первом несовпадающем символе и теоретически раскрывать информацию по времени ответа.
Перед сравнением убедитесь, что обе строки декодированы в ожидаемый формат: hex или Base64. Ошибка декодирования означает отказ, а не попытку угадать формат.
Timestamp и replay
Корректно подписанный запрос можно перехватить и повторить позднее. Добавление timestamp в подписываемую строку и проверка допустимого окна ограничивают replay. Часы серверов должны быть синхронизированы, а допустимое отклонение — документировано.
Одного времени недостаточно: внутри окна событие может прийти повторно законно. Обработчик остаётся идемпотентным и дедуплицирует webhook по event ID либо устойчивой комбинации message ID, статуса и времени события.
Ротация секрета
Во время ротации endpoint может короткое время принимать подписи от текущего и следующего секрета. Каждый имеет внутренний key ID и срок действия. После перехода старый секрет удаляется, а попытки его использования становятся сигналом.
Не храните webhook secret рядом с публичным URL или в исходном коде. Процесс управления такой же, как для ротации API-ключей SMS, но права входящего и исходящего канала разделяются.
IP allowlist
Список IP может быть дополнительным слоем, если провайдер публикует стабильные диапазоны. Он не заменяет подпись: адреса меняются, запрос может идти через прокси, а доверенная сеть тоже не гарантирует целостность тела.
Обновление allowlist автоматизируют и тестируют. В противном случае легитимные DLR внезапно перестанут приходить после изменения инфраструктуры провайдера.
Обработка ошибок
При неверной подписи endpoint возвращает отказ без подробного объяснения. В лог попадают время, источник, безопасный request ID и причина категории, но не секрет, полная подпись или чувствительный payload.
Алерт на каждую единичную ошибку создаёт шум: сканеры интернета регулярно находят публичные URL. Сигнал строят по устойчивому росту, совпадению с известным IP провайдера или появлению после ротации.
Быстрый ответ
После проверки подписи и минимальной схемы webhook лучше надёжно поставить во внутреннюю очередь и быстро вернуть ожидаемый 2xx. Долгая бизнес-обработка увеличивает число повторов со стороны провайдера.
Сам быстрый ответ не означает, что событие потеряно. Сначала оно сохраняется атомарно, затем обрабатывается асинхронно. Общий pipeline описан в статье про webhook статусов SMS.
Тесты
Обязательные сценарии:
- корректное тело и подпись;
- изменение одного байта payload;
- неверный секрет;
- отсутствующий заголовок;
- старый и будущий timestamp;
- повтор корректного события;
- два активных секрета во время ротации;
- JSON с другим порядком полей;
- слишком большое тело;
- запрос после отзыва старого секрета.
Тест должен использовать сырые байты официального примера, а не объект, который framework сериализует заново.
Чек-лист
- алгоритм реализован точно по документации провайдера;
- подпись считается по сырым байтам;
- сравнение выполняется constant-time функцией;
- timestamp входит в проверку, если это предусмотрено контрактом;
- повторы дедуплицируются;
- секрет хранится и ротируется отдельно от API-ключа;
- IP allowlist остаётся дополнительным слоем;
- до аутентификации не меняются бизнес-данные;
- ошибки не раскрывают детали проверки.
Итог
Подпись webhook защищает доверие к DLR. Правильная реализация начинается с точного контракта провайдера, сырых байтов и безопасного сравнения, а заканчивается replay-защитой, идемпотентностью и ротацией секрета. Только после этих проверок входящий статус может влиять на сообщение и бизнес-процесс.