Open in
Проверка на подписите на webhook заявки
Всяка webhook заявка и заявка към инструмент, изпратена от ThunderPhone, е подписана. Проверете подписа веднъж с рецептата тук, след което използвайте същата проверка за всяка крайна точка, която изпълнявате.
Всяка заявка, която изпращаме към вашия сървър — webhook доставки и
извиквания на крайни точки за инструменти — съдържа HMAC-SHA256 подпис в
заглавката X-ThunderPhone-Signature. Настройте проверката правилно веднъж и
включете същия помощен метод във всеки обработчик.
Алгоритъмът
- Прочетете суровото тяло на заявката — точните байтове, които сме ви изпратили чрез POST.
- Изчислете
hmac_sha256(secret, body).hexdigest(). - Сравнете с
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 с един URL | secret за организацията, върната при GET /v1/webhook |
Извикване на крайна точка за инструмент (директно извикване към вашия endpoint.url) | Webhook тайната на ниво организация (същата като за остарелия webhook с един URL) — не тайна за конкретна крайна точка |
Съхранявайте тайната във вашия мениджър за тайни или променлива на средата — никога не я записвайте в хранилището с код.
Референтни имплементации
И четирите проверяват суровото тяло на заявката:
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 "")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),
);
}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))
}require "openssl"
def verify(body, signature, secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
Rack::Utils.secure_compare(expected, signature.to_s)
endСвързване според фреймуърка
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}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);
},
);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() работи без промени, с две особености:
- Инструментите
GET/DELETEнямат тяло. Аргументите се предават като параметри на заявката, а подписът се изчислява върху празния байтов низ — тоестverify(b"", sig, secret)(Python) илиverify(Buffer.alloc(0), sig, secret)(Node). Не хеширайте низа на заявката. - Организациите без конфигуриран наследен уебхук нямат тайна на организацията. В този случай извикванията на инструменти съдържат само
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, ако проверката е неуспешна.
Следващи стъпки
Семантика на доставката, повторни опити, IP адреси на източника.
Управлявайте множество URL адреси, сменяйте секретни ключове.
Двата начина за извикване на инструменти и форматите на заявките им.
Създайте цялостна интеграция с инструменти от край до край.