Open in
Overenie podpisov webhookov
Každý webhook a každá požiadavka nástroja, ktorú ThunderPhone odosiela, je podpísaná. Podpis raz overte podľa postupu uvedeného tu a potom rovnakú kontrolu použite na každom spustenom koncovom bode.
Každá požiadavka, ktorú odosielame na váš server — doručenia webhookov a
volania koncových bodov nástrojov — obsahuje podpis HMAC-SHA256 v hlavičke
X-ThunderPhone-Signature. Overenie nastavte raz správne a
rovnakého pomocníka použite v každom obslužnom programe.
Algoritmus
- Prečítajte surové telo požiadavky — presné bajty, ktoré sme vám odoslali prostredníctvom POST.
- Vypočítajte
hmac_sha256(secret, body).hexdigest(). - Porovnajte v konštantnom čase s hodnotou
X-ThunderPhone-Signature. (Naivné porovnanie reťazcov prezrádza časové informácie.)
Podpisujeme presne tie bajty, ktoré prenášame, takže overenie surového tela
funguje vždy. Tieto bajty sú zároveň kanonickou serializáciou JSON
obsahu — kľúče zoradené abecedne, kompaktné oddeľovače
(, a : bez medzier), UTF-8. To vám poskytuje druhý, úplne
ekvivalentný postup, keď váš framework sprístupňuje iba spracovaný JSON:
znova ho kanonicky serializujte a vypočítajte HMAC z neho.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Uprednostnite surové telo — je to o jeden krok menej a je odolné voči zvláštnostiam opätovného prevodu čísel JSON v niektorých jazykoch.
Ktorý tajný kľúč?
| Zdroj | Tajný kľúč |
|---|---|
Koncový bod webhooku (/v1/developer/webhook-endpoints) | secret pre konkrétny koncový bod (48 hexadecimálnych znakov), vrátený iba pri vytvorení |
| Starší webhook s jednou URL | secret pre organizáciu vrátený pri GET /v1/webhook |
Volanie koncového bodu nástroja (priame volanie vášho endpoint.url) | Tajný kľúč webhooku na úrovni organizácie (rovnaký ako pre starší webhook s jednou URL) — nie tajný kľúč pre konkrétny koncový bod |
Tajný kľúč uložte do správcu tajných údajov alebo premennej prostredia — nikdy ho neukladajte do repozitára.
Referenčné implementácie
Všetky štyri overujú surové telo požiadavky:
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)
endZapojenie špecifické pre framework
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})Overovanie volaní nástrojov
Keď agent priamo vyvolá jeden z vašich
funkčných nástrojov (nástroj má
endpoint), požiadavka obsahuje dve hlavičky ThunderPhone spolu
s nakonfigurovanými hlavičkami endpoint.headers:
X-ThunderPhone-Call-ID— číselné ID prebiehajúceho hovoru.X-ThunderPhone-Signature— HMAC-SHA256 s kľúčom vo forme vášho tajného kľúča webhooku na úrovni organizácie nad presnými bajtmi tela požiadavky.
Rovnaký pomocník verify() funguje bez zmien, s dvoma odlišnosťami:
- Nástroje
GET/DELETEnemajú telo. Argumenty sa prenášajú ako parametre dopytu a podpis sa vypočíta nad prázdnym bajtovým reťazcom — tedaverify(b"", sig, secret)(Python) aleboverify(Buffer.alloc(0), sig, secret)(Node). Reťazec dopytu nehášujte. - Organizácie bez nakonfigurovaného staršieho webhooku nemajú tajný kľúč organizácie. V
takom prípade volania nástrojov obsahujú iba
X-ThunderPhone-Call-IDa žiadnu hlavičku podpisu. Nakonfigurujte starší webhook (PUT /v1/webhook), aby ste získali tajný kľúč na podpisovanie, alebo overujte volania nástrojov vlastnou hlavičkou prostredníctvomendpoint.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)
...Dispečovanie nástrojov v režime webhooku (nástroje bez endpoint, doručené
do webhooku vašej organizácie ako telephony.tool / web.tool) je bežný
podpísaný webhook — platí štandardný postup uvedený vyššie. Pre oba
tvary požiadaviek si pozrite Funkčné nástroje.
Bežné úskalia
Opätovná serializácia s predvoleným formátovaním
Spracovanie tela a jeho opätovný výpis s predvolenými nastaveniami
vašej knižnice JSON (medzery po , / :, kľúče v poradí vloženia) vytvorí
odlišné bajty a naruší HMAC. Overujte nespracované telo — alebo ak ho
musíte znovu serializovať, presne dodržte náš kanonický formát: zoradené
kľúče, kompaktné oddeľovače, UTF-8.
Framework automaticky spracúva JSON
Middleware express.json() v Express spotrebuje stream tela
a stratíte nespracované bajty. Použite express.raw() konkrétne na
trase webhooku alebo uložte nespracované telo do vyrovnávacej pamäte v
predbežnom middleware. To isté platí pre NestJS / Koa — pozrite si ich
dokumentáciu k „raw body“.
Porovnanie nebezpečné z hľadiska časovania
expected === signature v JS alebo expected == signature v
Pythone sú porovnania s premenlivým časovaním. Použite príslušne crypto.timingSafeEqual
alebo hmac.compare_digest. Rozdiel vo výkone je zanedbateľný.
Nesprávny secret pre endpointy nástrojov
Priame volania endpointov nástrojov sú podpisované pomocou secretu webhooku
na úrovni organizácie (GET /v1/webhook) — nie secretom konkrétneho endpointu
z /v1/developer/webhook-endpoints. Znovu použite rovnakú funkciu verify(),
ale uistite sa, že na trasách nástrojov jej odovzdávate secret organizácie.
Hašovanie query stringu pri nástrojoch GET/DELETE
Pri metódach nástrojov bez tela podpis pokrýva prázdny bajtový reťazec, takže stačí jeden univerzálny postup: vypočítajte HMAC z nespracovaného tela požiadavky, nech je akékoľvek. Hašovanie URL alebo query stringu sa nikdy nebude zhodovať.
Nevrátenie 401 pri nezhode
Vrátenie 200 pri neúspešnom overení robí z obsluhy cieľ opakovaných útokov. Ak overenie zlyhá, vždy odpovedzte stavom iným než 2xx.
Ďalšie kroky
Sémantika doručovania, opakovania, zdrojové IP adresy.
Spravujte viacero URL adries, rotujte secrety.
Dve cesty vyvolania nástrojov a tvary ich požiadaviek.
Vytvorte kompletnú integráciu založenú na nástrojoch od začiatku do konca.