Como verificar firmas HMAC-SHA256 en tu backend
Tutorial tecnico para verificar firmas HMAC-SHA256 en webhooks de pagos. Ejemplos en PHP, Node.js y Python.
Tutorial tecnico para verificar firmas HMAC-SHA256 en webhooks de pagos. Ejemplos en PHP, Node.js y Python.
En el dinámico mundo del e-commerce y los pagos digitales en Perú, verificar HMAC-SHA256 webhooks es una práctica de seguridad indispensable para proteger tu negocio de transacciones fraudulentas. Este tutorial técnico te guiará paso a paso para implementar la verificación de firmas HMAC-SHA256 en tu backend, asegurando que solo los webhooks legítimos de tu pasarela de pagos procesen cambios en tu sistema. Con ejemplos en PHP, Node.js y Python, aprenderás a fortalecer la integración de pagos de tu comercio electrónico.
En el ecosistema fintech peruano, donde operan soluciones como Yape, Plin, y las pasarelas de pagos de bancos como BCP, BBVA, Interbank y Scotiabank, la seguridad de las transacciones no termina cuando el cliente paga. Los webhooks son notificaciones HTTP enviadas desde un servidor (como el de tu proveedor de pagos) al tuyo, informando eventos clave: un pago exitoso, uno rechazado o un reembolso. Si tu backend no verifica la autenticidad de estos mensajes, estás abriendo la puerta a ataques donde un tercero malintencionado podría simular notificaciones falsas, alterar el estado de órdenes o incluso registrar pagos que nunca existieron.
La Superintendencia de Banca, Seguros y AFP (SBS) y el Banco Central de Reserva del Perú (BCRP) enfatizan la gestión robusta de riesgos operacionales y cibernéticos. En un contexto donde la Ley contra el Lavado de Activos y el Financiamiento del Terrorismo (PLAFT) aplica también a comercios digitales, no verificar la procedencia de los datos financieros es un riesgo inadmisible. Implementar la verificación HMAC-SHA256 no es solo una buena práctica de desarrollo; es un componente crítico de tu compliance y defensa frente a fraudes. Un número creciente de comercios en Perú ha sufrido pérdidas por no validar estos callbacks, especialmente con la masificación de pagos QR y digitales.
Antes de sumergirnos en el código, es vital entender la tecnología que usarás. HMAC (Keyed-Hash Message Authentication Code) es un mecanismo criptográfico que, combinado con una función hash como SHA-256, permite verificar tanto la integridad como la autenticidad de un mensaje.
Imagínalo como un sello de seguridad único para cada mensaje. El proceso funciona así:
X-Signature, X-Webhook-Signature o similar). El cuerpo de la solicitud contiene el payload en JSON.La fortaleza de SHA-256, un estándar reconocido mundialmente, garantiza que cualquier mínimo cambio en el payload o en el secreto resulte en una firma completamente distinta, haciendo inviable la falsificación.
Para implementar la verificación correctamente, debes identificar y manejar varios componentes. Aquí te los desgloso:
.env) y nunca hardcodearla en tu aplicación.raw request body es clave.X-Taypi-Signature).La siguiente tabla resume estos componentes y su origen:
| Componente | Origen | Dónde se encuentra/usa | Ejemplo |
|---|---|---|---|
| Secreto | Proveedor (ej. TAYPI, otro PSP) | Variable de entorno en tu server | sk_live_abc123...xyz |
| Payload | Cuerpo de la petición HTTP POST | rawBody de la request | {"id":"evt_123","type":"payment.succeeded"} |
| Firma Recibida | Header de la petición HTTP POST | Encabezado personalizado (p.ej. X-Signature) | a7f4d8c3b1e9... |
| Algoritmo | Estándar acordado | Lógica de tu código | HMAC-SHA256 |
| Firma Calculada | Tu backend | Resultado de aplicar HMAC al payload con el secreto | a7f4d8c3b1e9... |
Ahora sí, pasemos a la parte práctica. A continuación, te muestro cómo implementar la verificación en tres lenguajes backend muy populares. El flujo general es siempre el mismo:
En PHP, es crucial acceder al php://input stream para obtener el cuerpo en bruto sin interpretar.
<?php
// 1. Obtener el secreto desde las variables de entorno
$secret = getenv('WEBHOOK_SECRET');
// 2. Obtener la firma enviada por el proveedor (nombre del header puede variar)
$receivedSignature = $_SERVER['HTTP_X_SIGNATURE'] ?? ''; // Ejemplo para header 'X-Signature'
// 3. Obtener el payload en bruto (RAW)
$rawPayload = file_get_contents('php://input');
// 4. Calcular la firma esperada usando HMAC-SHA256
$calculatedSignature = hash_hmac('sha256', $rawPayload, $secret);
// 5. Comparar las firmas de forma segura (evita ataques de timing)
if (hash_equals($calculatedSignature, $receivedSignature)) {
// ¡Firma válida! Procesar el webhook de forma segura.
$payload = json_decode($rawPayload, true);
http_response_code(200);
echo "Webhook verificado y procesado.";
// Aquí tu lógica: actualizar orden, base de datos, etc.
} else {
// Firma inválida. Rechazar la petición.
http_response_code(401);
echo "Firma no válida. Webhook rechazado.";
// Debes registrar este intento fallido para auditoría.
}
?>
En Node.js con Express, necesitarás un middleware para capturar el raw body. El paquete body-parser permite esto.
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');
const app = express();
// Middleware para obtener el raw body como Buffer
const rawBodyBuffer = (req, res, buf, encoding) => {
if (buf && buf.length) {
req.rawBody = buf.toString(encoding || 'utf8');
}
};
app.use(bodyParser.json({ verify: rawBodyBuffer }));
app.use(bodyParser.urlencoded({ verify: rawBodyBuffer, extended: true }));
app.use(bodyParser.raw({ verify: rawBodyBuffer, type: '*/*' }));
app.post('/tu-endpoint-webhook', (req, res) => {
// 1. Obtener el secreto
const secret = process.env.WEBHOOK_SECRET;
// 2. Obtener la firma del header (ej. 'x-signature')
const receivedSignature = req.headers['x-signature'];
// 3. Obtener el raw payload (usando el middleware)
const rawPayload = req.rawBody;
// 4. Calcular la firma esperada
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(rawPayload)
.digest('hex');
// 5. Comparación segura (evita ataques de timing)
const signatureIsValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
if (signatureIsValid) {
// Firma válida
const payload = JSON.parse(rawPayload);
console.log('Webhook verificado:', payload);
res.status(200).send('OK');
// Procesa el evento aquí
} else {
// Firma inválida
console.error('Firma HMAC no válida. Posible intento fraudulento.');
res.status(401).send('Firma no válida');
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Servidor escuchando en puerto ${PORT}`));
Para Flask en Python, es necesario leer el request.data para obtener los bytes sin procesar.
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
app = Flask(__name__)
# 1. Obtener el secreto desde las variables de entorno
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET').encode()
@app.route('/webhook', methods=['POST'])
def handle_webhook():
# 2. Obtener la firma del header
received_signature = request.headers.get('X-Signature')
# 3. Obtener el raw payload como bytes
raw_payload = request.data
# 4. Calcular la firma esperada
# Nota: Asegúrate de que el secreto ya está en bytes (por eso el .encode() arriba)
expected_signature = hmac.new(
WEBHOOK_SECRET,
msg=raw_payload,
digestmod=hashlib.sha256
).hexdigest()
# 5. Comparación segara (usando hmac.compare_digest para evitar timing attacks)
if hmac.compare_digest(expected_signature, received_signature):
# Firma válida
payload = request.get_json()
print(f"Webhook verificado: {payload}")
# Aquí procesas el evento (ej. actualizar una orden en tu BD)
return jsonify({'status': 'success'}), 200
else:
# Firma inválida
print("Error: Firma HMAC no válida.")
return jsonify({'error': 'Firma no válida'}), 401
if __name__ == '__main__':
app.run(debug=False, port=5000)
Implementar la verificación es el primer paso, pero para un sistema robusto, sigue estas recomendaciones:
hash_equals() (PHP), crypto.timingSafeEqual() (Node.js) o hmac.compare_digest() (Python). Una comparación con == o === simple puede ser vulnerable.==) hace que la comparación sea vulnerable a ataques de temporización (timing attacks). Solución: Usa siempre las funciones de comparación segura en tiempo que provee tu lenguaje.sha256=). Debes extraer solo la parte relevante antes de comparar.La verificación de firmas HMAC-SHA256 es el guardián esencial de la integridad y autenticidad de los webhooks de pagos en tu comercio electrónico. En el mercado peruano, donde la adopción de Yape, Plin y los QR interoperables crece aceleradamente, saltarse este paso es exponer tu negocio a riesgos financieros y operativos graves. Como has visto, la implementación es técnica pero accesible, con ejemplos claros en PHP, Node.js y Python.
Al seguir este tutorial y las buenas prácticas adjuntas, no solo cumples con estándares de seguridad básicos, sino que también fortaleces la confianza con tus clientes y te alineas con las expectativas de reguladores como la SBS y el BCRP. La seguridad en los pagos digitales no es un lujo, es la base sobre la que se construye un e-commerce sostenible.
Call-to-Action: Te invito a revisar inmediatamente las integraciones de pago de tu tienda online. Si ya usas webhooks, confirma que la verificación HMAC esté correctamente implementada. Si estás evaluando opciones para recibir pagos QR y digitales en Perú, prioriza aquellos proveedores que ofrezcan este mecanismo de seguridad de forma clara y documentada.
Este artículo fue creado con el fin de educar y fortalecer las prácticas de seguridad en el e-commerce peruano. Para integrar pagos QR interoperables con Yape, Plin y más, con webhooks seguros y una comisión clara de 2.50% + S/ 0.20 + IGV, explora las soluciones que ofrecemos en TAYPI.
Crea tu cuenta gratis y genera tu primer QR en minutos.
Abrir cuenta gratis