Autenticação¶
Toda chamada do seu conector ao Dracma passa por três camadas independentes:
- mTLS (obrigatório em produção) — certificado de cliente da sua operação.
- Allowlist de IP da operação — só os IPs/CIDRs de saída que você cadastrou.
- Assinatura HMAC-SHA256 de cada requisição, com timestamp e nonce anti-replay. Esta camada sobrevive a um TLS terminado num proxy no meio do caminho.
E cada chave tem escopos: uma chave só de leitura não cria depósito.
Estado: tudo o que está nesta página está implementado no servidor (a pilha de autenticação de toda rota
/v1). A exceção (mTLS na borda) está marcada.
1. Os quatro headers¶
Dracma-Key-Id: key_test_01J9ABCDEF (key_live_… em produção)
Dracma-Timestamp: 1790000000 (segundos Unix, UTC)
Dracma-Nonce: 0123456789abcdef0123456789abcdef
Dracma-Signature: v1=a58fa1956941157aad72536512b2b09f234de3670050c865071ddc56df3b4964
| Header | Formato exato aceito pelo servidor |
|---|---|
Dracma-Key-Id |
key_test_ ou key_live_ seguido de 1–64 caracteres [A-Za-z0-9] |
Dracma-Timestamp |
só dígitos, sem sinal, sem espaço e sem zero à esquerda; segundos (não milissegundos) |
Dracma-Nonce |
exatamente 32 caracteres hex minúsculos (^[0-9a-f]{32}$) — 16 bytes de CSPRNG |
Dracma-Signature |
v1= + 64 caracteres hex minúsculos |
Todo POST também leva Idempotency-Key (erros-e-idempotencia.md) e
Content-Type: application/json; charset=utf-8.
2. Assinar, passo a passo¶
2.1 Pedido canônico¶
Seis linhas separadas por \n (LF, byte 0x0A), sem \n no final:
v1
<Dracma-Timestamp>
<Dracma-Nonce>
<MÉTODO em maiúsculas>
<path + "?" + query EXATAMENTE como enviados, ou só o path se não houver query>
<sha256 hex minúsculo do corpo cru (corpo vazio = sha256 de "")>
2.2 Assinatura¶
assinatura = hex_minúsculo( HMAC-SHA256( segredo_da_chave, pedido_canônico ) )
Dracma-Signature: v1=<assinatura>
O segredo é usado como os bytes da string que você recebeu (UTF-8), não decodificado de hex nem de base64.
2.3 As cinco armadilhas que mais quebram integração¶
- O path inclui
/v1. Se a sua base éhttps://sandbox.dracma.example/v1e você chama/deposits, o path assinado é/v1/deposits— o path completo que chega ao servidor. - Query exatamente como vai na linha de requisição. Não reordene parâmetros, não decodifique
%XX, não normalize. Monte a URL uma vez e assine a mesma string que o seu cliente HTTP envia (/v1/deposits?external_id=dep_x&limit=10). - 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 em hex minúsculo, gerado por CSPRNG.
crypto/rand,crypto.randomBytes,random_bytes,openssl rand -hex 16. NuncaMath.random()/math/rand. Nonce com letra maiúscula é recusado. - Timestamp em segundos.
Date.now()devolve milissegundos — divida por 1000 e trunque.
2.4 Exemplo trabalhado (vetor normativo nº 1)¶
Os valores abaixo vêm de exemplos/vetores-assinatura.json. São
valores de teste públicos; toda implementação deve reproduzi-los byte a byte.
Entradas:
| Campo | Valor |
|---|---|
| segredo | sk_test_vetor_0123456789abcdef |
| key_id | key_test_vetor |
| timestamp | 1790000000 |
| nonce | 0123456789abcdef0123456789abcdef |
| método | POST |
| path | /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 (6 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 path | Corpo | O que ele prova | Assinatura |
|---|---|---|---|---|
| 2 | GET /v1/deposits?external_id=dep_x&limit=10 |
vazio → e3b0c442…b855 |
query entra como enviada; corpo vazio = sha256 de "" |
v1=83b5e8b97bccfc97db1fdaa7f847b2a229a0835b88ce95032ab7c8f951487316 |
| 3 | POST /v1/withdrawals/wdr_01J9ZQ4/cancel |
vazio | POST sem corpo também assina o sha256 de "" |
v1=64df5c1a75e4cfc367e165f55be0d6b50678848069f96abb27a4f2559abdae5d |
Os clientes de exemplos/ (Go, Node/TypeScript, PHP, shell) têm testes que
reproduzem os três vetores.
2.5 Pseudocódigo¶
função chamar(método, path_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" + path_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; headers[Content-Type] = "application/json; charset=utf-8"
envia EXATAMENTE corpo_bytes para base_host + "/v1" + path_com_query, timeout 10 s
Uma nova tentativa é uma nova requisição: novo timestamp, novo nonce, nova assinatura — mas a
mesma Idempotency-Key (erros-e-idempotencia.md).
3. O que o servidor verifica, e em que ordem¶
A ordem abaixo é a do código atual (dracma_api@50ee6a6, Q010). Ela importa para você diagnosticar: o
primeiro erro encontrado é o que volta.
| # | Verificação | Falha |
|---|---|---|
| 1 | Corpo ≤ 64 KiB | 413 com code: validation_failed, errors[0].reason = "too_large" |
| 2 | Rate limit por IP de origem (antes de autenticar) | 429 rate_limited + Retry-After |
| 3 | Dracma-Key-Id bem formado e do ambiente certo pelo prefixo (key_test_ só em sandbox, key_live_ só em produção). Decidido só pelo header, sem consultar a chave |
401 unknown_key |
| 4 | Dracma-Timestamp canônico (só dígitos, sem zero à esquerda; ex.: +1790000000 e 01790000000 são recusados) e abs(agora − timestamp) ≤ 300 s |
401 timestamp_out_of_window |
| 5 | Dracma-Nonce no formato (32 hex minúsculos) |
401 invalid_signature |
| 6 | A chave existe e a assinatura confere com o segredo dela (comparação em tempo constante sobre os bytes decodificados). Chave inexistente e assinatura errada dão a mesma resposta | 401 invalid_signature (a partir de 20 falhas/min do mesmo IP: 429 rate_limited) |
| 7 | Só com a assinatura válida: chave ativa, não revogada e do ambiente certo no cadastro | 401 unknown_key |
| 8 | Só com a assinatura válida: IP de origem está na allowlist da operação | 403 ip_not_allowed (gera alerta no Dracma) |
| 9 | Só então o nonce é gravado; (key_id, nonce) já visto nos últimos ≥ 600 s |
401 replayed_nonce |
| 10 | Operação ativa | 403 operation_suspended |
| 11 | Rate limit por chave e por operação (e de listagem) | 429 rate_limited + Retry-After |
| 12 | Escopo da chave cobre a rota | 403 insufficient_scope |
| 13 | Idempotency-Key presente e válida em todo POST (inclusive /sandbox/*) |
400 validation_failed (errors[0].field = "Idempotency-Key", reason = "required") |
| 14 | Sandbox: queda simulada ligada (/sandbox/outage) em rota fora de /sandbox/* |
503 service_unavailable + Retry-After: 5 |
- Sem oráculo de existência de chave. Quem não prova a posse do segredo não descobre se a chave existe,
se foi revogada nem se o IP está na allowlist: chave desconhecida (bem formada), assinatura errada e IP
fora da allowlist sem assinatura válida voltam todos como
401 invalid_signature.401 unknown_keyde chave revogada e403 ip_not_allowedsó aparecem depois de a assinatura de uma chave real conferir. - Diagnóstico prático:
401 invalid_signaturecom o canônico conferindo com os vetores quase sempre éDracma-Key-Iddigitado errado (chave que não existe) ou segredo de outra chave.403 ip_not_allowedsó aparece com chave e segredo certos. - Header ausente:
Dracma-Key-Idausente →unknown_key; timestamp ausente →timestamp_out_of_window; nonce ou assinatura ausentes →invalid_signature. - A mensagem de erro nunca traz a assinatura esperada nem o pedido canônico calculado pelo servidor (seria um oráculo). Para depurar, compare o seu canônico com os vetores.
- Se o armazenamento de nonces, o cofre de segredos ou o limitador estiverem fora, o servidor falha
fechado com
503 service_unavailable(retryable) — nunca "aceita sem checar". - Todos os
401/403são definitivos para aquela requisição: não repita igual. Corrija a causa.
3.1 Janela de 300 s e relógio¶
A janela é de ±300 s, mas não conte com ela: mantenha NTP (chrony com ≥ 3 fontes) no host do
conector. O GET /v1/ping devolve server_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% de timestamp_out_of_window.
3.2 Nonce¶
- Um nonce por requisição, único por chave. Nunca reaproveite nonce em retry.
- O Dracma guarda
(key_id, nonce)por pelo menos 600 s; a mesma combinação é recusada comreplayed_nonce. Isso é o que mata o replay de uma requisição capturada. - Há um teto de nonces vivos por chave; estourar vira
429.
4. mTLS (produção)¶
- TLS 1.3 (1.2 só com suítes AEAD). Em produção o mTLS é obrigatório.
- O certificado de cliente é por operação, emitido pela CA privada do Dracma, validade ≤ 90 dias. Renove aos 60. Durante a troca, dois certificados válidos convivem, então a rotação não tem downtime.
- As CAs de sandbox e produção são distintas: certificado de sandbox não abre produção.
- A chave privada do certificado fica só no host do conector, legível apenas pelo usuário do serviço
(permissão
0400), fora do repositório e fora da imagem. De preferência, não exportável (TPM/HSM). - Tenant e operação são derivados só da chave e do certificado. Chave de uma operação com certificado
de outra →
401.
Estado real. A verificação do certificado de cliente fica na borda (proxy TLS) de produção, fora do código da API. O cruzamento "chave de uma operação com certificado de outra → 401" ainda não aparece no código da API (divergência D-A1 em divergencias.md). Trate-o como normativo: nunca use o certificado de uma operação com a chave de outra.
5. Allowlist de IP¶
- Cada operação tem uma lista de IPs/CIDRs de saída (IPv4 e IPv6): seu conector pode ser um cluster,
ter NAT redundante ou DR. Prefixo máximo aceito:
/24em IPv4,/56em IPv6. - Lista vazia em produção = operação sem acesso (fail-closed), nunca "libera tudo". Em sandbox, lista vazia aceita qualquer IP (é o comportamento do código); mesmo assim cadastre os IPs do seu conector desde o sandbox, para ensaiar a mesma configuração de produção.
- Mudar a allowlist é ação de painel com aprovação dupla; não há rota de API para isso. Planeje: IP novo de saída (troca de NAT, nova região) precisa entrar na lista antes do tráfego mudar.
- O Dracma usa o IP do socket (ou o do proxy de borda confiável dele). Não adianta mandar
X-Forwarded-For. - Qualquer
403 ip_not_allowedcom chave válida gera alerta no Dracma (credencial usada fora do lugar).
No sentido contrário, o Dracma envia webhooks de IPs de saída fixos e publicados; aplique-os na sua rota de webhook como defesa extra (webhooks.md).
6. Escopos¶
Cada chave tem um ou mais escopos. A política abaixo é a do código atual (deny-by-default: rota ou
método não mapeado → 403):
| Rota | Escopo exigido |
|---|---|
GET /v1/ping |
nenhum (qualquer chave válida) |
Qualquer outro GET |
read |
POST /deposits, POST /deposits/{id}/cancel |
deposits:write |
POST /withdrawals, POST /withdrawals/{id}/cancel, POST /withdrawals/{id}/recall |
withdrawals:write |
POST /rules/change-requests |
rules:propose |
POST /fx/quotes |
deposits:write ou withdrawals:write |
POST /sandbox/deposits/{id}/pay, …/expire, POST /sandbox/bank-transfers/incoming |
deposits:write |
POST /sandbox/withdrawals/{id}/{ação} |
withdrawals:write |
POST /sandbox/outage, POST /sandbox/webhooks/redeliver |
deposits:write ou withdrawals:write |
Recomendação: o conector que faz fetch-back e o job de /events precisa de read; dê
withdrawals:write só ao componente que de fato paga. Duas chaves com escopos menores são melhores que
uma chave com tudo.
7. Rotação de chaves e segredos¶
| Material | Validade | Sobreposição | Revogação de emergência |
|---|---|---|---|
Chave de chamada (key_live_… + segredo) |
rotação ≤ 90 dias (alerta aos 75) | até 2 chaves ativas por operação; a antiga expira ≤ 7 dias depois da nova entrar em uso | efetiva em ≤ 5 min em todos os nós |
Segredo de webhook (whsec_…) |
≤ 90 dias | o Dracma assina com os dois v1 por ≤ 7 dias |
≤ 5 min |
| Certificado mTLS de cliente | ≤ 90 dias, renovar aos 60 | 2 certificados válidos | ≤ 5 min |
Procedimento sem downtime para a chave de chamada:
sequenceDiagram
participant A as Admin do tenant (painel)
participant A2 as Segundo aprovador
participant D as Dracma
participant C as Seu conector
A->>D: pede chave nova (step-up)
A2->>D: aprova (4-eyes)
D-->>A: key_live_NOVA + segredo (exibido 1 vez)
A->>C: grava no cofre/seed do conector (pela sua ferramenta de deploy, nunca à mão no servidor)
C->>D: redeploy do conector → passa a assinar com a NOVA
D-->>A: painel mostra "último uso" de cada chave
A->>D: revoga a ANTIGA quando "último uso" > 1 h e 0 erros
- O segredo aparece uma vez. Se você perdeu, gere outra chave; o Dracma não consegue mostrar de novo.
- Para o segredo de webhook, seu verificador precisa aceitar uma lista de segredos ativos (atual + anterior) durante a sobreposição (webhooks.md).
- Vazou? Revogue na hora no painel ("Revogar agora", uma pessoa + step-up) e gere outra. Há também
"Suspender operação": todas as rotas passam a
403 operation_suspended.
8. Rate limit¶
Padrões do código atual (middleware.DefaultLimits; ajustáveis por contrato):
| Balde | Padrão | Estouro |
|---|---|---|
| Por IP de origem, antes da autenticação | 200 req/s | 429 rate_limited |
Por chave — escrita (POST) |
50 req/s, rajada 100 | 429 |
| Por operação — escrita | 50 req/s, rajada 100 | 429 |
Por chave — leitura (GET) |
100 req/s | 429 |
| Por operação — leitura | 200 req/s | 429 |
Listagens por chave: GET /deposits, GET /withdrawals, GET /events (sem id) |
5 req/s | 429 |
Depósitos criados por jogador (player.external_ref) na operação |
10 por hora | 429 (só conta corpo válido) |
| Falhas de assinatura por IP | 20 por minuto | a partir daí a resposta vira 429 em vez de 401 |
Estouro → 429 rate_limited com Retry-After (segundos). Espere e repita com a mesma
Idempotency-Key: o 429 libera a chave, então a repetição é reavaliada e nunca duplica. Limitador
indisponível → 503 service_unavailable retryable (nunca "sem limite").
Consequências práticas:
GET /deposits/{id}eGET /withdrawals/{id}(o fetch-back) usam o balde de leitura (100/s), não o de listagem. Faça fetch-back por id; useGET /deposits?external_id=só para recuperar um POST perdido.GET /reconciliationnão está no balde de listagem (usa o de leitura). O contrato fala em 5 req/s para/eventse/reconciliation; o código aplica os 5 req/s a/eventse às listagens (divergência D-A2 em divergencias.md). Pagine devagar nos dois.- Os limites de 10 depósitos e de 3 saques por jogador por hora são do lado do Dracma. Mostre ao jogador "tente mais tarde" em vez de repetir em laço.