Autenticação HMAC¶
Para: Dev — Conferido em 2026-10-18.
Toda chamada à API /v1 leva uma assinatura HMAC-SHA256 calculada sobre o método, o caminho, o corpo, um carimbo de
tempo e um nonce. Esta página mostra os cabeçalhos, o pedido canônico, o exemplo trabalhado (com os vetores públicos),
a ordem em que o servidor confere e o que cada 401 e 403 significa.
O que é¶
A chave de API tem duas partes: o key_id (key_test_… ou key_live_…, público, vai no cabeçalho) e o
segredo (sk_test_… ou sk_live_…, que nunca sai do seu servidor). O segredo assina; o key_id diz ao Dracma
com qual segredo conferir. De posse de uma assinatura válida, o Dracma sabe qual operação está chamando: tenant e
operação vêm só da chave, nunca do corpo.
A assinatura é a camada de autenticação que vale em toda chamada. Camadas de rede opcionais (mTLS e lista de IPs) estão descritas em Rede, lista de IPs e mTLS.
Não disponível
O Dracma não aplica a lista de IPs às chamadas hoje. A tela de IPs do painel registra a lista, mas as chamadas
não são recusadas por IP. Não construa o seu controle de acesso em cima de 403 ip_not_allowed: ele existe no
catálogo de erros, mas você não deve vê-lo. Veja IPs do conector.
Cada chave também tem escopos: uma chave só de leitura não cria depósito (seção Escopos, abaixo).
Quando usar¶
Em toda chamada a /v1/*, inclusive GET /v1/ping e as rotas /v1/sandbox/*: nenhuma rota de /v1 dispensa
assinatura. O documento OpenAPI não passa pela API; baixe-o em OpenAPI.
Como funciona¶
Os quatro cabeçalhos¶
Dracma-Key-Id: key_test_01J9ABCDEF (key_live_… em operação Real)
Dracma-Timestamp: 1790000000 (segundos Unix, UTC)
Dracma-Nonce: 0123456789abcdef0123456789abcdef
Dracma-Signature: v1=a58fa1956941157aad72536512b2b09f234de3670050c865071ddc56df3b4964
| Cabeçalho | Formato exato aceito |
|---|---|
Dracma-Key-Id |
key_test_ ou key_live_ seguido de 1 a 64 caracteres [A-Za-z0-9] |
Dracma-Timestamp |
só dígitos ASCII, sem sinal, sem espaço e sem zero à esquerda; em segundos (não milissegundos) |
Dracma-Nonce |
exatamente 32 caracteres hexadecimais minúsculos (^[0-9a-f]{32}$), isto é, 16 bytes de gerador criptográfico |
Dracma-Signature |
v1= seguido de 64 hexadecimais minúsculos |
Todo POST leva também Idempotency-Key (Idempotência e repetição).
O prefixo do key_id diz em que tipo de operação a chave vale: key_test_ em operação Teste e key_live_ em
operação Real. A API que atende o host só aceita o prefixo do seu tipo; o contrário volta 401 unknown_key
(Ambientes, tipos de operação e hosts).
Pedido canônico e assinatura¶
O pedido canônico tem seis linhas separadas por \n (LF, byte 0x0A), sem \n no final:
v1
<Dracma-Timestamp>
<Dracma-Nonce>
<MÉTODO em maiúsculas>
<caminho + "?" + query exatamente como enviados, ou só o caminho se não houver query>
<sha256 hexadecimal minúsculo do corpo cru; corpo vazio = sha256 de "">
A assinatura é o HMAC-SHA256 do pedido canônico, com o segredo da chave:
assinatura = hex_minúsculo( HMAC-SHA256( segredo, pedido_canonico ) )
Dracma-Signature: v1=<assinatura>
O segredo é usado como os bytes da string que você recebeu (por exemplo sk_test_vetor_0123456789abcdef,
com o prefixo sk_test_), em UTF-8. Não decodifique de hexadecimal nem de base64.
As cinco armadilhas que mais quebram integração¶
- O caminho inclui
/v1. Se a sua base éhttps://<host>/v1e você chama/deposits, o caminho assinado é/v1/deposits: o caminho completo que chega ao servidor. - A query vai como enviada. Não reordene parâmetros, não decodifique
%XX, não normalize. Monte a URL uma vez e assine a mesma string que o cliente HTTP envia (/v1/deposits?external_id=dep_x&limit=10). Desligue qualquer normalização de caminho do seu cliente (curl --path-as-is). - Assine os bytes que você envia. Serialize o JSON uma vez, calcule o sha256 sobre esses bytes e mande esses mesmos bytes. Reserializar (outra ordem de chaves, outro espaçamento, outro escape de Unicode) muda o hash.
- Nonce hexadecimal minúsculo, de gerador criptográfico.
crypto/rand,crypto.randomBytes,random_bytes,openssl rand -hex 16. NuncaMath.random()nemmath/rand. Um nonce com letra maiúscula é recusado. - Timestamp em segundos.
Date.now()devolve milissegundos: divida por 1000 e trunque.
Janela de tempo, nonce e anti-replay¶
- Janela de ±300 s.
abs(agora do servidor − Dracma-Timestamp)precisa ser no máximo 300 s, comparado em segundos inteiros. - Relógio. Mantenha NTP no host do conector.
GET /v1/pingdevolveserver_time; compare com o seu relógio e alerte se o desvio passar de 5 s. Um relógio que deriva vira, um dia, 100% detimestamp_out_of_window. - Nonce. O Dracma guarda
(key_id, nonce)por 600 s e recusa o par repetido com401 replayed_nonce. Não existe teto de nonces por chave: quem limita o volume é o limite de taxa. - Quando o nonce é consumido. Só depois de a assinatura conferir e de a chave estar utilizável. Uma requisição recusada antes disso (janela, assinatura, chave revogada) não queima o nonce.
- Repetir uma chamada é uma chamada nova. Mesmo depois de um
5xxou de um timeout, a nova tentativa leva novoDracma-Timestamp, novo nonce e nova assinatura, e a mesmaIdempotency-Key. Reusar o nonce dáreplayed_nonce. Passada a verificação de nonce, qualquer recusa seguinte (429,403,400) também o consome.
Exemplo trabalhado (vetor normativo 1)¶
Os valores abaixo vêm de vetores-assinatura.json. São valores de teste
públicos; toda implementação deve reproduzi-los byte a byte.
| Entrada | Valor |
|---|---|
| segredo | sk_test_vetor_0123456789abcdef |
key_id |
key_test_vetor |
| timestamp | 1790000000 |
| nonce | 0123456789abcdef0123456789abcdef |
| método | POST |
| caminho | /v1/deposits |
| corpo | {"external_id":"dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b","player":{"external_ref":"u-1","document":{"type":"CPF","number":"12345678909","country":"BR"}},"amount":{"amount":"5000","currency":"BRL"},"method":"pix"} |
Passo 1, sha256 do corpo cru:
fed01c42dbe44bee43439f86a259262975544b122ac57e75977fb5520bbb6edb
Passo 2, pedido canônico (seis linhas, sem LF final):
v1
1790000000
0123456789abcdef0123456789abcdef
POST
/v1/deposits
fed01c42dbe44bee43439f86a259262975544b122ac57e75977fb5520bbb6edb
Passo 3, HMAC-SHA256 com o segredo:
Dracma-Signature: v1=a58fa1956941157aad72536512b2b09f234de3670050c865071ddc56df3b4964
Conferência em uma linha de shell:
printf 'v1\n1790000000\n0123456789abcdef0123456789abcdef\nPOST\n/v1/deposits\nfed01c42dbe44bee43439f86a259262975544b122ac57e75977fb5520bbb6edb' \
| openssl dgst -sha256 -hmac 'sk_test_vetor_0123456789abcdef'
# SHA2-256(stdin)= a58fa1956941157aad72536512b2b09f234de3670050c865071ddc56df3b4964
Os outros dois vetores cobrem os casos de borda:
| Vetor | Método e caminho | Corpo | O que prova | Assinatura |
|---|---|---|---|---|
| 2 | GET /v1/deposits?external_id=dep_x&limit=10 |
vazio (sha256 de "", começa com e3b0c442) |
a query entra como enviada; corpo vazio também é assinado | v1=83b5e8b97bccfc97db1fdaa7f847b2a229a0835b88ce95032ab7c8f951487316 |
| 3 | POST /v1/withdrawals/wdr_01J9ZQ4/cancel |
vazio | POST sem corpo também assina o sha256 de "" |
v1=64df5c1a75e4cfc367e165f55be0d6b50678848069f96abb27a4f2559abdae5d |
O arquivo vetores-assinatura-ts-negativos.json traz timestamps
fora da forma canônica (+1790000000, 01790000000, com espaço, em notação científica) que o Dracma recusa todos com
401 timestamp_out_of_window. Os clientes de Exemplos (Go, Node/TypeScript, PHP e shell) têm
testes que reproduzem os vetores.
Pseudocódigo¶
função chamar(método, caminho_com_query, corpo_bytes, idempotency_key?):
ts = str(floor(agora_unix_segundos()))
nonce = hex_minusculo(csprng(16))
canon = join("\n", ["v1", ts, nonce, upper(método), "/v1" + caminho_com_query, sha256_hex(corpo_bytes)])
sig = "v1=" + hex(hmac_sha256(segredo, canon))
headers = {Dracma-Key-Id: key_id, Dracma-Timestamp: ts, Dracma-Nonce: nonce, Dracma-Signature: sig}
se método == POST: headers[Idempotency-Key] = idempotency_key
envia EXATAMENTE corpo_bytes para base_host + "/v1" + caminho_com_query, com timeout de 10 s
Ordem de verificação¶
O servidor confere nesta ordem, e o primeiro problema encontrado é o que volta. Isso ajuda a diagnosticar: um 403
significa que a autenticação já passou.
| # | Verificação | Falha |
|---|---|---|
| 1 | Corpo de até 64 KiB | 413 validation_failed (errors[0].reason = too_large) |
| 2 | Limite de taxa por IP de origem, antes de autenticar | 429 rate_limited + Retry-After |
| 3 | Dracma-Key-Id bem formado e com o prefixo do tipo de operação deste host. Decidido só pelo cabeçalho, sem consultar a chave |
401 unknown_key |
| 4 | Dracma-Timestamp na forma canônica e dentro de ±300 s |
401 timestamp_out_of_window |
| 5 | Dracma-Nonce no formato (32 hexadecimais minúsculos) |
401 invalid_signature |
| 6 | A chave existe e a assinatura confere. Chave inexistente e assinatura errada dão a mesma resposta | 401 invalid_signature; depois de 20 falhas por minuto do mesmo IP, 429 rate_limited |
| 7 | Só com assinatura válida: a chave está ativa (não revogada) | 401 unknown_key |
| 8 | Só com assinatura válida: IP de origem na lista da operação | 403 ip_not_allowed (a lista não é aplicada hoje) |
| 9 | Só então o nonce é gravado; o par (key_id, nonce) já visto nos últimos 600 s |
401 replayed_nonce |
| 10 | Operação ativa | 403 operation_suspended |
| 11 | Limite de taxa por chave e por operação (e o de listagem) | 429 rate_limited + Retry-After |
| 12 | O escopo da chave cobre a rota | 403 insufficient_scope |
| 13 | Idempotency-Key válida em todo POST |
400 validation_failed (field: Idempotency-Key, reason: required) |
| 14 | Operação Teste com a queda simulada ligada (/sandbox/outage), em rota fora de /sandbox/* |
503 service_unavailable + Retry-After: 5 |
Depois disso vêm a validação do corpo (400) e as regras da rota.
- Sem oráculo de existência. Quem não prova a posse do segredo não descobre se uma chave existe nem se foi
revogada.
401 unknown_keypor chave revogada só aparece depois de a assinatura de uma chave real conferir. - Cabeçalho ausente.
Dracma-Key-Idausente viraunknown_key; timestamp ausente viratimestamp_out_of_window; nonce ou assinatura ausentes viraminvalid_signature. - O erro nunca traz a assinatura esperada nem o pedido canônico calculado pelo servidor. Para depurar, compare o seu canônico com os vetores.
- Dependência fora. Se o armazenamento de nonces, o cofre de segredos ou o limitador estiverem fora, o servidor
responde
503 service_unavailablecomRetry-After: 1. Nunca aceita "sem checar". Repita com backoff. - Rota inexistente. A autenticação vem antes do roteamento: uma rota errada com assinatura errada devolve
401, não404.
Escopos¶
Cada chave tem de um a quatro escopos. A regra é deny-by-default: método ou rota não mapeados voltam 403
insufficient_scope.
| Rota | Escopo exigido |
|---|---|
GET /v1/ping |
nenhum (qualquer chave válida) |
Qualquer outro GET ou HEAD, inclusive GET /sandbox/webhooks |
read |
POST /deposits, POST /deposits/{id}/cancel |
deposits:write |
POST /sandbox/deposits/{id}/pay, …/expire, …/refund, …/chargeback e POST /sandbox/bank-transfers/incoming |
deposits:write |
POST /withdrawals, POST /withdrawals/{id}/cancel, POST /withdrawals/{id}/recall |
withdrawals:write |
POST /sandbox/withdrawals/{id}/{ação} |
withdrawals:write |
POST /rules/change-requests |
rules:propose |
POST /fx/quotes, POST /sandbox/outage, POST /sandbox/webhooks/redeliver |
deposits:write ou withdrawals:write |
O painel mostra os escopos com rótulos em português. A criação de chave marca read por padrão:
| Rótulo no painel | Escopo |
|---|---|
| Ler dados | read |
| Criar depósitos | deposits:write |
| Pedir saques | withdrawals:write |
| Propor mudança de regra | rules:propose |
Escopos mínimos por componente. Duas chaves com escopos menores são melhores que uma chave com tudo:
| Componente do conector | Escopos |
|---|---|
| Serviço que cria e cancela depósitos | deposits:write e read (o fetch-back é GET) |
| Serviço que pede saques e paga | withdrawals:write e read |
| Job de eventos, conciliação e saldos | read |
| Ferramenta interna de propor regras | rules:propose (e read, para o GET /rules que dá o ETag) |
A criação, a revelação do segredo (uma única vez), a revogação e a troca de chaves são feitas no painel, com aprovação de outra pessoa: veja Chaves de API. Uma operação tem no máximo 2 chaves ativas, o que permite trocar sem parada: crie a nova, faça o deploy do conector, confira o "último uso" no painel e revogue a antiga. A revogação vale em até 60 s em todos os nós.
Exemplos¶
Ping assinado, com curl e as funções de exemplos/shell/lib.sh. O host é o da sua
operação (Ambientes, tipos de operação e hosts):
source exemplos/shell/lib.sh
ts="$(date +%s)"; nonce="$(openssl rand -hex 16)"
sig="$(dracma_assinar "$(cat "$DRACMA_KEY_SECRET_FILE")" "$ts" "$nonce" GET /v1/ping)"
curl -sS --path-as-is --max-time 10 \
-H "Dracma-Key-Id: $DRACMA_KEY_ID" -H "Dracma-Timestamp: $ts" \
-H "Dracma-Nonce: $nonce" -H "Dracma-Signature: $sig" \
"$DRACMA_BASE_URL/ping"
DRACMA_BASE_URL termina em /v1. O segredo fica em arquivo com permissão 0400, fora do repositório; o lib.sh é
para depurar, e em produção você usa a biblioteca do seu cliente.
Erros e casos de borda¶
| Resposta | Causa mais provável | O que fazer |
|---|---|---|
401 invalid_signature com o canônico igual ao dos vetores |
Dracma-Key-Id digitado errado, segredo de outra chave, segredo com espaço ou quebra de linha no fim |
Releia o segredo do cofre sem \n; confira o key_id |
401 invalid_signature só em POST |
Corpo reserializado depois de assinado, ou Content-Length diferente do corpo assinado |
Assine e envie os mesmos bytes |
401 invalid_signature só com query |
Query reordenada ou decodificada pelo cliente HTTP | Assine a URL exatamente como enviada |
401 timestamp_out_of_window |
Relógio fora de ±300 s; timestamp em milissegundos; com sinal, espaço ou zero à esquerda | NTP; floor(agora / 1000) |
401 replayed_nonce |
Nonce reutilizado, inclusive em retry | Gere nonce novo a cada tentativa |
401 unknown_key |
Prefixo key_test_ ou key_live_ do outro tipo de operação; chave revogada; key_id mal formado |
Confira a chave e o host |
403 insufficient_scope |
Chave sem o escopo da rota; método fora de GET, HEAD, POST |
Use a chave do componente certo |
403 operation_suspended |
Operação suspensa pelo Dracma | Perguntas frequentes |
429 rate_limited |
Limite de taxa ou 20 falhas de assinatura por minuto do mesmo IP | Limites e quotas |
503 service_unavailable |
Dependência do Dracma fora (nonces, segredos, limitador) | Espere Retry-After e repita |
Roteiro de diagnóstico completo, com os passos na ordem, em Problemas de integração.