Ir al contenido

Verificación de Firma

SIEMPRE verifica la firma antes de procesar un webhook. Esto garantiza que:

  1. El webhook realmente viene de Aloha Pay
  2. El contenido no fue modificado en tránsito
  3. No es un replay attack
1. Obtén el timestamp del header X-Webhook-Timestamp
2. Obtén la firma del header X-Webhook-Signature
3. Verifica que el timestamp no sea mayor a 5 minutos
4. Construye el signed_payload: "{timestamp}.{body}"
5. Calcula HMAC-SHA256 del signed_payload usando tu secret
6. Compara con la firma recibida (comparación segura)
function verifyWebhookSignature(
string $payload,
string $signature,
string $timestamp,
string $secret
): bool {
// Protección contra replay attacks
if (abs(time() - (int)$timestamp) > 300) {
return false;
}
// Calcular firma esperada
$signedPayload = "{$timestamp}.{$payload}";
$expectedSignature = 'sha256=' . hash_hmac('sha256', $signedPayload, $secret);
// Comparación segura (evita timing attacks)
return hash_equals($expectedSignature, $signature);
}
// Uso en tu controlador
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$secret = 'whsec_tu_secret_aqui';
if (!verifyWebhookSignature($payload, $signature, $timestamp, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
// Procesar el webhook...

Es crucial usar el body sin parsear para la verificación. Si tu framework parsea automáticamente el JSON, la firma no coincidirá.

// ❌ INCORRECTO - body ya parseado
app.post('/webhooks', express.json(), (req, res) => {
const payload = JSON.stringify(req.body); // No funcionará
});
// ✅ CORRECTO - body raw
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const payload = req.body.toString(); // Funcionará
});

Siempre usa funciones de comparación en tiempo constante para evitar timing attacks:

LenguajeFunción
PHPhash_equals()
Node.jscrypto.timingSafeEqual()
Pythonhmac.compare_digest()
Gohmac.Equal()
RubyActiveSupport::SecurityUtils.secure_compare()

La verificación del timestamp evita que un atacante reenvíe webhooks antiguos:

// Rechazar webhooks con más de 5 minutos de antigüedad
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
return false; // Webhook muy antiguo o del futuro
}
  1. Verifica que uses el body raw, no el JSON parseado
  2. Verifica el orden: {timestamp}.{body} (con punto entre ellos)
  3. Verifica el secret: debe ser el que recibiste al crear el webhook
  4. Verifica el prefijo: la firma esperada debe tener sha256= al inicio

Si tu servidor tiene el reloj desincronizado, puede rechazar webhooks válidos. Asegúrate de que tu servidor esté sincronizado con NTP.

Si crees que tu secret fue comprometido, puedes rotarlo:

Ventana de terminal
curl -X POST https://api.alohapay.co/api/external/v1/webhooks/{id}/rotate-secret \
-H "X-API-Key: tu_api_key"