---
title: "Kontrolli veebikonksu allkirju"
description: "Iga ThunderPhone'i saadetud veebikonksu- ja tööriistapäring on allkirjastatud. Kontrolli allkirja siin toodud juhise abil üks kord, seejärel kasuta sama kontrolli igas käitatavas lõpp-punktis."
---

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.

```python
# 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](/et/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | Lõpp-punkti `secret` (48 kuueteistkümnendsümbolit), mis tagastatakse loomisel üks kord |
| [Pärandatud ühe URL-iga webhook](/api-reference/organizations#legacy-single-url-webhook) | Organisatsiooni `secret`, mis tagastatakse käsuga `GET /v1/webhook` |
| [Tööriista lõpp-punkti kutse](/et/tools/overview) (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:

<CodeGroup>
```python 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 "")
```

```javascript 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 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 Ruby
require "openssl"

def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end
```
</CodeGroup>

## Raamistikupõhine seadistamine

<CodeGroup>
```python 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}
```

```javascript 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);
  },
);
```

```python 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})
```
</CodeGroup>

## Tööriistakutsete kontrollimine

Kui häälagent kutsub otse esile ühe sinu
[funktsioonitööriistadest](/et/tools/overview) (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`.

```python
@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](/et/tools/overview).

## Levinud vead

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

---

## Järgmised sammud

<CardGroup cols={2}>
  <Card title="Veebikonksude ülevaade" icon="bolt" href="/et/webhooks/overview">
    Edastamise semantika, korduskatsed, lähte-IP-aadressid.
  </Card>
  <Card title="Veebikonksu lõpp-punktid" icon="plug" href="/et/webhooks/endpoints">
    Halda mitut URL-i, vaheta saladusi.
  </Card>
  <Card title="Funktsioonitööriistad" icon="screwdriver-wrench" href="/et/tools/overview">
    Kaks tööriista kutsumise teed ja nende päringuvormid.
  </Card>
  <Card title="Tööriistaintegratsioonid" icon="wrench" href="/et/guides/build-tool-integration">
    Loo täielik tööriistatoega integratsioon algusest lõpuni.
  </Card>
</CardGroup>
