Verificación HMAC
Cada POST que te reenviamos viene firmado con X-Kalipto-Signature (formato
sha256=<hex>). Es HMAC-SHA256 de los bytes UTF-8 exactos del body, con
tu webhook_secret (en texto plano) como clave.
Verificá contra los bytes exactos del body recibido, nunca después de re-parsear el JSON. Re-serializar en tu stack cambia el byte layout (espacios, orden de keys, unicode escape) y el hash deja de coincidir.
Python
Section titled “Python”import hmac, hashlib
def verify_kalipto_signature(secret: str, raw_body: bytes, header: str) -> bool: if not header or not header.startswith("sha256="): return False expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header.split("=", 1)[1])
# FastAPI ejemplofrom fastapi import FastAPI, Request, HTTPException
app = FastAPI()SECRET = os.environ["KALIPTO_WEBHOOK_SECRET"]
@app.post("/webhook")async def webhook(request: Request): raw = await request.body() sig = request.headers.get("X-Kalipto-Signature", "") if not verify_kalipto_signature(SECRET, raw, sig): raise HTTPException(status_code=401, detail="invalid signature") payload = json.loads(raw) # … process payload return {"ok": True}Node.js (Express + body-parser raw)
Section titled “Node.js (Express + body-parser raw)”import express from 'express';import crypto from 'crypto';
const app = express();const SECRET = process.env.KALIPTO_WEBHOOK_SECRET;
// IMPORTANTE: capturá el raw body — express.json() consume el stream antes.app.use('/webhook', express.raw({ type: 'application/json' }));
function verifySignature(secret, rawBody, header) { if (!header || !header.startsWith('sha256=')) return false; const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const received = header.split('=')[1]; return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'));}
app.post('/webhook', (req, res) => { const sig = req.header('X-Kalipto-Signature') || ''; if (!verifySignature(SECRET, req.body, sig)) { return res.status(401).json({ detail: 'invalid signature' }); } const payload = JSON.parse(req.body.toString('utf-8')); // … process payload res.json({ ok: true });});package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "os" "strings")
var secret = []byte(os.Getenv("KALIPTO_WEBHOOK_SECRET"))
func verifySignature(rawBody []byte, header string) bool { if !strings.HasPrefix(header, "sha256=") { return false } mac := hmac.New(sha256.New, secret) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) received := strings.SplitN(header, "=", 2)[1] return hmac.Equal([]byte(expected), []byte(received))}
func webhook(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) sig := r.Header.Get("X-Kalipto-Signature") if !verifySignature(body, sig) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } // … process body w.WriteHeader(http.StatusOK)}Comparación constant-time
Section titled “Comparación constant-time”Siempre usá una función constant-time (hmac.compare_digest,
crypto.timingSafeEqual, hmac.Equal). El operador == filtra info por
timing y deja un side-channel para forzar firmas válidas.
Si el canal no tiene secret
Section titled “Si el canal no tiene secret”Si tu canal no tiene webhook_secret, el header X-Kalipto-Signature viene
omitido. En ese caso podés:
- Rechazar el request (recomendado para producción).
- Procesarlo sin verificar (degradación graceful, solo para testing local).
Te recomendamos siempre tener un secret seteado en producción. La UI te genera uno seguro al crear el canal.
Próximo paso
Section titled “Próximo paso”- Idempotencia: cómo deduplicar reintentos.