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.
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.
¿Por qué es crucial verificar HMAC-SHA256 en webhooks?
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.
¿Qué es HMAC-SHA256 y cómo funciona?
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í:
- Tu y tu proveedor comparten un secreto: Al configurar los webhooks en tu panel de control (por ejemplo, en la documentación de TAYPI o de cualquier otra pasarela), se te proporciona una clave secreta (secret). Esta clave nunca debe viajar en las comunicaciones; solo la conocen tu servidor y el servidor del proveedor.
- El proveedor firma el mensaje: Cuando ocurre un evento (ej. pago confirmado), el servidor del proveedor toma el cuerpo completo (payload) de la notificación y, usando el secreto compartido y el algoritmo HMAC-SHA256, genera una firma hexadecimal única.
- El proveedor envía la firma y el payload: Te envía una solicitud HTTP POST a tu endpoint de webhook. En los encabezados (headers) de esta solicitud, incluye la firma calculada (a menudo en un header llamado
X-Signature,X-Webhook-Signatureo similar). El cuerpo de la solicitud contiene el payload en JSON. - Tu backend replica y compara: Tu servidor recibe la solicitud. Con el mismo secreto almacenado de forma segura, toma el payload recibido y calcula su propia firma HMAC-SHA256. Luego, compara esta firma generada con la que llegó en el header. Si coinciden exactamente, puedes estar seguro de que el mensaje es auténtico y no fue alterado.
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.
Componentes necesarios para la verificación
Para implementar la verificación correctamente, debes identificar y manejar varios componentes. Aquí te los desgloso:
- Secreto (Secret Key): La piedra angular. Es una cadena de caracteres larga y aleatoria proporcionada por tu proveedor de pagos. Debes almacenarla de forma segura en variables de entorno (por ejemplo,
.env) y nunca hardcodearla en tu aplicación. - Payload o Cuerpo del Mensaje: Los datos en bruto (raw body) de la solicitud HTTP POST. Es fundamental leer este cuerpo exactamente como fue enviado, sin modificaciones, parsing previo o alteraciones de espacios en blanco. Usar el
raw request bodyes clave. - Firma Recibida (Received Signature): La firma que el proveedor envía. Normalmente se encuentra en un encabezado HTTP. Algunos proveedores la envían en formato hexadecimal, otros en Base64. Debes leer la documentación de tu pasarela para saber el formato exacto y el nombre del header (p.ej.,
X-Taypi-Signature). - Algoritmo: En nuestro caso, siempre será HMAC-SHA256.
- Firma Calculada (Computed Signature): La firma que tú generas en tu backend repitiendo el proceso con tu secreto y el payload recibido.
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... |
Tutorial: Verificar firmas HMAC-SHA256 en tu backend
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:
- Obtener el secreto de tu entorno.
- Leer la firma enviada en el header correspondiente.
- Leer el payload en bruto (raw) de la solicitud.
- Calcular la firma HMAC-SHA256 usando el secreto y el payload en bruto.
- Comparar la firma calculada con la recibida de manera segura (comparación constante en tiempo) para evitar ataques de timing.
Verificación en PHP
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.
}
?>
Verificación en Node.js (Express)
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}`));
Verificación en Python (Flask)
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)
Buenas prácticas de seguridad para webhooks
Implementar la verificación es el primer paso, pero para un sistema robusto, sigue estas recomendaciones:
- Guarda el secreto con máxima seguridad: Utiliza un gestor de secretos (como AWS Secrets Manager, HashiCorp Vault) o, como mínimo, variables de entorno. Nunca lo comites a tu repositorio de código.
- Usa comparación segura contra timing attacks: Como viste en los ejemplos, siempre utiliza funciones como
hash_equals()(PHP),crypto.timingSafeEqual()(Node.js) ohmac.compare_digest()(Python). Una comparación con==o===simple puede ser vulnerable. - Valida y sanitiza el payload después de verificar: Una vez confirmada la autenticidad, parsea el JSON y valida los campos críticos (ID de transacción, monto, moneda) contra tu base de datos. Esto evita procesar datos malformados.
- Implementa idempotencia: Los webhooks pueden llegar más de una vez. Diseña tu lógica de procesamiento para que manejar el mismo evento múltiples veces no cause duplicados (ej., usando un ID único del evento como llave).
- Registra (log) toda la actividad: Guarda logs de los webhooks recibidos, tanto los exitosos como los fallidos. Esto es vital para la auditoría, el debugging y la detección de ataques. Plataformas como TAYPI ofrecen un panel para ver el historial de eventos enviados, lo que puedes cruzar con tus logs.
- Usa HTTPS obligatoriamente: Tu endpoint de webhook debe ser servido exclusivamente bajo HTTPS. Esto cifra la comunicación en tránsito, protegiendo el payload y la firma de ser interceptados.
- Mantén tu software actualizado: Asegúrate de que tu stack tecnológico (librerías, framework, runtime) tenga los últimos parches de seguridad.
Errores comunes y cómo evitarlos
- Error: No usar el raw body. Parsear el payload como JSON antes de calcular la firma altera los espacios, saltos de línea o el orden de las claves, cambiando la firma. Solución: Siempre accede al cuerpo de la solicitud en su formato crudo y original.
- Error: Comparar firmas de forma insegura. Usar operadores de igualdad simples (
==) 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. - Error: Mal manejo de la codificación. El secreto y el payload deben pasarse a la función HMAC con la codificación correcta (generalmente bytes/UTF-8). Solución: Asegúrate de que tanto el secreto como el dato estén en el formato que la función HMAC espera (consulta la documentación de tu lenguaje).
- Error: Confiar en un solo header o formato. Algunos proveedores pueden enviar la firma en múltiples formatos (hex, Base64) o en diferentes headers. Solución: Lee detenidamente la documentación de tu proveedor. Por ejemplo, algunos incluyen un prefijo en la firma (ej.,
sha256=). Debes extraer solo la parte relevante antes de comparar. - Error: No tener un plan para la rotación de secretos. Si tu secreto se ve comprometido, debes poder cambiarlo sin interrumpir tu servicio. Solución: Diseña un proceso donde puedas generar un nuevo secreto en el panel de tu proveedor y actualizarlo en tu backend de forma coordinada. Algunas pasarelas permiten tener secretos activos y de respaldo.
Conclusión
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.
¿Listo para cobrar con QR?
Crea tu cuenta gratis y genera tu primer QR en minutos.