Open in
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
- Loe päringu töötlemata sisu — täpselt need baidid, mille sulle POSTisime.
- Arvuta
hmac_sha256(secret, body).hexdigest(). - 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?
| Allikas | Saladus |
|---|---|
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 webhook | Organisatsiooni 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:
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)
endRaamistikupõhine seadistamine
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})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:
GET/DELETEtööriistadel puudub päringukeha. Argumendid edastatakse päringuparameetritena ning allkiri arvutatakse tühja baidistringi põhjal — seegaverify(b"", sig, secret)(Python) võiverify(Buffer.alloc(0), sig, secret)(Node). Ära räsi päringustringi.- 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, kasutadesendpoint.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.