Mosce ERP · Centro de ayuda
Webhooks

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:

EncabezadoDescripción
X-Helix-SignatureFirma HMAC-SHA256 del cuerpo de la solicitud. Formato: sha256=<hex>
X-Helix-Event-IdIdentificador único de la entrega. Úsalo para deduplicar entregas en reintentos.
X-Helix-Event-TypeTipo de evento (ej. invoice.created).
X-Helix-TimestampTimestamp 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}, 200

Verificació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"
fi

Idempotencia 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:

  1. Ve a Configuración → Integraciones → Webhooks y abre el webhook.
  2. Haz clic en Rotar secreto.
  3. Mosce ERP genera un nuevo secreto y lo muestra una sola vez. Cópialo de inmediato.
  4. Actualiza el secreto en tu servidor receptor con el nuevo valor.
  5. 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íntomaCausa probableSolución
La verificación siempre falla aunque el secreto es correctoEl framework parseó el JSON antes de que puedas leer el raw bodyLee el body crudo (raw bytes) antes de cualquier middleware que parsee JSON
Crypto.timingSafeEqual lanza error de longitudEl encabezado llega vacío o con un formato inesperadoVerifica que X-Helix-Signature esté presente antes de comparar
El mismo evento llega dos veces y se procesa dos vecesNo hay deduplicación por X-Helix-Event-IdImplementa 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