Verificar firmas
Comprueba que cada solicitud de webhook realmente viene de Pulsar y no se alteró en el camino.
Tu endpoint es una URL pública, así que cualquiera puede enviarle una solicitud. Cada notificación de Pulsar incluye un encabezado X-Pulsar-Signature, un hash del cuerpo de la solicitud calculado con tu secreto de firma. Solo Pulsar y tú conocen el secreto, así que una firma que coincide demuestra que la solicitud viene de Pulsar y que el cuerpo no se modificó.
La firma es el HMAC-SHA256 del cuerpo sin procesar de la solicitud, con tu secreto de firma como clave, escrito en hexadecimal en minúsculas.
Usa el secreto exactamente como te lo da Pulsar
El secreto de firma es una cadena de 32 caracteres. Usa su texto (bytes UTF-8) como clave del HMAC, tal cual. No lo decodifiques de Base64 ni de hexadecimal. La decodificación suele funcionar sin errores pero produce una clave incorrecta, y entonces todas las verificaciones de firma fallan.
Cómo verificar una solicitud
Lee el cuerpo sin procesar
Calcula la firma sobre los bytes exactos que envió Pulsar, antes de cualquier parseo de JSON. Parsear y volver a serializar el JSON puede cambiar los espacios o el orden de las claves, y eso cambia el hash. La mayoría de los frameworks te dan el cuerpo sin procesar por separado; consulta los ejemplos más abajo.
Calcula la firma esperada
Calcula el HMAC-SHA256 del cuerpo sin procesar con tu secreto de firma como clave, y codifica el resultado en hexadecimal.
Compárala con el encabezado
Compara tu resultado con el encabezado X-Pulsar-Signature usando una comparación de tiempo constante (hmac.compare_digest en Python, crypto.timingSafeEqual en Node.js). Un == simple puede revelar, por el tiempo de respuesta, qué tan acertada era una firma adivinada.
Acepta o rechaza
Si las firmas coinciden, procesa el evento y devuelve 2XX. Si falta el encabezado o no coincide, devuelve 401 e ignora la solicitud.
Prueba tu verificación con un ping primero
Pulsar no reintenta una respuesta 4XX (consulta Reintentos). Así que si tu secreto está mal configurado, los eventos reales se rechazan y quedan sin entregar hasta que Pulsar los vuelva a enviar. Pide un ping y confirma que tu endpoint devuelve 2XX antes de depender de él.
Código de ejemplo
Ambos ejemplos leen el secreto de una variable de entorno PULSAR_WEBHOOK_SECRET.
import hashlib
import hmac
import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["PULSAR_WEBHOOK_SECRET"].encode("utf-8")
@app.post("/webhooks/pulsar")
def pulsar_webhook():
signature = request.headers.get("X-Pulsar-Signature", "")
# Los bytes exactos que envió Pulsar, antes de parsear el JSON.
body = request.get_data()
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return "", 401
notification = request.get_json()
if notification["eventType"] == "Ping":
return "", 200
# Encola el trabajo y responde de inmediato; consulta "Responde con un 2XX, rápido".
enqueue(notification)
return "", 202Solicitudes repetidas
La firma cubre solo el cuerpo, así que cualquiera que obtenga una copia de una solicitud, por ejemplo de un log, podría enviártela de nuevo sin cambios y seguiría pasando la verificación. Protégete de la misma forma en que manejas los duplicados: guarda cada eventId que proceses y omite los que ya hayas visto.
No rechaces solicitudes solo porque eventTimestamp sea antiguo. Los reintentos y reenvíos vuelven a enviar a propósito el evento original, con su marca de tiempo original, posiblemente mucho después.