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
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
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
- Ejecute su punto de conexión en un lugar accesible desde internet.
- Fije su dirección y su secreto en el webhook y guarde.
- Elija Send test. Phynxa firma el ejemplo exactamente igual que una entrega real.
- 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-V2con 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.
