Skip to main content

Verificación de Firma HMAC

Todos los webhooks incluyen una firma HMAC-SHA256 para verificar su autenticidad. Siempre verifica la firma antes de procesar un webhook.

Headers de Webhook

Cada webhook incluye estos headers:
HeaderDescripción
X-Webhook-SignatureFirma HMAC-SHA256 del payload
X-Webhook-EventTipo de evento (ej: message.received)
X-Webhook-TimestampFecha/hora ISO 8601 del envío
X-Webhook-IDID único del webhook

Cómo Funciona

  1. Whaapy calcula un HMAC-SHA256 del payload JSON usando tu secret
  2. El resultado se envía en el header X-Webhook-Signature
  3. Tú calculas el mismo HMAC con el payload recibido
  4. Si coinciden, el webhook es auténtico
Nunca proceses webhooks sin verificar la firma. Un atacante podría enviar webhooks falsos a tu endpoint.

Verificación en Node.js

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// En tu endpoint Express
app.post('/webhook', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const payload = req.body;
  
  if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Webhook verificado, procesar...
  console.log('Webhook received:', req.body.event);
  res.json({ received: true });
});
Usa crypto.timingSafeEqual() para comparar firmas. Esto previene ataques de timing.

Verificación en Python

import hmac
import hashlib
import json
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "tu_secret_aqui"

def verify_webhook_signature(payload: dict, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        json.dumps(payload, separators=(',', ':')).encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')
    payload = request.get_json()
    
    if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401
    
    # Webhook verificado, procesar...
    print(f"Webhook received: {payload['event']}")
    return jsonify({'received': True})

Verificación en PHP

<?php
$webhookSecret = getenv('WEBHOOK_SECRET');

function verifyWebhookSignature($payload, $signature, $secret) {
    $expected = hash_hmac('sha256', json_encode($payload), $secret);
    return hash_equals($expected, $signature);
}

// Recibir webhook
$payload = json_decode(file_get_contents('php://input'), true);
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

if (!verifyWebhookSignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Webhook verificado, procesar...
echo json_encode(['received' => true]);
?>

Verificación en Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "net/http"
)

func verifyWebhookSignature(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Webhook-Signature")
    
    var payload map[string]interface{}
    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    payloadBytes, _ := json.Marshal(payload)
    if !verifyWebhookSignature(payloadBytes, signature, webhookSecret) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }
    
    // Webhook verificado, procesar...
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"received": true}`))
}

Regenerar Secret

Si crees que tu secret fue comprometido, puedes regenerarlo:
curl -X POST https://api.whaapy.com/user-webhooks/<id>/regenerate-secret \
  -H "Authorization: Bearer wha_TU_API_KEY"
Después de regenerar, actualiza tu código con el nuevo secret. Los webhooks con el secret anterior fallarán la verificación.

Próximos Pasos