# Verificar la firma

Comprueba que cada entrega viene de FITAL, con muestras que corren tal cual.

Antes de confiar en una entrega, comprueba su firma con el secreto de ese destino. Si no coincide o está vencida, responde `400` y no proceses nada.

## Qué se firma

La firma es HMAC-SHA256 hexadecimal, con el secreto del destino como clave, sobre el timestamp, un punto y el cuerpo crudo:

```text
hmac_sha256(secreto, "<X-Fital-Timestamp>.<cuerpo crudo>")
```

El secreto es el que viste al crear el destino o al rotarlo. Úsalo tal como lo recibiste, como texto.

## Paso a paso

1. Lee el cuerpo como bytes, antes de que tu framework lo interprete como JSON.
2. Lee `X-Fital-Timestamp`. Rechaza la entrega si trae algo que no sean dígitos, o si difiere de tu reloj por más de 5 minutos en cualquier sentido.
3. Calcula la firma esperada.
4. Separa `X-Fital-Signature` por comas y compara cada `v1=` en tiempo constante. Basta que una coincida.
5. Ignora las versiones que no conozcas, como `v2=`: una versión nueva llegará junto a `v1=`.

Cada intento lleva su propio timestamp y su propia firma. Durante una rotación llegan dos firmas `v1=`: ver [Rotar el secreto](/docs/webhooks/#rotar-el-secreto).

## Vector de prueba

Estas muestras corren tal cual y responden `aceptar`. Todos sus datos son ficticios y están publicados a propósito: el secreto no pertenece a ningún destino y no sirve en ningún ambiente. Sustituye los datos por los de tu entrega para usarlas.

```bash
# Vector público: todos los datos son ficticios.
CUERPO='{"id":"evt_5mPqW2xZr8TnC1vH7kLdS4bJ","type":"payment.completed","sequence":3}'
TS='1793800000'                   # X-Fital-Timestamp
FIRMAS='v1=e056165e0bbfa2c675a224a926a2cb2bf184f3cfbfc53a12fc9e7f0f6f2dddfe'
SECRETO_FICTICIO=a6906be5688f523ee1111208ff109c902394503a725418caf3a33c2f22a0a671
AHORA=1793800010             # en tu receptor: AHORA=$(date +%s)

ESPERADA=$(printf '%s' "$TS.$CUERPO" | openssl dgst -sha256 -hmac "$SECRETO_FICTICIO" -r | cut -d' ' -f1)
# Sólo dígitos: otra cosa no pasa por la ventana.
if printf '%s' "$TS" | grep -Eq '^[0-9]+$' && [ "$((AHORA > TS ? AHORA - TS : TS - AHORA))" -le 300 ]; then
  case ",$FIRMAS," in
    *",v1=$ESPERADA,"*) echo aceptar ;;
    *) echo rechazar ;;
  esac
else
  echo rechazar
fi
```

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

// Vector público: todos los datos son ficticios.
const cuerpoCrudo = Buffer.from('{"id":"evt_5mPqW2xZr8TnC1vH7kLdS4bJ","type":"payment.completed","sequence":3}');   // los bytes tal como llegan
const timestamp = '1793800000';                 // X-Fital-Timestamp
const firmas = 'v1=e056165e0bbfa2c675a224a926a2cb2bf184f3cfbfc53a12fc9e7f0f6f2dddfe';   // X-Fital-Signature
const secretoFicticio = 'a6906be5688f523ee1111208ff109c902394503a725418caf3a33c2f22a0a671';
const ahora = 1793800010;                    // en tu receptor: Date.now() / 1000

function verificar(crudo, ts, cabecera, secreto, segundos) {
  if (!/^[0-9]+$/.test(ts)) return false;              // sólo dígitos: otra cosa no pasa por la ventana
  if (Math.abs(segundos - Number(ts)) > 300) return false;
  const esperada = createHmac('sha256', secreto).update(`${ts}.`).update(crudo).digest();
  return cabecera.split(',').some((f) => {
    const m = /^v1=([0-9a-f]{64})$/.exec(f.trim());      // otras versiones se ignoran
    return m !== null && timingSafeEqual(Buffer.from(m[1], 'hex'), esperada);
  });
}

console.log(verificar(cuerpoCrudo, timestamp, firmas, secretoFicticio, ahora) ? 'aceptar' : 'rechazar');
```

```python
import hashlib, hmac, re, time

# Vector público: todos los datos son ficticios.
cuerpo_crudo = b'{"id":"evt_5mPqW2xZr8TnC1vH7kLdS4bJ","type":"payment.completed","sequence":3}'   # los bytes tal como llegan
timestamp = '1793800000'                  # X-Fital-Timestamp
firmas = "v1=e056165e0bbfa2c675a224a926a2cb2bf184f3cfbfc53a12fc9e7f0f6f2dddfe"
secreto_ficticio = b"a6906be5688f523ee1111208ff109c902394503a725418caf3a33c2f22a0a671"
ahora = 1793800010                     # en tu receptor: time.time()

def verificar(crudo: bytes, ts: str, cabecera: str, secreto: bytes, segundos: float) -> bool:
    if not re.fullmatch(r"[0-9]+", ts or ""):   # sólo dígitos: otra cosa no pasa por la ventana
        return False
    if abs(segundos - int(ts)) > 300:
        return False
    esperada = hmac.new(secreto, ts.encode() + b"." + crudo, hashlib.sha256).hexdigest()
    for f in cabecera.split(","):
        m = re.fullmatch(r"v1=([0-9a-f]{64})", f.strip())   # otras versiones se ignoran
        if m and hmac.compare_digest(esperada, m.group(1)):
            return True
    return False

print("aceptar" if verificar(cuerpo_crudo, timestamp, firmas, secreto_ficticio, ahora) else "rechazar")
```

```php
<?php
// Vector público: todos los datos son ficticios.
$cuerpoCrudo = '{"id":"evt_5mPqW2xZr8TnC1vH7kLdS4bJ","type":"payment.completed","sequence":3}';   // los bytes tal como llegan
$timestamp = '1793800000';                  // X-Fital-Timestamp
$firmas = 'v1=e056165e0bbfa2c675a224a926a2cb2bf184f3cfbfc53a12fc9e7f0f6f2dddfe';
$secretoFicticio = 'a6906be5688f523ee1111208ff109c902394503a725418caf3a33c2f22a0a671';
$ahora = 1793800010;                     // en tu receptor: time()

function verificar(string $crudo, string $ts, string $cabecera, string $secreto, int $segundos): bool {
  if (!preg_match('/^[0-9]+$/D', $ts)) return false;   // sólo dígitos: (int) recortaría el resto
  if (abs($segundos - (int) $ts) > 300) return false;
  $esperada = hash_hmac('sha256', $ts . '.' . $crudo, $secreto);
  foreach (explode(',', $cabecera) as $f) {
    // otras versiones se ignoran
    if (preg_match('/^v1=([0-9a-f]{64})$/D', trim($f), $m) && hash_equals($esperada, $m[1])) return true;
  }
  return false;
}

echo verificar($cuerpoCrudo, $timestamp, $firmas, $secretoFicticio, $ahora) ? "aceptar\n" : "rechazar\n";
```

En la variante con `openssl`, la comparación no es en tiempo constante: úsala para depurar, no en tu receptor.

## Comprueba los rechazos

Tu receptor debe rechazar estos casos. Cambia un dato del vector y confirma que responde `rechazar`:

| Caso | Qué cambiar |
|---|---|
| Firma de otro secreto | Una letra del secreto |
| Cuerpo alterado | Un espacio al final del cuerpo |
| Timestamp vencido | Adelanta el reloj más de 300 segundos |
| Timestamp no numérico | Agrega una letra al timestamp |

`fital-webhook-test.sh` envía esos casos a tu receptor en tu máquina, además de un timestamp en el futuro, y termina con un código distinto de `0` si alguno no se rechaza: ver el [inicio rápido](/docs/webhooks/inicio-rapido/).

## Errores comunes

- Firmar el JSON interpretado o vuelto a serializar. Cambia los bytes.
- Convertir el secreto a bytes desde hexadecimal. Va como texto, tal como lo recibiste.
- Comparar con `==`. Usa una comparación en tiempo constante.
- Convertir el timestamp a número sin validarlo. `Number("abc")` da `NaN` y se salta la ventana, y `(int)` recorta lo que sigue a los dígitos.
- Mirar la ventana en un solo sentido. Un timestamp en el futuro también se rechaza.
- Tomar sólo la primera firma. Durante una rotación pueden llegar dos.

## Qué sigue

[Catálogo de eventos](/docs/webhooks/eventos/).
