MailLandDocs
Webhooks

Verificación de Firma

Verifique las firmas de webhook para garantizar la autenticidad de las cargas útiles de webhook entrantes

Cada entrega de webhook incluye una firma criptográfica para que pueda verificar que la solicitud proviene de MailLand y no fue manipulada durante el tránsito. Verifique siempre las firmas en producción.

Cómo Funciona la Firma

MailLand firma cada carga útil de webhook usando HMAC-SHA256 con la clave secreta de su webhook. La firma se incluye en el encabezado X-MLND-Signature.

Formato de la firma:

X-MLND-Signature: t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67
ComponenteDescripción
tMarca de tiempo Unix (segundos) cuando se generó la firma
v1Firma hexadecimal HMAC-SHA256 (minúsculas)

Carga útil firmada:

{timestamp}.{raw_json_body}

La firma se calcula sobre la marca de tiempo concatenada con un punto y el cuerpo JSON sin procesar de la solicitud.

Ejemplo resuelto

Compruebe su implementación con estos valores exactos antes de pasar a producción. Todo lo de abajo es real: la firma es la que MailLand produce para este secreto, esta marca de tiempo y este cuerpo.

EntradaValor
Secretowhsec_docs_example_secret_do_not_use
Marca de tiempo (t)1708171200
Cuerpo sin procesar{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}

Cadena firmada: la marca de tiempo, un punto literal y luego el cuerpo tal cual:

1708171200.{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}

HMAC-SHA256 de esa cadena, con el secreto como clave, en hexadecimal minúscula:

b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67

Por lo tanto, el encabezado de esta entrega sería:

X-MLND-Signature: t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67

Reprodúzcala en una sola línea:

printf '%s' '1708171200.{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}' \
  | openssl dgst -sha256 -hmac 'whsec_docs_example_secret_do_not_use'

La marca de tiempo de este ejemplo está en el pasado, por lo que un verificador real la rechazaría por tolerancia. Pruebe primero el HMAC con la comprobación de tiempo desactivada y vuelva a activarla después.

Encabezados Enviados

Cada entrega de webhook incluye estos encabezados:

EncabezadoEjemploDescripción
X-MLND-Signaturet=1708171200,v1=abc123...Marca de tiempo y firma HMAC-SHA256
X-MLND-Event-Idevt_a1b2c3d4e5f6...ID de evento único para deduplicación
X-MLND-Eventmessage.deliveredTipo de evento
X-MLND-Event-Version1Versión del esquema de la carga útil
X-MLND-Webhook-Idwh_abc123def456ID público de su webhook
X-MLND-Attempt1Número de intento de entrega (basado en 1)
X-MLND-Delivery-Idp8r2kx7m9wq4rtId de este intento, nuevo en cada reintento; el publicId del historial de entregas. No sirve para deduplicar: vea Versionado
X-MLND-Delivery-Timestamp1708171200Hora de envío de este intento (segundos Unix); siempre igual a t en la firma

Otros dos encabezados son condicionales según el evento al que te suscribiste:

EncabezadoEjemploSe envía cuando
X-MLND-SourceinboundPresente solo en entregas de inbound.received
X-MLND-Inbound-Email-Idinb_a1b2c3d4e5f6Presente solo en entregas de inbound.received

X-MLND-Message-Id no se envía actualmente en las entregas de message.*. Use el campo data.message_id del payload del evento en su lugar.

Pasos de Verificación

Extraer la marca de tiempo y la firma

Analice el encabezado X-MLND-Signature. Divida por , para obtener los componentes t=... y v1=....

Preparar la carga útil firmada

Concatene la marca de tiempo, un punto (.) y el cuerpo sin procesar de la solicitud:

signed_payload = timestamp + "." + raw_body

Calcular la firma esperada

Genere un hash HMAC-SHA256 usando su secreto de webhook como clave y la carga útil firmada como mensaje. Conviértalo a hexadecimal en minúsculas.

Comparar las firmas

Utilice una comparación de tiempo constante para verificar si la firma calculada coincide con el valor v1 del encabezado.

Validar la marca de tiempo

Verifique que la marca de tiempo esté dentro de los 5 minutos del tiempo actual para prevenir ataques de repetición.

Ejemplos de Código

verify-signature.sh
#!/usr/bin/env bash
# Compute and verify an HMAC-SHA256 webhook signature using openssl

SIGNATURE_HEADER="t=1708171200,v1=b9f747ea899eafe911777060ca8f3c90828d37f1e16ee6e9ee98dacf8b316b67"
RAW_BODY='{"event":"message.delivered","event_id":"evt_a1b2c3d4e5f6"}'
SECRET="whsec_docs_example_secret_do_not_use"
TOLERANCE=300

# Parse timestamp and signature from header
TIMESTAMP=$(echo "$SIGNATURE_HEADER" | grep -oP '(?<=t=)[^,]+')
RECEIVED_SIG=$(echo "$SIGNATURE_HEADER" | grep -oP '(?<=v1=).+')

# Check timestamp tolerance
NOW=$(date +%s)
DIFF=$(( NOW - TIMESTAMP ))
if [ "${DIFF#-}" -gt "$TOLERANCE" ]; then
  echo "Error: Timestamp outside tolerance window" >&2
  exit 1
fi

# Compute expected signature
SIGNED_PAYLOAD="${TIMESTAMP}.${RAW_BODY}"
EXPECTED=$(printf '%s' "$SIGNED_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

# Compare signatures
if [ "$EXPECTED" = "$RECEIVED_SIG" ]; then
  echo "Signature valid"
else
  echo "Signature invalid" >&2
  exit 1
fi

Prevención de Ataques de Repetición

La marca de tiempo en el encabezado de la firma previene los ataques de repetición. Si un atacante captura una entrega de webhook válida y la reenvía posteriormente, la verificación de la marca de tiempo la rechazará.

La tolerancia predeterminada de MailLand es de 5 minutos (300 segundos). Puede ajustar esto en su código de verificación, pero manténgalo lo suficientemente corto para prevenir ataques de repetición significativos.

Además, cada evento tiene un event_id único (en el encabezado X-MLND-Event-Id). Almacene los IDs de eventos procesados y rechace los duplicados para una protección adicional.

MailLand deduplica el envío de su lado: un evento generado dos veces solo se despacha una vez por webhook dentro de una ventana de 24 horas. Eso no es lo mismo que "entregado como máximo una vez": una entrega fallida se reintenta, y cada reintento lleva el mismo event_id y el mismo cuerpo; solo cambian X-MLND-Attempt y la marca de tiempo de la firma. Deduplique por event_id y trate las entregas repetidas como algo esperado.

Toda entrega está firmada

No existe una ruta de entrega sin firmar. Si un webhook no tiene un secreto de firma utilizable, MailLand hace fallar la entrega y registra el fallo en lugar de enviar la carga útil sin un encabezado X-MLND-Signature: así que una firma faltante nunca es algo que su verificador deba tolerar. Rechace cualquier solicitud que no lleve una.

Regeneración de Secretos

Si sospecha que el secreto de su webhook ha sido comprometido:

  1. Llame a POST /v1/webhooks/{id}/regenerate-secret para obtener un nuevo secreto
  2. Actualice el código de verificación de su servidor con el nuevo secreto
  3. El secreto antiguo se invalida inmediatamente

No hay período de gracia al regenerar secretos. Actualice su código de verificación antes de la próxima entrega de webhook para evitar rechazar eventos válidos.

Errores Comunes

  • Analizar el cuerpo como JSON antes de verificar: la firma se calcula sobre los bytes sin procesar, no sobre un objeto JSON re-serializado. Verifique primero, luego analice.
  • Usar comparación de cadenas en lugar de comparación de tiempo constante: la comparación de cadenas ordinaria es vulnerable a ataques de temporización.
  • Ignorar la marca de tiempo: sin validación de la marca de tiempo, las cargas útiles capturadas pueden reproducirse indefinidamente.
  • Codificar el secreto de forma fija: utilice variables de entorno o un administrador de secretos.

Próximos Pasos

En esta página