Перевірка підпису
Кожна доставка підписується секретом підпису ендпоінта-отримувача. Саме перевірка цього підпису відрізняє «на мій URL надійшов POST» від «NimbusNexus надіслав мені цю подію»: URL вебхука — не секрет, і будь-що в інтернеті може його знайти.
Ця сторінка — контракт. Якщо ви використовуєте один із SDK, його хелпер verify уже реалізує все це, і можна одразу перейти до розділу що робити після перевірки.
Заголовки
| Заголовок | Вміст |
|---|---|
X-Webhook-Signature | sha256=<hex> — HMAC, який ви перевіряєте. Під час ротації секрету — кілька токенів через кому; див. правила |
X-Webhook-Timestamp | Unix-секунди; частина підпису й replay-вікна |
X-Webhook-Event-Id | Ідентифікатор події, що збігається з id у тілі, — використовуйте його для дедуплікації |
X-Webhook-Event-Type | Тип події |
X-Webhook-Delivery-Id | Ідентифікатор цієї доставки; вказуйте його у зверненнях у підтримку |
Це не ті заголовки, які використовують Вебхуки платформи: вони підписують сповіщення інфраструктури заголовками з префіксом
NN-. Ці два продукти — окремі системи. Обробник, написаний під неправильні назви заголовків, не знайде, що перевіряти, і відхилятиме кожну доставку.
Побудова
signature = "sha256=" + hex( HMAC_SHA256( secret, "<timestamp>." + raw_body ) )Timestamp, буквальна ., потім сире тіло запиту — усе це конкатенується, підписується HMAC секретом підпису вашого ендпоінта, кодується в hex і отримує префікс sha256=.
Саме включення timestamp-у в підписувані дані робить replay-вікно дієвим. Якби timestamp був лише заголовком поряд із підписом, зловмисник, який повторно надсилає перехоплену доставку, міг би вільно його переписати; а коли timestamp підписано, будь-яка зміна робить HMAC недійсним.
Саме тіло — компактний JSON із відсортованими ключами, серіалізований один раз під час публікації і збережений. Кожен повтор доставки надсилає ідентичні байти, тож доставка, яка нарешті вдається на 4-й спробі, перевіряється точно так само, як перевірилася б перша спроба. Кожна спроба підписується наново зі свіжим timestamp, і саме тому повтор після довгої затримки все одно потрапляє у вузьке replay-вікно.
Три правила
1. Підписуйте сирі байти. Прочитайте тіло один раз, як байти, і перевіряйте саме їх. Розбір JSON і повторна серіалізація змінюють порядок ключів і пробіли, а підпис обчислено по тому, що надіслали ми, а не по еквівалентному обʼєкту. Це найпоширеніша причина скарг «підпис ніколи не збігається»: з payload-ом усе гаразд, а порівнюються інші байти.
Фреймворки, які одразу розбирають JSON, зазвичай окремо надають і оригінал: request.body у Flask, req.rawBody з verify-хуком в Express, await request.body() у FastAPI.
2. Порівнюйте за сталий час — з кожним токеном. Використовуйте hmac.compare_digest, crypto.timingSafeEqual, hmac.Equal, а не ==. Побайтове порівняння, що завершується достроково, через час виконання видає, яка частина вгаданого підпису правильна, і це перетворює підробку на здійсненний перебір. Зробити правильно нічого не коштує; ціна неправильної реалізації не помітна під час тестування, бо неправильна реалізація проходить усі функціональні тести.
Спершу розділіть заголовок за , і приймайте доставку, якщо збігається будь-який токен. Зазвичай токен рівно один, тож це здається зайвим, але під час ротації секрету підпису ми підписуємо і новим, і попереднім секретом та надсилаємо sha256=<a>,sha256=<b>. Перевірка, що порівнює все значення заголовка з одним очікуваним токеном, не збігається з жодним і відхиляє кожну доставку, поки триває перекриття. Усі офіційні SDK розділяють заголовок; помиляється саме власноруч написана перевірка.
3. Відхиляйте застарілі timestamp-и. Якщо X-Webhook-Timestamp відрізняється від вашого годинника більш ніж на 300 секунд, відхиляйте доставку незалежно від того, чи збігається HMAC. Без цього перехоплену доставку можна відтворити повторно будь-коли. З цим правилом вразливість обмежена пʼятьма хвилинами, навіть якщо payload перехоплено.
Правило 3 передбачає, що годинник вашого сервера йде більш-менш точно. Якщо після змін в інфраструктурі доставки масово перестають проходити перевірку, перевірте NTP, перш ніж перевіряти код.
Повний обробник
import hashlib, hmac, time
WINDOW_SECONDS = 300
def verify(secret: str, raw_body: bytes, signature: str, timestamp: str) -> bool:
# Rule 3 — replay window, checked first because it is the cheapest rejection.
try:
if abs(int(time.time()) - int(timestamp)) > WINDOW_SECONDS:
return False
except (TypeError, ValueError):
return False # absent or non-numeric header
# Rule 1 — the raw bytes, exactly as received.
expected = "sha256=" + hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
# Rule 2 — constant-time comparison against EVERY token. One token normally; several,
# comma-separated, while a rotation overlap is in effect.
return any(
hmac.compare_digest(expected, token.strip())
for token in signature.split(",")
)Відповідь
Поверніть 2xx, і ми вважатимемо доставку виконаною. Будь-що інше — або відсутність відповіді протягом тайм-ауту — вважається невдачею й потрапляє в розклад повторів.
Підтверджуйте швидко, а роботу виконуйте потім. Обробник, який завершує повільну задачу, перш ніж відповісти, тримає доставку відкритою, і якщо він перевищить тайм-аут, робота виконається, і доставка повториться, — ви отримаєте саме той дублікат, якого намагалися уникнути. Поставте задачу в чергу, поверніть 200, обробіть окремо від запиту.
І в будь-якому разі будьте ідемпотентними. Доставка відбувається щонайменше один раз, тож розраховуйте побачити ту саму подію двічі: дедуплікуйте за X-Webhook-Event-Id, який не змінюється в усіх повторах доставки.
Ротація секрету
curl -sX POST https://webhooks.nimbusnexus.net/v1/endpoints/ep_3f9a/rotate-secret \
-H "Authorization: Bearer $ADMIN_KEY"Новий секрет повертається один раз. Вам не потрібно тримати в обробнику два секрети: приблизно 24 години після ротації ми підписуємо кожну доставку і новим, і попереднім секретом та надсилаємо обидва токени в одному заголовку, sha256=<new>,sha256=<previous>. Перевірка, що дотримується правила 2 й перевіряє кожен токен, працює весь цей час, хоч би який секрет у неї був, тож на новий секрет можна перейти одразу.
Звідси й пастка: перевірка, яка порівнює все значення заголовка, чудово працює аж до першої ротації, а потім добу відхиляє все. Якщо після ротації доставки масово перестають проходити перевірку, першим ділом перевірте саме це порівняння.
Що далі
- SDK — хелпери
verify, що реалізують усе з цієї сторінки. - Швидкий старт — повний процес, частиною якого це є.
- Огляд Webhooks — семантика доставки й dead-letter черга.
- Вебхуки платформи — окремі сповіщення інфраструктури, підписані заголовками
NN-.