HMAC-подпись для API: как подписывать запросы и проверять authenticity
Узнайте, как генерировать и проверять HMAC-подписи для безопасного взаимодействия между сервисами без передачи ключей в открытом виде.
API-ключи — не самая надёжная защита. Их передают в заголовках, логируют, случайно коммитят в Git. HMAC-подписи решают эту проблему: секретный ключ остаётся на сервере, а в запросе передаётся только хеш, который нельзя подделать без знания секрета. Разбираемся, как это работает и как внедрить на практике.
Что такое HMAC и зачем он нужен
HMAC (Hash-based Message Authentication Code) — криптографический механизм, который проверяет целостность данных и подлинность отправителя. Вместо передачи API-ключа в каждом запросе вы подписываете запрос секретным ключом и отправляете только подпись.
Принцип работы:
- Клиент и сервер заранее обмениваются секретным ключом (один раз, по защищённому каналу)
- Клиент формирует строку из параметров запроса и вычисляет HMAC-хеш с этим ключом
- Сервер повторяет вычисление и сравнивает результат
- Если хеши совпадают — запрос подлинный и не изменялся по дороге
Популярные алгоритмы: HMAC-SHA256, HMAC-SHA512. SHA1 считается устаревшим.
Что именно подписывать
Стандартная практика — включать в подпись:
- HTTP-метод (GET, POST)
- Путь запроса (
/api/users/123) - Временную метку (защита от replay-атак)
- Тело запроса (для POST/PUT)
- Опционально: заголовки, параметры запроса
Пример строки для подписи:
POST\n/api/orders\n1704988800\n{"product_id":42,"quantity":2}
Временная метка обязательна: сервер должен отклонять запросы старше 5-15 минут, иначе злоумышленник может перехватить подпись и повторить запрос позже (replay-атака).
Генерация подписи на клиенте
Node.js
const crypto = require('crypto');
function signRequest(method, path, timestamp, body, secret) {
const message = `${method}\n${path}\n${timestamp}\n${body}`;
return crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
}
const timestamp = Math.floor(Date.now() / 1000);
const body = JSON.stringify({ product_id: 42, quantity: 2 });
const signature = signRequest('POST', '/api/orders', timestamp, body, 'my-secret-key');
// Отправляем запрос
fetch('https://api.example.com/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Timestamp': timestamp.toString(),
'X-Signature': signature
},
body: body
});
Python
import hmac
import hashlib
import time
import json
def sign_request(method, path, timestamp, body, secret):
message = f"{method}\n{path}\n{timestamp}\n{body}"
signature = hmac.new(
secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature
timestamp = int(time.time())
body = json.dumps({"product_id": 42, "quantity": 2})
signature = sign_request('POST', '/api/orders', timestamp, body, 'my-secret-key')
# Отправка с requests
import requests
requests.post('https://api.example.com/api/orders',
headers={
'X-Timestamp': str(timestamp),
'X-Signature': signature
},
data=body
)
PHP
function signRequest($method, $path, $timestamp, $body, $secret) {
$message = "$method\n$path\n$timestamp\n$body";
return hash_hmac('sha256', $message, $secret);
}
$timestamp = time();
$body = json_encode(['product_id' => 42, 'quantity' => 2]);
$signature = signRequest('POST', '/api/orders', $timestamp, $body, 'my-secret-key');
$ch = curl_init('https://api.example.com/api/orders');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-Timestamp: $timestamp",
"X-Signature: $signature"
]
]);
curl_exec($ch);
Проверка подписи на сервере
Сервер повторяет вычисление и сравнивает результаты в constant-time режиме (чтобы избежать timing-атак):
from flask import Flask, request, abort
import hmac
import hashlib
import time
app = Flask(__name__)
SECRET_KEY = 'my-secret-key'
MAX_AGE = 300 # 5 минут
@app.route('/api/orders', methods=['POST'])
def create_order():
# Извлекаем заголовки
timestamp = request.headers.get('X-Timestamp')
signature = request.headers.get('X-Signature')
if not timestamp or not signature:
abort(401, 'Missing authentication headers')
# Проверяем свежесть запроса
if abs(time.time() - int(timestamp)) > MAX_AGE:
abort(401, 'Request expired')
# Вычисляем ожидаемую подпись
body = request.get_data(as_text=True)
message = f"{request.method}\n{request.path}\n{timestamp}\n{body}"
expected = hmac.new(
SECRET_KEY.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Сравниваем в constant-time
if not hmac.compare_digest(signature, expected):
abort(401, 'Invalid signature')
# Запрос подлинный, обрабатываем
return {'status': 'success'}
Важно: используйте hmac.compare_digest() вместо обычного ==. Это защищает от timing-атак, когда злоумышленник измеряет время ответа сервера, чтобы угадать правильную подпись посимвольно.
Частые ошибки и как их избежать
Разный порядок параметров. Клиент подписывает method + path + timestamp, а сервер ждёт path + method + timestamp — подписи не совпадут. Документируйте формат строго.
Проблемы с кодировками. JSON с пробелами и без — разные строки. Используйте canonical JSON или подписывайте сырые байты тела запроса.
Забыли про Content-Type. Если клиент отправляет application/x-www-form-urlencoded, а вы читаете request.json, получите разные данные.
Игнорирование временных меток. Без проверки свежести запроса HMAC бесполезен против replay-атак.
Логирование подписей. Подпись — это фактически производная от секретного ключа. Не пишите её в логи в открытом виде, особенно вместе с timestamp и телом запроса.
Альтернативные подходы
OAuth 2.0 — если нужна авторизация от имени пользователя, а не сервис-сервис взаимодействие. Сложнее в реализации, но стандартизирован.
JWT (JSON Web Tokens) — подходит для stateless-авторизации. В отличие от HMAC, JWT содержит payload с данными (claims). Можно использовать HMAC-подпись внутри JWT.
API Gateway с mutual TLS — самый надёжный вариант для микросервисов. Сертификаты проверяются на уровне TLS, без дополнительной логики в коде.
AWS Signature Version 4 — промышленный стандарт от Amazon. Сложнее базового HMAC, но защищает от большего числа атак. Используется в AWS API.
Инструменты для работы с хешами
Для тестирования HMAC-подписей и отладки полезны онлайн-инструменты. HMAC Generator позволяет быстро сгенерировать подпись для проверки логики на клиенте и сервере. Если нужно закодировать параметры перед подписью, пригодится URL Encoder для правильного экранирования спецсимволов. А для Base64-кодирования бинарных подписей (если передаёте не hex, а raw bytes) используйте Base64 Encoder.
HMAC — простой и эффективный способ защитить API без передачи секретов в каждом запросе. Главное — правильно формировать строку для подписи, проверять временные метки и использовать constant-time сравнение на сервере. Остальное — дело техники.