# Eine Signatur prüfen

> Code zum Kopieren in Shell, Python und TypeScript, um zu prüfen, dass eine Webhook-Zustellung von SmartChat kommt. Mit Schutz vor Wiederholung und Erneuerung des Geheimnisses.

Stand: 2026-10-30. Quelle: https://docs.smartchat.marketing/de/webhooks/signature-verification/

Jeder, der deine Webhook-Adresse kennt, kann eine Anfrage an sie senden. Die Signatur beweist, dass eine Zustellung von SmartChat kommt und nicht verändert wurde.

## So funktioniert die Signatur

- Jede Zustellung hat den Header `SmartChat-Signature: t=<unix seconds>,v1=<hex>`.
- Der signierte Text ist der Zeitstempel, ein Punkt und der **rohe Request-Body**: `t + "." + body`.
- Die Signatur ist HMAC-SHA256 dieses Textes, in Kleinbuchstaben als Hex geschrieben. Der Schlüssel ist das **Geheimnis des Endpunkts**, genau wie angezeigt (es beginnt mit `whsec_`).
- Bei einer Erneuerung des Geheimnisses hat der Header **zwei** `v1=`-Werte. Einer muss passen.

## Prüfe diese Dinge, in dieser Reihenfolge

1. Der Header ist da und hat `t` und mindestens ein `v1`.
2. Der Zeitstempel liegt innerhalb von 5 Minuten deiner Uhr. Das verhindert, dass jemand eine alte Zustellung noch einmal sendet (ein Replay).
3. Die Signatur passt. Vergleiche in konstanter Zeit.
4. Du hast diese Ereignis-`id` noch nicht bearbeitet.

Nutze den **rohen Body**. Parst du das JSON und schreibst es wieder, ändern sich die Bytes und die Prüfung scheitert.

## Code

Kopiere den Code für deine Sprache. Es ist derselbe Code, mit dem SmartChat seine eigenen Zustellungen testet. Jede Funktion gibt `true` zurück (oder gibt `valid` aus), wenn die Zustellung gültig ist. Übergib den **rohen Body** und das Geheimnis. Bei einer Erneuerung kannst du beide Geheimnisse übergeben (die Funktionen in Python und TypeScript nehmen eine Liste).

**Shell (openssl)**

```bash
#!/bin/sh
# Verify a SmartChat webhook with curl-friendly shell tools (needs openssl).
# Usage: ./verify.sh <secret> "<SmartChat-Signature header>" <body-file>
# The body file must contain the RAW request body exactly as received (save it with: curl ... --data-binary @- or from your server log).
secret="$1"; header="$2"; file="$3"
timestamp=$(printf '%s' "$header" | tr ',' '\n' | sed -n 's/^t=//p' | head -n 1)
case "$timestamp" in ''|*[!0-9]*) echo invalid; exit 1;; esac
now=$(date +%s)
diff=$((now - timestamp)); [ "$diff" -lt 0 ] && diff=$((0 - diff))
[ "$diff" -gt 300 ] && { echo invalid; exit 1; }   # replay protection: more than 5 minutes
expected=$( { printf '%s.' "$timestamp"; cat "$file"; } | openssl dgst -sha256 -hmac "$secret" | sed 's/^.*= //')
for sig in $(printf '%s' "$header" | tr ',' '\n' | sed -n 's/^v1=//p'); do
  if [ "$sig" = "$expected" ]; then echo valid; exit 0; fi
done
echo invalid; exit 1
```

**Python**

```python
# Verify a SmartChat webhook in Python 3 (standard library only).
# Pass the RAW request body (bytes, exactly as received, before any JSON parsing).
import hashlib
import hmac
import sys
import time


def verify_smartchat_signature(raw_body: bytes, header: str, secrets: list, tolerance_seconds: int = 300) -> bool:
    if not header:
        return False
    timestamp = ""
    signatures = []
    for part in header.split(","):
        key, _, value = part.partition("=")
        if key == "t":
            timestamp = value
        elif key == "v1" and value:
            signatures.append(value)
    if not timestamp.isdigit() or not signatures:
        return False
    if abs(time.time() - int(timestamp)) > tolerance_seconds:  # replay protection
        return False
    for secret in secrets:
        signed = timestamp.encode() + b"." + raw_body
        expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
        if any(hmac.compare_digest(sig, expected) for sig in signatures):
            return True
    return False


if __name__ == "__main__" and len(sys.argv) >= 4:
    # python3 verify.py <secret> <header> <body-file>
    with open(sys.argv[3], "rb") as f:
        ok = verify_smartchat_signature(f.read(), sys.argv[2], [sys.argv[1]])
    print("valid" if ok else "invalid")
    sys.exit(0 if ok else 1)
```

**TypeScript**

```typescript
// Verify a SmartChat webhook in TypeScript (Node 18+). No dependencies.
// Usage in your server: pass the RAW request body (a string or Buffer, exactly as received, before any JSON parsing).
import { createHmac, timingSafeEqual } from "node:crypto";
import { readFileSync } from "node:fs";

export function verifySmartChatSignature(rawBody: string | Buffer, header: string | undefined, secrets: string[], toleranceSeconds = 300): boolean {
  if (!header) return false;
  let timestamp = "";
  const signatures: string[] = [];
  for (const part of header.split(",")) {
    const [key, value] = part.split("=", 2);
    if (key === "t") timestamp = value ?? "";
    if (key === "v1" && value) signatures.push(value);
  }
  if (!/^\d+$/.test(timestamp) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false; // replay protection
  const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
  for (const secret of secrets) {
    const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
    for (const sig of signatures) {
      if (sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return true;
    }
  }
  return false;
}

// Command line use: node --import tsx verify.ts <secret> <header> <body-file>
if (process.argv[1]?.endsWith("verify.ts") && process.argv.length >= 5) {
  const ok = verifySmartChatSignature(readFileSync(process.argv[4]!, "utf8"), process.argv[3], [process.argv[2]!]);
  console.log(ok ? "valid" : "invalid");
  process.exit(ok ? 0 : 1);
}
```

## Den rohen Body in deinem Web-Framework holen

Der häufigste Fehler ist, das Framework zuerst das JSON parsen zu lassen. Dann hast du die ursprünglichen Bytes nicht mehr. Frage das Framework nach dem rohen Body, prüfe die Signatur und parse erst dann. Diese zwei Beispiele nutzen die Funktion `verifySmartChatSignature` aus dem TypeScript-Code oben. Dafür braucht es ein Web-Framework, das hier nicht installiert ist, deshalb stehen die Beispiele nur als Text da und wurden nicht von den SmartChat-Tests ausgeführt.

**Express**

```typescript
import express from "express";

const app = express();

// express.raw keeps the body as a Buffer. Do NOT use express.json() on this route.
app.post("/hooks/smartchat", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  const ok = verifySmartChatSignature(raw, req.header("SmartChat-Signature") ?? "", [process.env.SMARTCHAT_WEBHOOK_SECRET!]);
  if (!ok) return res.status(400).send("invalid signature");
  const event = JSON.parse(raw);
  // Handle the event. Answer quickly with 2xx.
  res.sendStatus(200);
});
```

**Next.js (route handler)**

```typescript
export async function POST(request: Request) {
  const raw = await request.text(); // the raw body, not request.json()
  const ok = verifySmartChatSignature(raw, request.headers.get("SmartChat-Signature") ?? "", [process.env.SMARTCHAT_WEBHOOK_SECRET!]);
  if (!ok) return new Response("invalid signature", { status: 400 });
  const event = JSON.parse(raw);
  // Handle the event.
  return new Response(null, { status: 200 });
}
```

## Das Geheimnis erneuern

Erneuere das Geheimnis, wenn es bekannt geworden sein kann. Der Aufruf zur Erneuerung legt ein neues Geheimnis an und gibt es einmal zurück. Für eine Zeit, die du wählst (`grace_hours`, 0 bis 72, Standard 24), werden Zustellungen mit **beiden** Geheimnissen signiert, damit du wechseln kannst, ohne Ereignisse zu verlieren. Danach wird nur noch das neue Geheimnis genutzt. Stelle deinen Code auf das neue Geheimnis um und warte dann, bis die Übergangszeit endet.

## Teste deinen Code

Nutze den Test-Aufruf des Endpunkts. Er sendet ein Ereignis `webhook.test` mit einer echten Signatur. Siehe [Webhook-Tests](https://docs.smartchat.marketing/de/webhooks/testing/).
