Mantener la integridad de las peticiones servidor‑servidor con firmas HMAC en Django/DRF

Cuando dos servidores se comunican, ¿cómo podemos comprobar que la petición que recibimos es realmente la que envié y que no ha sido alterada en tránsito? En este artículo describo cómo usar la autenticación basada en firmas HMAC en un entorno Django/DRF para garantizar la integridad y la confiabilidad de las peticiones entre servidores.

En particular, cubriremos:

  • ¿Cuál es el objetivo de esta técnica?
  • Qué incidentes puede prevenir
  • Por qué no es adecuada para la comunicación cliente‑servidor (limitaciones)
  • Cómo implementarla en un proyecto Django/DRF con hmac_mixin.py y CBV

¿Qué es la autenticación con firma HMAC?



HMAC (Hash-based Message Authentication Code) es un mecanismo que

combina una clave secreta compartida con el mensaje para generar un valor de firma.

  • Supongamos que los servidores A y B comparten la misma clave secreta.
  • Cuando A envía una petición POST a B:
  • Se concatena el cuerpo de la petición y un timestamp y se firma con HMAC.
  • La firma se incluye en un encabezado HTTP, por ejemplo X-HMAC-Signature.
  • B vuelve a calcular la firma con la misma clave y verifica que coincida:
  • Si coincide, la petición es auténtica.
  • Si no, la petición se rechaza.

En resumen, la firma HMAC verifica simultáneamente:

  • Integridad: el contenido no ha sido modificado.
  • Autenticación: el remitente conoce la clave secreta.

Nota: Esta técnica solo es viable cuando los servidores pueden compartir la clave de forma segura.


Propósito y incidentes que previene

1. Prevención de alteraciones en el cuerpo de la petición

Si un atacante intercepta la comunicación, podría intentar:

  • Cambiar el monto de una transferencia.
  • Modificar parámetros para obtener más beneficios.

Al recalcular la firma con el cuerpo y el timestamp, cualquier cambio, incluso un solo carácter, invalidará la firma y la petición será rechazada.

2. Prevención de suplantación de servidor (spoofing)

Un atacante que no conozca la clave secreta no podrá generar una firma válida. Por lo tanto, el servidor receptor podrá identificar que la petición no proviene de un servidor legítimo.

3. Prevención de ataques de repetición (replay)

Al incluir un timestamp o un nonce en la firma y rechazar peticiones demasiado antiguas o con un nonce ya usado, se mitigan los intentos de reenviar peticiones válidas.

Advertencia: HMAC debe usarse sobre HTTPS/TLS; es una capa adicional de seguridad, no un sustituto.


Limitaciones: ¿por qué no usar HMAC con clientes?



1. No se puede ocultar la clave en el cliente

En aplicaciones móviles, SPA o cualquier código que llegue al cliente, la clave secreta debe estar incluida en el binario o en el JavaScript. Un atacante puede extraerla mediante ingeniería inversa, lo que rompe la premisa de que solo dos servidores conocen la clave.

2. HMAC no cifra el contenido

La firma solo verifica que el contenido no cambie; no oculta la información. Los datos sensibles siguen estando protegidos por TLS.

Por lo tanto, HMAC es ideal para servidor ↔ servidor o microservicios backend, pero no para la comunicación con clientes finales.


Escenarios de uso típico

1. Aplicación Django → Servidor de autenticación DRF

  • Un servidor de autenticación centralizado (DRF) gestiona usuarios.
  • La aplicación Django envía peticiones POST firmadas con HMAC para validar credenciales o emitir tokens.

2. Aplicación Django → Servidor de inferencia AI (DRF/FastAPI)

  • La lógica de negocio corre en Django, pero las tareas de inferencia se delegan a un servidor especializado.
  • La petición POST /v1/infer se firma con HMAC, garantizando que solo el servidor de inferencia acepte peticiones legítimas.

Patrón práctico en Django/DRF: hmac_mixin.py + CBV

Una estructura limpia consiste en:

  1. Colocar hmac_mixin.py en la raíz del proyecto o de la app.
  2. Extraer la lógica de firma y envío POST en un mixin.
  3. Hacer que cualquier CBV que necesite enviar peticiones firmadas herede de ese mixin.

1. hmac_mixin.py – firma y método POST

# hmac_mixin.py
import hmac
import hashlib
import json
import time

import requests
from django.conf import settings


class HMACRequestMixin:
    # Se define en settings.py
    # HMAC_SECRET_KEY = "super-secret-key-from-env"
    HMAC_SECRET_KEY = settings.HMAC_SECRET_KEY.encode("utf-8")

    HMAC_HEADER_NAME = "X-HMAC-Signature"
    HMAC_TIMESTAMP_HEADER = "X-HMAC-Timestamp"
    HMAC_ALGORITHM = hashlib.sha256

    def build_hmac_signature(self, body: bytes, timestamp: str) -> str:
        """Genera la firma HMAC a partir del cuerpo y el timestamp."""
        message = timestamp.encode("utf-8") + b"." + body
        digest = hmac.new(self.HMAC_SECRET_KEY, message, self.HMAC_ALGORITHM).hexdigest()
        return digest

    def post_with_hmac(self, url: str, payload: dict, timeout: int = 5):
        """Envía una petición POST firmada con HMAC."""
        body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
        timestamp = str(int(time.time()))
        signature = self.build_hmac_signature(body, timestamp)

        headers = {
            "Content-Type": "application/json",
            self.HMAC_HEADER_NAME: signature,
            self.HMAC_TIMESTAMP_HEADER: timestamp,
        }

        response = requests.post(url, data=body, headers=headers, timeout=timeout)
        response.raise_for_status()
        return response

2. Ejemplo de CBV emisora

# views.py
from django.conf import settings
from django.http import JsonResponse
from django.views import View

from .hmac_mixin import HMACRequestMixin


class AIInferenceRequestView(HMACRequestMixin, View):
    """Recibe la petición del cliente y la reenvía al servidor de AI con HMAC."""

    def post(self, request, *args, **kwargs):
        data = json.loads(request.body.decode("utf-8"))
        payload = {
            "text": data.get("text", ""),
            "user_id": request.user.id if request.user.is_authenticated else None,
        }

        url = settings.AI_SERVER_URL  # e.g., "https://ai-service.internal/v1/infer"

        try:
            ai_response = self.post_with_hmac(url, payload)
        except requests.RequestException as e:
            return JsonResponse({"detail": "AI server error", "error": str(e)}, status=502)

        return JsonResponse(ai_response.json(), status=ai_response.status_code)

Con este patrón, cualquier nueva llamada a otro servidor solo requiere heredar HMACRequestMixin y usar self.post_with_hmac.


En el receptor: clase de autenticación personalizada en DRF

# authentication.py
import hmac
import hashlib
import time

from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed


class HMACSignatureAuthentication(BaseAuthentication):
    SECRET_KEY = settings.HMAC_SECRET_KEY.encode("utf-8")
    HMAC_HEADER_NAME = "X-HMAC-Signature"
    HMAC_TIMESTAMP_HEADER = "X-HMAC-Timestamp"
    ALGORITHM = hashlib.sha256
    MAX_SKEW_SECONDS = 60

    def _build_signature(self, body: bytes, timestamp: str) -> str:
        message = timestamp.encode("utf-8") + b"." + body
        return hmac.new(self.SECRET_KEY, message, self.ALGORITHM).hexdigest()

    def authenticate(self, request):
        signature = request.headers.get(self.HMAC_HEADER_NAME)
        timestamp = request.headers.get(self.HMAC_TIMESTAMP_HEADER)

        if not signature or not timestamp:
            raise AuthenticationFailed("Missing HMAC headers")

        try:
            ts = int(timestamp)
        except ValueError:
            raise AuthenticationFailed("Invalid timestamp")

        now = int(time.time())
        if abs(now - ts) > self.MAX_SKEW_SECONDS:
            raise AuthenticationFailed("Request timestamp too old")

        expected_signature = self._build_signature(request.body, timestamp)

        if not hmac.compare_digest(signature, expected_signature):
            raise AuthenticationFailed("Invalid HMAC signature")

        return (None, None)

Aplicación en una vista de DRF

# views.py (servidor DRF)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny

from .authentication import HMACSignatureAuthentication


class AIInferenceView(APIView):
    authentication_classes = [HMACSignatureAuthentication]
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        input_text = request.data.get("text", "")
        result = {"answer": f"AI result for: {input_text}"}
        return Response(result)

Con esta configuración:

  • El emisor (Django) envía peticiones firmadas con HMACRequestMixin.
  • El receptor (DRF) valida la firma con HMACSignatureAuthentication.

Resumen

  • La firma HMAC utiliza una clave secreta compartida para verificar la integridad y la autenticación de peticiones entre servidores.
  • Previene:
  • Alteraciones en el cuerpo de la petición.
  • Suplantación de servidor.
  • Ataques de repetición.
  • No es adecuada para clientes finales porque la clave no puede mantenerse oculta y HMAC no cifra el contenido.
  • En proyectos Django/DRF, un mixin (hmac_mixin.py) y una clase de autenticación personalizada (HMACSignatureAuthentication) proporcionan una solución limpia y reutilizable.

image