Verificar assinatura
Toda entrega de webhook leva o cabeçalho:
X-LiteZap-Signature: t=1792252800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt: momento da assinatura (Unix, segundos).v1:HMAC-SHA256(secret, "<t>.<corpo>")em hexadecimal, ondesecreté o segredo do webhook e<corpo>é o corpo bruto da requisição.
Como validar
Seção intitulada “Como validar”- Leia o corpo bruto, antes de qualquer parse de JSON (reserializar muda os bytes e quebra a assinatura).
- Separe
tev1do cabeçalho. - Recuse se
testiver a mais de 5 minutos do seu relógio (anti-replay). - Calcule o HMAC de
"<t>.<corpo>"e compare comv1em tempo constante.
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "math" "strconv" "strings" "time")
// verify confere X-LiteZap-Signature ("t=<unix>,v1=<hex>") com o corpo bruto.func verify(secret, header string, body []byte) bool { var ts string var sigs []string for _, p := range strings.Split(header, ",") { k, v, _ := strings.Cut(p, "=") switch k { case "t": ts = v case "v1": sigs = append(sigs, v) } } t, err := strconv.ParseInt(ts, 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-t)) > 300 { return false // sem timestamp ou mais de 5 minutos de diferença (anti-replay) } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts + "." + string(body))) want := hex.EncodeToString(mac.Sum(nil)) for _, s := range sigs { if hmac.Equal([]byte(s), []byte(want)) { return true } } return false}import crypto from "node:crypto";
// rawBody: o corpo exatamente como chegou (string), antes de JSON.parse.export function verify(secret, header, rawBody) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2))); const t = Number(parts.t); if (!parts.v1 || !Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; const want = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"); const a = Buffer.from(parts.v1, "hex"); const b = Buffer.from(want, "hex"); return a.length === b.length && crypto.timingSafeEqual(a, b);}import hashlibimport hmacimport time
def verify(secret: str, header: str, raw_body: bytes) -> bool: """Confere X-LiteZap-Signature ("t=<unix>,v1=<hex>") com o corpo bruto.""" parts = {} sigs = [] for p in header.split(","): k, _, v = p.partition("=") if k == "v1": sigs.append(v) else: parts[k] = v try: t = int(parts.get("t", "")) except ValueError: return False if abs(time.time() - t) > 300: # anti-replay: 5 minutos return False want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return any(hmac.compare_digest(s, want) for s in sigs)<?php// $rawBody = file_get_contents('php://input');// $header = $_SERVER['HTTP_X_LITEZAP_SIGNATURE'] ?? '';function litezap_verify(string $secret, string $header, string $rawBody): bool{ $ts = null; $sigs = []; foreach (explode(',', $header) as $part) { [$k, $v] = array_pad(explode('=', $part, 2), 2, ''); if ($k === 't') $ts = $v; if ($k === 'v1') $sigs[] = $v; } if ($ts === null || !ctype_digit($ts) || abs(time() - (int) $ts) > 300) { return false; // anti-replay: 5 minutos } $want = hash_hmac('sha256', $ts . '.' . $rawBody, $secret); foreach ($sigs as $s) { if (hash_equals($want, $s)) return true; } return false;}O cabeçalho pode trazer mais de um v1 no futuro (rotação de segredo): aceite se qualquer um bater, como nos exemplos
em Go e Python.
Testar localmente
Seção intitulada “Testar localmente”SECRET=whsec_exemplo_para_teste_1234BODY='{"id":"evt_teste","type":"instance.status","version":1,"instance_id":"inst_x","timestamp":"2026-10-17T13:40:02Z","data":{"state":"connected"}}'T=$(date +%s)SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')curl -s -X POST http://127.0.0.1:9000/ -H "X-LiteZap-Signature: t=$T,v1=$SIG" -d "$BODY"
