Como implementar verificação de assinatura SHA256 em webhooks
Boa parte de proteger webhooks envolve verificar a origem e o destino do webhook, além de validar o payload. Em um artigo anterior, analisamos diferentes estratégias de autenticação de webhooks, incluindo seus pontos fortes e limitações. Depois de avaliar as estratégias disponíveis, a verificação de assinatura se destaca como a forma mais robusta de proteção para webhooks.
Neste artigo vou falar sobre a teoria da verificação de assinatura e como ela funciona, e demonstrar como usá-la para proteger os nossos webhooks. Também veremos exemplos de código para implementar verificação de assinatura SHA256 nas linguagens de programação mais populares.
Proteja os seus webhooks automaticamente.
A Hookdeck verifica assinaturas de webhook de mais de 160 provedores — para você focar no processamento, não na autenticação.
Como funciona a verificação de assinatura
Antes de começar, vamos discutir o processo de verificação de webhooks usando assinaturas criptográficas.
A verificação de assinatura usa o Hash-based Message Authentication Code (HMAC) para autenticar e validar webhooks. Um HMAC é calculado com uma chave secreta e uma função de hash criptográfica como SHA-2 ou SHA-3. O HMAC resultante, que se torna a assinatura do webhook, é então usado para autenticar o webhook e validar o seu payload.
A robustez da segurança oferecida por um HMAC depende de 3 fatores:
- A força criptográfica da função de hash usada.
- O tamanho da saída do hash (224, 256, 384 ou 512 bits de digest).
- O tamanho e a qualidade (comprimento e caracteres) da chave.
A imagem abaixo mostra como a verificação de assinatura acontece, nos seguintes passos:

- Uma chave secreta é conhecida tanto pelo produtor quanto pelo consumidor do webhook. Esse segredo costuma ser chamado de chave de assinatura do webhook.
- Ao enviar um webhook, o produtor usa essa chave e um algoritmo de hash HMAC (por exemplo, SHA256) para criar um hash criptográfico do payload do webhook. Esse hash criptográfico é a assinatura única do webhook.
- A assinatura é enviada em um header customizado junto com a requisição do webhook. Às vezes o tipo de algoritmo usado também é enviado.
- Quando o webhook chega à URL de destino, a aplicação receptora pega o payload e usa a chave secreta e o algoritmo criptográfico para calcular a assinatura.
- A assinatura calculada é então comparada com a que o produtor enviou no header customizado. Se houver correspondência, a requisição é válida; caso contrário, o webhook é rejeitado.
Escolhendo um algoritmo criptográfico
Já vimos que, para criar uma assinatura HMAC para verificação de webhook, precisamos de uma chave secreta, um algoritmo de hash e o payload do webhook.
A chave secreta é um valor compartilhado entre produtor e consumidor do webhook. Muitas vezes é uma API key/secret que você obtém no dashboard do provedor de webhook (a Stripe, por exemplo) ou, alternativamente, você pode informar uma string aleatória como segredo ao configurar o webhook (como acontece no GitHub).
Quanto ao algoritmo de hash, há várias opções, mas HMACs são majoritariamente calculados com funções de hash das séries SHA-2 ou SHA-3. Isso porque funções de hash criptográficas mais antigas das séries MD5 e SHA-1 já estão criptograficamente quebradas. Além disso, outras funções da série MD (MD2, MD4 etc.) são fracas, altamente comprometidas ou criptograficamente quebradas.
Hoje, praticamente todos os grandes provedores de webhook que encontrei usam a função de hash SHA-256, da série SHA-2, para verificação de assinatura dos seus webhooks. Entre esses provedores estão Stripe, GitHub, CircleCI, Zendesk, Shopify e Okta. Para maior segurança, provedores como a Stripe proíbem o uso de funções de hash de qualidade inferior, a fim de prevenir ataques de downgrade.
Usando verificação de assinatura HMAC para autenticar e validar webhooks
Agora que entendemos bem como a verificação de assinatura funciona, vamos ver como implementá-la.
Para autenticar os nossos webhooks com verificação de assinatura, vamos seguir alguns passos obrigatórios, que implementei em diferentes linguagens abaixo. Vamos supor que o header customizado que carrega a assinatura do webhook seja X-Signature-SHA256. Os passos necessários são:
- Obter o corpo bruto (raw body) da requisição;
- Extrair o valor do header de assinatura;
- Calcular o HMAC do corpo bruto usando a função de hash SHA-256 e o segredo; e
- Comparar o HMAC calculado com o que foi enviado no header
X-Signature-SHA256, garantindo que os dois valores usem a mesma codificação.
Observe que o nome do header de assinatura é diferente em cada provedor de webhook, então consulte a documentação do seu provedor para saber o nome real desse header.
Por fim, vamos ver como fica a autenticação dos nossos webhooks com verificação de assinatura em algumas das linguagens mais populares. Entre em contato se precisar de um exemplo em outra linguagem.
Exemplo em Node.js
const express = require("express");
const routes = require("./routes");
const bodyParser = require("body-parser");
const crypto = require("crypto");
// App
const app = express();
const sigHeaderName = "X-Signature-SHA256";
const sigHashAlg = "sha256";
const sigPrefix = ""; //set this to your signature prefix if any
const secret = "my_webhook_api_secret";
//Get the raw body
app.use(
bodyParser.json({
verify: (req, res, buf, encoding) => {
if (buf && buf.length) {
req.rawBody = buf.toString(encoding || "utf8");
}
},
}),
);
//Validate payload
function validatePayload(req, res, next) {
if (req.get(sigHeaderName)) {
//Extract Signature header
const sig = Buffer.from(req.get(sigHeaderName) || "", "utf8");
//Calculate HMAC
const hmac = crypto.createHmac(sigHashAlg, secret);
const digest = Buffer.from(
sigPrefix + hmac.update(req.rawBody).digest("hex"),
"utf8",
);
//Compare HMACs
if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) {
return res.status(401).send({
message: `Request body digest (${digest}) did not match ${sigHeaderName} (${sig})`,
});
}
}
return next();
}
app.use(validatePayload);
app.use("/", routes);
const port = process.env.PORT || "1337";
app.set("port", port);
app.listen(port, () => console.log(`Server running on localhost:${port}`));
Exemplo em PHP
<?php
define('API_SECRET_KEY', 'my_webhook_api_secret');
function verify_webhook($data, $hmac_header)
{
# Calculate HMAC
$calculated_hmac = base64_encode(hash_hmac('sha256', $data, API_SECRET_KEY, true));
return hash_equals($hmac_header, $calculated_hmac);
}
# Extract the signature header
$hmac_header = $_SERVER['X-Signature-SHA256'];
# Get the raw body
$data = file_get_contents('php://input');
# Compare HMACs
$verified = verify_webhook($data, $hmac_header);
error_log('Webhook verified: '.var_export($verified, true));
if ($verified) {
# Do something with the webhook
} else {
http_response_code(401);
}
?>
Exemplo em Python (Flask)
from flask import Flask, request, abort
import hmac
import hashlib
import base64
app = Flask(__name__)
API_SECRET_KEY = 'my_webhook_api_secret'
def verify_webhook(data, hmac_header):
# Calculate HMAC
digest = hmac.new(API_SECRET_KEY.encode('utf-8'), data, digestmod=hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest)
return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8'))
@app.route('/webhook', methods=['POST'])
def handle_webhook():
# Get raw body
data = request.get_data()
# Compare HMACs
verified = verify_webhook(data, request.headers.get('X-Signature-SHA256'))
if not verified:
abort(401)
# Do something with the webhook
return ('', 200)
Exemplo em Ruby
require 'rubygems'
require 'base64'
require 'openssl'
require 'sinatra'
require 'active_support/security_utils'
API_SECRET_KEY = 'my_webhook_api_secret'
helpers do
def verify_webhook(data, hmac_header)
# Calculate HMAC
calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', API_SECRET_KEY, data))
ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
end
end
post '/' do
request.body.rewind
# Get raw body
data = request.body.read
# Compare HMACs
verified = verify_webhook(data, env["X-Signature-SHA256"])
halt 401 unless verified
# Do something with the webhook
end
Exemplo em Go
package verifywebhook
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"strings"
)
// Hook is an inbound webhook
type Hook struct {
Signature string
Payload []byte
}
const signaturePrefix = "" ////set this to your signature prefix if any
const signatureLength = // Your signature length = len(SignaturePrefix) + len(hex(sha1))
func signBody(secret, body []byte) []byte {
// Calculate HMAC
computed := hmac.New(sha1.New, secret)
computed.Write(body)
return []byte(computed.Sum(nil))
}
func (h *Hook) SignedBy(secret []byte) bool {
if len(h.Signature) != signatureLength || !strings.HasPrefix(h.Signature, signaturePrefix) {
return false
}
actual := make([]byte, 20)
hex.Decode(actual, []byte(h.Signature[5:]))
return hmac.Equal(signBody(secret, h.Payload), actual)
}
func (h *Hook) Extract(dst interface{}) error {
return json.Unmarshal(h.Payload, dst)
}
func New(req *http.Request) (hook *Hook, err error) {
hook = new(Hook)
if !strings.EqualFold(req.Method, "POST") {
return nil, errors.New("Unknown method!")
}
// Extract signature
if hook.Signature = req.Header.Get("X-Signature-SHA256"); len(hook.Signature) == 0 {
return nil, errors.New("No signature!")
}
// Get raw body
hook.Payload, err = ioutil.ReadAll(req.Body)
return
}
func Parse(secret []byte, req *http.Request) (hook *Hook, err error) {
hook, err = New(req)
//Compare HMACs
if err == nil && !hook.SignedBy(secret) {
err = errors.New("Invalid signature")
}
return
}
Usando o pacote Go para verificação de assinatura
Para uma *http.Request recebida representando um webhook assinado com um secret, use verifywebhook para validar e fazer o parse do conteúdo, como mostrado abaixo.
secret := []byte("my_webhook_api_secret")
webhook, err := verifywebhook.Parse(secret, req)
Exemplo em Java
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
class SignatureVerificationHandler implements HttpHandler {
private String encodingAlgorithm = "HmacSHA256";
private String secretKey = "someSecretKeyThatShouldBeSecure";
private String headerThatContainsSignature = "X-Signature-SHA256";
private boolean verifySignature(String payload, String signature) throws NoSuchAlgorithmException, InvalidKeyException {
var sha256_HMAC = Mac.getInstance(encodingAlgorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), encodingAlgorithm);
sha256_HMAC.init(secretKeySpec);
byte[] hash = sha256_HMAC.doFinal(payload.getBytes());
String message = Base64.getEncoder().encodeToString(hash);
System.out.println("Payload : "+ payload);
System.out.println("Message : "+ message);
System.out.println("Signature : "+ signature);
return message.equals(signature);
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
Headers headers = httpExchange.getRequestHeaders();
String signature = headers.getFirst(headerThatContainsSignature);
String payload = readBody(httpExchange);
try {
boolean isValidMessage = verifySignature(payload, signature);
if (isValidMessage){
System.out.println("Got valid signature, returning 200");
returnWithStatus(httpExchange, 200);
return;
}
} catch (Exception e) {
System.out.println("Exception encountered, return 500 server error");
returnWithStatus(httpExchange, 500);
return;
}
System.out.println("Invalid signature, returning 401 Unauthorized");
returnWithStatus(httpExchange, 401);
}
private String readBody(HttpExchange httpExchange) throws IOException {
BufferedInputStream stream = new BufferedInputStream(httpExchange.getRequestBody());
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
for (int result = stream.read(); result != -1; result = stream.read()) {
byteBuffer.write((byte) result);
}
return byteBuffer.toString(StandardCharsets.UTF_8);
}
private void returnWithStatus(HttpExchange httpExchange, int httpStatusCode) throws IOException {
httpExchange.sendResponseHeaders(httpStatusCode, 0);
httpExchange.getResponseBody().close();
}
}
Como a Hookdeck ajuda
Implementar verificação de assinatura SHA-256 corretamente é simples para um provedor de webhook, mas vira um peso de manutenção no momento em que você integra mais três, cinco ou cinquenta. Cada um tem os próprios nomes de header, codificação de payload e particularidades de assinatura. Uma única divergência significa rejeitar webhooks legítimos ou, pior, aceitar webhooks forjados (e o peso de manutenção cresce a cada novo provedor que você integra).
O Event Gateway da Hookdeck cuida da verificação de assinatura de mais de 160 provedores de webhook por padrão. Você seleciona o provedor, informa o segredo de assinatura, e toda requisição recebida é verificada na borda antes de chegar à sua aplicação, com as requisições não verificadas sendo rejeitadas automaticamente. Isso significa que os seus handlers podem confiar em cada payload que chega, sem que você precise manter lógica de verificação HMAC específica por provedor. Comece a usar a Hookdeck e tire a autenticação de webhooks das suas costas em minutos.
Conclusão
Neste artigo mergulhamos nos detalhes da verificação de assinatura e no que a torna tão poderosa e eficaz para proteger os nossos webhooks. Também vimos exemplos de código nas linguagens mais usadas na web para processar webhooks, com uma implementação prática do processo de verificação.
A verificação de assinatura pode ser combinada com outros controles de segurança do nosso checklist para garantir a proteção ideal dos seus webhooks. Um desses controles é a criptografia SSL: ela garante que toda a sua comunicação seja criptografada, porque, embora os seus dados sejam codificados, a verificação de assinatura não criptografa os dados do webhook.
Bom código!
Infraestrutura de webhooks, gerenciada para você
A Hookdeck cuida da ingestão, entrega, observabilidade e recuperação de erros — para que você não precise.