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 faz | O que quebra sem isso |
|---|---|---|
dedupe | Guarda 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ógio | Recusa 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 chaves | Busca 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. |
sondagem | Separa 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.parseseguido deJSON.stringifytroca 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_idfor 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.