O que uma solicitação assinada prova?
A maior parte do tráfego automatizado se identifica com uma user agent string. Essa string é uma declaração, e copiá-la não custa nada. Solicitações assinadas mudam esse cenário. Quando um cliente automatizado assina suas solicitações HTTP com uma chave privada e publica a chave pública correspondente em um local ligado ao seu operador, seu servidor pode verificar que a solicitação foi realmente produzida por alguém que possui essa chave.
O padrão por trás disso é HTTP Message Signatures, publicado como RFC 9421. O grupo de trabalho Web Bot Auth do IETF se baseia nele com convenções para clientes automatizados: como um agent aponta para seu diretório de chaves e quais componentes da solicitação ele deve assinar. Em novembro de 2026, o documento de protocolo do grupo de trabalho é draft-ietf-webbotauth-httpsig-protocol, que define o cabeçalho Signature-Agent, um formato de diretório de chaves baseado em JSON Web Key Sets e uma URI well-known para servi-lo. Vale a pena entender ambos em detalhe, porque a verificação é fácil de errar de forma sutil.
Antes da mecânica, seja preciso sobre o resultado. Uma assinatura válida mostra que a solicitação foi assinada por uma chave associada a um identificador publicado, como o domínio de um operador, e que as partes assinadas da solicitação não foram alteradas. Ela não mostra quem instruiu o agent, se uma pessoa delegou autoridade a ele, nem se a ação é segura. Evidência de identidade e autorização são questões separadas, e sua aplicação responde à segunda. Nosso post sobre o que solicitações assinadas de agentes de IA provam detalha esse limite com exemplos.
Os cabeçalhos envolvidos
Uma solicitação assinada normalmente carrega três cabeçalhos:
Signature-Input(RFC 9421, Seção 4.1) descreve cada assinatura: um rótulo, a lista de componentes cobertos e parâmetros comocreated,expires,keyid,alg,nonceetag.Signature(RFC 9421, Seção 4.2) carrega os bytes da assinatura para cada rótulo, codificados em base64, como um structured field.Signature-Agent(das convenções do Web Bot Auth) aponta para onde as chaves públicas do cliente são publicadas, normalmente uma URL HTTPS no domínio do operador.
Um exemplo simplificado:
GET /api/catalog?page=2 HTTP/1.1
Host: shop.example
Signature-Agent: "https://agent.example"
Signature-Input: sig1=("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"
Signature: sig1=:Base64SignatureBytes==:O exemplo está simplificado para facilitar a leitura. No rascunho atual do grupo de trabalho, Signature-Agent é ele mesmo um structured field indexado por rótulo de assinatura (por exemplo sig1="https://agent.example"), então verifique qual forma sua library de verificação espera.
Etapa 1: Analise os structured fields corretamente
Signature-Input e Signature são HTTP Structured Fields (RFC 9651, que torna obsoleto RFC 8941). Não os analise com divisão por string. Strings entre aspas, parâmetros e listas internas têm gramática precisa, e um parser descuidado cria tanto rejeições falsas quanto lacunas reais de segurança.
Use um parser de structured fields mantido, ou melhor, uma library RFC 9421 mantida que trate parsing e verificação juntos. Implementar a especificação do zero é um bom exercício de aprendizado e uma má escolha para produção. As etapas abaixo descrevem o que uma library assim faz, para que você possa configurá-la corretamente e revisar sua saída.
Etapa 2: Escolha qual assinatura avaliar
Uma solicitação pode carregar mais de um rótulo de assinatura. Selecione o que corresponde à sua policy, por exemplo o rótulo cujo tag é web-bot-auth. Rejeite a solicitação para fins de verificação, e não de atendimento, se:
- nenhuma assinatura tiver os parâmetros de que você precisa,
- o algoritmo não estiver na sua allowlist, por exemplo
ed25519, ou - os componentes cobertos não incluírem o conjunto mínimo exigido.
Um mínimo sensato para clientes automatizados é @method, @authority e @path, além de signature-agent quando esse cabeçalho estiver presente. Sem @authority, uma assinatura capturada para um site poderia ser reutilizada contra outro. Sem @path, ela poderia ser reutilizada entre endpoints. O rascunho do protocolo Web Bot Auth exige que agents cubram @authority ou @target-uri, e alerta que uma assinatura que cobre apenas a autoridade é válida para qualquer método ou path nesse host até expirar.
Etapa 3: Verifique os parâmetros de tempo
Leia created e expires, os parâmetros de tempo definidos em RFC 9421, Seção 2.3. Depois:
- Rejeite assinaturas cujo horário de
createdesteja no futuro além de uma pequena tolerância de clock skew. - Rejeite assinaturas que já tenham passado de
expires. - Se
expiresestiver ausente, imponha sua própria idade máxima com base emcreated.
Janelas curtas de validade limitam por quanto tempo uma solicitação capturada pode ser reutilizada.
Etapa 4: Descubra a chave
O cabeçalho Signature-Agent aponta para o diretório de chaves do operador. Busque-o via HTTPS, e apenas HTTPS. O rascunho do protocolo Web Bot Auth descreve o diretório como um JSON Web Key Set (chaves em formato JWK) servido a partir da http-message-signatures-directory well-known URI nesse origin. Em seguida, encontre a chave cujo identificador corresponde ao parâmetro keyid.
type Jwk = { kid: string; kty: string; crv?: string; x?: string };
const keyCache = new Map<string, { keys: Jwk[]; fetchedAt: number }>();
const KEY_TTL_MS = 60 * 60 * 1000;
export async function getKey(directoryUrl: string, keyId: string): Promise<Jwk | null> {
const url = new URL(directoryUrl);
if (url.protocol !== "https:") return null;
const cached = keyCache.get(url.origin);
const fresh = cached && Date.now() - cached.fetchedAt < KEY_TTL_MS;
const keys = fresh ? cached.keys : await fetchKeys(url.origin);
return keys.find((k) => k.kid === keyId) ?? null;
}
async function fetchKeys(origin: string): Promise<Jwk[]> {
const res = await fetch(`${origin}/.well-known/http-message-signatures-directory`, {
headers: { accept: "application/http-message-signatures-directory+json, application/json" },
signal: AbortSignal.timeout(2000),
});
if (!res.ok) return [];
const body = (await res.json()) as { keys?: Jwk[] };
const keys = Array.isArray(body.keys) ? body.keys : [];
keyCache.set(origin, { keys, fetchedAt: Date.now() });
return keys;
}Regras práticas para descoberta de chaves:
- Armazene em cache as chaves com vida útil limitada para que a verificação não adicione uma chamada de rede a cada solicitação.
- Defina um timeout curto e trate uma busca malsucedida como "não foi possível verificar", nunca como "verificado".
- Restrinja de quais origins você busca se puder ou, no mínimo, bloqueie faixas de endereços privados para evitar server-side request forgery. O rascunho do protocolo lista server-side request forgery nas considerações de segurança exatamente por esse motivo.
- Vincule a identidade ao origin, e não ao nome em texto livre na user agent string. O identificador que você registra é o origin do diretório.
Etapa 5: Reconstrua a base da assinatura
O signatário não assinou a solicitação bruta. Ele assinou uma string canônica chamada signature base (RFC 9421, Seção 2.5), construída a partir dos componentes cobertos na ordem correta, seguida da linha @signature-params. Seu verificador reconstrói a mesma string a partir da solicitação recebida.
Para o exemplo acima, a base fica assim:
"@method": GET
"@authority": shop.example
"@path": /api/catalog
"signature-agent": "https://agent.example"
"@signature-params": ("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"Fique atento aos detalhes que quebram a verificação atrás de proxies:
@authoritydeve ser o host para o qual o cliente fez a chamada. Se um load balancer reescreverHost, use o valor original que ele encaminha.@pathexclui a query string. Se o cliente cobriu@query, reconstrua exatamente como recebido.- Os valores de header são canonicalizados conforme especificado: aparados, com múltiplos valores combinados. Uma boa library faz isso por você.
Etapa 6: Verifique a assinatura
Com a base e a chave pública, a verificação é uma única chamada, seguindo as etapas em RFC 9421, Seção 3.2. Para Ed25519 em Node.js, usando o crypto module integrado:
import { createPublicKey, verify } from "node:crypto";
export function verifyEd25519(jwk: Jwk, base: string, signatureB64: string): boolean {
const key = createPublicKey({ key: jwk as any, format: "jwk" });
return verify(null, Buffer.from(base, "utf8"), key, Buffer.from(signatureB64, "base64"));
}Se a checagem falhar, a solicitação não está verificada. Ela ainda pode ser atendida. Apenas não recebe a basis "verified".
Etapa 7: Evite replay
Uma assinatura válida pode ser capturada e reenviada até expirar. RFC 9421, Seção 7.2.2 trata de signature replay, e o rascunho do protocolo Web Bot Auth remete a ele em vez de adicionar suas próprias regras de nonce. Dois controles fecham a maior parte dessa janela:
- Expiração curta, aplicada na Etapa 3.
- Rastreamento de nonce. Quando um
nonceestiver presente, armazene-o junto com seu identificador de chave até a assinatura expirar, e rejeite um segundo uso.
const seen = new Map<string, number>();
export function acceptNonce(keyId: string, nonce: string, expiresAt: number): boolean {
const id = `${keyId}:${nonce}`;
const now = Date.now() / 1000;
for (const [k, exp] of seen) if (exp < now) seen.delete(k);
if (seen.has(id)) return false;
seen.set(id, expiresAt);
return true;
}Em uma implantação com várias instâncias, mantenha os nonces em um armazenamento compartilhado, como Redis, com um time to live igual à validade restante.
Etapa 8: Registre o resultado com sua basis
A verificação produz evidência, não um veredito. Registre-a em um formato que suas analytics e policies possam usar:
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};Quando a verificação falhar ou não puder ser executada, registre isso também, com o motivo: cabeçalhos ausentes, algoritmo não suportado, chave não encontrada, expirada, replay ou mismatch de assinatura. Esses motivos são valiosos. Um pico em "key not found" vindo de um operador frequentemente significa que ele rotacionou as chaves, e não que está atacando você.
O que você deve registrar da verificação de assinatura?
Registre o origin do operador, o identificador da chave, os componentes cobertos, os parâmetros de tempo, o resultado e o motivo da falha. Não registre os bytes completos da assinatura nem os corpos inteiros das solicitações. Mantenha os logs por tempo suficiente para estudar padrões e por tempo curto o bastante para respeitar seus compromissos de privacidade.
Como uma assinatura verificada deve alterar sua policy?
Quando as solicitações podem ser verificadas, você pode tratá-las de forma diferente de maneira significativa. Nosso post sobre como desenhar policies de app para humanos e agentes de IA cobre o modelo mais amplo de policy:
- Permita que operadores verificados acessem rotas somente de leitura com um rate limit mais alto do que automação não verificada.
- Ofereça respostas estruturadas, como JSON ou Markdown, em vez de páginas HTML completas.
- Mantenha ações de alto impacto atrás da sua autorização normal. Uma assinatura verificada informa qual cliente está chamando, não que ele possa alterar uma conta.
Comece em modo de observação. Meça quanto do seu tráfego automatizado é assinado, quais operadores aparecem e com que frequência a verificação falha, antes de qualquer policy depender disso.
Onde Proof of Actor se encaixa
A verificação de assinatura é uma fonte de evidência entre várias. Proof of Actor a combina com outros sinais e mantém a basis visível em cada resultado, para que um agent verificado nunca seja confundido com um que apenas se declarou. Sua aplicação decide o que acontece em seguida.
Para incorporar isso em um app de produção com nossos engenheiros, candidate-se ao acesso antecipado. Para ver o quadro completo do que a evidência pode e não pode mostrar, leia Detection & Limitations.
Fontes
- RFC 9421: HTTP Message Signatures: o padrão de assinatura e verificação, incluindo parâmetros de assinatura, a signature base e considerações sobre replay
- RFC 9651: Structured Field Values for HTTP: a gramática usada por
Signature-InputeSignature - IETF Web Bot Auth working group: charter e status do trabalho de autenticação de clientes automatizados
- draft-ietf-webbotauth-httpsig-protocol: Internet-Draft que define
Signature-Agent, o diretório de chaves JWKS e sua well-known URI (trabalho em andamento em novembro de 2026) - RFC 7517: JSON Web Key (JWK): o formato de chave usado em diretórios de chaves publicados
- RFC 8615: Well-Known URIs: como paths well-known, como o diretório de chaves, são registrados
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA): o algoritmo Ed25519 usado no exemplo
- Node.js crypto documentation:
createPublicKeyeverifyusados na Etapa 6