Тема
HMAC подпись — пошагово
Цель — научиться правильно подписывать запросы. 90% багов интеграции — именно здесь.
Алгоритм
- Соберите сырое тело запроса — те самые байты, что вы отправите на сервер.
- Возьмите
hmac_secret(тот, что выдан менеджером). - Посчитайте
HMAC-SHA256(body, hmac_secret). - Закодируйте результат в hex.
- Поставьте в заголовок:
X-Signature: sha256=<hex>.
Самая частая ошибка — переформатирование JSON
В большинстве HTTP-клиентов, когда вы передаёте объект, библиотека сама сериализует его в JSON перед отправкой. При этом форматирование может быть другим, чем то, что вы посчитали для подписи.
Неправильно (axios сериализует объект сам):
js
const body = { phone: '+380501234567' }
const sig = hmac(JSON.stringify(body), secret)
axios.post(url, body, { headers: { 'X-Signature': `sha256=${sig}` } })
// ❌ axios передал свой JSON, а вы подписали свойПравильно — сериализовать один раз и отправить ту же строку:
js
const body = JSON.stringify({ phone: '+380501234567' })
const sig = hmac(body, secret)
axios.post(url, body, {
headers: {
'Content-Type': 'application/json',
'X-Signature': `sha256=${sig}`,
},
})
// ✅ подписана та же строка, что улетает на серверПримеры на разных языках
Node.js (crypto)
js
import crypto from 'node:crypto'
const body = JSON.stringify({
external_id: 'aff42-lead-001',
phone: '+380501234567',
source: 'facebook_ads',
})
const signature = crypto
.createHmac('sha256', process.env.HMAC_SECRET)
.update(body)
.digest('hex')
// header: `X-Signature: sha256=${signature}`PHP
php
$body = json_encode([
'external_id' => 'aff42-lead-001',
'phone' => '+380501234567',
'source' => 'facebook_ads',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$signature = hash_hmac('sha256', $body, $hmacSecret);
// header: "X-Signature: sha256={$signature}"Python
python
import hmac, hashlib, json, os
body = json.dumps({
"external_id": "aff42-lead-001",
"phone": "+380501234567",
"source": "facebook_ads",
}, separators=(',', ':'), ensure_ascii=False).encode('utf-8')
signature = hmac.new(
os.environ['HMAC_SECRET'].encode(),
body,
hashlib.sha256,
).hexdigest()
# header: f"X-Signature: sha256={signature}"Go
go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
mac := hmac.New(sha256.New, []byte(hmacSecret))
mac.Write(body) // body — те же байты, что отправите как тело
signature := hex.EncodeToString(mac.Sum(nil))
// header: "X-Signature: sha256=" + signatureПроверка нашей подписи (для callback'ов)
Когда мы шлём callback вам, мы подписываем его callback_secret. Ваша задача — проверить.
Node.js
js
import crypto from 'node:crypto'
function verifyCallback(rawBody: string, sigHeader: string, secret: string) {
const sig = sigHeader.startsWith('sha256=') ? sigHeader.slice(7) : sigHeader
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
// timing-safe compare — критично, не используйте обычное ===
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(sig, 'hex'),
)
}PHP
php
function verifyCallback(string $rawBody, string $sigHeader, string $secret): bool {
$sig = str_starts_with($sigHeader, 'sha256=')
? substr($sigHeader, 7)
: $sigHeader;
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $sig); // timing-safe
}Используйте timing-safe сравнение
Обычное === / == для подписей — уязвимость к timing attack. Используйте hash_equals (PHP), crypto.timingSafeEqual (Node), hmac.compare_digest (Python).
Как отладить, если подпись не сходится
Логируйте точные байты body, которые улетают на сервер (до сериализации сетевым клиентом).
Логируйте hex-подпись, которую вы посчитали.
На вашей стороне посчитайте подпись от того же body тем же secret в
curlилиopenssl:bashecho -n '<тело>' | openssl dgst -sha256 -hmac '<secret>'Если ваша подпись и
opensslсовпали, а сервер всё равно отвергает — значит, сетевой клиент переформатировал JSON. Соберите body как строку один раз и шлите её as-is.