Exemplos executáveis — assinatura e webhooks

Clientes mínimos, só com biblioteca padrão, que:

  1. assinam chamadas operador → Dracma (pedido canônico + HMAC-SHA256, ver autenticação);
  2. verificam webhooks Dracma → operador (t=, um ou mais v1, janela de 300 s, ver webhooks);
  3. calculam o digest da reconciliação e verificam a assinatura dele (ver reconciliação).

Cada linguagem tem um teste que lê vetores-assinatura.json, cópia fiel dos vetores normativos do contrato, e os reproduz byte a byte. Antes de ir para produção, seu código tem de reproduzir os mesmos vetores, inclusive o negativo.

Os negativos de timestamp não canônico (+ts, 0ts, espaço, 1e9, -0) estão em vetores-assinatura-ts-negativos.json: a API recusa todos com 401 timestamp_out_of_window.

Pasta Conteúdo Teste
go/ pacote dracma (assinatura.go, webhook.go) + cmd/ping (GET /v1/ping assinado, mede o desvio de relógio) cd go && go test ./... && go vet ./...
node/ dracma.ts: TypeScript que o Node ≥ 22.18 executa direto, sem build nem dependência cd node && node --test vetores.test.ts
php/ Dracma.php: classe final, PHP ≥ 8.1, extensões hash e curl cd php && php vetores_test.php
shell/ lib.sh (funções), assinar.sh (curl assinado), verificar-webhook.sh, vetores.sh bash shell/vetores.sh

Executado nesta máquina (última rodada: 2026-09-27, revisão docs-nero)

Exemplo Executado? Runtime Resultado
Go sim go1.27.1 linux/amd64 (o módulo declara go 1.22) go test -count=1 ./...: verde; go vet e go build limpos
Node (TypeScript) sim Node v24.16.0 node --test vetores.test.ts: 5/5 passam
PHP não PHP não instalado nesta máquina código escrito, nunca executado; rode php vetores_test.php antes de usar
Shell sim GNU bash + OpenSSL 3.5.0 (JSON lido com python3, porque não há jq) vetores.sh: 24 verificações, todas passam

Os testes de Go e Node também foram vistos falhar: com a mudança proposital de um byte no código (remover o . de "<t>.", ou acrescentar \n ao fim do pedido canônico), eles ficam vermelhos. O teste de desempate do digest também foi visto vermelho (Go com a ordem de kind invertida; Node sem o desempate).

O que os testes cobrem, em todas as linguagens:

  • os 3 vetores de chamada: sha256_corpo, pedido_canonico e Dracma-Signature idênticos;
  • o webhook válido é aceito;
  • o webhook de rotação é aceito com o segredo atual e também só com o anterior. O 2º v1 do vetor é de fato o HMAC do segredo anterior;
  • o vetor negativo (corpo alterado em 1 byte) é recusado;
  • t= a +301 s ou −301 s do relógio é recusado e a +300 s é aceito;
  • Dracma-Timestamp diferente do t= é recusado;
  • v1 com hex inválido, curto ou vazio não confere e não gera exceção;
  • digest: linhas ordenadas por object_id e, no empate (mesmo objeto em dois itens, ex. deposit e refund), por kind — como o Dracma faz (Go e Node testam o empate; PHP tem a mesma regra, não executada); dia vazio = sha256 de ""; assinatura aceita e digest adulterado recusado;
  • o nonce tem 32 hex minúsculos.

Variáveis de ambiente

Variável Uso
DRACMA_BASE_URL base com /v1. Default: https://sandbox.dracma.example/v1. Em produção, a URL recebida no onboarding
DRACMA_KEY_ID key_test_… (sandbox) ou key_live_… (produção)
DRACMA_KEY_SECRET_FILE caminho de um arquivo com o segredo da chave. Permissão 0400, dono = usuário do serviço, fora do repositório
DRACMA_WEBHOOK_SECRETS_FILE arquivo com os segredos de webhook, um por linha: o atual e, durante a rotação, o anterior

O segredo nunca fica no código, no repositório, em variável de build de frontend nem em log. Os únicos segredos literais aqui são os dos vetores públicos (sk_test_vetor_…, whsec_test_vetor_…), que não abrem nada.

Armadilhas (cada uma já custou um 401 a alguém)

  1. O path assinado inclui /v1. Com base https://sandbox.dracma.example/v1, o path de POST /deposits é /v1/deposits. Os exemplos tiram o path da URL final (req.URL.RequestURI(), url.pathname + url.search, parse_url) e evitam o erro.
  2. Query exatamente como enviada. Não reordene, não decodifique %XX e não troque + por %20 depois de assinar. Monte a URL uma vez e assine essa string. No curl, use --path-as-is.
  3. Assine os mesmos bytes que envia. Serialize o JSON uma vez, guarde os bytes, calcule o sha256 sobre eles e envie esses mesmos bytes. Nunca serialize de novo entre assinar e enviar. No webhook, o HMAC é sobre o corpo cru recebido, antes de JSON.parse/json_decode.
  4. Nonce em hex minúsculo, 32 caracteres, de CSPRNG (crypto/rand, crypto.randomBytes, random_bytes, openssl rand -hex 16). Hex maiúsculo é recusado com 401 invalid_signature. Use um nonce novo por requisição, inclusive ao repetir um POST.
  5. Timestamp em segundos. Milissegundos (Date.now()) caem em 401 timestamp_out_of_window.
  6. Relógio. A janela é de ±300 s. Use NTP (chrony com ≥ 3 fontes). Chame GET /v1/ping periodicamente: go run ./cmd/ping compara server_time com o relógio local e alerta com desvio acima de 5 s.
  7. Repetição de POST. Em 5xx, 429 ou erro de rede, repita com a mesma Idempotency-Key (e timestamp, nonce e assinatura novos). Nunca gere chave nova para "tentar de novo" um POST de dinheiro. 4xx não se repete. Ver erros e idempotência.
  8. Comparação em tempo constante. Os exemplos usam hmac.Equal, crypto.timingSafeEqual (com checagem de tamanho) e hash_equals. O lib.sh do shell não tem comparação em tempo constante e passa o segredo como argumento do openssl, que fica visível em ps. Use o shell só para depurar, nunca em produção.
  9. A verificação da assinatura não basta. Ela cobre só os itens 1–3 do webhook. Depois: Dracma-Event-Id igual ao id do corpo (senão 400), dedup por Dracma-Event-Id e fetch-back antes de mover dinheiro. Ver webhooks.

Uso rápido

# Go: ping assinado (sandbox)
cd go && DRACMA_KEY_ID=key_test_SEU_ID DRACMA_KEY_SECRET_FILE=/etc/seu-servico/dracma.key go run ./cmd/ping

# Shell: ver os headers que seriam enviados, sem chamar a rede
SO_HEADERS=1 DRACMA_KEY_ID=key_test_SEU_ID DRACMA_KEY_SECRET_FILE=/etc/seu-servico/dracma.key \
  bash shell/assinar.sh POST /deposits corpo.json dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b

# Shell: conferir um webhook capturado
DRACMA_WEBHOOK_SECRETS_FILE=/etc/seu-servico/dracma-webhook.keys \
  bash shell/verificar-webhook.sh corpo.json "$TS" "$SIG"
// Node: criar um depósito (bytes exatos + Idempotency-Key = external_id)
import { chamar, configDoAmbiente } from "./dracma.ts";
const corpo = JSON.stringify({ external_id: "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", /* … */ });
const r = await chamar(configDoAmbiente(), "POST", "/deposits", corpo, "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b");