ThunderPhone 2.0 on nüüd saadaval.Iseteenindusena alates 2 senti/min.Loe uudist

Operations

Kontrolli veebikonksu allkirju

Iga ThunderPhone

Iga päring, mille sinu serverile saadame — webhooki edastused ja tööriista lõpp-punkti kutsed — sisaldab päises X-ThunderPhone-Signature HMAC-SHA256 allkirja. Seadista kontrollimine üks kord õigesti ja kasuta sama abifunktsiooni igas töötlejas.

Algoritm

  1. Loe päringu töötlemata sisu — täpselt need baidid, mille sulle POSTisime.
  2. Arvuta hmac_sha256(secret, body).hexdigest().
  3. Võrdle seda konstantsel ajal väärtusega X-ThunderPhone-Signature. (Lihtne stringivõrdlus lekitab ajastusteavet.)

Allkirjastame täpselt need baidid, mille edastame, seega töötlemata sisu kontrollimine toimib alati. Need baidid on ühtlasi payloadi kanooniline JSON-serialiseering — võtmed on sorditud tähestikulises järjekorras, eraldajad on kompaktsed (, ja : ilma tühikuteta), UTF-8. See annab sulle teise, täielikult samaväärse meetodi juhul, kui sinu raamistik pakub ainult parsitud JSON-i: serialiseeri see uuesti kanooniliselt ja arvuta selle HMAC.

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

Eelista töötlemata sisu — see on üks samm vähem ja väldib mõne keele JSON-arvude edasi-tagasi teisendamise iseärasusi.

Milline saladus?

AllikasSaladus
Webhooki lõpp-punkt (/v1/developer/webhook-endpoints)Lõpp-punkti secret (48 kuueteistkümnendsümbolit), mis tagastatakse loomisel üks kord
Pärandatud ühe URL-iga webhookOrganisatsiooni secret, mis tagastatakse käsuga GET /v1/webhook
Tööriista lõpp-punkti kutse (otsene kutse sinu endpoint.url-ile)Organisatsioonitaseme webhooki saladus (sama mis pärandatud ühe URL-iga webhookil) — mitte lõpp-punktipõhine saladus

Hoia saladust oma saladuste halduris või keskkonnamuutujas — ära kunagi lisa seda versioonihaldusse.

Võrdlusimplementatsioonid

Kõik neli kontrollivad töötlemata päringu sisu:

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

Raamistikupõhine seadistamine

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

Tööriistakutsete kontrollimine

Kui häälagent kutsub otse esile ühe sinu funktsioonitööriistadest (tööriistal on endpoint), sisaldab päring lisaks sinu seadistatud endpoint.headers-ile kahte ThunderPhone'i päist:

  • X-ThunderPhone-Call-ID — käimasoleva kõne numbriline ID.
  • X-ThunderPhone-Signature — HMAC-SHA256, mille võtmena kasutatakse sinu organisatsioonitaseme veebikonksu saladust, täpselt päringukeha baitide alusel.

Sama verify() abifunktsioon töötab muutmata kujul, kuid kahe eripäraga:

  1. GET / DELETE tööriistadel puudub päringukeha. Argumendid edastatakse päringuparameetritena ning allkiri arvutatakse tühja baidistringi põhjal — seega verify(b"", sig, secret) (Python) või verify(Buffer.alloc(0), sig, secret) (Node). Ära räsi päringustringi.
  2. Organisatsioonidel, millel pole seadistatud pärand-veebikonksu, puudub organisatsiooni saladus. Sel juhul sisaldavad tööriistakutsed ainult X-ThunderPhone-Call-ID-d, mitte allkirjapäist. Allkirjastamissaladuse saamiseks seadista pärand-veebikonks (PUT /v1/webhook) või autendi tööriistakutsed oma päise abil, kasutades 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)
    ...

Veebikonksu-režiimis tööriistade edastamine (tööriistad ilma endpoint-ita, mis saadetakse sinu organisatsiooni veebikonksu kui telephony.tool / web.tool) on tavaline allkirjastatud veebikonks — rakendub ülaltoodud standardne juhis. Mõlema päringukuju kohta vaata funktsioonitööriistu.

Levinud vead

Uuesti serialiseerimine vaikevorminguga

Keha parsimine ja selle uuesti väljastamine JSON-teegi vaikeseadetega (tühikud pärast , / :, sisestusjärjestuses võtmed) annab erinevad baidid ja rikub HMAC-i. Kontrolli töötlemata keha — või kui pead selle uuesti serialiseerima, järgi täpselt meie kanoonilist vormi: sorditud võtmed, kompaktsed eraldajad, UTF-8.

Raamistik parsib JSON-i automaatselt

Expressi express.json() vahetarkvara tarbib kehavoo ja töötlemata baidid lähevad kaotsi. Kasuta veebikonksu marsruudil eraldi express.raw()-i või puhverdage töötlemata keha eelvahetarkvaras. Sama kehtib NestJS-i / Koa kohta — vaata nende „raw body” dokumentatsiooni.

Ajastusrünnakutele ebaturvaline võrdlus

expected === signature JS-is või expected == signature Pythonis on ajastusest sõltuvad võrdlused. Kasuta vastavalt crypto.timingSafeEqual või hmac.compare_digest. Jõudluse erinevus on olematu.

Tööriista lõpp-punktide jaoks vale saladus

Otsesed tööriista lõpp-punkti kutsed allkirjastatakse organisatsioonitaseme veebikonksu saladusega (GET /v1/webhook) — mitte ühegi lõpp-punktipõhise saladusega asukohast /v1/developer/webhook-endpoints. Kasuta sama verify() funktsiooni, kuid veendu, et annad sellele tööriista marsruutidel organisatsiooni saladuse.

Päringustringi räsimine GET/DELETE tööriistadel

Kehata tööriistameetodite puhul katab allkiri tühja baidijada, säilitades ühe universaalse retsepti: arvuta HMAC töötlemata päringukehale, mis iganes see on. URL-i või päringustringi räsimine ei sobi kunagi.

Mittevastavuse korral 401 tagastamata jätmine

Ebaõnnestunud kinnitamise korral 200 tagastamine muudab töötleja kordusrünnaku sihtmärgiks. Kui kinnitamine ebaõnnestub, vasta alati mitte-2xx koodiga.


Järgmised sammud