Аутентификация и подпись запросов (RSA)

В этом разделе описана аутентификация запросов к HighHelp API с использованием алгоритма RSA-SHA256 и формат HTTP-заголовков, которые необходимо передавать в каждом запросе.

Используется асимметричная криптография: приватный ключ хранится в вашей интеграции и применяется для подписи запросов, публичный ключ хранится на стороне HighHelp и используется для проверки подписи.

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

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

Подпись запроса используется для:

  • аутентификации запроса (подтверждение владения ключом);

  • контроля целостности JSON-тела запроса.

Регистрация в системе

Регистрацию выполняйте через личный кабинет мерчанта. Доступ в личный кабинет предоставляет специалист HighHelp.

  1. Создайте кассу во вкладке Кассы.

  2. Обратитесь к прикреплённому специалисту HighHelp для настройки кассы и проведения верификации.

Описание формата подписи оповещений и алгоритма проверки приведено в разделе Подпись оповещений (RSA).

Получение API-ключей

Чтобы получить RSA-ключи для подписи запросов:

  1. После регистрации в системе перейдите во вкладку API.

  2. Найдите нужную кассу и нажмите на иконку шестерёнки.

  3. В открывшемся окне нажмите кнопку Обновить API ключи.

  4. В окне API ключи проверьте информацию о кассе.

  5. Нажмите и удерживайте кнопку Сгенерировать ключ до завершения генерации.

  6. Скачайте и сохраните:

    • private_key_<id>.pem — приватный RSA-ключ для подписи запросов;

    • public_key_<id>.pem — публичный RSA-ключ для подписи запросов;

    • hmac_private_key_<id>.txt — HMAC-ключ для подписи запросов (используется при HMAC-SHA512).

Приватный RSA-ключ генерируется на стороне браузера и не сохраняется на стороне HighHelp. На стороне HighHelp сохраняется только публичный RSA-ключ кассы, который используется для проверки подписи запросов.

Обновление API-ключей

Чтобы обновить API-ключи кассы:

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

  2. Перейдите во вкладку API.

  3. Найдите нужную кассу и нажмите на иконку шестеренки.

  4. В открывшемся окне нажмите кнопку Обновить API ключи.

  5. В модальном окне API ключи проверьте информацию о кассе.

  6. Нажмите и удерживайте кнопку Сгенерировать ключ до завершения генерации.

  7. Сохраните результат генерации:

    • ID — идентификатор кассы (UUID). Используется при взаимодействии с API и передаётся в заголовке x-access-merchant-id;

    • private_key_<id>.pem — новый приватный RSA-ключ для подписи запросов;

    • public_key_<id>.pem — новый публичный RSA-ключ для подписи запросов;

    • hmac_private_key_<id>.txt — новый HMAC-ключ для подписи запросов.

  8. Обновите конфигурацию вашей интеграции, заменив старые ключи на новые.

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

Кнопка Обновить API ключи обновляет только ключи для подписи запросов и не влияет на ключи для подписи оповещений.

Аутентификация в API

Для аутентификации запросов API и проверки целостности тела запроса используйте следующие HTTP-заголовки:

  • x-access-timestamp

  • x-access-merchant-id

  • x-access-merchant-algorithm (необязательный для RSA)

  • x-access-signature

  • x-access-token

Заголовок x-access-timestamp

x-access-timestamp содержит время формирования запроса.

  • Формат — Unix timestamp в секундах (количество секунд с 01.01.1970 00:00:00 UTC).

  • Тип — строка с десятичным целым числом.

Пример:

x-access-timestamp: 1716299720

Заголовок x-access-merchant-id

x-access-merchant-id содержит идентификатор кассы.

  • Значение — ID (UUID), полученный при генерации ключей для кассы.

  • Тип — строка.

В примерах кода идентификатор кассы передается через переменную project_id.

Пример:

x-access-merchant-id: 57aff4db-b45d-42bf-bc5f-b7a499a01782

Заголовок x-access-merchant-algorithm

x-access-merchant-algorithm позволяет явно указать используемый алгоритм подписи.

  • Для RSA заголовок является необязательным.

  • Возможное значение для RSA: RSA-SHA256 (значение согласуйте со специалистом HighHelp).

Заголовок x-access-token

x-access-token содержит публичный ключ кассы, закодированный в формате Base64Url.

Порядок формирования значения x-access-token:

  1. Загрузите приватный RSA-ключ в формате PEM.

  2. Получите из него публичный ключ.

  3. Экспортируйте публичный ключ в бинарном виде.

  4. Закодируйте полученные байты в Base64Url.

  5. Передавайте полученную строку в заголовке x-access-token.

Пример расчета значения x-access-token на Python (библиотека cryptography):

from cryptography.hazmat.primitives import serialization
import base64

# private_key: объект приватного ключа, загруженный из PEM

public_key_bytes = private_key.public_key().public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
api_key = base64.urlsafe_b64encode(public_key_bytes).decode("utf-8")

# api_key используйте как значение заголовка x-access-token

Заголовок x-access-signature

x-access-signature содержит цифровую подпись тела запроса и метки времени.

Алгоритм подписи (схематично):

message = base64url(normalized_payload) + str(timestamp)
signature = RSA-SHA256(message)
x-access-signature = base64url(signature)

Где:

  • normalized_payload — нормализованное представление JSON-тела запроса;

  • timestamp — значение заголовка x-access-timestamp (Unix timestamp в секундах).

Если тело запроса отсутствует, используйте пустой объект {}.

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

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

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

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

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

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

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

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

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

Пример реализации нормализации (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-структуры.

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

  1. Сформируйте объект payload с телом запроса.

  2. Нормализуйте payload функцией normalize_message():

    joined_result = normalize_message(payload)
  3. Закодируйте нормализованную строку в Base64Url и добавьте метку времени:

    timestamp = int(time.time())
    message = "{}{}".format(
        base64.urlsafe_b64encode(joined_result.encode()).decode("utf-8"),
        str(timestamp),
    ).encode('utf-8')
  4. Подпишите байтовую строку message приватным RSA-ключом по алгоритму RSA-SHA256 (PKCS#1 v1.5).

  5. Закодируйте байты подписи в Base64Url и передайте результат в заголовке x-access-signature.

Пример запроса с RSA-подписью (Python3)

import base64
import json
import time
import requests

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization

url = "https://api.hh-processing.com/api/v1/payment/p2p/payin"

# Идентификатор кассы (UUID)
project_id = "57aff4db-b45d-42bf-bc5f-b7a499a01782"

# Приватный RSA-ключ (PEM)
with open("private_key.pem", "rb") as key_file:
    private_key = serialization.load_pem_private_key(
        key_file.read(),
        password=None,
    )

payload = {
    "general": {
        "project_id": project_id
    }
}


def parse_json(prefix, obj, result):
    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(data):
    result = []
    parse_json("", data, result)
    result.sort()
    return ";".join(result)


# Публичный ключ (Base64Url)
public_key_bytes = private_key.public_key().public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
api_key = base64.urlsafe_b64encode(public_key_bytes).decode("utf-8")

# Метка времени Unix (секунды)
timestamp = int(time.time())

# Нормализация тела запроса
joined_result = normalize_message(payload)

# Формирование сообщения для подписи
message = "{}{}".format(
    base64.urlsafe_b64encode(joined_result.encode()).decode("utf-8"),
    str(timestamp),
).encode("utf-8")

# Подпись RSA-SHA256
signature_bytes = private_key.sign(
    message,
    padding.PKCS1v15(),
    hashes.SHA256()
)
signature_b64url = base64.urlsafe_b64encode(signature_bytes).decode("utf-8")

# Заголовки
headers = {
    "content-type": "application/json",
    "x-access-token": api_key,
    "x-access-signature": signature_b64url,
    "x-access-merchant-id": project_id,
    "x-access-timestamp": str(timestamp),
}

# Тело запроса (JSON)
dumped = json.dumps(payload, separators=(",", ":")) if payload else "{}"

response = requests.post(url, headers=headers, data=dumped)
print(response.status_code)

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

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

  • Не логируйте приватный ключ.

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

  • Публичный ключ используйте для проверки подписи.

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

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

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

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

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

  2. Вставьте приватный RSA-ключ (PEM).

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

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

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

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

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

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

JSON-тело запроса:

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

Приватный RSA-ключ (PEM):

-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

Timestamp: 1716299720

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

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

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

  2. base64url(normalized).

  3. message = base64url(normalized) + timestamp.

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

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