Verificación de Firma
SIEMPRE verifica la firma antes de procesar un webhook. Esto garantiza que:
- El webhook realmente viene de Aloha Pay
- El contenido no fue modificado en tránsito
- No es un replay attack
Algoritmo de Verificación
Sección titulada «Algoritmo de Verificación»1. Obtén el timestamp del header X-Webhook-Timestamp2. Obtén la firma del header X-Webhook-Signature3. Verifica que el timestamp no sea mayor a 5 minutos4. Construye el signed_payload: "{timestamp}.{body}"5. Calcula HMAC-SHA256 del signed_payload usando tu secret6. Compara con la firma recibida (comparación segura)Implementaciones
Sección titulada «Implementaciones»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...const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) { // Protección contra replay attacks const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - parseInt(timestamp)) > 300) { return false; }
// Calcular firma esperada const signedPayload = `${timestamp}.${payload}`; const expectedSignature = 'sha256=' + crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex');
// Comparación segura return crypto.timingSafeEqual( Buffer.from(expectedSignature), Buffer.from(signature) );}
// Uso en Express (con raw body)app.post('/webhooks/alohapay', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString(); const signature = req.headers['x-webhook-signature']; const timestamp = req.headers['x-webhook-timestamp']; const secret = 'whsec_tu_secret_aqui';
if (!verifyWebhookSignature(payload, signature, timestamp, secret)) { return res.status(401).send('Invalid signature'); }
// Procesar el webhook... });import hmacimport hashlibimport time
def verify_webhook_signature( payload: str, signature: str, timestamp: str, secret: str) -> bool: # Protección contra replay attacks current_time = int(time.time()) if abs(current_time - int(timestamp)) > 300: return False
# Calcular firma esperada signed_payload = f"{timestamp}.{payload}" expected_signature = 'sha256=' + hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest()
# Comparación segura return hmac.compare_digest(expected_signature, signature)
# Uso en Flask@app.route('/webhooks/alohapay', methods=['POST'])def handle_webhook(): payload = request.get_data(as_text=True) signature = request.headers.get('X-Webhook-Signature', '') timestamp = request.headers.get('X-Webhook-Timestamp', '') secret = 'whsec_tu_secret_aqui'
if not verify_webhook_signature(payload, signature, timestamp, secret): return 'Invalid signature', 401
# Procesar el webhook...package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "math" "strconv" "time")
func verifyWebhookSignature(payload, signature, timestamp, secret string) bool { // Protección contra replay attacks ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false }
currentTime := time.Now().Unix() if math.Abs(float64(currentTime-ts)) > 300 { return false }
// Calcular firma esperada signedPayload := fmt.Sprintf("%s.%s", timestamp, payload) h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(signedPayload)) expectedSignature := "sha256=" + hex.EncodeToString(h.Sum(nil))
// Comparación segura return hmac.Equal([]byte(expectedSignature), []byte(signature))}require 'openssl'
def verify_webhook_signature(payload, signature, timestamp, secret) # Protección contra replay attacks current_time = Time.now.to_i return false if (current_time - timestamp.to_i).abs > 300
# Calcular firma esperada signed_payload = "#{timestamp}.#{payload}" expected_signature = 'sha256=' + OpenSSL::HMAC.hexdigest( 'sha256', secret, signed_payload )
# Comparación segura ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)end
# Uso en Railsclass WebhooksController < ApplicationController skip_before_action :verify_authenticity_token
def alohapay payload = request.raw_post signature = request.headers['X-Webhook-Signature'] timestamp = request.headers['X-Webhook-Timestamp'] secret = 'whsec_tu_secret_aqui'
unless verify_webhook_signature(payload, signature, timestamp, secret) return head :unauthorized end
# Procesar el webhook... endendConsideraciones Importantes
Sección titulada «Consideraciones Importantes»Usa el Body Raw
Sección titulada «Usa el Body Raw»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 parseadoapp.post('/webhooks', express.json(), (req, res) => { const payload = JSON.stringify(req.body); // No funcionará});
// ✅ CORRECTO - body rawapp.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString(); // Funcionará});Comparación Segura de Strings
Sección titulada «Comparación Segura de Strings»Siempre usa funciones de comparación en tiempo constante para evitar timing attacks:
| Lenguaje | Función |
|---|---|
| PHP | hash_equals() |
| Node.js | crypto.timingSafeEqual() |
| Python | hmac.compare_digest() |
| Go | hmac.Equal() |
| Ruby | ActiveSupport::SecurityUtils.secure_compare() |
Protección contra Replay Attacks
Sección titulada «Protección contra Replay Attacks»La verificación del timestamp evita que un atacante reenvíe webhooks antiguos:
// Rechazar webhooks con más de 5 minutos de antigüedadconst currentTime = Math.floor(Date.now() / 1000);if (Math.abs(currentTime - parseInt(timestamp)) > 300) { return false; // Webhook muy antiguo o del futuro}Troubleshooting
Sección titulada «Troubleshooting»La firma no coincide
Sección titulada «La firma no coincide»- Verifica que uses el body raw, no el JSON parseado
- Verifica el orden:
{timestamp}.{body}(con punto entre ellos) - Verifica el secret: debe ser el que recibiste al crear el webhook
- Verifica el prefijo: la firma esperada debe tener
sha256=al inicio
Rechazando webhooks válidos por timestamp
Sección titulada «Rechazando webhooks válidos por timestamp»Si tu servidor tiene el reloj desincronizado, puede rechazar webhooks válidos. Asegúrate de que tu servidor esté sincronizado con NTP.
Rotar el Secret
Sección titulada «Rotar el Secret»Si crees que tu secret fue comprometido, puedes rotarlo:
curl -X POST https://api.alohapay.co/api/external/v1/webhooks/{id}/rotate-secret \ -H "X-API-Key: tu_api_key"Próximos Pasos
Sección titulada «Próximos Pasos»- Gestionar Webhooks - API para administrar webhooks
- Mejores Prácticas - Recomendaciones para producción