Docutray Logo

Seguridad y Verificación de Firma

Métodos de verificación de firma para webhooks y protección contra replay attacks

Seguridad y Verificación de Firma

Docutray proporciona dos métodos de verificación de firma para adaptarse a diferentes arquitecturas:

Método 1: Firma basada en body (Tradicional)

Valida el contenido completo del payload. Ideal para implementaciones tradicionales:

const crypto = require('crypto');

function verifyWebhookBody(bodyString, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(bodyString)
    .digest('hex');

  return `sha256=${expectedSignature}` === signature;
}

// Uso en Express.js
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-docutray-signature'];
  const secret = process.env.WEBHOOK_SECRET;

  if (!verifyWebhookBody(req.body.toString(), signature, secret)) {
    return res.status(401).send('Invalid signature');
  }

  // Procesar webhook
  const payload = JSON.parse(req.body);
  // ...
});

Método 2: Firma de autenticación (Compatible con Lambda Authorizers)

Valida usando solo metadatos en headers, sin acceso al body. Ideal para AWS Lambda Authorizers, Azure Functions, o Google Cloud Functions:

const crypto = require('crypto');

function verifyWebhookAuth(headers, webhookUrl, secret) {
  const authSignature = headers['x-docutray-auth-signature'];
  const timestamp = headers['x-docutray-timestamp'];
  const requestId = headers['x-docutray-request-id'];
  const eventType = headers['x-docutray-event'];

  // Validar timestamp (ventana de 5 minutos)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false; // Timestamp expirado
  }

  // Calcular firma esperada
  const authPayload = `${requestId}|${timestamp}|${webhookUrl}|${eventType}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(authPayload)
    .digest('hex');

  return `sha256=${expectedSignature}` === authSignature;
}

Ejemplo completo: AWS Lambda Authorizer

// Lambda Authorizer para AWS API Gateway
exports.handler = async (event) => {
  const crypto = require('crypto');

  try {
    // Extraer headers
    const authSignature = event.headers['x-docutray-auth-signature'];
    const timestamp = parseInt(event.headers['x-docutray-timestamp']);
    const requestId = event.headers['x-docutray-request-id'];
    const eventType = event.headers['x-docutray-event'];
    const webhookUrl = `https://${event.headers.host}${event.path}`;

    // Validar presencia de headers
    if (!authSignature || !timestamp || !requestId || !eventType) {
      return generatePolicy('user', 'Deny', event.methodArn);
    }

    // Validar timestamp (ventana de 5 minutos)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - timestamp) > 300) {
      console.log('Webhook timestamp expired');
      return generatePolicy('user', 'Deny', event.methodArn);
    }

    // Recalcular firma esperada
    const secret = process.env.DOCUTRAY_WEBHOOK_SECRET;
    const authPayload = `${requestId}|${timestamp}|${webhookUrl}|${eventType}`;
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(authPayload)
      .digest('hex');

    // Validar firma
    if (`sha256=${expectedSignature}` !== authSignature) {
      console.log('Signature verification failed');
      return generatePolicy('user', 'Deny', event.methodArn);
    }

    // Firma válida - permitir request
    return generatePolicy('user', 'Allow', event.methodArn);

  } catch (error) {
    console.error('Error in authorizer:', error);
    return generatePolicy('user', 'Deny', event.methodArn);
  }
};

function generatePolicy(principalId, effect, resource) {
  return {
    principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: resource
      }]
    }
  };
}

Ejemplo: Python para AWS Lambda Authorizer

import hmac
import hashlib
import time
import os

def lambda_handler(event, context):
    try:
        # Extraer headers
        headers = {k.lower(): v for k, v in event['headers'].items()}
        auth_signature = headers.get('x-docutray-auth-signature')
        timestamp = int(headers.get('x-docutray-timestamp', 0))
        request_id = headers.get('x-docutray-request-id')
        event_type = headers.get('x-docutray-event')
        webhook_url = f"https://{headers['host']}{event['path']}"

        # Validar presencia de headers
        if not all([auth_signature, timestamp, request_id, event_type]):
            return generate_policy('user', 'Deny', event['methodArn'])

        # Validar timestamp (ventana de 5 minutos)
        now = int(time.time())
        if abs(now - timestamp) > 300:
            print('Webhook timestamp expired')
            return generate_policy('user', 'Deny', event['methodArn'])

        # Recalcular firma esperada
        secret = os.environ['DOCUTRAY_WEBHOOK_SECRET']
        auth_payload = f"{request_id}|{timestamp}|{webhook_url}|{event_type}"
        expected_signature = hmac.new(
            secret.encode(),
            auth_payload.encode(),
            hashlib.sha256
        ).hexdigest()

        # Validar firma
        if f"sha256={expected_signature}" != auth_signature:
            print('Signature verification failed')
            return generate_policy('user', 'Deny', event['methodArn'])

        # Firma válida - permitir request
        return generate_policy('user', 'Allow', event['methodArn'])

    except Exception as error:
        print(f'Error in authorizer: {error}')
        return generate_policy('user', 'Deny', event['methodArn'])

def generate_policy(principal_id, effect, resource):
    return {
        'principalId': principal_id,
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                'Action': 'execute-api:Invoke',
                'Effect': effect,
                'Resource': resource
            }]
        }
    }

Protección contra replay attacks

El sistema incluye protección automática contra replay attacks mediante:

  1. Timestamp único: Cada envío incluye X-Docutray-Timestamp (Unix seconds)
  2. Request ID único: Cada envío tiene un X-Docutray-Request-Id (UUID) único

Recomendaciones de seguridad

  • Validar timestamp: Rechazar requests con timestamps fuera de una ventana razonable (recomendado: 5 minutos)
  • Cachear request-id: Guardar temporalmente los request-id procesados para detectar duplicados
  • Usar HTTPS: Siempre usar endpoints HTTPS para prevenir interceptación
// Ejemplo de cache de request-id con Redis
const redis = require('redis');
const client = redis.createClient();

async function isReplayAttack(requestId) {
  const key = `webhook:${requestId}`;
  const exists = await client.exists(key);

  if (exists) {
    return true; // Request-id ya procesado
  }

  // Marcar como procesado (TTL de 10 minutos)
  await client.setex(key, 600, '1');
  return false;
}

Páginas relacionadas