ThunderPhone 2.0 je tu.Začnite sami, už od 2 ¢/min.Prečítať oznámenie

Operations

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

  1. Prečítajte surové telo požiadavky — presné bajty, ktoré sme vám odoslali prostredníctvom POST.
  2. Vypočítajte hmac_sha256(secret, body).hexdigest().
  3. 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ľúč?

ZdrojTajný 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 URLsecret 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:

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

Zapojenie špecifické pre framework

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})

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:

  1. Nástroje GET / DELETE nemajú telo. Argumenty sa prenášajú ako parametre dopytu a podpis sa vypočíta nad prázdnym bajtovým reťazcom — teda verify(b"", sig, secret) (Python) alebo verify(Buffer.alloc(0), sig, secret) (Node). Reťazec dopytu nehášujte.
  2. 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-ID a ž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íctvom 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)
    ...

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