Аутентификация и подпись

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

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

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

Порядок действий:

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

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

  3. После настройки и верификации кассы перейдите во вкладку API.

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

  5. В открывшемся окне будут указаны:

    • название кассы;

    • URL продукта.

  6. Нажмите кнопку Сгенерировать ключ.

  7. Скопируйте и сохраните значения:

    • UUID — идентификатор кассы. Используется при взаимодействии с API;

    • Private key — приватный RSA-ключ в формате PEM. Используется для формирования цифровой подписи.

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

  8. Передайте значения UUID и Private key в команду разработки или DevOps и обеспечьте их защищенное хранение.

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

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

  • x-access-timestamp

  • x-access-merchant-id

  • 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 содержит идентификатор кассы. Используется значение UUID, полученное при генерации ключей для кассы.

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

Пример:

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

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

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

Пример расчета значения заголовка x-access-token:

public_key = private_key.public_key().export_key()
api_key = base64.urlsafe_b64encode(public_key).decode("utf-8")
# api_key используется как значение x-access-token

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

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

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

  1. Нормализовать тело запроса в строку joined_result с помощью функции normalize_message.

  2. Закодировать строку joined_result в Base64Url.

  3. Конкатенировать полученную Base64Url-строку и значение timestamp в виде строки.

  4. Вычислить хеш полученной строки по алгоритму SHA-256.

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

  6. Закодировать подпись в Base64Url.

  7. Передать полученное значение в заголовке x-access-signature.

Формат строки сообщения:

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

Где:

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

  • timestamp — значение заголовка x-access-timestamp.

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

Схема формирования подписи запроса

flowchart TD

  %% Входные данные
  A["JSON-тело запроса (payload)"] --> B["Нормализовать payload\n(normalize_message)"]
  B --> C["Собрать строку параметров"]
  C --> D["Закодировать строку в Base64Url"]

  %% Добавление метки времени
  D --> E["Соединить с timestamp"]
  T["Метка времени (timestamp)"] --> E
  E --> F["Вычислить SHA-256 от строки"]
  F --> G["Подписать результат RSA-ключом"]
  G --> H["Заголовок x-access-signature"]

  %% Формирование токена
  P["Публичный ключ кассы"] --> Q["Закодировать ключ в Base64Url"]
  Q --> R["Заголовок x-access-token"]

  %% Общий результат
  H --> Z["HTTP-запрос с заголовками"]
  R --> Z

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

import base64
import json
import time
import requests

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

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

# Идентификатор кассы. Получен при создании кассы и генерации ключей для нее
project_id = "57aff4db-b45d-42bf-bc5f-b7a499a01782"

# Путь до файла с приватным ключом, который вы получили в личном кабинете
private_key_path = "./private.pem"

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


def parse_json(prefix, _obj, _result):
    if isinstance(_obj, dict):
        for key, value in _obj.items():
            new_prefix = f"{prefix}:{key}" if prefix else key
            parse_json(new_prefix, value, _result)
    elif isinstance(_obj, list):
        for index, item in enumerate(_obj):
            new_prefix = f"{prefix}:{index}"
            parse_json(new_prefix, item, _result)
    else:
        _result.append(f"{prefix}:{_obj or 'None'}")


def normalize_message(_payload):
    _result = []
    parse_json("", _payload, _result)
    _result.sort()
    _joined_result = ";".join(_result)
    return _joined_result


with open(private_key_path, "rb") as f:
    private_key = RSA.import_key(f.read())

public_key = private_key.public_key().export_key()
api_key = base64.urlsafe_b64encode(public_key).decode("utf-8")
timestamp = int(time.time())

if payload:
    dumped = json.dumps(payload, separators=(",", ":"))
else:
    dumped = "{}"

joined_result = normalize_message(payload)

message = "{}{}".format(
    base64.urlsafe_b64encode(joined_result.encode()).decode("utf-8"),
    str(timestamp),
).encode("utf-8")

signer = PKCS115_SigScheme(private_key)
signature = signer.sign(SHA256.new(message))
base64_sign = base64.urlsafe_b64encode(signature).decode("ascii")

headers = {
    "content-type": "application/json",
    "x-access-token": api_key,
    "x-access-signature": base64_sign,
    "x-access-merchant-id": project_id,
    "x-access-timestamp": str(timestamp),
}

if payload:
    response = requests.post(
        url,
        headers=headers,
        data=dumped,
    )
else:
    response = requests.get(
        url,
        headers=headers,
    )

print(response.status_code)