ThunderPhone 2.0 вече е тук.Започнете самостоятелно — от 2 цента/мин.Прочетете съобщението

Operations

Проверка на подписите на webhook заявки

Всяка webhook заявка и заявка към инструмент, изпратена от ThunderPhone, е подписана. Проверете подписа веднъж с рецептата тук, след което използвайте същата проверка за всяка крайна точка, която изпълнявате.

Всяка заявка, която изпращаме към вашия сървър — webhook доставки и извиквания на крайни точки за инструменти — съдържа HMAC-SHA256 подпис в заглавката X-ThunderPhone-Signature. Настройте проверката правилно веднъж и включете същия помощен метод във всеки обработчик.

Алгоритъмът

  1. Прочетете суровото тяло на заявката — точните байтове, които сме ви изпратили чрез POST.
  2. Изчислете hmac_sha256(secret, body).hexdigest().
  3. Сравнете с X-ThunderPhone-Signature за константно време. (Наивното сравнение на низове разкрива информация за времето.)

Подписваме точно байтовете, които предаваме, затова проверката на суровото тяло винаги работи. Тези байтове са също каноничната JSON сериализация на полезния товар — ключове, сортирани по азбучен ред, компактни разделители (, и : без интервали), UTF-8. Това ви дава втори, напълно еквивалентен подход, когато вашата рамка предоставя само анализиран JSON: сериализирайте отново канонично и изчислете HMAC върху него.

# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")

Предпочитайте суровото тяло — това е с една стъпка по-малко и не се влияе от особености при повторното преобразуване на JSON числа в някои езици.

Коя тайна?

ИзточникТайна
Webhook крайна точка (/v1/developer/webhook-endpoints)secret за конкретната крайна точка (48 шестнадесетични знака), върната еднократно при създаване
Остарял webhook с един URLsecret за организацията, върната при GET /v1/webhook
Извикване на крайна точка за инструмент (директно извикване към вашия endpoint.url)Webhook тайната на ниво организация (същата като за остарелия webhook с един URL) — не тайна за конкретна крайна точка

Съхранявайте тайната във вашия мениджър за тайни или променлива на средата — никога не я записвайте в хранилището с код.

Референтни имплементации

И четирите проверяват суровото тяло на заявката:

Python
import hashlib
import hmac
 
 
def verify(body: bytes, signature: str, secret: str) -> bool:
    """Constant-time HMAC-SHA256 verification."""
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")
Node.js
import crypto from "node:crypto";
 
export function verify(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  if (!signature || expected.length !== signature.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}
Go
package webhook
 
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)
 
func Verify(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}
Ruby
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

Свързване според фреймуърка

FastAPI
from fastapi import FastAPI, HTTPException, Request
 
app = FastAPI()
 
@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()           # raw bytes, NOT request.json()
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify(body, sig, SECRET):
        raise HTTPException(status_code=401)
 
    import json
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
Express
import express from "express";
 
const app = express();
 
app.post(
  "/thunderphone-webhook",
  // IMPORTANT: parse as raw; do NOT use express.json() here.
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-ThunderPhone-Signature") || "";
    if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // … dispatch on event.type …
    res.sendStatus(204);
  },
);
Django
import json
 
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
 
 
@csrf_exempt
@require_POST
def hook(request):
    body = request.body  # raw bytes
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify(body, sig, SECRET):
        return HttpResponseForbidden("invalid signature")
    event = json.loads(body)
    # … dispatch on event["type"] …
    return JsonResponse({"ok": True})

Проверка на извиквания на инструменти

Когато агентът извика директно един от вашите инструменти с функции (инструментът има endpoint), заявката съдържа два заглавни реда на ThunderPhone наред с конфигурираните от вас endpoint.headers:

  • X-ThunderPhone-Call-ID — числовият идентификатор на текущото обаждане.
  • X-ThunderPhone-Signature — HMAC-SHA256, с ключ вашата тайна за уебхук на ниво организация, върху точните байтове на тялото на заявката.

Същият помощен метод verify() работи без промени, с две особености:

  1. Инструментите GET / DELETE нямат тяло. Аргументите се предават като параметри на заявката, а подписът се изчислява върху празния байтов низ — тоест verify(b"", sig, secret) (Python) или verify(Buffer.alloc(0), sig, secret) (Node). Не хеширайте низа на заявката.
  2. Организациите без конфигуриран наследен уебхук нямат тайна на организацията. В този случай извикванията на инструменти съдържат само X-ThunderPhone-Call-ID и нямат заглавен ред за подпис. Конфигурирайте наследения уебхук (PUT /v1/webhook), за да получите тайна за подписване, или удостоверявайте извикванията на инструменти със собствен заглавен ред чрез endpoint.headers.
@app.post("/tools/search-appointments")
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    if not verify(body, sig, ORG_WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    args = json.loads(body)
    ...

Изпращането на инструменти в режим на уебхук (инструменти без endpoint, доставяни до уебхука на вашата организация като telephony.tool / web.tool) е обикновен подписан уебхук — приложете стандартната процедура по-горе. Вижте Инструменти с функции за двата формата на заявки.

Често срещани проблеми

Повторна сериализация с форматиране по подразбиране

Анализирането на тялото и повторното му записване с настройките по подразбиране на вашата JSON библиотека (интервали след , / :, ключове в реда на вмъкване) създава различни байтове и нарушава HMAC. Проверявайте необработеното тяло — или ако трябва да го сериализирате повторно, спазвайте точно нашата канонична форма: сортирани ключове, компактни разделители, UTF-8.

Рамката автоматично анализира JSON

Междинният обработчик express.json() на Express консумира потока на тялото и губите необработените байтове. Използвайте express.raw() конкретно за маршрута на уебхука или буферирайте необработеното тяло в предходен междинен обработчик. Същото важи за NestJS / Koa — проверете документацията им за „необработено тяло“.

Небезопасно спрямо времето сравнение

expected === signature в JS или expected == signature в Python са сравнения с променливо време. Използвайте crypto.timingSafeEqual или съответно hmac.compare_digest. Разликата в производителността е нулева.

Грешен секретен ключ за крайни точки на инструменти

Директните извиквания на крайни точки на инструменти се подписват с секретния ключ за уебхуки на ниво организация (GET /v1/webhook) — не с таен ключ за конкретна крайна точка от /v1/developer/webhook-endpoints. Използвайте повторно същата функция verify(), но се уверете, че подавате секретния ключ на организацията за маршрутите на инструментите.

Хеширане на низа на заявката при GET/DELETE инструменти

При методи на инструменти без тяло подписът обхваща празния байтов низ, като се запазва една универсална схема: изчислявайте HMAC върху необработеното тяло на заявката, каквото и да е то. Хеширането на URL адреса или низа на заявката никога няма да съвпадне.

Невръщане на 401 при несъответствие

Връщането на 200 при неуспешна проверка прави обработчика цел за атаки с повторно изпращане. Винаги връщайте отговор, различен от 2xx, ако проверката е неуспешна.


Следващи стъпки