Pular para o conteúdo principal

Validacao de Assinatura

Toda requisicao de webhook enviada pelo Tesselys inclui uma assinatura HMAC-SHA256 no header X-Webhook-Signature. Validar essa assinatura e essencial para garantir que o payload foi realmente enviado pelo Tesselys e nao foi adulterado.

Como Funciona

  1. O Tesselys combina o timestamp e o body da requisicao em uma string
  2. Gera um hash HMAC-SHA256 usando o secret da assinatura do webhook
  3. Envia o hash no header X-Webhook-Signature

Headers relevantes:

HeaderDescricaoExemplo
X-Webhook-SignatureAssinatura HMAC-SHA256 (prefixo sha256=)sha256=a1b2c3d4e5f6...
X-Webhook-TimestampTimestamp Unix do envio (segundos)1711324800
X-Webhook-IdID unico do eventoevt_clx123abc456

Algoritmo de Validacao

payload_assinado = "{timestamp}.{body}"
assinatura_esperada = HMAC-SHA256(secret, payload_assinado)
comparar(assinatura_recebida, assinatura_esperada)

Passos:

  1. Extraia o X-Webhook-Timestamp do header
  2. Concatene o timestamp + . + body bruto da requisicao
  3. Calcule o HMAC-SHA256 usando o secret da assinatura
  4. Compare com o valor recebido em X-Webhook-Signature (removendo o prefixo sha256=)
  5. Opcional: verifique se o timestamp nao e muito antigo (tolerancia de 5 minutos) para prevenir ataques de replay

Implementacao em Node.js

const crypto = require('crypto');

function validateWebhookSignature(req, secret) {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];

if (!signature || !timestamp) {
return false;
}

// 1. Verificar tolerancia de tempo (5 minutos)
const currentTime = Math.floor(Date.now() / 1000);
const webhookTime = parseInt(timestamp, 10);
const tolerance = 300; // 5 minutos em segundos

if (Math.abs(currentTime - webhookTime) > tolerance) {
console.warn('Webhook rejeitado: timestamp fora da tolerancia.');
return false;
}

// 2. Calcular assinatura esperada
const rawBody = typeof req.body === 'string'
? req.body
: JSON.stringify(req.body);

const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf-8')
.digest('hex');

// 3. Comparar de forma segura (timing-safe)
const receivedSig = signature.replace('sha256=', '');
const expected = Buffer.from(expectedSignature, 'hex');
const received = Buffer.from(receivedSig, 'hex');

if (expected.length !== received.length) {
return false;
}

return crypto.timingSafeEqual(expected, received);
}

// Uso com Express
const express = require('express');
const app = express();

// IMPORTANTE: usar raw body para validacao correta da assinatura
app.post('/webhooks/tesselys', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.TESSELYS_WEBHOOK_SECRET;
const rawBody = req.body.toString('utf-8');

// Sobrescrever body para string crua
req.body = rawBody;

if (!validateWebhookSignature(req, secret)) {
console.error('Assinatura invalida do webhook');
return res.status(401).json({ error: 'Assinatura invalida' });
}

const event = JSON.parse(rawBody);
console.log(`Evento recebido: ${event.event} (ID: ${event.id})`);

// Processar evento de forma assincrona
processWebhookEvent(event).catch(console.error);

// Responder imediatamente com 200
res.status(200).json({ received: true });
});

async function processWebhookEvent(event) {
switch (event.event) {
case 'person.created':
// Sincronizar pessoa no sistema local
break;
case 'deal.won':
// Disparar fluxo de pos-venda
break;
case 'financial.settled':
// Atualizar status de pagamento
break;
default:
console.log(`Evento nao tratado: ${event.event}`);
}
}

app.listen(3000, () => console.log('Webhook listener rodando na porta 3000'));

Implementacao em Python

import hmac
import hashlib
import time
import json
from flask import Flask, request, jsonify

app = Flask(__name__)

WEBHOOK_SECRET = "whsec_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTuVwXyZ" # Use variavel de ambiente


def validate_webhook_signature(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
"""Valida a assinatura HMAC-SHA256 do webhook Tesselys."""

if not signature or not timestamp:
return False

# 1. Verificar tolerancia de tempo (5 minutos)
current_time = int(time.time())
webhook_time = int(timestamp)
tolerance = 300 # 5 minutos

if abs(current_time - webhook_time) > tolerance:
print("Webhook rejeitado: timestamp fora da tolerancia.")
return False

# 2. Calcular assinatura esperada
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
expected_signature = hmac.new(
secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()

# 3. Comparar de forma segura (timing-safe)
received_sig = signature.replace("sha256=", "")
return hmac.compare_digest(expected_signature, received_sig)


@app.route("/webhooks/tesselys", methods=["POST"])
def handle_webhook():
payload = request.get_data()
signature = request.headers.get("X-Webhook-Signature", "")
timestamp = request.headers.get("X-Webhook-Timestamp", "")

if not validate_webhook_signature(payload, signature, timestamp, WEBHOOK_SECRET):
return jsonify({"error": "Assinatura invalida"}), 401

event = json.loads(payload)
print(f"Evento recebido: {event['event']} (ID: {event['id']})")

# Processar evento (idealmente via fila/worker)
process_event(event)

return jsonify({"received": True}), 200


def process_event(event: dict):
event_type = event.get("event")

if event_type == "person.created":
# Sincronizar pessoa no sistema local
pass
elif event_type == "deal.won":
# Disparar fluxo de pos-venda
pass
elif event_type == "financial.settled":
# Atualizar status de pagamento
pass
else:
print(f"Evento nao tratado: {event_type}")


if __name__ == "__main__":
app.run(port=3000)

Troubleshooting

ProblemaCausa provavelSolucao
Assinatura sempre invalidaBody foi parseado antes da validacao (middleware JSON)Use o raw body (bytes) para calcular o HMAC
Timestamp fora de toleranciaRelógio do servidor esta desincronizadoSincronize com NTP (ntpd ou chrony)
Secret incorretoSecret foi regenerado mas o codigo nao foi atualizadoAtualize o secret na sua aplicacao
Raw Body

E fundamental usar o body bruto (antes de qualquer parse JSON) para calcular a assinatura. Se seu framework faz parse automatico do JSON, configure o endpoint para receber o body como bytes/string primeiro.