Ir al contenido principal

Verificar la firma de webhooks

Cómo comprobar que las notificaciones de webhooks provienen realmente de FacturaDirecta usando la firma HMAC SHA256. Incluye ejemplos en Node.js, Python y PHP.

Actualizado esta semana

⚠️ AVISO: Los webhooks están disponibles en los planes Avanzado y Total.

Cuando FacturaDirecta envía un evento a tu endpoint, incluye una firma digital en las cabeceras HTTP. Verificar esta firma te permite confirmar que la notificación proviene realmente de FacturaDirecta y que no ha sido manipulada por un tercero.

💡 Recomendación: Verifica siempre la firma antes de procesar un evento. Si la firma no coincide, descarta la petición.

Cómo funciona la firma

Cada petición webhook incluye dos cabeceras HTTP:

Cabecera

Formato

Descripción

Webhook-Signature

t=<timestamp>,v1=<hmac_hex>

Timestamp del envío y firma HMAC SHA256

Webhook-Timestamp

<unix_timestamp>

Timestamp del envío (en segundos)

La firma se calcula sobre una combinación del timestamp y el body de la petición, usando tu signing secret como clave.

Verificación paso a paso

Para verificar la firma de un evento:

  1. Extrae el timestamp y la firma de la cabecera Webhook-Signature. El formato es t=<timestamp>,v1=<firma>

  2. Construye el string a firmar concatenando el timestamp, un punto (.) y el body completo de la petición: <timestamp>.<body>

  3. Calcula el HMAC SHA256 del string anterior usando tu signing secret como clave

  4. Compara la firma calculada con la firma recibida usando una comparación de tiempo constante (timing-safe)

  5. Verifica la ventana de tiempo: comprueba que el timestamp no tiene más de 5 minutos de antigüedad para protegerte contra ataques de repetición

Ejemplos de código

Node.js

const crypto = require('crypto');function verifyWebhookSignature(body, signatureHeader, secret) {
  // 1. Extraer timestamp y firma
  const parts = {};
  signatureHeader.split(',').forEach(part => {
    const [key, value] = part.split('=');
    parts[key] = value;
  });
  const timestamp = parts['t'];
  const signature = parts['v1'];  // 2. Construir el string a firmar
  const signedPayload = `${timestamp}.${body}`;  // 3. Calcular HMAC SHA256
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');  // 4. Comparar con timing-safe
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );  // 5. Verificar ventana de tiempo (5 minutos)
  const now = Math.floor(Date.now() / 1000);
  const isFresh = Math.abs(now - parseInt(timestamp)) < 300;  return isValid && isFresh;
}

Python

import hmac
import hashlib
import timedef verify_webhook_signature(body, signature_header, secret):
    # 1. Extraer timestamp y firma
    parts = dict(p.split('=', 1) for p in signature_header.split(','))
    timestamp = parts['t']
    signature = parts['v1']    # 2. Construir el string a firmar
    signed_payload = f"{timestamp}.{body}"    # 3. Calcular HMAC SHA256
    expected = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()    # 4. Comparar con timing-safe
    is_valid = hmac.compare_digest(signature, expected)    # 5. Verificar ventana de tiempo (5 minutos)
    is_fresh = abs(time.time() - int(timestamp)) < 300    return is_valid and is_fresh

PHP

function verifyWebhookSignature($body, $signatureHeader, $secret) {
    // 1. Extraer timestamp y firma
    $parts = [];
    foreach (explode(',', $signatureHeader) as $part) {
        list($key, $value) = explode('=', $part, 2);
        $parts[$key] = $value;
    }
    $timestamp = $parts['t'];
    $signature = $parts['v1'];    // 2. Construir el string a firmar
    $signedPayload = "{$timestamp}.{$body}";    // 3. Calcular HMAC SHA256
    $expected = hash_hmac('sha256', $signedPayload, $secret);    // 4. Comparar con timing-safe
    $isValid = hash_equals($signature, $expected);    // 5. Verificar ventana de tiempo (5 minutos)
    $isFresh = abs(time() - intval($timestamp)) < 300;    return $isValid && $isFresh;
}

Protección anti-replay

La verificación del timestamp (paso 5) protege contra ataques de repetición (replay attacks). Si alguien intercepta un evento válido e intenta reenviarlo más tarde, la ventana de 5 minutos hace que la firma se considere expirada.

Comprueba siempre que el timestamp del evento no difiere en más de 300 segundos (5 minutos) respecto a la hora actual de tu servidor.

El signing secret

El signing secret es la clave que se usa para calcular y verificar las firmas. Tiene el formato whsec_<valor_hexadecimal>.

El signing secret solo se muestra en dos momentos:

  • Al crear un nuevo endpoint

  • Al rotar el secret de un endpoint existente

En ambos casos, guárdalo en un lugar seguro (por ejemplo, en las variables de entorno de tu servidor). No podrás volver a consultarlo después.

Si pierdes el signing secret, puedes rotar el secret desde el detalle del endpoint en FacturaDirecta. Al hacerlo, se genera uno nuevo y el anterior deja de funcionar inmediatamente.

Buenas prácticas

  • Verifica siempre la firma: nunca proceses un evento sin antes comprobar que la firma es válida

  • Usa comparación timing-safe: evita comparar strings directamente (===). Usa las funciones de comparación de tiempo constante de tu lenguaje (timingSafeEqual, compare_digest, hash_equals)

  • Responde rápido con HTTP 200: tu servidor debe responder con un código 2xx lo antes posible. Si necesitas hacer procesamiento largo, acepta el evento primero y procésalo de forma asíncrona

  • Responde HTTP 410 para desuscribirte: si ya no quieres recibir eventos en un endpoint, responde con 410 Gone y FacturaDirecta lo desactivará automáticamente

¿Ha quedado contestada tu pregunta?