Skip to content

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.

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 ejemplo
from 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}
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)
}

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 tu canal no tiene webhook_secret, el header X-Kalipto-Signature viene omitido. En ese caso podés:

  1. Rechazar el request (recomendado para producción).
  2. 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.