Assinatura de Requisição
Toda requisição de webhook do Emailit inclui um cabeçalho X-Emailit-Signature contendo uma assinatura HMAC-SHA256 e um cabeçalho X-Emailit-Timestamp com o timestamp Unix de quando a requisição foi assinada. Você deve verificar esta assinatura para garantir que a requisição é autêntica e não foi alterada.
Como funciona
- Quando você cria um webhook, o Emailit gera um segredo de assinatura para esse endpoint
- Para cada entrega, o Emailit concatena o timestamp e o corpo bruto da requisição como
{'{'}timestamp{'}'}.{'{'}corpoRaw{'}'} - Um hash HMAC-SHA256 é calculado sobre essa string usando seu segredo de assinatura
- A assinatura é enviada no cabeçalho
X-Emailit-Signaturee o timestamp noX-Emailit-Timestamp - Seu servidor recalcula a assinatura e a compara com o valor do cabeçalho
Verificando a assinatura
Para verificar uma assinatura de webhook:
- Extraia os cabeçalhos
X-Emailit-SignatureeX-Emailit-Timestampda requisição - Concatene o timestamp e o corpo bruto da requisição como
{'{'}timestamp{'}'}.{'{'}corpoRaw{'}'} - Calcule um hash HMAC-SHA256 dessa string usando seu segredo de assinatura do webhook
- Compare o hash calculado com a assinatura usando uma comparação segura contra timing
- Opcionalmente, rejeite requisições onde o timestamp seja mais antigo que alguns minutos para proteger contra ataques de replay
import crypto from 'crypto';
function verifyWebhookSignature(rawBody, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const computed = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed, 'hex'),
Buffer.from(signature, 'hex')
);
}
// No seu manipulador de webhook:
const rawBody = req.body; // corpo bruto da requisição como string
const signature = req.headers['x-emailit-signature'];
const timestamp = req.headers['x-emailit-timestamp'];
const secret = process.env.WEBHOOK_SIGNING_SECRET;
// Proteção contra ataques de replay (tolerância de 5 minutos)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
return res.status(401).send('Requisição muito antiga');
}
if (!verifyWebhookSignature(rawBody, signature, timestamp, secret)) {
return res.status(401).send('Assinatura inválida');
} import hmac
import hashlib
import time
def verify_webhook_signature(raw_body, signature, timestamp, secret):
signed_payload = f"{timestamp}.{raw_body}"
computed = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
# Proteção contra ataques de replay (tolerância de 5 minutos)
age = int(time.time()) - int(timestamp)
if age > 300:
raise ValueError("Requisição muito antiga") function verifyWebhookSignature(
string $rawBody,
string $signature,
string $timestamp,
string $secret
): bool {
$signedPayload = "{$timestamp}.{$rawBody}";
$computed = hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($computed, $signature);
}
// Proteção contra ataques de replay (tolerância de 5 minutos)
$age = time() - intval($timestamp);
if ($age > 300) {
http_response_code(401);
exit('Requisição muito antiga');
} require 'openssl'
def verify_webhook_signature(raw_body, signature, timestamp, secret)
signed_payload = "#{timestamp}.#{raw_body}"
computed = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)
Rack::Utils.secure_compare(computed, signature)
end
# Proteção contra ataques de replay (tolerância de 5 minutos)
age = Time.now.to_i - timestamp.to_i
raise 'Requisição muito antiga' if age > 300 import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"strconv"
)
func verifyWebhookSignature(rawBody, signature, timestamp, secret string) bool {
signedPayload := fmt.Sprintf("%s.%s", timestamp, rawBody)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
computed := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computed), []byte(signature))
}
// Proteção contra ataques de replay (tolerância de 5 minutos)
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().Unix()-ts > 300 {
// rejeitar requisição
} Observações importantes
- A assinatura é calculada sobre
{'{'}timestamp{'}'}.{'{'}corpoRaw{'}'}— sempre use a string do corpo bruto da requisição, não uma versão re-serializada (ex:JSON.stringify(req.body)). Diferenças de espaçamento ou ordenação de chaves irão quebrar a assinatura. - Sempre use uma função de comparação segura contra timing (ex:
crypto.timingSafeEqual,hmac.compare_digest,hash_equals) para prevenir ataques de timing. - Considere rejeitar requisições onde o
X-Emailit-Timestampseja mais antigo que alguns minutos para proteger contra ataques de replay. - Mantenha seu segredo de assinatura seguro — armazene-o em variáveis de ambiente, não no código fonte.
- Você pode encontrar seu segredo de assinatura do webhook no painel do Emailit nas configurações do webhook, ou através da API de Webhooks.