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

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

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

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

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

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

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

Перед валидацией подписи выполните стандартные проверки (формат JSON, наличие обязательных полей и т.п.).

Публичный RSA-ключ отображается в личном кабинете только если для кассы настроен алгоритм RSA-SHA256. Текущий алгоритм подписи отображается в разделе APIНастройки Callback (например, Алгоритм: RSA или Алгоритм: HMAC). Если для кассы настроен алгоритм HMAC, используйте раздел Подпись оповещений (HMAC).

Получение публичного ключа для проверки подписи

Для проверки подписи оповещений используйте публичный RSA-ключ, настроенный для кассы.

Порядок получения ключа:

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

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

  3. В модальном окне найдите блок:

    • Public Key — если для кассы настроен алгоритм RSA.

    • HMAC key — если для кассы настроен алгоритм HMAC (в этом случае используйте раздел Подпись оповещений (HMAC)).;

  4. Для RSA нажмите на иконку скачивания в блоке Public key и сохраните файл ключа.

  5. Настройте сервис обработки оповещений на использование этого ключа в алгоритме проверки подписи.

Нормализация тела запроса

Нормализация выполняется рекурсивным обходом 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)
        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: преобразуются в строку None. Не используйте пустые строки или пробелы.

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

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

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

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

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

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

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

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

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

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

  4. Вычисление хеша SHA-256 от результирующей строки.

  5. Подписание хеша приватным RSA-ключом по схеме RSA-SHA256.

  6. Кодирование подписи в Base64Url.

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

На стороне мерчанта необходимо воспроизвести шаги 1-6 и проверить подпись с использованием публичного ключа кассы.

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

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

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

    • меткой времени;

    • подписью;

    • публичным RSA-ключом кассы (если он передается в заголовке), либо заранее сохраните публичный ключ из личного кабинета.

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

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

  4. Сконструируйте строку message = encoded + str(timestamp).

  5. Вычислите SHA-256 от message.

  6. Декодируйте подпись из Base64Url.

  7. Проверьте подпись с использованием публичного RSA-ключа.

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

import base64

from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature.pkcs1_15 import PKCS115_SigScheme


def verify_rsa_callback_signature(
    payload: dict,
    signature_b64url: str,
    public_key_pem: bytes,
    timestamp: int,
) -> bool:
    """
    Проверить подпись оповещения по алгоритму RSA-SHA256.
    """
    try:
        # Нормализация и подготовка сообщения
        normalized = normalize_message(payload)
        normalized_bytes = normalized.encode("utf-8")
        encoded_base64url = base64.urlsafe_b64encode(normalized_bytes).decode("utf-8")
        concatenated_with_ts = f"{encoded_base64url}{timestamp}"
        message = concatenated_with_ts.encode("utf-8")

        # Подготовка ключа и подписи
        public_key = RSA.import_key(public_key_pem)
        verifier = PKCS115_SigScheme(public_key)
        signature = base64.urlsafe_b64decode(signature_b64url)

        # Проверка подписи
        verifier.verify(SHA256.new(message), signature)
        return True
    except Exception:
        return False

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

  • Сохраните публичный ключ кассы в конфигурации приложения.

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

  • Проверяйте идемпотентность оповещений по полям project_id, payment_id, status, sub_status.

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