Подпись оповещений (HMAC)

В этом разделе описан алгоритм подписи и проверки оповещений (callback-сообщений), использующий HMAC-ключ.

Для подписи используется симметричный алгоритм HMAC-SHA512 и общий секретный ключ, известный только вашей системе и HighHelp.

Назначение подписи

Подпись оповещений с HMAC решает следующие задачи:

  • подтверждает, что сообщение отправлено платформой HighHelp, владеющей секретным ключом;

  • гарантирует, что данные в теле оповещения не были изменены по пути.

По умолчанию для подписи оповещений используется асимметричный алгоритм RSA-SHA256 (см. раздел Подпись оповещений (RSA)). HMAC-SHA512 доступен в качестве альтернативного метода.

Смена алгоритма подписи с RSA на HMAC

Переключение RSA на HMAC применяется только к подписи оповещений.

Для подписи запросов можно использовать RSA-ключ или HMAC-ключ из набора API-ключей. Алгоритм выбирается заголовком x-access-merchant-algorithm.

Перед переключением подготовьте обработчик оповещений к проверке HMAC-подписи.

Для переключения алгоритма подписи оповещений обратитесь к вашему менеджеру HighHelp. Смена выполняется на стороне HighHelp после согласования.

После переключения:

  • в окне APIНастройки Callback внизу отображается строка Текущий алгоритм: HMAC;

  • в окне отображается только блок HMAC key; блок Public Key не отображается.

Получение HMAC-ключа

Для проверки подписи оповещений используйте HMAC-ключ для подписи оповещений из окна APIНастройки Callback.

Ключ выгружается в файл hmac_callback_key_<id>.txt и доступен для скачивания только в момент генерации или обновления.

Для подписи запросов к API используется отдельный HMAC-ключ (см. Аутентификация и подпись запросов (HMAC)).

Генерация и обновление HMAC-ключа

При создании ключей кассы формируются HMAC-ключ и пара RSA-ключей.

Если требуется сгенерировать HMAC-ключ для подписи оповещений отдельно или обновить существующий ключ, используйте инструкции ниже.

HMAC-ключ для подписи оповещений доступен для скачивания только в момент генерации или обновления. После скачивания в разделе APIНастройки Callback отображается маскированное значение ключа; повторное скачивание недоступно, доступно только обновление ключа.

Для касс, созданных до релиза 2.4.0, HMAC-ключ для подписи оповещений может отсутствовать. Воспользуйтесь инструкцией по генерации HMAC-ключа для подписи оповещений.

Генерация HMAC-ключа

Если для кассы не был сгенерирован HMAC-ключ для подписи оповещений при первоначальной настройке, выполните следующие действия:

  1. Откройте личный кабинет мерчанта.

  2. Перейдите в раздел APIНастройки Callback.

  3. Нажмите на иконку + в блоке HMAC key.

  4. Сохраните ключ в защищенном хранилище и передайте команде разработки.

Обновление HMAC-ключа

Если требуется обновить HMAC-ключ для подписи оповещений, выполните следующие действия:

  1. Откройте личный кабинет мерчанта.

  2. Перейдите в раздел APIНастройки Callback.

  3. Нажмите на иконку обновления в блоке HMAC key.

  4. В открывшемся окне подтвердите операцию.

  5. Нажмите и удерживайте кнопку подтверждения до завершения.

  6. Скачайте файл hmac_callback_key_<id>.txt.

  7. Обновите конфигурацию вашей интеграции, заменив старый HMAC-ключ для подписи оповещений на новый.

Обновление HMAC-ключа не переключает алгоритм подписи оповещений.

После обновления HMAC-ключа старый ключ перестанет работать немедленно. Убедитесь, что новый ключ корректно настроен в вашей интеграции до начала использования. Рекомендуется выполнять обновление в период минимальной нагрузки на систему.

Нормализация тела оповещения

Для формирования подписи используется нормализованное представление JSON-тела оповещения.

Алгоритм нормализации:

  1. Обойдите JSON-структуру рекурсивно.

  2. Для каждого значения сформируйте путь в виде ключ1:ключ2:…​:значение.

  3. Для массивов используйте индексы элементов: :0, :1, …​

  4. Для булевых значений используйте: true1, false0.

  5. Отсортируйте все строки по алфавиту.

  6. Соедините строки через ;.

Пример исходных данных:

{
  "amount": 100,
  "status": "success",
  "is_paid": true,
  "data": {
    "id": 123,
    "is_active": false
  }
}

Результат нормализации:

amount:100;data:id:123;data:is_active:0;is_paid:1;status:success

Пример реализации нормализации (Python3)

def parse_json(prefix, obj, result):
    """
    Рекурсивный обход JSON-структуры для формирования пар путь:значение.
    """
    if isinstance(obj, dict):
        for key, value in obj.items():
            if isinstance(key, bool):
                key = int(key)
            new_prefix = f"{prefix}:{key}" if prefix else str(key)
            parse_json(new_prefix, value, result)
    elif isinstance(obj, list):
        for index, item in enumerate(obj):
            if isinstance(item, bool):
                item = int(item)
            new_prefix = f"{prefix}:{index}"
            parse_json(new_prefix, item, result)
    else:
        if isinstance(obj, bool):
            obj = int(obj)
        if obj is None:
            obj = ""
        result.append(f"{prefix}:{obj}")


def normalize_message(payload: dict) -> str:
    """
    Нормализация JSON в детерминированную строку (формат: путь:значение через ;).
    """
    items: list[str] = []
    parse_json("", payload, items)
    items.sort()
    return ";".join(items)

Требования к нормализации

При реализации алгоритма нормализации учитывайте следующие требования:

  • Булевы значения: преобразуются в целочисленное представление (true1, false0).

  • Значения null: преобразуются в пустую строку "".

  • Числа: используйте стандартное строковое представление без локализации (разделителей групп разрядов, локальных форматов). Не добавляйте незначащие нули.

  • Массивы: порядок элементов сохраняется в исходной последовательности. Индексы элементов добавляются к пути как :0, :1, :2, …​

  • Объекты: после формирования всех пар путь:значение выполняется сортировка по алфавиту по полной строке.

  • Кодировка символов: используйте UTF-8 для кодирования перед применением Base64Url. Не изменяйте регистр символов.

  • Base64Url: кодируйте строку в формате Base64Url (RFC 4648): замените + на -, / на _; символы выравнивания = не удаляйте.

  • Пробелы и форматирование: не добавляйте и не удаляйте пробелы в значениях. Используйте точные значения из JSON-структуры.

Алгоритм формирования подписи

Подпись формируется на стороне HighHelp по следующему алгоритму:

  1. Нормализация JSON-тела оповещения функцией normalize_message.

  2. Кодирование нормализованной строки в Base64Url.

  3. Конкатенация полученной строки и timestamp (строка).

  4. Вычисление HMAC-SHA512 от UTF-8 байт результирующей строки с использованием секретного ключа.

  5. Кодирование значения HMAC в Base64Url.

  6. Передача подписи и метки времени в HTTP-заголовках оповещения.

В вашей интеграции воспроизведите шаги 1—​5 и сравните вычисленную подпись с полученной.

Проверка подписи на стороне мерчанта

Для проверки подписи обработчик оповещений выполняет следующие проверки:

  • тело оповещения присутствует и содержит корректный JSON;

  • присутствуют заголовки x-access-token, x-access-timestamp, x-access-signature;

  • значение x-access-token совпадает с маской секретного HMAC-ключа, настроенного для кассы;

  • подпись корректна.

Коды ответа примера:

  • 409 — пустое тело оповещения, невалидный JSON, отсутствуют обязательные заголовки, несовпадение маски x-access-token, некорректный формат x-access-signature (ошибка base64url decode);

  • 403 — несовпадение подписи;

  • 200 — подпись корректна.

Для проверки подписи:

  1. Получите JSON-тело оповещения и значения заголовков с:

    • маской HMAC-ключа (x-access-token);

    • меткой времени (x-access-timestamp);

    • подписью (x-access-signature);

    • идентификатором кассы (x-access-merchant-id) для выбора корректного HMAC-ключа.

  2. Найдите соответствующий секретный ключ для кассы.

  3. Сравните маску из заголовка x-access-token с маской вашего HMAC-ключа.

  4. Нормализуйте тело оповещения в строку normalized по описанному алгоритму.

  5. Закодируйте normalized в Base64Url.

  6. Сконструируйте строку message = encoded + timestamp.

  7. Вычислите HMAC-SHA512 от UTF-8 байт message с использованием секретного ключа.

  8. Декодируйте подпись x-access-signature из Base64Url в байты и сравните байты подписи с вычисленным значением.

Пример проверки подписи (Python3)

import base64
import binascii
import hmac
import hashlib


def masked_hmac_key(key: str) -> str:
    if not key or len(key) <= 6:
        return "*******"
    return key[:3] + "*******" + key[-3:]


def base64url_decode(data: str) -> bytes:
    normalized = str(data).strip().replace("-", "+").replace("_", "/")
    if not normalized:
        raise ValueError("Invalid base64url string")
    padding = "=" * (-len(normalized) % 4)
    try:
        return base64.b64decode(f"{normalized}{padding}", validate=True)
    except binascii.Error as exc:
        raise ValueError("Invalid base64url string") from exc


def verify_hmac_callback_signature(
    payload: dict,
    signature_b64url: str,
    access_token: str,
    secret_key: str,
    timestamp: str,
) -> bool:
    """
    Проверить подпись оповещения по алгоритму HMAC-SHA512.
    """
    # Проверка маски ключа (x-access-token)
    expected_mask = masked_hmac_key(secret_key)
    if not hmac.compare_digest(expected_mask, access_token):
        raise ValueError("Invalid x-access-token mask")

    # Нормализация и подготовка сообщения
    normalized = normalize_message(payload)
    encoded = base64.urlsafe_b64encode(normalized.encode("utf-8")).decode("utf-8")
    message = f"{encoded}{timestamp}".encode("utf-8")

    # Вычисление ожидаемой подписи
    expected_signature = hmac.new(secret_key.encode("utf-8"), message, hashlib.sha512).digest()
    received_signature = base64url_decode(signature_b64url)

    # Сравнение подписи, устойчивое к тайминговым атакам
    return hmac.compare_digest(expected_signature, received_signature)

Рекомендации по безопасности

  • Храните секретный HMAC-ключ на стороне сервера.

  • Обновляйте ключи по запросу через специалиста HighHelp.

  • Не передавайте ключи по незащищенным каналам.

  • Не логируйте ключ полностью. Маскируйте значение: первые 3 символа + 7 астерисков (*) + последние 3 символа.

    Пример маскирования ключа
    def masked_hmac_key(key: str) -> str:
        if not key or len(key) <= 6:
            return "*******"
        return key[:3] + "*******" + key[-3:]
  • Проверяйте идемпотентность оповещений по полям project_id, payment_id, status, sub_status.

    Проверка индемпотентности гаррантирует, что повторная обработка одного и того же оповещения не изменяет конечный результат. Сохраняйте комбинацию этих полей для предотвращения дублирования операций.
  • Проверяйте допустимое окно времени для timestamp.

Форма для проверки подписи

Используйте форму ниже для проверки корректности формирования подписи HMAC-SHA512 для оповещений HighHelp.

  • Для проверки подписи оповещений используйте HMAC-ключ для подписи оповещений из APIНастройки Callback (hmac_callback_key_<id>.txt).

Обработка введенных данных выполняется локально в браузере; данные не передаются на сервер.

Выполнение проверки

  1. Вставьте JSON-тело в первое поле.

  2. Введите секретный ключ.

  3. Укажите временную метку в формате Unix timestamp.

  4. Вставьте подпись, которую необходимо проверить.

  5. Нажмите кнопку Проверить подпись.

Форма отобразит пошаговый процесс формирования подписи и результат проверки предоставленной подписи.

Пример тестовых данных

Для проверки подписи можно использовать следующие тестовые данные:

JSON-тело:

{"general":{"project_id":"test-project-123"},"payment":{"amount":100000,"currency":"USD"}}

Secret key: test-secret-key-123

Timestamp: 1716299720

Полученная подпись: signature-to-verify

После нажатия на кнопку Проверить подпись форма отобразит:

  1. Нормализованное представление данных.

  2. base64url(normalized).

  3. message = base64url(normalized) + timestamp.

  4. Вычисленную подпись (x-access-signature).

  5. Результат сравнения подписи.