Recepción y validación de firma
Recepción y validación de firma
Objetivo
Garantizar que el webhook recibido:
- Proviene de Bidzi (autenticidad).
- No fue alterado durante el envío (integridad).
- No corresponde a una petición antigua reutilizada maliciosamente (protección contra replay).
- No sea procesado más de una vez (idempotencia).
¿Qué envía Bidzi?
Bidzi envía una petición HTTP POST al endpoint configurado para el webhook con:
Content-Type: application/json- Un body en formato
JSON. - Headers utilizados para trazabilidad, autenticación y validación de la petición.
Los headers de seguridad son:
| Header | Descripción |
|---|---|
X-Bidzi-Id | Identificador único del envío. Su formato es whk_{webhookId}_tx_{transactionId}. |
X-Bidzi-Timestamp | Momento en el que se realizó el envío, expresado en segundos epoch UTC (Unix time). |
X-Bidzi-Signature | Firma HMAC-SHA256 codificada en Base64 estándar. |
El signing secret utilizado para validar la firma es único para cada webhook configurado y debe obtenerse desde el portal de comercios.
Debe almacenarse de manera segura, por ejemplo, en una variable de entorno o en un administrador de secretos, y nunca directamente en el código fuente o repositorio.
Firma del request
Bidzi genera la firma utilizando los siguientes elementos:
{X-Bidzi-Id}.{X-Bidzi-Timestamp}.{raw_body}La cadena que se firma se construye de la siguiente manera:
signed_content = X-Bidzi-Id + "." + X-Bidzi-Timestamp + "." + raw_bodyPosteriormente, se calcula la firma utilizando HMAC-SHA256 con el signing secret correspondiente al webhook:
X-Bidzi-Signature = Base64(HMAC_SHA256(signed_content, signing_secret))Para la generación y validación de la firma se deben considerar las siguientes reglas:
signed_contenty elsigning secretutilizan codificaciónUTF-8.- La firma utiliza Base64 estándar, incluyendo padding
=. - No se utiliza Base64-URL ni representación hexadecimal.
X-Bidzi-Timestampestá expresado en segundos, no en milisegundos.
Importante: utilizar el raw body
Para validar correctamente la firma se debe utilizar el cuerpo de la petición exactamente como fue recibido.
No se debe parsear el JSON y posteriormente volver a serializarlo antes de calcular la firma.
Por ejemplo, modificaciones como:
- El orden de las propiedades.
- Espacios en blanco.
- Formato de valores decimales.
- Caracteres escapados.
pueden provocar que la cadena resultante sea diferente y, por lo tanto, que la firma calculada no coincida con X-Bidzi-Signature.
Por esta razón, el receptor debe conservar y utilizar el raw body / raw bytes antes de realizar cualquier transformación o deserialización.
Validación del webhook
Al recibir un webhook se debe realizar el siguiente proceso:
-
Obtener el raw body de la petición sin modificarlo ni parsearlo.
-
Obtener los headers:
X-Bidzi-IdX-Bidzi-TimestampX-Bidzi-Signature
-
Si alguno de estos headers no está presente, rechazar la petición con un código HTTP
400. -
Validar la antigüedad de
X-Bidzi-Timestamp. -
Se recomienda rechazar peticiones cuya diferencia con el reloj del servidor sea superior a aproximadamente 5 minutos para protegerse contra ataques de tipo replay.
-
Construir la cadena:
{X-Bidzi-Id}.{X-Bidzi-Timestamp}.{raw_body}- Calcular nuevamente la firma utilizando
HMAC-SHA256y elsigning secretcorrespondiente al webhook. - Codificar el resultado utilizando Base64 estándar.
- Comparar la firma calculada con el valor recibido en
X-Bidzi-Signature. - La comparación debe realizarse utilizando un mecanismo de tiempo constante, evitando comparaciones directas como
==. - Si las firmas no coinciden, responder con HTTP
401y no procesar el evento. - Si la firma es válida, continuar con el procesamiento del webhook y responder con un código HTTP
2xx.
De esta manera se confirma que el webhook fue generado utilizando el secreto compartido correspondiente y que su contenido no fue modificado durante el envío.
Protección contra replay
X-Bidzi-Timestamp puede utilizarse para evitar que una petición válida sea capturada y enviada nuevamente posteriormente.
Se recomienda aceptar únicamente peticiones cuya diferencia entre el timestamp recibido y el reloj del servidor se encuentre dentro de una ventana aproximada de 5 minutos.
Bidzi no impone esta ventana automáticamente; su implementación es responsabilidad del receptor del webhook.
Idempotencia
Además de validar la firma, se debe utilizar X-Bidzi-Id como clave de idempotencia.
Antes de procesar el evento se debe comprobar si dicho identificador ya fue procesado.
Si el X-Bidzi-Id ya existe, el evento debe considerarse duplicado y no debe volver a ejecutar la lógica asociada a la transacción.
Esto permite proteger el sistema ante posibles reenvíos manuales o entregas duplicadas.
Respuesta esperada
Cuando el webhook sea recibido y validado correctamente, el endpoint debe responder con un código HTTP 200.
El endpoint debe responder en menos de 30 segundos. Si el procesamiento del evento requiere operaciones pesadas o de larga duración, se recomienda encolar el evento y responder al webhook lo antes posible.
Bidzi realiza un único intento automático de entrega por evento, por lo que es importante evitar procesamiento pesado antes de enviar la respuesta HTTP.
Todos los intentos de entrega, exitosos o fallidos, quedan registrados junto con su código HTTP y cuerpo de respuesta para fines de diagnóstico.
Ejemplos de implementación
@RestController
public class BidziWebhookController {
private static final long TOLERANCE_SECONDS = 300;
@Value("${bidzi.webhook.secret}")
private String secret;
@PostMapping(value = "/webhooks/bidzi", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> receive(
@RequestHeader("X-Bidzi-Id") String id,
@RequestHeader("X-Bidzi-Timestamp") String timestamp,
@RequestHeader("X-Bidzi-Signature") String signature,
@RequestBody String rawBody) throws Exception {
long age = Math.abs(Instant.now().getEpochSecond() - Long.parseLong(timestamp));
if (age > TOLERANCE_SECONDS) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = Base64.getEncoder().encodeToString(
mac.doFinal(String.format("%s.%s.%s", id, timestamp, rawBody)
.getBytes(StandardCharsets.UTF_8)));
if (!MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8))) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// ... procesar de forma idempotente usando `id`
return ResponseEntity.ok().build();
}
}Ejemplo de validación del mensaje
Para comprobar que la implementación de la validación de firma funciona correctamente, se puede utilizar el siguiente ejemplo.
Datos de prueba
signing_secret: test_secret
X-Bidzi-Id: whk_1_tx_1
X-Bidzi-Timestamp: 1700000000Body recibido:
{"a":1}1. Construir la cadena a firmar
Se concatenan X-Bidzi-Id, X-Bidzi-Timestamp y el raw body, separados por un punto:
whk_1_tx_1.1700000000.{"a":1}Es importante utilizar el body exactamente como fue recibido, sin volver a serializarlo ni modificar espacios, saltos de línea u orden de propiedades.
2. Calcular la firma
La cadena anterior debe firmarse utilizando HMAC-SHA256 con el siguiente signing_secret:
test_secretPosteriormente, el resultado se codifica utilizando Base64 estándar:
Base64(HMAC_SHA256(signed_content, signing_secret))Para estos datos de prueba, la firma resultante es:
hYghz33oZVEe3IYqHdA0p8gaB1kuofwmENAVH1Qp0Z8=También se puede validar desde una terminal utilizando OpenSSL:
printf '%s' 'whk_1_tx_1.1700000000.{"a":1}' \
| openssl dgst -sha256 -hmac 'test_secret' -binary \
| openssl base64El comando debe producir:
hYghz33oZVEe3IYqHdA0p8gaB1kuofwmENAVH1Qp0Z8=3. Comparar la firma
La firma calculada debe compararse con el valor recibido en el header:
X-Bidzi-Signature: hYghz33oZVEe3IYqHdA0p8gaB1kuofwmENAVH1Qp0Z8=Si ambas firmas coinciden, el mensaje puede considerarse auténtico e íntegro y continuar con su procesamiento.
Si las firmas no coinciden, el mensaje debe rechazarse con:
HTTP 401 Unauthorizedy el evento no debe ser procesado.
La comparación de firmas debe realizarse utilizando un mecanismo de tiempo constante, por ejemplo
MessageDigest.isEqualen Java.
Updated about 1 month ago

