Термин · Python
HMAC-подпись
Также: HMAC-signature
Коротко
Способ доказать, что запрос пришёл от своих и его никто не подменил.
HMAC-подпись запросов: как сервисам доверять друг другу без логинов и токенов
Когда два сервиса общаются друг с другом, им тоже нужна «авторизация». Обычно для этого используют токены: API-ключ в заголовке, JWT, OAuth. Но есть и другой способ, простой и очень надёжный: подписывать каждый запрос с помощью HMAC.
Этот подход широко распространён. Его используют вебхуки GitHub, Stripe и Slack, а в AWS на похожей идее построена вся подпись запросов. Так что вполне может оказаться, что сервис, с которым вы интегрируетесь, уже умеет так работать.
Что такое HMAC
HMAC (Hash-based Message Authentication Code) — это способ получить «отпечаток» сообщения с помощью хеш-функции (например, SHA-256) и секретного ключа.
Главная идея простая:
- у отправителя и получателя есть один и тот же секретный ключ;
- отправитель берёт данные запроса, считает от них HMAC и кладёт результат в заголовок;
- получатель берёт те же данные, считает HMAC тем же ключом и сравнивает.
Если подписи совпали, значит запрос отправил тот, кто знает ключ, и по дороге его никто не изменил. Подделать подпись без ключа невозможно, а изменение хотя бы одного байта в теле даёт совсем другую подпись.
Чем это лучше обычного API-ключа
Если просто передавать ключ в заголовке, то любой, кто его перехватил (в логах, в прокси, в дампе трафика), может отправлять запросы от вашего имени.
С HMAC сам ключ по сети не передаётся никогда. Передаётся только подпись конкретного запроса. Даже если её украдут, подписать ею другой запрос не получится.
Это особенно удобно для интеграций и внутренних запросов, где обе стороны заранее договорились о формате: что именно подписываем, в каком порядке и в каком заголовке передаём.
Простой пример
Договоримся так: подписываем сырое тело запроса, а подпись кладём в заголовок X-Signature в виде hex-строки.
Отправитель:
import hmac
import hashlib
import json
import requests
SECRET = b"super-secret-shared-key"
body = json.dumps({"order_id": 42, "status": "paid"}).encode()
signature = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
requests.post(
"https://partner.example.com/webhook",
data=body,
headers={
"Content-Type": "application/json",
"X-Signature": signature,
},
)Получатель:
import hmac
import hashlib
SECRET = b"super-secret-shared-key"
def verify(raw_body: bytes, received_signature: str) -> bool:
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_signature)Две важные мелочи:
- Подписывайте и проверяйте сырые байты тела, а не распарсенный JSON. Если получатель сначала распарсит JSON и соберёт его заново, порядок ключей или пробелы могут поменяться, и подпись не сойдётся.
- Сравнивайте через
hmac.comparedigest, а не через==. Обычное сравнение останавливается на первом несовпавшем символе, и по времени ответа злоумышленник теоретически может подобрать подпись по кусочкам.comparedigestвсегда работает за одинаковое время.
Как сделать подпись ещё надёжнее
Базовая схема защищает от подделки и изменения тела, но не от всего. Вот что стоит добавить.
Временная метка против повторной отправки. Злоумышленник может перехватить валидный запрос и отправить его ещё раз (replay-атака). Подпись при этом останется правильной. Решение: добавить в подпись время отправки и отклонять старые запросы.
Одноразовый идентификатор (nonce). Метка времени оставляет окно, например в 5 минут, в которое повтор всё ещё возможен. Если это критично, добавьте случайный nonce и храните использованные значения (например, в Redis с TTL на то же окно). Повторный nonce отклоняется.
Метод и путь запроса. Если подписывать только тело, подпись от POST /orders формально подойдёт и для POST /refunds с тем же телом. Включите в подпись метод и путь.
Идентификатор ключа. Передавайте в заголовке X-Key-Id, каким ключом подписан запрос. Так у разных клиентов будут разные ключи, а ключи можно спокойно менять: какое-то время принимать и старый, и новый.
Собранная вместе подписываемая строка может выглядеть так:
import time, uuid
timestamp = str(int(time.time()))
nonce = uuid.uuid4().hex
method = "POST"
path = "/webhook"
message = b"\n".join([
method.encode(),
path.encode(),
timestamp.encode(),
nonce.encode(),
body,
])
signature = hmac.new(SECRET, message, hashlib.sha256).hexdigest()
headers = {
"X-Key-Id": "partner-1",
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
}А получатель делает так:
- По
X-Key-Idнаходит нужный секрет. - Проверяет, что
X-Timestampотличается от текущего времени не больше чем на 5 минут. - Проверяет, что такой
X-Nonceещё не встречался, и запоминает его. - Собирает ту же строку из метода, пути, метки, nonce и сырого тела, считает HMAC и сравнивает через
compare_digest.
И пара общих правил: используйте SHA-256 или сильнее, делайте ключи длинными и случайными (32 байта и больше), храните их в секрет-хранилище, а не в коде, и не забывайте про HTTPS. HMAC защищает целостность и подлинность, но не шифрует данные.
Итог
HMAC-подпись — это простой стандартный способ доказать, что запрос пришёл от своих и не был изменён по дороге, при этом ни разу не передавая секрет по сети. Для внутренних сервисов и интеграций с заранее оговорённым форматом это отличная альтернатива токенам, а метка времени, nonce и подпись метода с путём закрывают большинство типичных атак.
Тренажёр запоминания
Проверьте себя
1 вопросЧто передаётся по сети при использовании HMAC-подписи?
Войдите, чтобы проверить ответ и получить XP.
Ревизия 2 · обновлено 23.10.2026