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

<https://unifokal.com/blog/assinatura-de-mensagem-http-rfc-9421>

Norma explicada · UNIFOKAL · 18 de setembro de 2026 · 8 min de leitura · Produto e integração

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](https://unifokal.com/blog/mtls-e-assinatura-de-requisicao).

## 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](https://unifokal.com/blog/idempotencia-em-apis), e o controle é a chave de evento no seu lado, como em [janela anti-replay em webhook](https://unifokal.com/blog/janela-anti-replay-em-webhook) e no desenho de [webhooks seguros](https://unifokal.com/blog/webhooks-seguros-em-kyc). Na UNIFOKAL, por exemplo, a [documentação pública](https://unifokal.com/docs) 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

- [RFC 9421: HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421.html), IETF, fevereiro de 2024, Proposed Standard.
- [RFC 9530: Digest Fields](https://www.rfc-editor.org/rfc/rfc9530.html), IETF, fevereiro de 2024, Proposed Standard, obsoleta a RFC 3230.
- [RFC 8941: Structured Field Values for HTTP](https://www.rfc-editor.org/rfc/rfc8941.html), IETF, fevereiro de 2021, obsoletada pela RFC 9651.
- [RFC 9651: Structured Field Values for HTTP](https://www.rfc-editor.org/rfc/rfc9651.html), IETF.
- [RFC 3230: Instance Digests in HTTP](https://www.rfc-editor.org/rfc/rfc3230.html), IETF, obsoletada pela RFC 9530.
