Перейти к основному содержимому

Подпись HTML-формы

Любое поле формы покупатель может изменить прямо в браузере — например, поставить сумму меньше. Поэтому поля счета передаются в подписанном контейнере: сервис считает подпись заново и, если она не совпала, счет не создает.

Подпись считает сервер магазина. В браузер уходит только готовая пара data и signature.

Что подписывается

data = base64url(json)
signature = HMAC-SHA256(data, formSecret)

HMAC считается от строки data, а не от байтов JSON. Приемник сверяет подпись с присланной строкой как она есть и только потом раскодирует ее.

Отсюда следует, что канонизации нет: порядок ключей, отступы в JSON и способ экранирования юникода на подпись не влияют. Подходит любой штатный JSON-кодировщик.

Эталонный набор

Подставьте эти значения в свой код и сверьте результат.

Секрет для примера:

dvc3khto5WpjEPojZfYJsbJzFEPPVnRa

Сумма и срок оплаты числами, строкой они тоже принимаются:

{"projectId":"a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d","amountFiat":10.5,"currencyFiat":"USD","timeToPay":1,"description":"Order #7, delivery","serviceData":"order-7"}

Строка data — base64url этих байтов. Паддинг здесь отброшен, приемник принимает и с ним:

eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0

Подпись этой строки на секрете выше:

09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e

Примеры ниже с этими значениями печатают ровно такие data и подпись. Тот же JSON с ключом "output":"errors" в конце дает другую строку data и другую подпись.

Если data не совпала, а JSON выглядит так же — дело в кодировщике JSON: другой порядок ключей или пробелы после разделителей. Приемник примет и такой контейнер, но с эталоном он дословно не сойдется.

Примеры

<?php

function signedContainer(array $fields, string $formSecret): array {
$data = rtrim(strtr(base64_encode(json_encode($fields)), '+/', '-_'), '=');

return [
'data' => $data,
'signature' => hash_hmac('sha256', $data, $formSecret),
];
}

// Test values from the reference set. Before going live,
// replace the secret with the form secret from your project settings
$container = signedContainer([
'projectId' => 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
'amountFiat' => 10.5,
'currencyFiat' => 'USD',
'timeToPay' => 1,
'description' => 'Order #7, delivery',
'serviceData' => 'order-7',
], 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa');

echo $container['data'], "\n";
echo $container['signature'], "\n";

Значения в примерах латиницей, но код работает с любым юникодом без изменений. PHP и Python по умолчанию экранируют не-ASCII в \uXXXX, JavaScript отправляет сырой UTF-8 — приемник принимает оба вида.

Две частые ошибки

Подписан JSON вместо контейнера. HMAC считается от строки data, то есть от base64url. Ошибка выглядит как «все совпадает, а подпись нет».

Контейнер пересобран после подписи. Изменился хоть байт в data — подпись недействительна. Собирайте пару в одном месте кода.

Если подпись не сходится

Идите по цепочке JSON → data → подпись, где первое звено разошлось с эталоном — там и причина. Сначала сверьте data: она не зависит от секрета.