Термин · 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-строки.

Отправитель:

python
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,
    },
)

Получатель:

python
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)

Две важные мелочи:

  1. Подписывайте и проверяйте сырые байты тела, а не распарсенный JSON. Если получатель сначала распарсит JSON и соберёт его заново, порядок ключей или пробелы могут поменяться, и подпись не сойдётся.
  2. Сравнивайте через hmac.comparedigest, а не через ==. Обычное сравнение останавливается на первом несовпавшем символе, и по времени ответа злоумышленник теоретически может подобрать подпись по кусочкам. comparedigest всегда работает за одинаковое время.

Как сделать подпись ещё надёжнее

Базовая схема защищает от подделки и изменения тела, но не от всего. Вот что стоит добавить.

Временная метка против повторной отправки. Злоумышленник может перехватить валидный запрос и отправить его ещё раз (replay-атака). Подпись при этом останется правильной. Решение: добавить в подпись время отправки и отклонять старые запросы.

Одноразовый идентификатор (nonce). Метка времени оставляет окно, например в 5 минут, в которое повтор всё ещё возможен. Если это критично, добавьте случайный nonce и храните использованные значения (например, в Redis с TTL на то же окно). Повторный nonce отклоняется.

Метод и путь запроса. Если подписывать только тело, подпись от POST /orders формально подойдёт и для POST /refunds с тем же телом. Включите в подпись метод и путь.

Идентификатор ключа. Передавайте в заголовке X-Key-Id, каким ключом подписан запрос. Так у разных клиентов будут разные ключи, а ключи можно спокойно менять: какое-то время принимать и старый, и новый.

Собранная вместе подписываемая строка может выглядеть так:

python
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,
}

А получатель делает так:

  1. По X-Key-Id находит нужный секрет.
  2. Проверяет, что X-Timestamp отличается от текущего времени не больше чем на 5 минут.
  3. Проверяет, что такой X-Nonce ещё не встречался, и запоминает его.
  4. Собирает ту же строку из метода, пути, метки, nonce и сырого тела, считает HMAC и сравнивает через compare_digest.

И пара общих правил: используйте SHA-256 или сильнее, делайте ключи длинными и случайными (32 байта и больше), храните их в секрет-хранилище, а не в коде, и не забывайте про HTTPS. HMAC защищает целостность и подлинность, но не шифрует данные.

Итог

HMAC-подпись — это простой стандартный способ доказать, что запрос пришёл от своих и не был изменён по дороге, при этом ни разу не передавая секрет по сети. Для внутренних сервисов и интеграций с заранее оговорённым форматом это отличная альтернатива токенам, а метка времени, nonce и подпись метода с путём закрывают большинство типичных атак.

Тренажёр запоминания

Проверьте себя

1 вопрос
+15 XP

Что передаётся по сети при использовании HMAC-подписи?

Войти, чтобы проверить

Войдите, чтобы проверить ответ и получить XP.

Ревизия 2 · обновлено 23.10.2026