Перейти до вмісту
NimbusNexus

Перевірка підпису

Кожна доставка підписується секретом підпису ендпоінта-отримувача. Саме перевірка цього підпису відрізняє «на мій URL надійшов POST» від «NimbusNexus надіслав мені цю подію»: URL вебхука — не секрет, і будь-що в інтернеті може його знайти.

Ця сторінка — контракт. Якщо ви використовуєте один із SDK, його хелпер verify уже реалізує все це, і можна одразу перейти до розділу що робити після перевірки.

Заголовки

ЗаголовокВміст
X-Webhook-Signaturesha256=<hex> — HMAC, який ви перевіряєте. Під час ротації секрету — кілька токенів через кому; див. правила
X-Webhook-TimestampUnix-секунди; частина підпису й replay-вікна
X-Webhook-Event-IdІдентифікатор події, що збігається з id у тілі, — використовуйте його для дедуплікації
X-Webhook-Event-TypeТип події
X-Webhook-Delivery-IdІдентифікатор цієї доставки; вказуйте його у зверненнях у підтримку

Це не ті заголовки, які використовують Вебхуки платформи: вони підписують сповіщення інфраструктури заголовками з префіксом NN-. Ці два продукти — окремі системи. Обробник, написаний під неправильні назви заголовків, не знайде, що перевіряти, і відхилятиме кожну доставку.

Побудова

Text
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, перш ніж перевіряти код.

Повний обробник

Python
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, який не змінюється в усіх повторах доставки.

Ротація секрету

Shell
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-.