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)

  1. Timestamp: Se genera un timestamp Unix (segundos desde epoch)
  2. Canonicalización: El payload JSON se serializa de forma determinista (orden alfabético de claves, sin espacios)
  3. Construcción del mensaje: Se concatena: {timestamp}.{url}.{payload}
  4. Firma HMAC-SHA256: Se firma el mensaje con tu CALLBACK_SEED_KEY
  5. 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.