Pārbaudiet tīmekļa aizķeres parakstus
Katrs ThunderPhone nosūtītais tīmekļa aizķeres un rīka pieprasījums ir parakstīts. Vienreiz pārbaudiet parakstu, izmantojot šeit sniegto recepti, pēc tam izmantojiet to pašu pārbaudi katrā darbinātajā galapunktā.
Katram pieprasījumam, ko nosūtām uz jūsu serveri — tīmekļa aizķeres piegādēm un
rīku galapunktu izsaukumiem — galvenē
X-ThunderPhone-Signature ir HMAC-SHA256 paraksts. Vienreiz pareizi
ieviesiet pārbaudi un izmantojiet to pašu palīgfunkciju katrā apstrādātājā.
Algoritms
- Nolasiet neapstrādāto pieprasījuma pamattekstu — precīzus baitus, ko jums nosūtījām ar POST.
- Aprēķiniet
hmac_sha256(secret, body).hexdigest(). - Salīdziniet konstantā laikā ar
X-ThunderPhone-Signature. (Naiva virkņu salīdzināšana atklāj laika informāciju.)
Mēs parakstām tieši tos baitus, ko pārsūtām, tāpēc neapstrādātā pamatteksta
pārbaude vienmēr darbojas. Šie baiti ir arī slodzes kanoniskā JSON serializācija —
atslēgas sakārtotas alfabētiski, kompakti atdalītāji
(, un : bez atstarpēm), UTF-8. Tas sniedz otru, pilnībā
līdzvērtīgu risinājumu gadījumos, kad jūsu ietvars nodrošina tikai parsētu JSON:
serializējiet kanoniski atkārtoti un aprēķiniet tam HMAC.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Dodiet priekšroku neapstrādātajam pamattekstam — tas ir par vienu soli mazāk un nav pakļauts JSON skaitļu atkārtotas konvertēšanas īpatnībām dažās valodās.
Kurš noslēpums?
| Avots | Noslēpums |
|---|---|
Tīmekļa aizķeres galapunkts (/v1/developer/webhook-endpoints) | Katram galapunktam paredzēts secret (48 heksadecimālās rakstzīmes), kas tiek atgriezts vienreiz izveides laikā |
| Mantotā tīmekļa aizķere ar vienu URL | Organizācijai paredzēts secret, kas tiek atgriezts ar GET /v1/webhook |
Rīka galapunkta izsaukums (tiešs izsaukums uz jūsu endpoint.url) | Organizācijas līmeņa tīmekļa aizķeres noslēpums (tas pats, kas mantotajai tīmekļa aizķerei ar vienu URL) — nevis katram galapunktam paredzēts noslēpums |
Glabājiet noslēpumu savu noslēpumu pārvaldniekā vai vides mainīgajā — nekad to neiekļaujiet repozitorijā.
Atsauces ieviešanas
Visas četras pārbauda neapstrādāto pieprasījuma pamattekstu:
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)
endIetvarspecifiska pieslēgšana
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})Rīku izsaukumu verificēšana
Kad balss aģents tieši izsauc kādu no jūsu
funkciju rīkiem (rīkam ir
endpoint), pieprasījumā līdzās jūsu konfigurētajiem endpoint.headers
tiek nosūtītas divas ThunderPhone galvenes:
X-ThunderPhone-Call-ID— aktīvā zvana skaitliskais ID.X-ThunderPhone-Signature— HMAC-SHA256, kas izveidots ar jūsu organizācijas līmeņa tīmekļa aizķeres noslēpumu, izmantojot precīzus pieprasījuma pamatteksta baitus.
Tas pats verify() palīgrīks darbojas bez izmaiņām, ar divām niansēm:
GET/DELETErīkiem nav pamatteksta. Argumenti tiek nodoti kā vaicājuma parametri, un paraksts tiek aprēķināts, izmantojot tukšu baitu virkni — tātadverify(b"", sig, secret)(Python) vaiverify(Buffer.alloc(0), sig, secret)(Node). Neveidojiet jaucējvērtību no vaicājuma virknes.- Organizācijām bez konfigurētas mantotās tīmekļa aizķeres nav organizācijas noslēpuma.
Šādā gadījumā rīku izsaukumos ir tikai
X-ThunderPhone-Call-ID, bet nav paraksta galvenes. Konfigurējiet mantoto tīmekļa aizķeri (PUT /v1/webhook), lai iegūtu parakstīšanas noslēpumu, vai autentificējiet rīku izsaukumus ar savu galveni, izmantojotendpoint.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)
...Tīmekļa aizķeres režīma rīku nosūtīšana (rīki bez endpoint, kas tiek
piegādāti uz jūsu organizācijas tīmekļa aizķeri kā telephony.tool / web.tool)
ir parasta parakstīta tīmekļa aizķere — piemērojama iepriekš norādītā standarta
procedūra. Abus pieprasījumu formātus skatiet sadaļā
Funkciju rīki.
Biežākās kļūdas
Atkārtota serializēšana ar noklusējuma formatējumu
Pamatteksta parsēšana un atkārtota izvade ar JSON bibliotēkas
noklusējuma iestatījumiem (atstarpes pēc , / :, ievietošanas secībā kārtotas atslēgas) rada
atšķirīgus baitus un sabojā HMAC. Verificējiet neapstrādāto pamattekstu — vai, ja tas ir jāserializē atkārtoti, precīzi atbilstiet mūsu kanoniskajai formai: sakārtotas
atslēgas, kompakti atdalītāji, UTF-8.
Ietvars automātiski parsē JSON
Express express.json() starpprogrammatūra patērē pamatteksta plūsmu,
un jūs zaudējat neapstrādātos baitus. Konkrēti tīmekļa aizķeres maršrutā izmantojiet express.raw()
vai saglabājiet neapstrādāto pamattekstu buferī pirms starpprogrammatūras.
Tas pats attiecas uz NestJS / Koa — skatiet to dokumentāciju par “neapstrādātu pamattekstu”.
Salīdzināšana, kas nav droša pret laika uzbrukumiem
expected === signature JS vai expected == signature valodā
Python ir salīdzinājumi ar mainīgu izpildes laiku. Attiecīgi izmantojiet crypto.timingSafeEqual
vai hmac.compare_digest. Veiktspējas atšķirība
ir niecīga.
Nepareizs noslēpums rīku galapunktiem
Tiešie rīku galapunktu izsaukumi tiek parakstīti ar organizācijas līmeņa tīmekļa aizķeres
noslēpumu (GET /v1/webhook) — nevis ar kādu galapunktam specifisku noslēpumu
no /v1/developer/webhook-endpoints. Atkārtoti izmantojiet to pašu verify()
funkciju, bet pārliecinieties, ka rīku maršrutos tai nododat organizācijas noslēpumu.
Vaicājuma virknes jaucējkodēšana GET/DELETE rīkiem
Rīku metodēm bez pamatteksta paraksts aptver tukšo baitu virkni, saglabājot vienu universālu principu: HMAC neapstrādātajam pieprasījuma pamattekstam, lai kāds tas būtu. URL vai vaicājuma virknes jaucējkodēšana nekad neatbildīs.
401 neatgriešana neatbilstības gadījumā
200 atgriešana neveiksmīgas verifikācijas gadījumā padara apstrādātāju par atkārtotas atskaņošanas uzbrukuma mērķi. Ja verifikācija neizdodas, vienmēr atbildiet ar statusu, kas nav 2xx.
Nākamās darbības
Piegādes semantika, atkārtoti mēģinājumi, avota IP adreses.
Pārvaldiet vairākus URL, mainiet noslēpumus.
Abi rīku izsaukšanas ceļi un to pieprasījumu formas.
Izveidojiet pilnīgu, ar rīkiem nodrošinātu integrāciju no sākuma līdz beigām.