Webhooks

Verificar firmas

Cada POST de webhook lleva un header Fiscal-Web-Signature. Verificar la firma antes de confiar en el payload es obligatorio en producción.

Cómo se construye la firma

El header tiene la forma t=<timestamp>,v1=<hex>. El timestamp es UNIX seconds. La firma v1 es HMAC-SHA256(signing_secret, `<timestamp>.<raw_body>`).

Cuando recibas un POST:

  1. Parsea el header y separa t y v1.
  2. Rechaza si t es más viejo que 5 minutos (mitiga replays).
  3. Recalcula la firma usando el raw body (¡antes de parsear el JSON!).
  4. Compara con timingSafeEqual. Igual = procesa.

Usa el body crudo

Si parseas el JSON y lo re-serializas antes de verificar, cualquier diferencia de formato (espacios, orden de keys) rompe la firma. Mantén el body como string hasta haber verificado.

Node.js

verify-signature.js
import crypto from "node:crypto";

function verifyFiscalWebSignature(payload, signatureHeader, secret) {
  // signatureHeader = "t=1684164724,v1=abcd1234..."
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((kv) => kv.split("=")),
  );
  const timestamp = parts.t;
  const v1 = parts.v1;

  // 1. Rechaza payloads con >5min de antigüedad (mitiga replay)
  const age = Math.floor(Date.now() / 1000) - Number(timestamp);
  if (age > 300) return false;

  // 2. Recalcula HMAC-SHA256 sobre "${timestamp}.${payload}"
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${payload}`)
    .digest("hex");

  // 3. Compara con timing-safe equal
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Python

verify_signature.py
import hmac, hashlib, time

def verify_fiscal_web_signature(payload: str, signature_header: str, secret: str) -> bool:
    parts = dict(kv.split("=") for kv in signature_header.split(","))
    timestamp = parts["t"]
    v1 = parts["v1"]

    age = int(time.time()) - int(timestamp)
    if age > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{payload}".encode(),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, v1)

PHP

Si usas Laravel/Symfony, busca primero el helper hash_hmac() con hash_equals() para comparación timing-safe. Si usas el SDK oficial fiscal-web/php, llama $fw->webhooks->verify($payload, $header) y delega ahí.

Rotación de secret

Si sospechas que tu whsec_* se filtró, rota desde /app/webhooks. Durante 24h aceptamos firmas con el viejo Y el nuevo secret; después, solo el nuevo. Tu backend debe aceptar ambos durante la ventana de transición.