ttools
apiбезопасностькриптографияhmac

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 сравнение на сервере. Остальное — дело техники.

Инструменты по теме