Open in
Preverite podpise webhookov
Vsak webhook in zahteva za orodje, ki ju pošlje ThunderPhone, sta podpisana. Podpis enkrat preverite s tukajšnjim postopkom, nato pa isto preverjanje uporabite na vsaki končni točki, ki jo izvajate.
Vsaka zahteva, ki jo pošljemo vašemu strežniku — dostave webhookov in
klici končnih točk orodij — vsebuje podpis HMAC-SHA256 v glavi
X-ThunderPhone-Signature. Preverjanje pravilno nastavite enkrat in
isti pomožni program vključite v vsak obdelovalnik.
Algoritem
- Preberite surovo telo zahteve — natančne bajte, ki smo vam jih poslali z zahtevo POST.
- Izračunajte
hmac_sha256(secret, body).hexdigest(). - V konstantnem času primerjajte z
X-ThunderPhone-Signature. (Preprosta primerjava nizov razkriva informacije o času izvajanja.)
Podpišemo natanko bajte, ki jih prenesemo, zato preverjanje surovega telesa
vedno deluje. Ti bajti so tudi kanonična serializacija JSON
koristnega tovora — ključi so razvrščeni po abecedi, ločila so strnjena
(, in : brez presledkov), kodiranje pa je UTF-8. To vam ponuja drugi,
popolnoma enakovreden postopek, kadar vaše ogrodje omogoča le razčlenjeni JSON:
znova ga serializirajte kanonično in nad njim izračunajte HMAC.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Prednost dajte surovemu telesu — to je en korak manj in je odporno na posebnosti ponovnega pretvarjanja števil JSON v nekaterih jezikih.
Katera skrivnost?
| Vir | Skrivnost |
|---|---|
Končna točka webhooka (/v1/developer/webhook-endpoints) | secret za posamezno končno točko (48 šestnajstiških znakov), vrnjen enkrat ob ustvarjanju |
| Podedovani webhook z enim URL-jem | secret za posamezno organizacijo, vrnjen pri GET /v1/webhook |
Klic končne točke orodja (neposredni klic vaše endpoint.url) | Skrivnost webhooka na ravni organizacije (ista kot pri podedovanem webhooku z enim URL-jem) — ne skrivnost za posamezno končno točko |
Skrivnost shranite v upravitelju skrivnosti ali spremenljivki okolja — nikoli je ne potrdite v repozitorij.
Referenčne implementacije
Vse štiri preverjajo surovo telo zahteve:
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)
endPovezovanje, specifično za ogrodje
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})Preverjanje klicev orodij
Ko agent neposredno prikliče eno od vaših
funkcijskih orodij (orodje ima
endpoint), zahteva poleg vaših konfiguriranih
endpoint.headers vsebuje dve glavi ThunderPhone:
X-ThunderPhone-Call-ID— številčni ID aktivnega klica.X-ThunderPhone-Signature— HMAC-SHA256 z vašim skrivnim ključem webhooka na ravni organizacije kot ključem, izračunan nad natančnimi bajti telesa zahteve.
Isti pomožnik verify() deluje nespremenjeno, z dvema posebnostma:
- Orodja
GET/DELETEnimajo telesa. Argumenti se posredujejo kot parametri poizvedbe, podpis pa se izračuna nad praznim bajtnim nizom — torejverify(b"", sig, secret)(Python) aliverify(Buffer.alloc(0), sig, secret)(Node). Ne zgoščujte niza poizvedbe. - Organizacije brez konfiguriranega podedovanega webhooka nimajo skrivnega ključa organizacije. V tem primeru klici orodij vsebujejo samo
X-ThunderPhone-Call-IDin nobene glave s podpisom. Konfigurirajte podedovani webhook (PUT /v1/webhook), da pridobite skrivni ključ za podpisovanje, ali avtenticirajte klice orodij z lastno glavo prekendpoint.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)
...Usmerjanje orodij v načinu webhooka (orodja brez endpoint, dostavljena
na webhook vaše organizacije kot telephony.tool / web.tool) je običajen
podpisan webhook — uporabite zgornji standardni postopek. Za oba formata zahtevkov glejte
Funkcijska orodja.
Pogoste pasti
Ponovno serializiranje s privzetim oblikovanjem
Razčlenjevanje telesa in njegovo ponovno izpisovanje s privzetimi
nastavitvami knjižnice JSON (presledki za , / :, ključi v vrstnem redu vstavljanja) ustvari
drugačne bajte in pokvari HMAC. Preverite neobdelano telo — ali pa, če ga
morate ponovno serializirati, natančno uporabite našo kanonično obliko: razvrščene
ključe, strnjena ločila, UTF-8.
Ogrodje samodejno razčleni JSON
Vmesna programska oprema express.json() v Expressu porabi tok telesa
in izgubite neobdelane bajte. Posebej za pot webhooka uporabite express.raw()
ali shranite neobdelano telo v predhodni vmesni programski opremi.
Enako velja za NestJS / Koa — preverite njihovo dokumentacijo za »raw body«.
Primerjava, nevarna glede časa izvajanja
expected === signature v JS ali expected == signature v
Pythonu sta primerjavi s spremenljivim časom izvajanja. Uporabite crypto.timingSafeEqual
oziroma hmac.compare_digest. Razlike v zmogljivosti ni.
Napačna skrivnost za končne točke orodij
Neposredni klici končnih točk orodij so podpisani s skrivnostjo webhooka
na ravni organizacije (GET /v1/webhook) — ne s skrivnostjo posamezne končne točke
iz /v1/developer/webhook-endpoints. Znova uporabite isto funkcijo verify(),
vendar poskrbite, da ji na poteh orodij posredujete skrivnost organizacije.
Zgoščevanje poizvedbenega niza pri orodjih GET/DELETE
Pri metodah orodij brez telesa podpis zajema prazen bajtni niz, kar ohranja en univerzalen postopek: izračunajte HMAC neobdelanega telesa zahteve, ne glede na njegovo vsebino. Zgoščevanje URL-ja ali poizvedbenega niza se nikoli ne bo ujemalo.
Nevračanje odgovora 401 ob neujemanju
Vračanje 200 ob neuspelem preverjanju spremeni upravljalnik v cilj za ponovitvene napade. Če preverjanje ne uspe, vedno odgovorite s kodo, ki ni 2xx.