Seguridad y verificación de firma
Cómo verificar que los webhooks de Mosce ERP son auténticos usando la firma HMAC-SHA256, y cómo rotar el secreto de firma de forma segura.
Cada entrega de webhook de Mosce ERP incluye una firma HMAC-SHA256 en el encabezado
X-Helix-Signature. Verificar esta firma en tu servidor garantiza que el evento proviene de Mosce ERP y no de una fuente externa que intenta suplantar un evento. Este artículo muestra cómo implementar la verificación en los lenguajes más comunes y cómo rotar el secreto sin interrupción de servicio.
Tiempo de lectura: ~8 min
Cuándo usar esto
- Estás implementando el servidor receptor de webhooks y quieres validar la autenticidad de las entregas.
- Necesitas rotar el secreto de firma sin interrumpir las entregas.
- Un evento llegó a tu servidor y quieres verificar si es legítimo antes de procesarlo.
Antes de empezar
- Ya tienes un webhook configurado y guardaste el secreto de firma al crearlo. Si lo perdiste, rótalo siguiendo los pasos al final de este artículo.
- Tu endpoint acepta solicitudes POST con cuerpo JSON y procesa el encabezado
X-Helix-Signature. - Ver Configurar un webhook si todavía no tienes un endpoint registrado.
Cómo funciona la firma
Cuando Mosce ERP entrega un webhook, incluye dos encabezados HTTP relevantes para seguridad:
| Encabezado | Descripción |
|---|---|
X-Helix-Signature | Firma HMAC-SHA256 del cuerpo de la solicitud. Formato: sha256=<hex> |
X-Helix-Event-Id | Identificador único de la entrega. Úsalo para deduplicar entregas en reintentos. |
X-Helix-Event-Type | Tipo de evento (ej. invoice.created). |
X-Helix-Timestamp | Timestamp Unix (segundos) del momento del envío. |
La firma se calcula así:
HMAC-SHA256( secreto_de_firma, cuerpo_raw_de_la_solicitud )El secreto_de_firma es el que Mosce ERP mostró al crear el webhook. El cuerpo_raw es el body JSON sin modificar - cualquier proceso que reformatee o reparse el JSON antes de verificar la firma producirá una firma incorrecta.
Siempre verifica la firma sobre el body crudo (raw bytes), no sobre el JSON parseado.
Implementación de la verificación
Node.js
import crypto from 'crypto';
function verifyHelixSignature(rawBody, signatureHeader, secret) {
// signatureHeader tiene el formato "sha256=<hex>"
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody) // Buffer o string con encoding 'utf8'
.digest('hex');
// Comparación en tiempo constante para evitar timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected, 'utf8'),
Buffer.from(signatureHeader, 'utf8')
);
}
// Ejemplo con Express - IMPORTANTE: usa express.raw() antes del router
app.post('/webhooks/helix', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-helix-signature'];
const secret = process.env.HELIX_WEBHOOK_SECRET;
if (!verifyHelixSignature(req.body, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
const eventId = req.headers['x-helix-event-id'];
// Procesar el evento...
console.log(`Event ${event.type} (${eventId}) verified`);
res.status(200).json({ received: true });
});Python
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
def verify_helix_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
"""Verifica la firma HMAC-SHA256 de un webhook de Mosce ERP."""
expected = 'sha256=' + hmac.new(
key=secret.encode('utf-8'),
msg=raw_body,
digestmod=hashlib.sha256
).hexdigest()
# Comparación en tiempo constante
return hmac.compare_digest(expected, signature_header)
@app.route('/webhooks/helix', methods=['POST'])
def handle_webhook():
raw_body = request.get_data() # Body crudo sin parsear
signature = request.headers.get('X-Helix-Signature', '')
secret = os.environ['HELIX_WEBHOOK_SECRET']
if not verify_helix_signature(raw_body, signature, secret):
abort(401)
event = request.json
event_id = request.headers.get('X-Helix-Event-Id')
# Procesar el evento...
print(f"Event {event['type']} ({event_id}) verified")
return {'received': True}, 200Verificación con curl (para debugging)
# Calcular la firma esperada (Linux/macOS)
BODY='{"type":"invoice.created","data":{"id":"inv_abc123"}}'
SECRET="tu_secreto_aqui"
EXPECTED_SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print "sha256=" $2}')
echo "Firma esperada: $EXPECTED_SIG"
# Comparar con el encabezado recibido
RECEIVED_SIG="sha256=abc123..." # el valor de X-Helix-Signature
if [ "$EXPECTED_SIG" = "$RECEIVED_SIG" ]; then
echo "Firma VÁLIDA"
else
echo "Firma INVÁLIDA"
fiIdempotencia con X-Helix-Event-Id
El encabezado X-Helix-Event-Id contiene un identificador único por cada entrega. En un reintento, Mosce ERP entrega el mismo evento con el mismo X-Helix-Event-Id. Usa este identificador para evitar procesar el mismo evento dos veces:
// Ejemplo de tabla de idempotencia en memoria (usa Redis o DB en producción)
const processedEvents = new Set();
function processEvent(eventId, event) {
if (processedEvents.has(eventId)) {
console.log(`Duplicate event ${eventId} - skipping`);
return;
}
processedEvents.add(eventId);
// Procesar el evento...
}Rotar el secreto de firma
Si el secreto fue comprometido o necesitas rotarlo por política de seguridad:
- Ve a Configuración → Integraciones → Webhooks y abre el webhook.
- Haz clic en Rotar secreto.
- Mosce ERP genera un nuevo secreto y lo muestra una sola vez. Cópialo de inmediato.
- Actualiza el secreto en tu servidor receptor con el nuevo valor.
- El secreto anterior queda invalidado de inmediato - cualquier entrega firmada con el secreto viejo fallará la verificación.
Rota el secreto en tu servidor antes de invalidar el anterior, o habrá un breve período en que las entregas fallarán la verificación. La estrategia más segura es actualizar primero tu servidor para aceptar ambos secretos temporalmente, luego rotar en Mosce ERP, luego remover el secreto viejo de tu servidor.
Requisitos de seguridad del endpoint
- HTTPS obligatorio. Mosce ERP rechaza URLs con
http://- la comunicación siempre se cifra en tránsito. - Sin IPs privadas. Para proteger contra ataques SSRF (Server-Side Request Forgery), Mosce ERP rechaza URLs que resuelvan a rangos de IP privados (
10.x.x.x,192.168.x.x,172.16-31.x.x,127.x.x.x). Esto aplica también a URLs que parecen públicas pero resuelven internamente a una IP privada. - Responde con HTTP 200 o 2xx para confirmar recepción. Cualquier código 4xx o 5xx se trata como fallo y activa el reintento.
Errores comunes
| Síntoma | Causa probable | Solución |
|---|---|---|
| La verificación siempre falla aunque el secreto es correcto | El framework parseó el JSON antes de que puedas leer el raw body | Lee el body crudo (raw bytes) antes de cualquier middleware que parsee JSON |
| Crypto.timingSafeEqual lanza error de longitud | El encabezado llega vacío o con un formato inesperado | Verifica que X-Helix-Signature esté presente antes de comparar |
| El mismo evento llega dos veces y se procesa dos veces | No hay deduplicación por X-Helix-Event-Id | Implementa una tabla de idempotencia usando el X-Helix-Event-Id |
Preguntas frecuentes
¿Puedo verificar la firma sin leer el body crudo?
No. La firma se calcula sobre el body crudo (bytes exactos), no sobre el JSON parseado. Si reformateas el JSON (por ejemplo, reordenar campos), la firma no coincidirá.
¿Qué pasa si mi servidor no puede verificar la firma?
Rechaza la solicitud con HTTP 401 o 403 y no proceses el evento. Los eventos de fuente desconocida no deben ejecutar lógica de negocio.
¿Hay un plazo de expiración en la firma?
La firma en sí no tiene expiración de tiempo. Para protección adicional contra ataques de replay, puedes verificar que el X-Helix-Timestamp esté dentro de una ventana aceptable (ej. ±5 minutos de la hora actual).
Relacionados
Última actualización: 2026-05-09
Configurar un webhook
Cómo registrar un endpoint de webhook en Mosce ERP: URL HTTPS, selección de eventos, secreto de firma y verificación de conectividad.
Confiabilidad y reintentos
Cómo Mosce ERP reintenta la entrega de webhooks fallidos, cuándo desactiva un endpoint automáticamente y cómo rehabilitarlo.