Assinatura de mensagem HTTP: o que a RFC 9421 padroniza no webhook

Norma explicadaUNIFOKAL8 min de leituraProduto e integração
Ver em Markdown

Cada fornecedor inventa seu HMAC de webhook. A RFC 9421 padroniza o que é assinado, como se monta a base e o que o consumidor deve validar, na ordem certa.

Quem integra com três fornecedores de webhook aprende três esquemas de assinatura diferentes. Um manda o HMAC (código de autenticação de mensagem baseado em hash) em hexadecimal num cabeçalho próprio. O outro manda em base64, com o timestamp colado no corpo por um ponto. O terceiro manda uma lista separada por vírgula, porque embutiu a rotação de chave no formato. Nenhum está errado, e é esse o problema: cada integração exige um verificador novo para a mesma pergunta.

Desde fevereiro de 2024 existe um padrão para ela. A RFC 9421, HTTP Message Signatures, publicada pela IETF (Internet Engineering Task Force) como Standards Track com status de Proposed Standard, define um mecanismo para assinar componentes de uma mensagem HTTP e transportar a assinatura em dois campos de cabeçalho fixos. Ela não obsoleta nenhuma RFC anterior: é o resultado do grupo httpbis, que transformou em norma uma família de desenhos proprietários.

Este texto é para quem está do lado do consumidor, recebendo evento e decidindo o que validar. A tese é curta: assinar só o corpo responde metade da pergunta, e a metade que falta é onde mora o ataque.

O que a RFC 9421 assina, e por que não é o corpo

A primeira inversão de expectativa: a RFC 9421 não assina o corpo. Ela assina uma lista escolhida de componentes, e o corpo, por si só, não é um componente elegível. A seção 7.2.8 diz isso com todas as letras e aponta a saída, que veremos adiante.

Os componentes vêm de dois lugares. O primeiro são campos HTTP comuns, referenciados pelo nome em minúsculas. O segundo são os componentes derivados, marcados com arroba porque descrevem partes da mensagem que não são cabeçalhos: @method, @target-uri (a URI de destino completa, onde URI é o identificador uniforme de recurso), @authority (que a RFC recomenda no lugar de assinar o cabeçalho Host), @scheme, @request-target, @path, @query, @query-param e @status.

Com os componentes escolhidos, o signatário monta a base de assinatura, em inglês signature base: uma string ASCII com uma linha por componente, no formato nome entre aspas, dois-pontos, espaço e valor canonicalizado. A última linha é obrigatória e especial, "@signature-params", e carrega a lista ordenada dos componentes cobertos com seus parâmetros. É sobre essa string que o algoritmo criptográfico roda. A ordem é imutável: quem verifica reconstrói a base a partir do que recebeu, e qualquer permutação produz outra string.

Os dois campos e os parâmetros que decidem o replay

O transporte usa dois cabeçalhos, ambos dicionários no formato da RFC 8941, Structured Field Values for HTTP, a gramática comum de campos HTTP estruturados (publicada em 2021 como Proposed Standard e desde então obsoletada pela RFC 9651).

Signature-Input carrega os metadados: um rótulo, a lista de componentes cobertos e os parâmetros. Signature carrega, sob o mesmo rótulo, a assinatura como sequência de bytes. Uma mensagem pode carregar várias assinaturas com rótulos distintos, o que resolve o caso do proxy reverso que assina por cima da assinatura de origem. Os parâmetros da seção 2.3 são seis, e três existem por causa de replay:

  • created: instante da criação da assinatura, em segundos Unix, do tipo Integer, sem subsegundo. A RFC recomenda incluir.
  • expires: instante de expiração, no mesmo formato. A RFC lembra que expiração é uma dica do signatário, e que quem verifica pode recusar antes disso.
  • nonce: valor único, aleatório, gerado para aquela assinatura.
  • keyid: identificador do material de chave.
  • alg: nome do algoritmo no registro "HTTP Signature Algorithms" da IANA, a autoridade de números atribuídos da internet, cujo conteúdo inicial traz rsa-pss-sha512, rsa-v1_5-sha256, hmac-sha256, ecdsa-p256-sha256, ecdsa-p384-sha384 e ed25519.
  • tag: rótulo da aplicação, para identificar assinaturas relevantes a um protocolo.

A seção 7.2.2, sobre Signature Replay, é o coração do argumento. Ela nota que duas mensagens HTTP diferentes podem validar contra a mesma assinatura, e que o caso extremo é uma assinatura que não cobre componente nenhum: interceptada, ela pode ser colada em qualquer mensagem. A contramedida tem três camadas: cobrir porções suficientes da mensagem para diferenciá-la das outras, usar nonce para que o verificador detecte repetição, e usar created mais expires para limitar a utilidade de uma assinatura capturada.

Repare no que isso significa para o webhook que só assina o corpo. Se @method e @target-uri não entram na base, a mesma assinatura vale para o mesmo corpo entregue em qualquer endpoint seu, e um evento capturado no endpoint de teste pode ser reapresentado no de produção. Assinar a URL e o método fecha essa classe inteira, e é a diferença estrutural entre o padrão e o HMAC caseiro. A mesma lógica vale na direção oposta, quando é você que chama o fornecedor: veja mTLS e assinatura de requisição.

O corpo entra pela RFC 9530

Quem cobre o corpo é a RFC 9530, Digest Fields, publicada em fevereiro de 2024, também Proposed Standard, e que obsoleta a RFC 3230 junto com os campos Digest e Want-Digest.

Ela define Content-Digest, um dicionário cuja chave é o algoritmo de hash e cujo valor é a sequência de bytes do digest calculado sobre o conteúdo efetivo da mensagem, e Repr-Digest, calculado sobre os dados da representação selecionada. Para webhook, o campo relevante é Content-Digest. O registro de algoritmos marca sha-256 e sha-512 como Active, e md5, sha (SHA-1), adler e crc32c como Deprecated. A composição é direta: calcule o campo e inclua o nome dele na lista de componentes cobertos, e o corpo fica preso à assinatura por transitividade.

Aqui está a armadilha que a RFC 9421 sublinha na seção 7.2.8: quem verifica precisa validar o valor do Content-Digest contra o conteúdo realmente recebido, e não apenas conferir a assinatura, que cobre o valor do campo e não os bytes do corpo. Um atacante que troque o conteúdo e deixe o campo intacto passa sem problema. Conferir a assinatura e não recalcular o hash é a falha silenciosa mais fácil nessa arquitetura.

A ordem de validação do consumidor

A RFC 9421 define o algoritmo de verificação na seção 3.2 e, na 3.2.1, diz que a aplicação deve impor requisitos próprios e falhar quando não forem atendidos. Traduzido para a mesa de quem integra:

  1. Leia os bytes crus do corpo antes de qualquer interpretação e guarde-os.
  2. Localize a assinatura aplicável em Signature e o Signature-Input de mesmo rótulo. Sem par, erro.
  3. Confira a lista de componentes cobertos contra a sua política. Este é o passo que quase todo mundo esquece: se você não exige @method, @target-uri e content-digest na lista, o remetente pode assinar uma lista vazia e você aceita.
  4. Confira os parâmetros: idade máxima a partir de created, recusa após expires, unicidade de nonce, e tag esperado quando a aplicação define um.
  5. Resolva keyid para material de chave que você conhece e confia. Chave desconhecida é falha, nunca aceitação com aviso.
  6. Decida o algoritmo a partir do seu conjunto permitido. A RFC exige falhar quando a configuração estática, a chave e o alg discordam.
  7. Reconstrua a base de assinatura, verifique, e recalcule o hash do corpo para comparar com Content-Digest.
  8. Só então converta o corpo em objeto e processe.

Anti-replay e processamento duplicado são problemas distintos. O nonce impede que a mesma assinatura seja aceita duas vezes; não impede que o fornecedor reenvie um evento legítimo depois de um timeout, com assinatura nova e válida. Essa segunda metade é idempotência, e o controle é a chave de evento no seu lado, como em janela anti-replay em webhook e no desenho de webhooks seguros. Na UNIFOKAL, por exemplo, a documentação pública descreve a verificação do webhook que o integrador implementa.

Perguntas frequentes

Preciso trocar meu HMAC atual pela RFC 9421 hoje?

Não necessariamente. O ganho imediato não está no formato do campo, está em quais componentes entram no material assinado. Se o seu esquema já amarra método, URL de destino, timestamp e hash do corpo, ele cobre a mesma classe de ataque. Se assina só o corpo, o problema existe independentemente do padrão adotado.

A RFC 9421 substitui TLS ou mTLS?

Não. O TLS (Transport Layer Security) protege o canal entre dois pontos e cai fora assim que há um proxy terminando a conexão. A assinatura de mensagem viaja com a mensagem e sobrevive a intermediários, que é o caso de uso declarado no resumo da RFC. São camadas complementares.

Fontes citadas