Patikrinkite žiniatinklio kabliukų parašus

Kiekviena užklausa, kurią siunčiame į jūsų serverį – žiniatinklio kabliuko pristatymai ir įrankio galinio taško iškvietimai – turi HMAC-SHA256 parašą X-ThunderPhone-Signature antraštėje. Kartą tinkamai įgyvendinkite tikrinimą ir prijunkite tą pačią pagalbinę funkciją prie kiekvienos tvarkyklės.

Algoritmas

  1. Nuskaitykite neapdorotą užklausos turinį – tikslius baitus, kuriuos jums išsiuntėme POST metodu.
  2. Apskaičiuokite hmac_sha256(secret, body).hexdigest().
  3. Palyginkite pastoviuoju laiku su X-ThunderPhone-Signature. (Naivus eilučių palyginimas atskleidžia laiko informaciją.)

Pasirašome būtent baitus, kuriuos perduodame, todėl neapdoroto turinio tikrinimas visada veikia. Tie baitai taip pat yra kanoninis JSON serializavimas apkrovos – raktai surikiuoti abėcėlės tvarka, glausti skirtukai (, ir : be tarpų), UTF-8. Tai suteikia antrą, visiškai lygiavertį būdą, kai jūsų sistema pateikia tik išanalizuotą JSON: pakartotinai serializuokite kanoniškai ir apskaičiuokite jo HMAC.

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

Pirmenybę teikite neapdorotam turiniui – tai vienu žingsniu mažiau ir išvengsite JSON skaičių pakartotinio konvertavimo ypatumų kai kuriose programavimo kalbose.

Kuris slaptasis raktas?

ŠaltinisSlaptasis raktas
Žiniatinklio kabliuko galinis taškas (/v1/developer/webhook-endpoints)Vienam galiniam taškui skirtas secret (48 šešioliktainiai simboliai), grąžinamas vieną kartą kuriant
Senas vieno URL žiniatinklio kabliukasOrganizacijai skirtas secret, grąžinamas naudojant GET /v1/webhook
Įrankio galinio taško iškvietimas (tiesioginis iškvietimas į jūsų endpoint.url)Organizacijos lygio žiniatinklio kabliuko slaptasis raktas (tas pats kaip seno vieno URL žiniatinklio kabliuko) – ne vienam galiniam taškui skirtas slaptasis raktas

Saugokite slaptąjį raktą savo slaptųjų raktų tvarkytuvėje arba aplinkos kintamajame – niekada jo neįtraukite į kodo saugyklą.

Etaloninės realizacijos

Visos keturios tikrina neapdorotą užklausos turinį:

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

Konkrečių sistemų prijungimas

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

Įrankių iškvietimų tikrinimas

Kai agentas tiesiogiai iškviečia vieną iš jūsų funkcijų įrankių (įrankis turi endpoint), užklausoje kartu su jūsų sukonfigūruotais endpoint.headers pateikiamos dvi ThunderPhone antraštės:

Tas pats verify() pagalbinis metodas veikia be pakeitimų, tačiau yra du niuansai:

  1. GET / DELETE įrankiai neturi turinio. Argumentai perduodami kaip užklausos parametrai, o parašas apskaičiuojamas pagal tuščią baitų eilutę — todėl naudokite verify(b"", sig, secret) (Python) arba verify(Buffer.alloc(0), sig, secret) (Node). Neskaičiuokite maišos iš užklausos eilutės.
  2. Organizacijos, neturinčios sukonfigūruoto ankstesnės versijos žiniatinklio kabliuko, neturi organizacijos paslapties. Tokiu atveju įrankių iškvietimuose pateikiamas tik X-ThunderPhone-Call-ID, o parašo antraštės nėra. Sukonfigūruokite ankstesnės versijos žiniatinklio kabliuką (PUT /v1/webhook), kad gautumėte pasirašymo paslaptį, arba autentifikuokite įrankių iškvietimus naudodami savo antraštę per 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)
    ...

Įrankių išsiuntimas žiniatinklio kabliuko režimu (įrankiai be endpoint, pateikiami į jūsų organizacijos žiniatinklio kabliuką kaip telephony.tool / web.tool) yra įprastas pasirašytas žiniatinklio kabliukas — taikoma pirmiau pateikta standartinė procedūra. Abiejų užklausų formų aprašą rasite Funkcijų įrankiai.

Dažnos klaidos

Pakartotinis serializavimas naudojant numatytąjį formatavimą

Išanalizavus užklausos turinį ir pakartotinai jį išvedus naudojant JSON bibliotekos numatytuosius nustatymus (tarpai po , / :, įterpimo tvarka išdėstyti raktai), gaunami skirtingi baitai ir sugadinamas HMAC. Patikrinkite neapdorotą užklausos turinį arba, jei privalote jį pakartotinai serializuoti, tiksliai atitikite mūsų kanoninę formą: surikiuoti raktai, kompaktiški skirtukai, UTF-8.

Sistema automatiškai analizuoja JSON

Express express.json() tarpinė programinė įranga sunaudoja užklausos turinio srautą, todėl prarandate neapdorotus baitus. Konkrečiai webhook maršrutui naudokite express.raw() arba išsaugokite neapdorotą užklausos turinį prieš tarpinę programinę įrangą. Tas pats taikoma NestJS / Koa — peržiūrėkite jų „neapdoroto užklausos turinio“ dokumentaciją.

Nuo vykdymo laiko priklausantis palyginimas

expected === signature JS kalboje arba expected == signature Python kalboje yra palyginimai, kurių vykdymo laikas skiriasi. Atitinkamai naudokite crypto.timingSafeEqual arba hmac.compare_digest. Našumo skirtumas neegzistuoja.

Neteisingas įrankių galinių taškų slaptasis raktas

Tiesioginiai įrankių galinių taškų iškvietimai pasirašomi naudojant organizacijos lygmens webhook slaptąjį raktą (GET /v1/webhook) — ne naudojant atskiro galinio taško slaptąjį raktą iš /v1/developer/webhook-endpoints. Pakartotinai naudokite tą pačią verify() funkciją, tačiau įsitikinkite, kad įrankių maršrutuose jai perduodate organizacijos slaptąjį raktą.

Užklausos eilutės maišos skaičiavimas GET/DELETE įrankiams

Įrankių metodams be užklausos turinio parašas apima tuščią baitų eilutę, todėl išlieka vienas universalus metodas: apskaičiuokite HMAC neapdorotam užklausos turiniui, kad ir koks jis būtų. URL arba užklausos eilutės maiša niekada neatitiks.

401 negrąžinimas esant neatitikimui

Grąžinus 200, kai patikra nepavyksta, apdorojimo programa tampa pakartotinio atkūrimo taikiniu. Jei patikra nepavyksta, visada atsakykite ne 2xx kodu.


Kiti veiksmai