Skip to content

Firma de webhooks

Verificar HMAC-SHA256 sobre timestamp y body crudo antes de procesar un evento.

Updated Ver como Markdown

Firma de webhooks

Cada delivery incluye una firma HMAC-SHA256. Verificarla evita aceptar requests falsificados o alterados.

OrderFast-Event-Id: evt_019f7067f8b47b4e
OrderFast-Timestamp: 1784294108
OrderFast-Signature: v1=0c240b7f...

Contenido firmado

El mensaje se construye sin espacios adicionales:

{timestamp}.{raw_body}

raw_body son exactamente los bytes recibidos. Serializar otra vez el JSON puede cambiar espacios, escapes u orden de claves y producir una firma distinta.

Algoritmo

  1. Leer OrderFast-Timestamp y OrderFast-Signature.
  2. Rechazar headers faltantes o mal formados.
  3. Convertir el timestamp Unix y comprobar que está dentro de la tolerancia acordada.
  4. Concatenar el timestamp, un punto y el body crudo.
  5. Calcular HMAC-SHA256 con el secreto del endpoint.
  6. Codificar el digest en hexadecimal y anteponer v1=.
  7. Comparar los bytes en tiempo constante.
  8. Solo entonces parsear y procesar el JSON.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook({ rawBody, timestamp, signature, secret }) {
  const signed = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), rawBody]);
  const expected = `v1=${createHmac("sha256", secret)
    .update(signed)
    .digest("hex")}`;

  const receivedBytes = Buffer.from(signature, "utf8");
  const expectedBytes = Buffer.from(expected, "utf8");
  return (
    receivedBytes.length === expectedBytes.length &&
    timingSafeEqual(receivedBytes, expectedBytes)
  );
}

Configurá el framework para conservar rawBody como Buffer. No pases primero por JSON.parse().

Python

import hashlib
import hmac

def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    signed = timestamp.encode("utf-8") + b"." + raw_body
    digest = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    expected = f"v1={digest}"
    return hmac.compare_digest(expected, signature)

Tolerancia temporal

La comprobación del timestamp limita ataques de replay. Usá la tolerancia publicada por el ambiente; si todavía no está definida en el contrato, acordala antes de producción y mantené los relojes sincronizados. No aceptes timestamps arbitrariamente antiguos solo porque la firma es válida.

Rotación de secretos

Durante una rotación pueden coexistir temporalmente dos secretos:

  1. verificar primero con el secreto nuevo;
  2. si falla, verificar con el anterior mientras dure la ventana;
  3. registrar qué versión validó, sin registrar el secreto ni la firma completa;
  4. retirar el secreto anterior al cerrar la rotación.

Fallas comunes

Problema Resultado
Parsear y serializar JSON antes de verificar Cambian los bytes y falla la firma.
Proxy que transforma el body La aplicación ya no recibe el contenido firmado.
Comparación con == Puede filtrar información temporal.
Reloj desincronizado Se rechazan deliveries válidas o se amplía la ventana de replay.
Secreto de test usado en live Todas las firmas fallan.

Respondé 401 ante una firma inválida y no modifiques ningún recurso de negocio. Nunca registres el secreto ni el header completo de firma.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close