HMAC-подпись API-запроса: зачем она и как её считать
Платёжный сервис присылает на ваш сервер вебхук «платёж прошёл», и теперь нужно решить, верить ли этому сообщению. Запрос пришёл с чужого адреса, в теле сумма и номер заказа, и любой, кто узнает URL обработчика, может отправить точно такой же. Подпись запроса решает именно эту проблему: она доказывает получателю, что запрос создал владелец секретного ключа и что тело не менялось по дороге. В статье разберём, как устроен HMAC, как выглядит типовая схема подписи вебхука, почему сравнивать подписи через обычное равенство опасно и как посчитать подпись вручную и проверить результат.
Что именно доказывает подпись
Подпись отвечает на два вопроса одновременно. Первый вопрос: автор. Секретный ключ известен только отправителю и получателю, и если подпись сошлась, значит её посчитал тот, у кого ключ есть. Никто посторонний подпись воспроизвести не может, сколько бы примеров подписанных запросов он ни видел. Второй вопрос: целостность. Подпись вычисляется от точного содержимого запроса, поэтому изменение хотя бы одного байта делает её недействительной: подменённая сумма, добавленное поле или лишний перевод строки перестают сходиться.
Чего подпись не делает: она не прячет содержимое. Тело запроса читается всеми, кто видит трафик, включая прокси-серверы и системы логирования. Это не шифрование, а проверка подлинности. Данные, которые нельзя показывать, по-прежнему передают по HTTPS, а подпись добавляется сверху вторым слоем, который отвечает на вопрос «от кого и без изменений». Слой TCP или TLS защищает канал между точками, но не гарантирует, что запрос создан именно владельцем аккаунта: между вашим сервером и платёжной системой может стоять шлюз, балансировщик или партнерское приложение, и подпись работает именно на уровне содержания.
Как устроен HMAC простыми словами
HMAC это «хеш с ключом». Обычная хеш-функция, например SHA-256, считает отпечаток данных, но отпечаток может воспроизвести кто угодно: функция публичная, ключа у неё нет. HMAC замешивает в вычисление секретный ключ, и теперь отпечаток способен повторить только тот, кто ключ знает. Формула из стандарта выглядит так:
HMAC(K, m) = H((K ⊕ opad) || H((K ⊕ ipad) || m))
K ключ
m сообщение
H хеш-функция, например SHA-256
⊕ побитовое исключающее ИЛИ (XOR)
|| склейка последовательностей байтов
ipad константа 0x36, повторённая до размера блока хеша
opad константа 0x5C, повторённая до размера блока хешаЧитать формулу стоит как рецепт из двух шагов. Сначала ключ смешивается с константой ipad и склеивается с сообщением, и от этой склейки берётся хеш. Затем ключ смешивается с другой константой, opad, склеивается с первым хешем, и берётся второй хеш: он и есть подпись. Двойная конструкция не украшение: одиночная схема вида «хеш от ключа и сообщения» уязвима к достройке длины, когда атакующий, не зная ключа, продолжает чужой хеш своим сообщением. Второй проход с opad закрывает эту возможность. В математику структуры Меркла-Дамгарда углубляться не нужно: для практики достаточно рецепта из двух шагов, и его выполняет любая HMAC-библиотека.
Для работы важны три факта, а не вывод формулы. Ключ известен только двум сторонам и нигде не передаётся вместе с запросом. Одинаковые ключ и сообщение всегда дают одинаковую подпись: проверка детерминирована, поэтому приёмник может повторить вычисление самостоятельно. И из подписи ключ не восстанавливается: наблюдая тысячи подписанных запросов, атакующий видит только результат, а не вход.
Схема подписи вебхука
Типовая схема у разных платформ устроена одинаково, меняются названия заголовков и мелочи формата. Отправитель считает HMAC-SHA-256 от осмысленной строки и кладёт результат в заголовок, обычно X-Signature. Приёмник повторяет те же действия над полученным телом и сравнивает две подписи. Сошлись: запрос можно обрабатывать.
- отправитель и получатель заранее обмениваются секретным ключом: не по открытому каналу, а через панель разработчика или личный кабинет;
- при каждом запросе отправитель склеивает в одну строку отметку времени и точное тело запроса и считает от этой строки HMAC-SHA-256;
- в запрос уходят два заголовка: X-Signature с самой подписью и X-Timestamp с моментом отправки;
- приёмник берёт своё тело запроса ровно в тех байтах, в которых оно пришло, добавляет отметку времени из заголовка и считает HMAC со своим экземпляром ключа;
- совпадение подписи и свежая отметка времени означают, что запрос подлинный; подпись, которая пришла с отметкой недельной давности, отбрасывается как попытка повтора.
Отметка времени в подписи не украшение. Без неё перехваченный запрос можно было бы отправить заново сколько угодно раз: тело то же, подпись та же, и проверка каждый раз проходила бы. Время, замешанное в подписываемую строку, ограничивает жизнь каждой подписи несколькими минутами: приёмник просто отбрасывает запросы, чья отметка отстала слишком далеко. Так работают вебхуки Stripe и GitHub: у обеих платформ подпись считается по HMAC-SHA-256 и передаётся в специальном заголовке, и документация каждой отдельно предупреждает про безопасное сравнение подписей. Названия заголовков и точный формат подписываемой строки уточняйте в документации своей платформы: они различаются, а принцип везде один.
Почему сравнивать подписи нельзя через обычное равенство
Наивное сравнение строк завершается при первом несовпадающем символе. Кажется безобидной мелочью, но время сравнения зависит от того, сколько первых символов совпали, и этот отрезок времени технически измерим. Отправляя миллионы «чуть неправильных» подписей и замеряя отклик сервера, атакующий постепенно угадывает правильную подпись символ за символом: совпал очередной символ подлинной подписи, сравнение стало на микросекунду дольше, и разница видна в статистике откликов. Атака называется тайминг-атакой, и она была продемонстрирована на реальных системах проверки подписей.
Защита занимает одну строку: сравнение, которое всегда проходит одинаковый путь независимо от данных. В Python для этого есть hmac.compare_digest(), в Node.js crypto.timingSafeEqual(), в PHP hash_equals(). Названия везде похожи, и смысл один: обычные операторы равенства при проверке подписей меняются на сравнение с постоянным временем выполнения. Уязвимость при этом не требует ошибки в самом HMAC: алгоритм идеален, а дыра появляется в одной строке кода приёмника.
Где ещё встречается HMAC
Ближайший пример почти у каждого разработчика это JWT с алгоритмом HS256. Расшифровка имени прямая: HMAC-SHA-256. Подпись токена считается от склейки base64url-кодированного заголовка, точки и base64url-кодированного набора данных. Проверяющая сторона делает то же самое со своим ключом и сравнивает результат с третьей частью токена. Изменили одно поле внутри токена, подпись перестала сходиться, и сервер отклоняет токен до чтения содержимого. Устроить разбор такого токена по частям можно в декодере JWT, а собрать и подписать новый позволяет генератор JWT. Как устроен токен внутри, мы разбирали отдельно в статье про JWT.
Вторая большая область это облачные API. Amazon S3 и совместимые с ним хранилища подписывают каждый запрос: подпись покрывает метод, путь, отдельные заголовки и хеш тела, а ключ подписи выводится из секретного ключа аккаунта. Тот же приём встречается в платёжных шлюзах, в API мессенджеров и в системах обмена документами. Общий знаменатель один: TLS защищает канал, но не доказывает авторство запроса, а подпись доказывает.
Считаем HMAC-SHA-256 по шагам
Возьмём сообщение hello и ключ secret. В командной строке Node.js вычисление занимает одну строку:
const crypto = require('crypto');
crypto.createHmac('sha256', 'secret').update('hello').digest('hex');
// результат:
88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0bРезультат всегда один и тот же: 88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b. Подпись имеет фиксированную длину 256 бит, то есть 64 hex-символа, независимо от того, считали ли её от короткого слова или от файла размером в гигабайт. Поменяйте одну букву в сообщении или в ключе, и подпись изменится до последнего символа: это лавиновый эффект хеш-функции, и на нём держится обнаружение подмен. Для самопроверки тот же вектор легко воспроизвести: hello с ключом secret даёт указанную строку, а hello с заглавной O в конце даёт совсем другую подпись 66b79322f99cb3ab6dd28a3382acfb695a39036ccc8260f59b46b5e10c3e6b36, что наглядно показывает чувствительность к регистру.
Тот же результат сходится в генераторе HMAC: введите сообщение и ключ, выберите алгоритм SHA-256, и подпись совпадёт с командной строкой. Обычные хеши без ключа, MD5, SHA-1, SHA-256 и SHA-512 от одних и тех же входных данных, считает мультихеш-инструмент: удобно сравнить, как меняются отпечатки между алгоритмами. Разницу между хешем и ключевой подписью стоит держать в голове всегда: хеш доказывает только целостность, HMAC доказывает целостность и авторство.
Типичные ошибки
- ключ в URL или в теле запроса вместо заголовка. Ключ попадает в логи веб-сервера и в аналитику, и считать его секретным после этого нельзя;
- подпись от форматированного JSON. Отправитель сделал тело красивым, с отступами и переносами, подписал его, а получатель считает от строки без отступов: подпись сходится только при побайтовом совпадении, поэтому подписывают ровно те байты, которые уходят в сеть;
- разные алгоритмы на концах. HMAC-SHA-256 и HMAC-SHA-512 с одним ключом дают разные подписи, и ошибка проявится только на приёмнике в виде invalid signature;
- соль вместо ключа. Соль обычно хранится рядом с данными и не является секретом для кода, имеющего доступ к базе; ключ подписи обязан жить в хранилище секретов приложения;
- сравнение через обычное равенство вместо постоянного времени: тайминг-атака из раздела выше;
- отсутствие отметки времени. Без ограничения срока жизни каждая подпись вечна, и перехваченный запрос можно переиграть даже через год.
Частые вопросы
HMAC это шифрование?
Нет. Подпись не прячет данные: тело запроса читается всеми, у кого есть доступ к трафику. HMAC доказывает авторство и целостность, а секретность обеспечивает TLS. Формально HMAC строится на хеш-функции, которая необратима по определению, но восстановить сообщение из подписи и не задача, и невозможно.
Можно ли использовать HMAC для хранения паролей?
Не стоит. Для паролей существуют медленные функции: bcrypt, scrypt, Argon2. Скорость HMAC здесь недостаток: перебор паролей по быстрой функции дёшев, а по медленной дорого. Пересекаются эти задачи только в слове «хеш»: назначение у них разное. Подробнее о стойких паролях и хранении мы писали в статье про надёжный пароль.
Чем HMAC отличается от электронной подписи?
Классическая электронная подпись асимметрична: она ставится приватным ключом, а проверяется публичным, поэтому проверить её может кто угодно. HMAC симметричен: один и тот же ключ и подписывает, и проверяет, значит проверять подпись может только сторона, которой вы доверили секрет. Для вебхуков и интеграций между двумя сервисами симметричной схемы достаточно, и она дешевле асимметричной.
Что означает HS256 в JWT?
Это имя алгоритма подписи: HMAC-SHA-256. Поле alg в заголовке токена со значением HS256 говорит проверяющей стороне, что третья часть токена получена через HMAC-SHA-256 от первых двух частей с секретным ключом. Альтернативы вроде RS256 используют асимметричную подпись RSA, где проверка идёт публичным ключом.
Насколько длинным должен быть ключ?
Для HMAC-SHA-256 рекомендуется ключ не короче 32 случайных байтов. Более короткий ключ сокращает перебор, а более длинный преимуществ не даёт: ключ длиннее блока хеша сжимается хешем до внутреннего размера. Практическое правило: сгенерировать 32–64 случайных байта криптографическим генератором и хранить в хранилище секретов, а не в конфиге рядом с базой.
Зачем в подписи время, если тело и так подписано?
Тело подписано, но подпись вечна: один и тот же запрос с той же подписью остаётся действительным бесконечно. Отметка времени, входящая в подписываемую строку, ограничивает срок жизни каждой подписи. Приёмник отбрасывает запросы, чья отметка отстаёт больше чем на несколько минут, и переиграть старый перехваченный запрос уже не получается.
Генератор HMAC
Вычислите HMAC-SHA-1, SHA-256 или SHA-512 с вашим ключом через Web Crypto: HEX, Base64 и сверка с эталоном
Генератор JWT
Соберите и подпишите JWT по алгоритму HS256 прямо в браузере: sub, name, iat, exp и свои claims
Декодер JWT-токенов
Декодируйте JWT-токены для просмотра заголовка, полезной нагрузки и подписи
Мульти-хеш генератор
Генерация MD5, SHA-1, SHA-256, SHA-512 и CRC32 хешей одновременно из текста или файлов