Webhook EasyPay: проверка подписи
Этот документ описывает, как проверить, что webhook действительно отправлен EasyPay. Структуру данных самого платежа см. в Webhook EasyPay: структура данных платежей Stripe.
Общая информация
EasyPay подписывает каждый webhook, который отправляет на ваш сервер, — чтобы вы могли убедиться, что запрос действительно пришёл от EasyPay и не был изменён по дороге. Проверка занимает несколько строк кода (примеры для Node.js и Python — ниже).
Обратная совместимость. Пока у вас не настроен секрет подписи, webhook'и отправляются без подписи (без заголовков подписи), ровно как раньше. Подпись включается для вашего endpoint в тот момент, когда у вас появляется секрет.
Заголовки подписи
Тело JSON при этом не меняется — подпись передаётся только в заголовках:
Заголовок | Значение |
|---|---|
| Уникальный id события. Используйте для идемпотентной обработки (дедупликация повторных доставок). |
| Время подписи в Unix-секундах. |
|
|
Что подписывается
Подпись — это HMAC-SHA256(секрет, "{id}.{timestamp}.{тело}") в hex-кодировке, где:
id— заголовокEasyPay-Webhook-Idtimestamp— заголовокEasyPay-Timestampтело— точные байты тела запроса
⚠️ Проверяйте подпись над сырыми байтами тела, как их получили. Не парсите и не пере-сериализуйте JSON перед проверкой — изменение порядка ключей или пробелов сломает подпись.
Как получить секрет
Секрет выдаётся при регистрации вашего webhook endpoint:
register_partner_notifications_webhookвозвращаетregistered[].signing_secret. Ручка идемпотентна (get-or-create) и всегда возвращает текущий секрет: первый вызов минтит новый (secret_status: "issued"), повторный — возвращает тот же (secret_status: "returned"). Потеряли секрет — просто вызовите register ещё раз, он его вернёт.Хотите сменить секрет (утечка / плановая ротация)? Вызовите
rotate_partner_webhook_secret(см. раздел «Ротация секрета») — она выдаёт новый секрет, старый живёт grace-период.
Секреты выглядят как whsec_live_… / whsec_test_… и отдельны для каждой среды — у test и live свои секреты.
Как проверить подпись
Отклоните запрос, если
EasyPay-Timestampотличается от вашего времени более чем на 5 минут (300 секунд). Это ограничивает replay-атаки.Пересчитайте
HMAC-SHA256(секрет, "{id}.{timestamp}.{тело}").Сравните — в постоянном времени — с каждым значением
v1,<hex>в заголовкеEasyPay-Signature. Примите запрос, если совпало хотя бы одно.Дедуплицируйте по
EasyPay-Webhook-Id, чтобы повторная доставка обработалась только один раз.
Node.js
const crypto = require("crypto");
function verify(rawBody, headers, secret) {
const id = headers["easypay-webhook-id"];
const ts = headers["easypay-timestamp"];
// 1. окно времени (защита от replay)
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error("stale timestamp");
// 2. пересчитываем подпись над сырым телом
const expected = crypto.createHmac("sha256", secret)
.update(`${id}.${ts}.${rawBody}`)
.digest("hex");
// 3. сравниваем в постоянном времени с каждым v1-значением
const ok = headers["easypay-signature"].split(" ")
.map(p => p.split(",")[1])
.some(sig => sig && sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)));
if (!ok) throw new Error("bad signature");
}Python
import hmac, hashlib, time
def verify(raw_body: bytes, headers: dict, secret: str):
wid = headers["EasyPay-Webhook-Id"]
ts = headers["EasyPay-Timestamp"]
# 1. окно времени (защита от replay)
if abs(time.time() - int(ts)) > 300:
raise ValueError("stale timestamp")
# 2. пересчитываем подпись над сырым телом
msg = f"{wid}.{ts}.".encode() + raw_body
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
# 3. сравниваем в постоянном времени с каждым v1-значением
presented = [p.split(",")[1] for p in headers["EasyPay-Signature"].split(" ")]
if not any(hmac.compare_digest(expected, s) for s in presented):
raise ValueError("bad signature")Ротация секрета
Вызовите rotate_partner_webhook_secret с параметрами environment (test или live) и client_token. Он возвращает новый signing_secret (показывается один раз) и метку времени previous_valid_until.
В течение grace-периода (до previous_valid_until) EasyPay подписывает каждый webhook обоими секретами — новым и предыдущим (в заголовке появляются два значения v1,), — чтобы вы могли перейти на новый секрет без потери событий:
Разверните новый секрет рядом со старым и принимайте webhook, если проходит любой из них. Сниппеты выше уже перебирают все значения
v1,, поэтому достаточно прогнать проверку по каждому вашему секрету.Когда новый секрет разъедется на все инстансы — уберите старый. Успейте до
previous_valid_until.
Ротируйте секрет при подозрении на утечку или по расписанию. (Потеряли секрет — ротация не нужна: просто вызовите register_partner_notifications_webhook ещё раз, он вернёт текущий.)
Примечание
Схема повторяет то, как подписывают webhook'и Stripe и Slack (HMAC-SHA256 над телом с временной меткой) — применимы та же логика и те же готовые библиотеки проверки.