Подпись webhook не сходится: проверьте исходные байты JSON
Два JSON с одинаковыми данными могут иметь разные HMAC-подписи. Показываем проверенный пример с пробелами и границы проверки подлинности входящего уведомления.

В этой статье
Webhook приходит от ожидаемого сервиса, данные выглядят правильно, а подпись не сходится. Разработчик выводит JSON, сравнивает поля и не находит разницы. Причина может находиться между получением запроса и проверкой: приложение разобрало тело, затем собрало JSON заново. Значения сохранились, исходные байты — нет.
Для схемы, которая подписывает исходное тело запроса, проверять нужно именно его. Сначала сохраните доступ к исходным байтам предусмотренным способом своего фреймворка, затем проверьте подпись по контракту поставщика. Разбор данных для бизнес-логики не должен подменять проверяемое сообщение его новой сериализацией.
Одинаковые данные не означают одинаковое сообщение
Два учебных тела содержат один идентификатор заказа и одинаковый признак оплаты:
{"order_id":42,"paid":true}
{"order_id": 42, "paid": true}
Для JSON это одинаковые значения полей. Во втором теле добавлены пробелы. В локальной проверке Python разбор обоих тел дал равные объекты, а HMAC-SHA256 с одним учебным ключом — разные результаты. Подпись первого тела прошла проверку на исходных байтах и не прошла на втором варианте. Это небольшой воспроизводимый тест свойства байтов, не испытание обработчика конкретного магазина.
Похожее изменение может внести повторная сериализация: другой порядок полей, представление символов или завершающий перевод строки. Нельзя рассчитывать, что вывод объекта обратно в JSON восстановит ровно тот поток, который подписал отправитель. Если протокол специально задаёт каноническое представление, выполняют его правила; самовольная «нормализация для удобства» таким правилом не становится.
Что именно проверяет HMAC
HMAC использует сообщение, секретный ключ и выбранную хеш-функцию. Получатель вычисляет ожидаемое значение и сравнивает его с переданным. При сохранности общего секрета это позволяет проверять подлинность и целостность сообщения в рамках данной схемы. Обычный хеш без секретного ключа такую задачу сам по себе не решает.
Для конкретного примера можно свериться с документацией GitHub: рекомендуемая подпись webhook передаётся в заголовке X-Hub-Signature-256 и использует HMAC-SHA256. Проверяют исходное тело; документация отдельно предупреждает об изменениях полезной нагрузки и заголовков посредниками. Это контракт GitHub, его название заголовка нельзя автоматически переносить на платёжный или логистический сервис.
У другого поставщика в подписываемую строку могут входить время, путь запроса или другие элементы. Сначала выясняют состав сообщения, кодировку, алгоритм и формат подписи. Затем используют поддерживаемую библиотеку или официальный SDK. Угадывание по длине строки и подбор алгоритма до первого совпадения не заменяют контракт.
Для сравнения секретно-зависимых значений применяют предусмотренную криптографической библиотекой функцию, например hmac.compare_digest в Python. Она избегает раннего завершения сравнения в зависимости от содержимого. Типы и формат входа всё равно нужно проверять по документации; название функции не делает весь обработчик автоматически защищённым.
Как найти место, где тело изменилось
На тестовых обезличенных данных сравните байты на входе обработчика и байты, поступившие в вычисление подписи. Отдельно проверьте этапы чтения тела, разбора JSON и повторной сериализации. Поток запроса может оказаться уже прочитанным промежуточным обработчиком: конкретный способ повторного доступа зависит от фреймворка.
Не выводите рабочий секрет в журнал и не копируйте туда полные уведомления с данными покупателей ради диагностики. Для локального воспроизведения достаточно искусственного тела и отдельного тестового ключа. Если нужна проверка рабочего события, используйте согласованный защищённый процесс, а не публичный конвертер подписей.
В приёмке нужны как минимум исходное тело с верной подписью, изменённое тело со старой подписью, отсутствующая подпись и подпись другого тестового ключа. Для схемы подписания исходных байтов отдельно изменяют только пробел: отказ в этом случае ожидаем и показывает, что проверяется сообщение, а не сходство объектов.
Верная подпись не отменяет остальные проверки
Одна и та же корректная подпись может сопровождать повторную доставку неизменённого сообщения. Поэтому подлинность и обработка повторов — разные задачи. Сервис должен распознавать повтор события предусмотренным контрактом способом и не выполнять бизнес-действие ещё раз. У GitHub для отслеживания доставок есть отдельный идентификатор; правила другого отправителя проверяют отдельно.
После проверки подлинности остаются тип события, связь с нужным объектом, допустимость перехода состояния и уже выполненные действия. Для уведомления об оплате важно не только доверять источнику, но и корректно связать событие с заказом. Подписанный запрос не даёт разрешения применять любые переданные данные к любому объекту.
Если подпись перестала сходиться после изменения обработчика, первым проверяемым вопросом будет: «Какие именно байты мы теперь передаём в расчёт?» Отключение проверки скрывает ответ и снимает важную границу доверия. Возврат к исходному сообщению позволяет найти дефект, сохранив эту границу.
The webhook arrives from the expected service, the data looks correct, but the signature does not match. The developer outputs the JSON, compares the fields, and finds no difference. The cause may lie between receiving the request and the check: the application parsed the body, then reconstructed the JSON. The values were preserved, but the original bytes were not.
For a scheme that signs the original request body, you must verify that exact body. First, preserve access to the original bytes using your framework's prescribed method, then verify the signature against the provider's contract. Parsing data for business logic must not replace the message being verified with a new serialization.
Identical data does not mean an identical message
Two sample bodies contain the same order ID and the same payment flag:
{"order_id":42,"paid":true}
{"order_id": 42, "paid": true}
For JSON, these are identical field values. The second body includes extra spaces. In local Python testing, parsing both bodies yielded equal objects, but HMAC-SHA256 with a single test key produced different results. The first body's signature passed verification on the original bytes but failed on the second variant. This is a small, reproducible test of byte-level properties, not a test of a specific store's handler.
Similar changes can be introduced by re-serialization: different field order, character representation, or a trailing newline. You cannot rely on converting an object back to JSON to restore the exact byte stream that the sender signed. If the protocol explicitly defines a canonical representation, follow its rules; self-imposed normalization for convenience does not become such a rule.
What HMAC Actually Verifies
HMAC uses a message, a secret key, and a chosen hash function. The recipient computes the expected value and compares it with the transmitted one. If the shared secret is kept secure, this allows verifying the authenticity and integrity of the message within this scheme. A standard hash without a secret key cannot solve this task on its own.
For a specific example, refer to the GitHub documentation: the recommended webhook signature is passed in the X-Hub-Signature-256 header and uses HMAC-SHA256. The original body is verified; the documentation separately warns about changes to the payload and headers by intermediaries. This is a GitHub contract; its header name cannot be automatically transferred to a payment or logistics service.
For another provider, the signed string may include the timestamp, request path, or other elements. First, determine the message composition, encoding, algorithm, and signature format. Then use a supported library or the official SDK. Guessing based on string length and brute-forcing the algorithm until a match is found does not replace a contract.
To compare secret-dependent values, use a function provided by the cryptographic library, such as hmac.compare_digest in Python. This avoids early termination of the comparison based on content. You must still validate input types and formats according to the documentation; the function name alone does not automatically make the entire handler secure.
How to Find Where the Body Changed
On test anonymized data, compare the bytes entering the handler with the bytes passed to the signature calculation. Independently verify the stages of body reading, JSON parsing, and re-serialization. The request stream may have already been read by an intermediate handler; the specific method for re-accessing it depends on the framework.
Do not log the working secret or copy full notifications containing buyer data for diagnostics. For local reproduction, an artificial body and a separate test key are sufficient. If you need to verify a live event, use a coordinated secure process, not a public signature converter.
Acceptance testing requires at least: the original body with a valid signature, a modified body with an old signature, a missing signature, and a signature from a different test key. For the scheme signing original bytes, change only a space; a rejection in this case is expected and confirms that the message content is checked, not object similarity.
A Valid Signature Does Not Cancel Other Checks
The same valid signature can accompany a re-delivery of an unchanged message. Therefore, authenticity and duplicate handling are distinct tasks. The service must recognize a repeated event in the manner specified by the contract and must not perform the business action again. GitHub uses a separate identifier for tracking deliveries; rules for other senders are checked separately.
After verifying authenticity, the remaining concerns are the event type, the link to the correct object, state transition validity, and actions already performed. For a payment notification, it is important not only to trust the source but also to correctly associate the event with the order. A signed request does not authorize applying any of the transmitted data to any object.
If the signature no longer matches after changing the handler, the first question to check is: 'Which exact bytes are we now passing into the calculation?' Disabling verification hides the answer and removes an important trust boundary. Restoring the original message allows you to find the defect while preserving that boundary.





Обсуждение 0
Делись опытом и задавай вопросы. Комментарии без ссылок появляются после проверки редактором.
Пока никто не написал. Начни обсуждение.