Подпись обеспечивает целостность запроса и привязку к мерчанту, методу, пути, телу и времени. Подписывается канонический запрос, поэтому реализация одинакова на любом языке, и есть защита от replay-атак (timestamp + nonce).
Это текущая стандартная схема. Старая схема (HMAC-SHA512 поверх PHP-нормализованного JSON, заголовок
X-Authorization-Sign) считается legacy — см. раздел Legacy.
Реализации: Go, JavaScript / Node, Python, PHP. Все дают побайтно одинаковую подпись. Для отладки в Postman — готовый Pre-request Script: Подпись — Postman.
Клиент добавляет к запросу 4 заголовка:
| Заголовок | Значение |
|---|---|
X-Client-Id |
Идентификатор мерчанта. |
X-Timestamp |
Время формирования запроса, UTC Unix-секунды (строка). |
X-Nonce |
Уникальный идентификатор запроса. Рекомендуется UUID v4. |
X-Signature |
Подпись запроса, hex (64 символа). |
stringToSign = METHOD + "\n" +
PATH + "\n" +
CANONICAL_QUERY + "\n" +
HEX(SHA256(body)) + "\n" +
CLIENT_ID + "\n" +
TIMESTAMP + "\n" +
NONCE
X-Signature = HEX(HMAC_SHA256(stringToSign, secret))
| Компонент | Источник |
|---|---|
METHOD |
HTTP-метод в верхнем регистре (GET/POST/PUT/PATCH/DELETE). |
PATH |
Нормализованный путь без схемы, домена, query и fragment (см. ниже). |
CANONICAL_QUERY |
Каноническое представление query-параметров (см. ниже). Пусто, если query нет. |
HEX(SHA256(body)) |
SHA-256 от raw body. Пустое тело → SHA256(""). |
CLIENT_ID |
Значение X-Client-Id. |
TIMESTAMP |
Значение X-Timestamp. |
NONCE |
Значение X-Nonce. |
Секретный ключ выдаётся приложению индивидуально для каждого окружения (тест/прод). Ключ не должен попадать в открытый код или логи.
В подпись входит только путь — без схемы, домена, порта, query и fragment.
Правила нормализации:
/.//+ сжимается до одного /. ///api//x → /api/x./ убирается, кроме корня. /api/ → /api, / → /./.. и .. запрещены.A-Z a-z 0-9 / - _. Любой другой символ (точка в имени, %, пробел, не-ASCII) → запрос невалиден./Callback ≠ /callback.| URL | PATH |
|---|---|
https://gw.example.com/deposit/callback |
/deposit/callback |
https://gw.example.com/api/withdrawal/status/ |
/api/withdrawal/status |
https://gw.example.com/callback/ids_api?op=42 |
/callback/ids_api |
///api//withdrawal///status// |
/api/withdrawal/status |
Цель — одно предсказуемое представление query, одинаковое на клиенте и сервере. Канонизация не превращает query в map/object (это потеряло бы повторы и пустые значения).
Алгоритм:
? (без ?). Нет query → CANONICAL_QUERY пустой.&. Пустой фрагмент (a=1&&b=2, ведущий/завершающий &) → невалидно.=. Нет = → значение пустое. Пустой ключ (=value) → невалидно.%XX → байт, + → пробел. Невалидный %XX или невалидный UTF-8 после декодирования → невалидно.A-Z a-z 0-9 - . _ ~;%XX в верхнем регистре;%20 (никогда +); / → %2F.& в формате key=value (даже при пустом значении).| Исходная query | CANONICAL_QUERY |
|---|---|
b=2&a=hello+world |
a=hello%20world&b=2 |
tag=b&tag=a&empty=&flag |
empty=&flag=&tag=a&tag=b |
name=%D0%A2%D0%B5%D1%81%D1%82&path=a%2Fb |
name=%D0%A2%D0%B5%D1%81%D1%82&path=a%2Fb |
z=9&b=2&a=3&a=10&a=2 |
a=10&a=2&a=3&b=2&z=9 |
Подписывается SHA256 от тела ровно в том виде, в каком оно уходит в запрос (raw body, без JSON-нормализации). Пустое тело → SHA256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
⚠️ Подписывайте и отправляйте ровно одни и те же байты. Тело хэшируется как есть, без какой-либо JSON-нормализации (компактизации whitespace, сортировки ключей) — сервер хэширует полученные байты дословно. Если тело переформатируется между подписанием и отправкой (pretty-print, прокси, повторная сериализация) — подпись сломается. Сериализуйте тело один раз и передавайте одну и ту же строку и в подпись, и в запрос.
Best practice. Подпись считается над raw-телом (как в AWS SigV4, Stripe, GitHub webhooks), а не над «канонизированным» JSON. Канонизация (компакт, сортировка ключей) намеренно НЕ требуется: она зависит от языка/библиотеки (порядок ключей, запись чисел 1000.00, экранирование unicode/HTML) и возвращает кросс-язычную хрупкость. Целостность держится на инварианте «байты подписи = байты запроса»; компактный JSON — лишь рекомендация (гигиена), не условие корректности.
| Механизм | Правило |
|---|---|
timestamp |
Сервер принимает запрос, только если X-Timestamp в пределах ±5 минут от своего времени (UTC). Часы клиента должны быть синхронизированы по NTP. |
nonce |
Сервер хранит использованные nonce в Redis с TTL = окну подписи (5 минут). Повторный nonce в пределах окна → запрос отклоняется. |
timestamp ограничивает срок жизни подписи (и срок хранения nonce), nonce гарантирует однократность внутри этого окна. Вместе — базовая защита от повторной отправки перехваченного запроса.
timestamp (окно ±5 мин).nonce ещё не использовался; сохранить с TTL 5 мин.stringToSign из метода, пути, query, тела, X-Client-Id, X-Timestamp, X-Nonce и пересчитать HMAC_SHA256 секретом мерчанта.X-Signature (constant-time). Несовпадение → запрос отклоняется.Любая ошибка (истёкший timestamp, повторный nonce, неверная подпись, невалидный path/query) → запрос не проходит аутентификацию.
Секрет: krionyx_secret_key. Эти же векторы зашиты в тесты Go/JS/Python — по ним можно сверить свою реализацию.
| # | METHOD | PATH | rawQuery | body | clientId | timestamp | nonce | X-Signature |
|---|---|---|---|---|---|---|---|---|
| 1 | GET | /api/billing/payment-methods |
amount=20&hash=abc&paymentType=deposit |
(пусто) | merchant_1 |
1730390400 |
550e8400-e29b-41d4-a716-446655440000 |
b91ccfa5a51b7438b426c4db1326f56869fe90dfd7070b0bc933e86fdee62902 |
| 2 | POST | /api/accounts/external-app/deposit |
(нет) | {"amount":2004,"currency":"RUB"} |
merchant_1 |
1730390400 |
b2c3d4e5-f6a7-8901-bcde-f12345678901 |
45ddf408562b8f0d5a549cb19dc092d88d7ea55969f28c05d113c859803ea921 |
| 3 | post | /api/x |
(нет) | (пусто) | m |
0 |
n |
d174dcaeed0a08b0d3297039c7863ce190cc50b0827ec95fcba21923cc2b8169 |
| 4 | POST | /api/text |
(нет) | {"text":"Привет мир!"} |
merch |
1700000000 |
nonce-1 |
011366b50761f86384b65b78a00539915b5f06f701dd18c5e0f689d9a19409da |
| 5 | POST | /api/text |
(нет) | {"html":"<div>&my test</div>"} |
merch |
1700000000 |
nonce-2 |
7e2800e716c1cf75b166d19bf41f3d287a1858da5c2c26a84fe7963d84827029 |
| 6 | GET | ///api//withdrawal///status/ |
b=2&a=hello+world |
(пусто) | m |
1700000000 |
n |
a33f50abeb513a6ba7b5989ca6ed034b6792a3c91e69db14a03c0cb21d863cfa |
| 7 | GET | /list |
tag=b&tag=a&empty=&flag |
(пусто) | m |
1700000000 |
n |
0661370163a7911271d58d428df900dac16d9207c4a1a9aa5c3d055165f5b0d6 |
| 8 | PUT | /api/orders/123 |
(нет) | {"status":"approved","amount":100.50} |
m |
1700000000 |
n |
b267f362214607515fc6f62238bfe7f7f5e888196077b9cb22f35102e685fc4c |
| 9 | DELETE | /api/orders/123 |
reason=duplicate&forced=true |
(пусто) | m |
1700000000 |
n |
bd4bd8e778441c0a93c6fb0be354a3f8a0d76bb46e34b6abeb4ec700700c6d4d |
| 10 | GET | / |
(нет) | (пусто) | cl |
1 |
u |
a628409be660a17ddc18e3aa79111a7a1b6c46ab8cc37734b7211b8c9326bfb7 |
| 11 | POST | /api/payment |
v=1 |
{"text":"тест & проверка 1 <ok>","amount":100.50} |
merchant_xyz |
1730390400 |
nn |
07714c6d4bb71fe162bbba1f616d7e9f73effc148b74ffaff07604c7e8bbe7a5 |
| 12 | POST | /api/v2/external-app/withdraw |
(нет) | {"id":12345,"amount":1000} |
42 |
1730390400 |
abc-123 |
af8fe442f768927920b1502800282de5ec6db60dbc4903dce45214b8ae7f04da |
| 13 | POST | /api/x |
(нет) | { "x": 1 } (с пробелами — хэшируется как есть) |
m |
1700000000 |
n |
8b5e71fc75d6b83a6b316a973ba881f3d7044c5eb55153622e66672b851f7b35 |
Развёрнутый stringToSign для вектора #2 (символ \n показан как перевод строки):
POST
/api/accounts/external-app/deposit
85c656b5c57e99c587d22633664c27680561ef41bdf52f3b4ccb0cfe400b72ee
merchant_1
1730390400
b2c3d4e5-f6a7-8901-bcde-f12345678901
(третья строка — CANONICAL_QUERY — пустая; четвёртая — SHA256 тела).
Старая схема подписи. Поддерживается только на переходный период; после миграции всех клиентов будет отключена.
| Legacy | Текущая (стандарт) | |
|---|---|---|
| Заголовок подписи | X-Authorization-Sign |
X-Signature |
| Алгоритм | HMAC-SHA512 | HMAC-SHA256 |
| Что подписывается | PHP-нормализованный JSON тела | канонический запрос (method+path+query+body+client+ts+nonce) |
| Replay-защита | нет | X-Timestamp + X-Nonce |
Сервер выбирает стандартную схему, если пришёл заголовок X-Signature, иначе — legacy.