Pular para o conteúdo

Assinatura das entregas

Ed25519 sobre os bytes crus, com o Application ID de destino dentro do material assinado. A chave é da plataforma, e é uma só para todos os Apps — é isso que torna o destinatário parte da conta.

Verificar: o SDK faz

import { InteractionVerifier, interactionResponse } from "@trivo/sdk";

const verifier = new InteractionVerifier({
  // OBRIGATÓRIO, e é ele que entra no material assinado.
  applicationId: app.applicationId,
  baseUrl: process.env.TRIVO_BASE_URL!,
});

servidor.post("/trivo", async (req, res) => {
  // O corpo CRU. Nunca `JSON.parse` seguido de `JSON.stringify`:
  // reserializar troca os bytes e a assinatura deixa de fechar.
  const resultado = await verifier.verify({ headers: req.headers, body: req.rawBody });
  if (!resultado.ok) return res.status(401).json({ reason: resultado.reason });

  if (resultado.purpose === "probe") {
    return res.json(interactionResponse.probe(resultado.challenge));
  }
  // O corpo da resposta é um ATALHO para a rota de confirmar — o mesmo JSON,
  // sem uma segunda ida à rede dentro de um prazo de 3 segundos.
  return res.json(interactionResponse.reply("pong"));
});

Por que applicationId é obrigatório. A chave é da plataforma, uma só para todos os Apps. Um App hostil que recebe uma entrega legítima pode reencaminhá-la ao seu endpoint, e ela verifica — a menos que o material assinado carregue o Application ID de destino. O verificador monta o preâmbulo com o id que você configurou, e usa x-trivo-app apenas para escolher entre os ids configurados. Não existe opção para copiar o cabeçalho: se existisse, alguém a usaria.

O SDK também cuida do dedupe por x-trivo-delivery, da janela de tolerância do relógio e da rotação de chaves — inclusive do caso em que o cabeçalho traz duas assinaturas.

O que o verificador cuida por você

O quêO que ele fazO que quebra sem isso
dedupeGuarda os x-trivo-delivery já vistos, com um teto de memória declarado em DEFAULT_DEDUPE_MAX_ENTRIES.Uma entrega repetida vira efeito repetido. Guardar por menos tempo do que a janela que você aceita deixa um buraco entre o fim da sua memória e o fim da sua janela, e é dentro dele que o replay entra.
janela do relógioRecusa fora de DEFAULT_TIMESTAMP_TOLERANCE_SECONDS (300 s), e o valor é configurável.Uma entrega antiga capturada continuaria valendo para sempre. Com NTP disciplinado dá para apertar; sem relógio confiável, a tolerância é o que segura.
rotação de chavesBusca o chaveiro sozinho, aceita as duas assinaturas do período de rotação e recusa key_id desconhecido.Fixar a chave pública no código faz o seu endpoint recusar tudo no dia da rotação — e o desligamento automático o suspende por ele estar certo.
sondagemSepara a prova de vida da entrega real por rótulo próprio, e devolve o desafio em resultado.challenge.Você teria de decidir pelo corpo o que é sondagem — uma decisão de segurança tomada com dado que ainda não foi verificado.

Existe ainda uma carência depois de uma rotação de emergência: enquanto ela vale, a falha de um endpoint é registrada e não conta para o desligamento automático. Um incidente nosso não pode suspender em bloco os Apps que fizeram a coisa certa. Os prazos do desligamento estão em Endpoint de interações.

Quando a verificação recusa

Responda 401 e pare. O motivo da recusa é para o seu log: não o devolva detalhado no corpo, porque quem está sondando o seu endereço aprenderia com ele qual passo falta ajustar.

Recusar não custa nada do seu lado do Trivo — a interação simplesmente expira. Mas o contador de falhas do endpoint conta tudo o que não é 2xx no prazo, então um endpoint que recusa tudo por configuração errada acaba desligado sozinho. Esse é o comportamento certo: o silêncio seria pior, e o aviso por e-mail existe para essa hora.

Referência de transporte

A assinatura no fio, e os cinco passos que o verificador executa

O que viaja no fio, para transparência, depuração e compatibilidade — não é um segundo jeito de construir App. Se você está escrevendo um App, o SDK já faz isto.

Para quem precisa auditar a verificação, depurar uma recusa ou escrever um verificador em outra linguagem. A chave é da plataforma, Ed25519, e é uma só para todos os Apps: GET /api/chaves-de-assinatura (rota pública).

A resposta

{
  "keys": [
    { "key_id": "ed25519-…", "key": "<32 bytes crus em base64url>", "state": "current" },
    { "key_id": "ed25519-…", "key": "…", "state": "previous", "until": "2026-09-07T00:00:00Z" }
  ]
}

state, since e until são para você decidir quando reavisar alguém — não entram na decisão de aceitar. O que decide é: o key_id do cabeçalho está no conjunto publicado, e a assinatura fecha.

Os cinco passos

1. Confira x-trivo-version. Não é a que você implementa: RECUSE.

2. |agora - x-trivo-timestamp| <= 300 s. Fora disso: RECUSE.
   (com NTP disciplinado, pode apertar para 60 s)

3. SELECIONE a sua configuração por x-trivo-app.
   Ele é um ÍNDICE no seu mapa de Apps, e nada mais.
   Não conhece esse Application ID: RECUSE, aqui, antes de qualquer conta.

4. Monte o preâmbulo com o Application ID DA SUA CONFIGURAÇÃO —
   NUNCA com o valor que veio no cabeçalho:

     "trivo-interaction-v1" + "\n" + <o SEU Application ID>
     + "\n" + x-trivo-delivery + "\n" + x-trivo-timestamp + "\n"
     + <os BYTES CRUS do corpo>

5. Verifique Ed25519 sobre esses bytes com a chave do key_id.
   Alguma entrada do cabeçalho fecha? Aceite. Nenhuma? RECUSE.

Detalhes que decidem

  • Bytes crus. Não existe canonicalização de JSON: o Trivo assina os bytes que envia e você verifica os bytes que recebeu. Fazer JSON.parse seguido de JSON.stringify troca os bytes e a assinatura deixa de fechar por um motivo que não é o que parece.
  • O separador é \n, e não há quebra depois do corpo, nem prefixo de comprimento.
  • Rotação. O cabeçalho pode trazer duas assinaturas sobre o mesmo preâmbulo. Aceite se alguma fechar; recuse se nenhum key_id for conhecido, e não "aceite mesmo assim". Teto de 4 entradas.
  • Deduplique por x-trivo-delivery, por pelo menos a janela que você aceita. Guardar por menos deixa um buraco entre o fim da sua memória e o fim da sua janela, e é dentro dele que o replay entra.
  • A sondagem tem rótulo próprio (trivo-probe-v1): assinatura de sondagem não verifica como assinatura de interação, e a separação não depende de você inspecionar o corpo.