Introducción
⚠️ Importante: Todos los callbacks enviados desde nuestros servidores (callbacks de contratación y notificaciones de cambios) utilizan el mismo sistema de firma HMAC-SHA256. Es crítico validar esta firma para garantizar que las peticiones provienen realmente de Imagina Energía y no de un atacante.
📍 Aplicación
Este sistema de firma se aplica a:
- ✅ Callbacks de resultado de contratación (cuando especificas callback_url)
- ✅ Notificaciones de cambios en contratos (cuando especificas url_notificaciones_cambios_contrato)
🔐 Cómo Funciona la Firma
Proceso de Firma (en nuestros servidores)
- Timestamp: Se genera un timestamp Unix (segundos desde epoch)
- Canonicalización: El payload JSON se serializa de forma determinista (orden alfabético de claves, sin espacios)
- Construcción del mensaje: Se concatena: {timestamp}.{url}.{payload}
- Firma HMAC-SHA256: Se firma el mensaje con tu CALLBACK_SEED_KEY
- Codificación: La firma se codifica en base64url (sin padding)
📋 Headers HTTP Recibidos
| Header | Descripción | Ejemplo |
|---|---|---|
| X-Signature | Firma HMAC-SHA256 en base64url con prefijo de versión | v1=AbCd123Xyz... |
| X-Signature-Timestamp | Timestamp Unix en segundos (usado en la firma) | 1732543800 |
| X-Signature-Algorithm | Algoritmo de firma utilizado | HS256 |
| Content-Type | Tipo de contenido del body | application/json |
💻 Implementación de Validación
Python (Flask/FastAPI):
import hmac
import hashlib
import base64
import json
import os
def validar_firma_callback(request_url, payload, signature_header, timestamp):
"""
Valida la firma HMAC-SHA256 de cualquier callback/webhook.
Funciona tanto para callbacks de contratación como notificaciones de cambios.
Args:
request_url: URL completa donde se recibió el callback
payload: Dict con el body JSON recibido
signature_header: Valor del header X-Signature (ej: "v1=AbCd123...")
timestamp: Valor del header X-Signature-Timestamp
Returns:
bool: True si la firma es válida, False en caso contrario
"""
# 1. Obtener la clave secreta compartida
seed = os.getenv('CALLBACK_SEED_KEY')
if not seed:
print("❌ ERROR: CALLBACK_SEED_KEY no configurada")
return False
# 2. Validar formato del header de firma
if not signature_header or not signature_header.startswith('v1='):
print("❌ ERROR: Header X-Signature inválido o ausente")
return False
received_signature = signature_header[3:]
# 3. Reconstruir el mensaje exactamente como se firmó
canonical_payload = json.dumps(
payload,
separators=(',', ':'),
sort_keys=True,
ensure_ascii=False
)
message = f"{timestamp}.{request_url}.{canonical_payload}"
message_bytes = message.encode('utf-8')
# 4. Calcular la firma esperada usando HMAC-SHA256
key = seed.encode('utf-8')
digest = hmac.new(key, message_bytes, hashlib.sha256).digest()
# 5. Codificar en base64url (sin padding)
expected_signature = base64.urlsafe_b64encode(digest).decode('ascii').rstrip('=')
# 6. Comparación segura contra timing attacks
is_valid = hmac.compare_digest(received_signature, expected_signature)
if is_valid:
print("✅ Firma válida - Callback auténtico")
else:
print("❌ Firma inválida - Posible ataque")
return is_valid
# Ejemplo de uso en Flask
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/contratos', methods=['POST'])
def webhook_handler():
payload = request.get_json()
signature = request.headers.get('X-Signature')
timestamp = request.headers.get('X-Signature-Timestamp')
url = request.url
if not validar_firma_callback(url, payload, signature, timestamp):
return jsonify({"error": "Firma inválida"}), 401
print(f"📥 Callback recibido y validado: {payload}")
return jsonify({"status": "ok"}), 200
Node.js (Express):
const crypto = require('crypto');
function validarFirmaCallback(requestUrl, payload, signatureHeader, timestamp) {
/**
* Valida la firma HMAC-SHA256 de cualquier callback/webhook.
*/
const seed = process.env.CALLBACK_SEED_KEY;
if (!seed) {
console.error('❌ ERROR: CALLBACK_SEED_KEY no configurada');
return false;
}
if (!signatureHeader || !signatureHeader.startsWith('v1=')) {
console.error('❌ ERROR: Header X-Signature inválido');
return false;
}
const receivedSignature = signatureHeader.substring(3);
const canonicalPayload = JSON.stringify(payload, Object.keys(payload).sort());
const message = `${timestamp}.${requestUrl}.${canonicalPayload}`;
const hmac = crypto.createHmac('sha256', seed);
hmac.update(message);
const expectedSignature = hmac.digest('base64url');
const isValid = crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(expectedSignature)
);
if (isValid) {
console.log('✅ Firma válida - Callback auténtico');
} else {
console.error('❌ Firma inválida - Posible ataque');
}
return isValid;
}
// Ejemplo de uso en Express
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/contratos', (req, res) => {
const payload = req.body;
const signature = req.headers['x-signature'];
const timestamp = req.headers['x-signature-timestamp'];
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
if (!validarFirmaCallback(url, payload, signature, timestamp)) {
return res.status(401).json({ error: 'Firma inválida' });
}
console.log('📥 Callback recibido y validado:', payload);
res.status(200).json({ status: 'ok' });
});
⚠️ Consideraciones de Seguridad
- 🔑 Protege tu CALLBACK_SEED_KEY: Nunca la expongas en código público, usa variables de entorno
- ⏱️ Valida el timestamp: Rechaza callbacks con timestamps muy antiguos (>5 min) para prevenir replay attacks
- 🔒 Usa HTTPS: Tu endpoint debe usar HTTPS en producción (obligatorio)
- 🚫 No omitas la validación: Siempre valida la firma antes de procesar el callback
- 📝 Logging: Registra intentos fallidos de validación para detectar ataques
- ⚡ Comparación segura: Usa funciones de comparación constante en tiempo para prevenir timing attacks
🆘 Obtener tu CALLBACK_SEED_KEY
Para obtener tu clave secreta única (CALLBACK_SEED_KEY), contacta con nuestro equipo de soporte técnico en serviciosistemas@imaginaenergia.com. Esta clave es única para tu organización y se utiliza para firmar todos tus callbacks y webhooks.