Skip to content

HMAC подпись — пошагово

Цель — научиться правильно подписывать запросы. 90% багов интеграции — именно здесь.

Алгоритм

  1. Соберите сырое тело запроса — те самые байты, что вы отправите на сервер.
  2. Возьмите hmac_secret (тот, что выдан менеджером).
  3. Посчитайте HMAC-SHA256(body, hmac_secret).
  4. Закодируйте результат в hex.
  5. Поставьте в заголовок: 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).

Как отладить, если подпись не сходится

  1. Логируйте точные байты body, которые улетают на сервер (до сериализации сетевым клиентом).

  2. Логируйте hex-подпись, которую вы посчитали.

  3. На вашей стороне посчитайте подпись от того же body тем же secret в curl или openssl:

    bash
    echo -n '<тело>' | openssl dgst -sha256 -hmac '<secret>'
  4. Если ваша подпись и openssl совпали, а сервер всё равно отвергает — значит, сетевой клиент переформатировал JSON. Соберите body как строку один раз и шлите её as-is.

Закрытая партнёрская документация. Не для публичного распространения.