Verificar assinaturas

Confirme que cada pedido de webhook vem realmente do Pulsar e não foi alterado pelo caminho.

O seu endpoint é um URL público, por isso qualquer pessoa lhe pode enviar um pedido. Cada notificação do Pulsar inclui um cabeçalho X-Pulsar-Signature, um hash do corpo do pedido calculado com o seu segredo de assinatura. O segredo só é conhecido pelo Pulsar e por si, por isso uma assinatura que coincide prova que o pedido veio do Pulsar e que o corpo não foi alterado.

A assinatura é o HMAC-SHA256 do corpo em bruto do pedido, com o seu segredo de assinatura como chave, escrito em hexadecimal em minúsculas.

Use o segredo exatamente como o Pulsar lho fornece

O segredo de assinatura é uma cadeia de 32 caracteres. Use o seu texto (bytes UTF-8) como chave do HMAC, tal como está. Não o descodifique de Base64 nem de hexadecimal. A descodificação costuma funcionar sem erros, mas produz a chave errada, e então todas as verificações de assinatura falham.

Como verificar um pedido

Leia o corpo em bruto

Calcule a assinatura sobre os bytes exatos que o Pulsar enviou, antes de qualquer parsing do JSON. Fazer o parsing e voltar a serializar o JSON pode alterar os espaços ou a ordem das chaves, o que altera o hash. A maioria das frameworks fornece o corpo em bruto separadamente; consulte os exemplos abaixo.

Calcule a assinatura esperada

Calcule o HMAC-SHA256 do corpo em bruto com o seu segredo de assinatura como chave, e codifique o resultado em hexadecimal.

Compare-a com o cabeçalho

Compare o seu resultado com o cabeçalho X-Pulsar-Signature através de uma comparação de tempo constante (hmac.compare_digest em Python, crypto.timingSafeEqual em Node.js). Um == simples pode revelar, pelo tempo de resposta, que parte de uma assinatura adivinhada estava certa.

Aceite ou rejeite

Se as assinaturas coincidirem, processe o evento e devolva 2XX. Se o cabeçalho faltar ou não coincidir, devolva 401 e ignore o pedido.

Teste primeiro a sua verificação com um ping

O Pulsar não volta a tentar após uma resposta 4XX (consulte Novas tentativas). Por isso, se o seu segredo estiver mal configurado, os eventos reais são rejeitados e ficam por entregar até o Pulsar os reenviar. Peça um ping e confirme que o seu endpoint devolve 2XX antes de depender dele.

Código de exemplo

Ambos os exemplos leem o segredo de uma variável de ambiente PULSAR_WEBHOOK_SECRET.

import hashlib
import hmac
import os

from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["PULSAR_WEBHOOK_SECRET"].encode("utf-8")


@app.post("/webhooks/pulsar")
def pulsar_webhook():
    signature = request.headers.get("X-Pulsar-Signature", "")

    # Os bytes exatos que o Pulsar enviou, antes de fazer o parsing do JSON.
    body = request.get_data()
    expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return "", 401

    notification = request.get_json()
    if notification["eventType"] == "Ping":
        return "", 200

    # Coloque o trabalho em fila e responda de imediato; consulte "Responda com um 2XX, rapidamente".
    enqueue(notification)
    return "", 202

Pedidos repetidos

A assinatura cobre apenas o corpo, por isso qualquer pessoa que obtenha uma cópia de um pedido, por exemplo de um log, poderia voltar a enviá-lo sem alterações e continuaria a passar a verificação. Proteja-se da mesma forma que trata os duplicados: registe cada eventId que processar e ignore os que já tiver visto.

Não rejeite pedidos só porque eventTimestamp é antigo. As novas tentativas e os reenvios voltam a enviar propositadamente o evento original, com o seu carimbo de data/hora original, possivelmente muito mais tarde.

Nesta página