Saltar al contenido principal

Verifique las firmas de los webhooks

Demuestre que un webhook procede de Phynxa y no es una repetición, con código funcional en Node.js y Python.

Qué conseguirá

Su punto de conexión aceptará solo las solicitudes que Phynxa envió realmente y rechazará las repeticiones.

Requisitos previos

  • Un Signing secret fijado en el webhook (consulte Webhooks). Sin él, las solicitudes no se firman.
  • Acceso al cuerpo sin procesar de la solicitud, exactamente como se recibió, antes de analizar el JSON.

Cómo funciona la firma

Phynxa envía tres cabeceras cuando hay un secreto fijado:

  • X-Voice-Agent-Timestamp: la hora Unix (en segundos) en que se envió.
  • X-Voice-Agent-Signature-V2: el HMAC-SHA256 en hexadecimal y en minúsculas de la cadena <timestamp>.<raw body>, con su secreto como clave. Use esta.
  • X-Voice-Agent-Signature: el HMAC-SHA256 en hexadecimal del cuerpo sin procesar solo (forma antigua, sin protección frente a repeticiones).

Para verificar: reconstruya la firma a partir de su secreto y compárela con la cabecera en tiempo constante, y rechace después la solicitud si la marca de tiempo es demasiado antigua.

Advertencia. Firme los bytes sin procesar que recibió. Si analiza el JSON y lo vuelve a escribir, el espaciado y el orden de las claves pueden cambiar y la firma no coincidirá.

Python

python
import hashlib
import hmac
import time

SECRET = "whsec_your_secret"   # the Signing secret you set on the webhook
MAX_AGE_SECONDS = 300          # reject deliveries older than five minutes

def is_from_phynxa(raw_body: bytes, headers) -> bool:
    timestamp = headers.get("X-Voice-Agent-Timestamp", "")
    received = headers.get("X-Voice-Agent-Signature-V2", "")
    if not timestamp.isdigit() or not received:
        return False
    if abs(time.time() - int(timestamp)) > MAX_AGE_SECONDS:
        return False  # too old, or the clock is wrong: possible replay
    expected = hmac.new(
        SECRET.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(received, expected)

Con Flask, pase request.get_data() como raw_body; con FastAPI, await request.body().

Node.js

javascript
import crypto from "node:crypto";
import express from "express";

const SECRET = "whsec_your_secret";
const MAX_AGE_SECONDS = 300;
const app = express();

// Keep the raw bytes: the signature covers them exactly.
app.post("/hooks/calls", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Voice-Agent-Timestamp") ?? "";
  const received = req.get("X-Voice-Agent-Signature-V2") ?? "";
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.`)
    .update(req.body) // a Buffer, because of express.raw
    .digest("hex");

  const ok =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected)) &&
    age <= MAX_AGE_SECONDS;

  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  console.log(event.event, event.call_id);
  res.sendStatus(200);
});

app.listen(3000);

Prueba

  1. Ejecute su punto de conexión en un lugar accesible desde internet.
  2. Fije su dirección y su secreto en el webhook y guarde.
  3. Elija Send test. Phynxa firma el ejemplo exactamente igual que una entrega real.
  4. Cambie un carácter del secreto en su código y envíe de nuevo: la verificación debería fallar ahora.

Resultado esperado

El secreto correcto acepta la entrega de prueba; un secreto erróneo o una solicitud retrasada se rechazan con 401.

Solución de problemas

  • La firma nunca coincide. Está firmando un cuerpo vuelto a serializar, usando la cabecera equivocada (use X-Voice-Agent-Signature-V2 con la marca de tiempo), o el secreto es distinto. Vuelva a escribirlo en el webhook; el campo muestra Configured y no puede leerse de vuelta.
  • Solicitudes válidas rechazadas por demasiado antiguas. Revise el reloj de su servidor.
  • Un proxy modifica el cuerpo. Asegúrese de que nada entre Phynxa y su código lo reescribe.

Buenas prácticas de seguridad

  • Mantenga el secreto fuera del control de versiones y rótelo introduciendo uno nuevo en el webhook.
  • Compare siempre en tiempo constante.
  • Rechace las marcas de tiempo ausentes o no numéricas.
  • Responda rápido con 2xx y haga el trabajo pesado después, para que Phynxa no agote el tiempo de espera.

Guías relacionadas

¿Algo de esto no coincide con lo que ve en el panel? Avísenos.