X-Webhook-Signature:
X-Webhook-Signature: t=1786881906,v1=8f3a...
té o timestamp Unix em segundos.v1é o HMAC-SHA256, em hexadecimal, calculado sobre o timestamp, um ponto e o corpo bruto (t + "." + corpo), usando o segredo do webhook como chave.
Calcule sobre o corpo bruto da requisição, antes de qualquer parse. Reserializar o JSON muda os bytes e a assinatura não fecha.
import { createHmac, timingSafeEqual } from 'crypto'
import express from 'express'
const app = express()
// O 'verify' guarda o corpo bruto sem impedir o parse do JSON.
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf },
}))
function assinaturaValida(header, rawBody, secret) {
const partes = Object.fromEntries(
String(header || '').split(',').map((p) => p.split('=', 2)),
)
if (!partes.t || !partes.v1) return false
// Rejeita entregas antigas: sem isso, uma captura da requisição poderia ser
// reenviada mais tarde com a assinatura ainda válida.
const idadeEmSegundos = Math.abs(Date.now() / 1000 - Number(partes.t))
if (!Number.isFinite(idadeEmSegundos) || idadeEmSegundos > 300) return false
const esperado = createHmac('sha256', secret)
.update(`${partes.t}.${rawBody}`)
.digest('hex')
const a = Buffer.from(esperado, 'hex')
const b = Buffer.from(partes.v1, 'hex')
return a.length === b.length && timingSafeEqual(a, b)
}
app.post('/webhooks/bukiopay', (req, res) => {
const ok = assinaturaValida(
req.headers['x-webhook-signature'],
req.rawBody,
process.env.BUKIOPAY_WEBHOOK_SECRET,
)
if (!ok) return res.status(401).send('assinatura inválida')
// Responda antes de processar: o tempo limite da entrega é de 15s.
res.sendStatus(200)
})
import hashlib
import hmac
import time
def assinatura_valida(header: str, raw_body: bytes, secret: str) -> bool:
partes = dict(
p.split("=", 1) for p in (header or "").split(",") if "=" in p
)
t, v1 = partes.get("t"), partes.get("v1")
if not t or not v1:
return False
if abs(time.time() - int(t)) > 300:
return False
esperado = hmac.new(
secret.encode(),
f"{t}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(esperado, v1)
<?php
function assinatura_valida(string $header, string $rawBody, string $secret): bool
{
$partes = [];
foreach (explode(',', $header) as $par) {
[$chave, $valor] = array_pad(explode('=', $par, 2), 2, null);
$partes[$chave] = $valor;
}
if (empty($partes['t']) || empty($partes['v1'])) {
return false;
}
if (abs(time() - (int) $partes['t']) > 300) {
return false;
}
$esperado = hash_hmac('sha256', $partes['t'] . '.' . $rawBody, $secret);
return hash_equals($esperado, $partes['v1']);
}
