Firma y verificación
Cada entrega de webhook incluye un encabezado X-Watsi-Signature.
watsi calcula el valor aplicando un hash al cuerpo de la solicitud sin procesar con HMAC-SHA256 y el secreto de su suscripción.
Formato del encabezado
X-Watsi-Signature: 8a9f0d1c...
Trate el valor del encabezado como un digest codificado en hexadecimal. Para verificarlo, vuelva a calcular el mismo HMAC de su lado y compare los dos digests usando una comparación en tiempo constante.
Algoritmo de verificación
- Lea los bytes del cuerpo de la solicitud sin procesar exactamente como se recibieron.
- Lea
X-Watsi-Signaturede los encabezados de la solicitud. - Calcule
HMAC-SHA256(secret, rawBody). - Codifique en hexadecimal el digest calculado.
- Compare el digest calculado y el valor del encabezado en tiempo constante.
- Rechace la solicitud con un 401 si los valores no coinciden.
Ejemplo en Node.js
import crypto from 'crypto'
import express from 'express'
const app = express()
app.use(
express.json({
verify: (req, _res, buf) => {
req.rawBody = buf
},
})
)
function verifySignature(req) {
const signature = req.headers['x-watsi-signature']
if (!signature || !Buffer.isBuffer(req.rawBody)) return false
const computed = crypto
.createHmac('sha256', process.env.WATSI_WEBHOOK_SECRET ?? '')
.update(req.rawBody)
.digest('hex')
const expectedBuf = Buffer.from(String(signature), 'utf8')
const computedBuf = Buffer.from(computed, 'utf8')
if (expectedBuf.length !== computedBuf.length) return false
return crypto.timingSafeEqual(expectedBuf, computedBuf)
}
app.post('/webhooks/watsi', (req, res) => {
if (!verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' })
}
console.log('Verified event', req.body.type)
res.status(200).json({ received: true })
})Ejemplo en Python
import hashlib
import hmac
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_signature(req):
signature = req.headers.get('X-Watsi-Signature')
if not signature:
return False
raw_body = req.get_data()
computed = hmac.new(
os.environ['WATSI_WEBHOOK_SECRET'].encode(),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, computed)
@app.post('/webhooks/watsi')
def handle_webhook():
if not verify_signature(request):
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json(force=True)
print('Verified event', event['type'])
return jsonify({'received': True}), 200Ejemplo en Ruby
require 'openssl'
require 'rack/utils'
post '/webhooks/watsi' do
raw_body = request.body.read
signature = request.env['HTTP_X_WATSI_SIGNATURE']
secret = ENV.fetch('WATSI_WEBHOOK_SECRET')
computed = OpenSSL::HMAC.hexdigest('sha256', secret, raw_body)
halt 401, 'Invalid signature' unless signature &&
signature.bytesize == computed.bytesize &&
Rack::Utils.secure_compare(signature, computed)
status 200
body 'OK'
endEjemplo en Go
func verifySignature(rawBody []byte, signature string) bool {
mac := hmac.New(sha256.New, []byte(os.Getenv("WATSI_WEBHOOK_SECRET")))
mac.Write(rawBody)
computed := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(computed))
}Ejemplo en PHP
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WATSI_SIGNATURE'] ?? '';
$computed = hash_hmac('sha256', $rawBody, getenv('WATSI_WEBHOOK_SECRET'));
if (!hash_equals($computed, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}Consejos operativos
| Práctica | Por qué ayuda |
|---|---|
| Use el cuerpo de la solicitud sin procesar | Calcule el hash de los bytes exactos que llegaron por HTTP, antes de cualquier análisis JSON o reserialización. |
| Compare en tiempo constante | Use funciones de igualdad seguras frente a temporización para evitar filtrar información a través del tiempo de comparación de cadenas. |
| Almacene los secretos fuera del código | Mantenga su secreto de firma en una variable de entorno o en un gestor de secretos, nunca en el repositorio. |
| Registre el id de entrega | Persista X-Watsi-Delivery-Id o event.id para que los reintentos puedan correlacionarse de forma limpia. |