Exemplos executáveis — assinatura e webhooks¶
Clientes mínimos, só com biblioteca padrão, que:
- assinam chamadas operador → Dracma (pedido canônico + HMAC-SHA256, ver autenticação);
- verificam webhooks Dracma → operador (
t=, um ou maisv1, janela de 300 s, ver webhooks); - 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_canonicoeDracma-Signatureidê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º
v1do 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-Timestampdiferente dot=é recusado;v1com hex inválido, curto ou vazio não confere e não gera exceção;- digest: linhas ordenadas por
object_ide, no empate (mesmo objeto em dois itens, ex.depositerefund), porkind— 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)¶
- O path assinado inclui
/v1. Com basehttps://sandbox.dracma.example/v1, o path dePOST /depositsé/v1/deposits. Os exemplos tiram o path da URL final (req.URL.RequestURI(),url.pathname + url.search,parse_url) e evitam o erro. - Query exatamente como enviada. Não reordene, não decodifique
%XXe não troque+por%20depois de assinar. Monte a URL uma vez e assine essa string. No curl, use--path-as-is. - 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. - Nonce em hex minúsculo, 32 caracteres, de CSPRNG (
crypto/rand,crypto.randomBytes,random_bytes,openssl rand -hex 16). Hex maiúsculo é recusado com401 invalid_signature. Use um nonce novo por requisição, inclusive ao repetir um POST. - Timestamp em segundos. Milissegundos (
Date.now()) caem em401 timestamp_out_of_window. - Relógio. A janela é de ±300 s. Use NTP (chrony com ≥ 3 fontes). Chame
GET /v1/pingperiodicamente:go run ./cmd/pingcomparaserver_timecom o relógio local e alerta com desvio acima de 5 s. - 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. - Comparação em tempo constante. Os exemplos usam
hmac.Equal,crypto.timingSafeEqual(com checagem de tamanho) ehash_equals. Olib.shdo shell não tem comparação em tempo constante e passa o segredo como argumento doopenssl, que fica visível emps. Use o shell só para depurar, nunca em produção. - A verificação da assinatura não basta. Ela cobre só os itens 1–3 do webhook. Depois:
Dracma-Event-Idigual aoiddo corpo (senão 400), dedup porDracma-Event-Ide 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");