Weryfikowanie podpisów webhooków
Każdy webhook i każde żądanie narzędzia wysyłane przez ThunderPhone są podpisane. Zweryfikuj podpis raz, korzystając z zamieszczonej tutaj instrukcji, a następnie używaj tej samej weryfikacji w każdym obsługiwanym punkcie końcowym.
Każde żądanie, które wysyłamy na Twój serwer — dostarczenia webhooków i
wywołania endpointów narzędzi — zawiera podpis HMAC-SHA256 w nagłówku
X-ThunderPhone-Signature. Skonfiguruj weryfikację raz, a następnie
użyj tego samego pomocnika w każdym handlerze.
Algorytm
- Odczytaj surowe ciało żądania — dokładne bajty, które wysłaliśmy metodą POST.
- Oblicz
hmac_sha256(secret, body).hexdigest(). - Porównaj w czasie stałym z
X-ThunderPhone-Signature. (Naiwne porównanie ciągów ujawnia informacje o czasie.)
Podpisujemy dokładnie te bajty, które przesyłamy, dlatego weryfikacja surowego ciała
zawsze działa. Te bajty są również kanoniczną serializacją JSON
payloadu — klucze posortowane alfabetycznie, zwarte separatory
(, i : bez spacji), UTF-8. Daje to drugi, w pełni
równoważny sposób, gdy Twój framework udostępnia tylko sparsowany JSON:
ponownie serializuj kanonicznie i oblicz HMAC dla wyniku.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Preferuj surowe ciało — to o jeden krok mniej i nie jest podatne na problemy z ponownym przetwarzaniem liczb JSON w niektórych językach.
Który sekret?
| Źródło | Sekret |
|---|---|
Endpoint webhooka (/v1/developer/webhook-endpoints) | Sekret secret dla danego endpointu (48 znaków szesnastkowych), zwracany jednorazowo podczas tworzenia |
| Starszy webhook z pojedynczym adresem URL | Sekret secret dla organizacji zwracany przez GET /v1/webhook |
Wywołanie endpointu narzędzia (bezpośrednie wywołanie Twojego endpoint.url) | Sekret webhooka na poziomie organizacji (ten sam co dla starszego webhooka z pojedynczym adresem URL) — nie sekret dla pojedynczego endpointu |
Przechowuj sekret w menedżerze sekretów lub zmiennej środowiskowej — nigdy nie zapisuj go w repozytorium.
Implementacje referencyjne
Wszystkie cztery weryfikują surowe ciało żądania:
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)
endIntegracja specyficzna dla frameworka
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})Weryfikowanie wywołań narzędzi
Gdy agent wywołuje bezpośrednio jedno z Twoich
narzędzi funkcyjnych (narzędzie ma
endpoint), żądanie zawiera dwa nagłówki ThunderPhone obok
skonfigurowanych przez Ciebie endpoint.headers:
X-ThunderPhone-Call-ID— numeryczny identyfikator trwającego połączenia.X-ThunderPhone-Signature— HMAC-SHA256 z kluczem w postaci Twojego sekretu webhooka na poziomie organizacji, obliczany na podstawie dokładnych bajtów treści żądania.
Ten sam pomocnik verify() działa bez zmian, z dwoma wyjątkami:
- Narzędzia
GET/DELETEnie mają treści. Argumenty są przekazywane jako parametry zapytania, a podpis jest obliczany na podstawie pustego ciągu bajtów — więcverify(b"", sig, secret)(Python) lubverify(Buffer.alloc(0), sig, secret)(Node). Nie haszuj ciągu zapytania. - Organizacje bez skonfigurowanego starszego webhooka nie mają sekretu organizacji. W
takim przypadku wywołania narzędzi zawierają tylko
X-ThunderPhone-Call-IDi nie mają nagłówka podpisu. Skonfiguruj starszy webhook (PUT /v1/webhook), aby uzyskać sekret podpisywania, lub uwierzytelniaj wywołania narzędzi własnym nagłówkiem za pośrednictwemendpoint.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)
...Dystrybucja wywołań narzędzi w trybie webhooka (narzędzia bez endpoint, dostarczane
do webhooka organizacji jako telephony.tool / web.tool) to zwykły
podpisany webhook — obowiązuje standardowa procedura opisana powyżej. Zobacz
Narzędzia funkcyjne, aby poznać oba formaty żądań.
Typowe pułapki
Ponowna serializacja z domyślnym formatowaniem
Przetworzenie treści i ponowne zapisanie jej przy użyciu domyślnych
ustawień biblioteki JSON (spacje po , / :, klucze w kolejności wstawiania) daje
inne bajty i powoduje błąd HMAC. Weryfikuj surową treść — lub jeśli
musisz ją ponownie serializować, dokładnie dopasuj nasz kanoniczny format:
posortowane klucze, zwarte separatory, UTF-8.
Framework automatycznie przetwarza JSON
Oprogramowanie pośredniczące Express express.json() zużywa strumień treści
i tracisz surowe bajty. Użyj express.raw() konkretnie dla trasy webhooka
albo buforuj surową treść w oprogramowaniu pośredniczącym poprzedzającym parser.
Tak samo jest w NestJS / Koa — sprawdź ich dokumentację dotyczącą surowego ciała żądania.
Porównanie niebezpieczne czasowo
expected === signature w JS lub expected == signature w
Pythonie to porównania zależne od czasu wykonania. Użyj odpowiednio crypto.timingSafeEqual
lub hmac.compare_digest. Różnica w wydajności
jest zerowa.
Nieprawidłowy sekret dla endpointów narzędzi
Bezpośrednie wywołania endpointów narzędzi są podpisywane za pomocą sekretu webhooka
na poziomie organizacji (GET /v1/webhook) — nie za pomocą sekretu konkretnego endpointu
z /v1/developer/webhook-endpoints. Użyj ponownie tej samej funkcji verify(),
ale upewnij się, że na trasach narzędzi przekazujesz jej sekret organizacji.
Haszowanie ciągu zapytania w narzędziach GET/DELETE
W przypadku metod narzędzi bez treści podpis obejmuje pusty ciąg bajtów, co zachowuje jeden uniwersalny schemat: oblicz HMAC surowego ciała żądania, niezależnie od jego zawartości. Haszowanie adresu URL lub ciągu zapytania nigdy nie będzie zgodne.
Brak zwracania 401 przy niezgodności
Zwracanie 200 po nieudanej weryfikacji sprawia, że procedura obsługi staje się celem ataków typu replay. Zawsze odpowiadaj kodem innym niż 2xx, jeśli weryfikacja się nie powiedzie.
Kolejne kroki
Semantyka dostarczania, ponowne próby, źródłowe adresy IP.
Zarządzaj wieloma adresami URL, rotuj sekrety.
Dwie ścieżki wywoływania narzędzi i formaty ich żądań.
Zbuduj kompletną integrację opartą na narzędziach od początku do końca.