ThunderPhone 2.0 je stigao.Postavite sve sami, već od 2 ¢/min.Pročitajte objavu

Operations

Provjerite potpise webhookova

Svaki webhook i svaki zahtjev alata koji ThunderPhone šalje potpisan je. Jednom provjerite potpis prema ovdje navedenom postupku, a zatim istu provjeru ponovno upotrijebite na svakom endpointu koji pokrećete.

Svaki zahtjev koji šaljemo vašem poslužitelju — isporuke web-dojavnika i pozivi krajnjih točaka alata — sadrži HMAC-SHA256 potpis u zaglavlju X-ThunderPhone-Signature. Jednom ispravno implementirajte provjeru i uključite isti pomoćnik u svaki rukovatelj.

Algoritam

  1. Pročitajte neobrađeno tijelo zahtjeva — točne bajtove koje smo vam poslali metodom POST.
  2. Izračunajte hmac_sha256(secret, body).hexdigest().
  3. Usporedite u konstantnom vremenu sa zaglavljem X-ThunderPhone-Signature. (Naivna usporedba nizova otkriva informacije o vremenu.)

Potpisujemo točno one bajtove koje prenosimo, stoga provjera neobrađenog tijela uvijek funkcionira. Ti bajtovi ujedno su kanonska JSON serijalizacija korisnog sadržaja — ključevi poredani abecedno, sažeti razdjelnici (, i : bez razmaka), UTF-8. To vam pruža drugi, potpuno ekvivalentan postupak kada vaš okvir izlaže samo parsirani JSON: ponovno ga serijalizirajte kanonski i nad njim izračunajte HMAC.

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

Dajte prednost neobrađenom tijelu — to je jedan korak manje i otporno je na specifičnosti ponovnog pretvaranja JSON brojeva u nekim jezicima.

Koja tajna?

IzvorTajna
Krajnja točka web-dojavnika (/v1/developer/webhook-endpoints)secret za pojedinu krajnju točku (48 heksadekadskih znakova) vraćen jednokratno pri stvaranju
Naslijeđeni web-dojavnik s jednim URL-omsecret za organizaciju vraćen pri GET /v1/webhook
Poziv krajnje točke alata (izravni poziv vašeg endpoint.url)Tajna web-dojavnika na razini organizacije (ista kao za naslijeđeni web-dojavnik s jednim URL-om) — ne tajna pojedine krajnje točke

Pohranite tajnu u upravitelj tajni ili varijablu okruženja — nikada je nemojte predati u repozitorij.

Referentne implementacije

Sve četiri provjeravaju neobrađeno tijelo zahtjeva:

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

Povezivanje specifično za okvir

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

Provjera poziva alata

Kada agent izravno pozove jedan od vaših funkcijskih alata (alat ima endpoint), zahtjev uz vaša konfigurirana zaglavlja endpoint.headers sadrži dva zaglavlja ThunderPhonea:

  • X-ThunderPhone-Call-ID — numerički ID aktivnog poziva.
  • X-ThunderPhone-Signature — HMAC-SHA256 s ključem tajne webhooka na razini organizacije, izračunat nad točnim bajtovima tijela zahtjeva.

Isti pomoćnik verify() radi bez izmjena, uz dvije pojedinosti:

  1. Alati GET / DELETE nemaju tijelo. Argumenti se prenose kao parametri upita, a potpis se izračunava nad praznim nizom bajtova — stoga verify(b"", sig, secret) (Python) ili verify(Buffer.alloc(0), sig, secret) (Node). Nemojte raspršivati niz upita.
  2. Organizacije bez konfiguriranog naslijeđenog webhooka nemaju tajnu organizacije. U tom slučaju pozivi alata sadrže samo X-ThunderPhone-Call-ID, bez zaglavlja potpisa. Konfigurirajte naslijeđeni webhook (PUT /v1/webhook) da biste dobili tajnu za potpisivanje ili autentificirajte pozive alata vlastitim zaglavljem putem 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)
    ...

Slanje alata u webhook-načinu rada (alati bez endpoint, isporučeni na webhook vaše organizacije kao telephony.tool / web.tool) običan je potpisani webhook — primjenjuje se standardni postupak iznad. Pogledajte Funkcijski alati za oba oblika zahtjeva.

Uobičajene zamke

Ponovna serijalizacija sa zadanim formatiranjem

Raščlanjivanje tijela i njegovo ponovno zapisivanje sa zadanim postavkama vaše JSON biblioteke (razmaci nakon , / :, ključevi prema redoslijedu umetanja) stvara različite bajtove i kvari HMAC. Provjerite neobrađeno tijelo — ili, ako ga morate ponovno serijalizirati, točno uskladite naš kanonski oblik: sortirani ključevi, sažeti razdjelnici, UTF-8.

Okvir automatski raščlanjuje JSON

Expressov međuprogram express.json() troši tok tijela i gubite neobrađene bajtove. Upotrijebite express.raw() posebno na webhook ruti ili spremite neobrađeno tijelo u međuprogramu koji se izvršava ranije. Isto vrijedi za NestJS / Koa — provjerite njihovu dokumentaciju za „neobrađeno tijelo”.

Usporedba nesigurna na vremenske napade

expected === signature u JS-u ili expected == signature u Pythonu usporedbe su s promjenjivim vremenom izvršavanja. Upotrijebite crypto.timingSafeEqual odnosno hmac.compare_digest. Razlika u performansama je zanemariva.

Pogrešna tajna za krajnje točke alata

Izravni pozivi krajnjih točaka alata potpisuju se pomoću tajne webhooka na razini organizacije (GET /v1/webhook) — a ne pomoću tajne za pojedinačnu krajnju točku iz /v1/developer/webhook-endpoints. Ponovno upotrijebite istu funkciju verify(), ali provjerite prosljeđujete li joj tajnu organizacije na rutama alata.

Sažimanje niza upita za GET/DELETE alate

Za metode alata bez tijela potpis obuhvaća prazan niz bajtova, čime se zadržava jedan univerzalni postupak: izračunajte HMAC za neobrađeno tijelo zahtjeva, kakvo god ono bilo. Sažimanje URL-a ili niza upita nikada se neće podudarati.

Nevraćanje odgovora 401 pri nepodudaranju

Vraćanje odgovora 200 nakon neuspjele provjere čini rukovatelj metom ponavljanja zahtjeva. Uvijek odgovorite statusom koji nije 2xx ako provjera ne uspije.


Sljedeći koraci