Autenticação

Toda chamada do seu conector ao Dracma passa por três camadas independentes:

  1. mTLS (obrigatório em produção) — certificado de cliente da sua operação.
  2. Allowlist de IP da operação — só os IPs/CIDRs de saída que você cadastrou.
  3. 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

  1. O path inclui /v1. Se a sua base é https://sandbox.dracma.example/v1 e você chama /deposits, o path assinado é /v1/deposits — o path completo que chega ao servidor.
  2. 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).
  3. 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.
  4. Nonce em hex minúsculo, gerado por CSPRNG. crypto/rand, crypto.randomBytes, random_bytes, openssl rand -hex 16. Nunca Math.random()/math/rand. Nonce com letra maiúscula é recusado.
  5. 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_key de chave revogada e 403 ip_not_allowed só aparecem depois de a assinatura de uma chave real conferir.
  • Diagnóstico prático: 401 invalid_signature com o canônico conferindo com os vetores quase sempre é Dracma-Key-Id digitado errado (chave que não existe) ou segredo de outra chave. 403 ip_not_allowed só aparece com chave e segredo certos.
  • Header ausente: Dracma-Key-Id ausente → 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/403 sã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 com replayed_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: /24 em IPv4, /56 em 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_allowed com 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} e GET /withdrawals/{id} (o fetch-back) usam o balde de leitura (100/s), não o de listagem. Faça fetch-back por id; use GET /deposits?external_id= só para recuperar um POST perdido.
  • GET /reconciliation não está no balde de listagem (usa o de leitura). O contrato fala em 5 req/s para /events e /reconciliation; o código aplica os 5 req/s a /events e à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.