ThunderPhone 2.0 je tady.Začnete bez obchodníka, od 2 ¢/min.Přečíst oznámení

Operations

Ověřování podpisů webhooků

Každý webhook a každý požadavek nástroje, který ThunderPhone odesílá, je podepsán. Podpis jednou ověřte pomocí zde uvedeného postupu a stejnou kontrolu pak používejte na všech spuštěných koncových bodech.

Každý požadavek, který odesíláme na váš server — doručení webhooků a volání koncových bodů nástrojů — obsahuje podpis HMAC-SHA256 v hlavičce X-ThunderPhone-Signature. Ověření nastavte jednou správně a stejného pomocníka použijte v každém handleru.

Algoritmus

  1. Přečtěte neupravené tělo požadavku — přesné bajty, které jsme vám odeslali metodou POST.
  2. Vypočítejte hmac_sha256(secret, body).hexdigest().
  3. Porovnejte v konstantním čase s X-ThunderPhone-Signature. (Naivní porovnání řetězců odhaluje informace o časování.)

Podepisujeme přesně ty bajty, které přenášíme, takže ověřování neupraveného těla vždy funguje. Tyto bajty jsou zároveň kanonickou serializací JSON datové části — klíče jsou řazeny abecedně, oddělovače jsou kompaktní (, a : bez mezer), kódování UTF-8. Pokud váš framework zpřístupňuje pouze analyzovaný JSON, máte k dispozici druhý, plně ekvivalentní postup: proveďte kanonickou opětovnou serializaci a vypočítejte HMAC z ní.

# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")

Upřednostněte neupravené tělo — je to o krok méně a vyhnete se zvláštnostem při opětovném převodu čísel JSON v některých jazycích.

Který tajný klíč?

ZdrojTajný klíč
Koncový bod webhooku (/v1/developer/webhook-endpoints)Tajný klíč secret pro každý koncový bod (48 hexadecimálních znaků), vrácený jednou při vytvoření
Starší webhook s jedinou adresou URLTajný klíč secret pro organizaci vrácený při GET /v1/webhook
Volání koncového bodu nástroje (přímé volání vašeho endpoint.url)Tajný klíč webhooku na úrovni organizace (stejný jako pro starší webhook s jedinou adresou URL) — ne tajný klíč pro jednotlivý koncový bod

Tajný klíč uložte do správce tajných klíčů nebo proměnné prostředí — nikdy jej neukládejte do repozitáře.

Referenční implementace

Všechny čtyři ověřují neupravené tělo požadavku:

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

Zapojení specifické pro 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})

Ověřování volání nástrojů

Když agent přímo volá některý z vašich funkčních nástrojů (nástroj má endpoint), požadavek kromě vámi nakonfigurovaných endpoint.headers obsahuje dvě hlavičky ThunderPhone:

  • X-ThunderPhone-Call-ID — číselné ID probíhajícího hovoru.
  • X-ThunderPhone-Signature — HMAC-SHA256 s klíčem vašeho webhookového tajemství na úrovni organizace, vytvořený nad přesnými bajty těla požadavku.

Stejný pomocník verify() funguje beze změny, se dvěma rozdíly:

  1. Nástroje GET / DELETE nemají tělo. Argumenty se předávají jako parametry dotazu a podpis se vypočítá nad prázdným bajtovým řetězcem — tedy verify(b"", sig, secret) (Python) nebo verify(Buffer.alloc(0), sig, secret) (Node). Řetězec dotazu nehašujte.
  2. Organizace bez nakonfigurovaného staršího webhooku nemají tajemství organizace. V takovém případě volání nástrojů obsahují pouze X-ThunderPhone-Call-ID a žádnou hlavičku s podpisem. Nakonfigurujte starší webhook (PUT /v1/webhook) pro získání tajemství pro podepisování, nebo ověřujte volání nástrojů vlastní hlavičkou prostřednictvím 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)
    ...

Odesílání nástrojů v režimu webhooku (nástroje bez endpoint, doručované na webhook vaší organizace jako telephony.tool / web.tool) je běžný podepsaný webhook — platí pro něj standardní postup uvedený výše. Oba tvary požadavků najdete v části Funkční nástroje.

Běžné chyby

Opětovná serializace s výchozím formátováním

Parsování těla a jeho opětovné vypsání s výchozím nastavením vaší knihovny JSON (mezery po , / :, klíče v pořadí vložení) vytváří jiné bajty a naruší HMAC. Ověřujte nezpracované tělo — nebo pokud jej musíte znovu serializovat, přesně dodržte náš kanonický formát: seřazené klíče, kompaktní oddělovače, UTF-8.

Framework automaticky parsuje JSON

Middleware express.json() v Expressu spotřebuje datový proud těla a přijdete o nezpracované bajty. Použijte express.raw() přímo pro cestu webhooku nebo ukládejte nezpracované tělo do vyrovnávací paměti v předchozím middleware. Totéž platí pro NestJS / Koa — podívejte se do jejich dokumentace k „raw body“.

Porovnání nebezpečné z hlediska časování

expected === signature v JS nebo expected == signature v Pythonu jsou porovnání závislá na časování. Použijte příslušně crypto.timingSafeEqual nebo hmac.compare_digest. Rozdíl ve výkonu je nulový.

Nesprávný tajný klíč pro koncové body nástrojů

Přímá volání koncových bodů nástrojů jsou podepsána pomocí tajného klíče webhooku na úrovni organizace (GET /v1/webhook) — nikoli tajným klíčem pro jednotlivý koncový bod z /v1/developer/webhook-endpoints. Znovu použijte stejnou funkci verify(), ale ujistěte se, že jí u cest nástrojů předáváte tajný klíč organizace.

Hashování řetězce dotazu u nástrojů GET/DELETE

U metod nástrojů bez těla podpis pokrývá prázdný bajtový řetězec, což zachovává jeden univerzální postup: vypočítejte HMAC z nezpracovaného těla požadavku, ať je jakékoli. Hashování adresy URL nebo řetězce dotazu nikdy nebude odpovídat.

Nevracení 401 při neshodě

Vrácení 200 při neúspěšném ověření dělá z handleru cíl pro opakované útoky. Pokud ověření selže, vždy odpovězte stavem mimo 2xx.


Další kroky