EagleIX
PIX (CNAB)

Autenticacao HMAC

Como assinar requisicoes para a API de PIX usando HMAC-SHA256

Toda chamada a POST /api/v1/pix exige assinatura HMAC-SHA256 valida. Chamadas sem assinatura, com assinatura invalida ou fora da janela de tempo sao rejeitadas com HTTP 401/403.

Conceitos

TermoSignificado
Client IDIdentificador publico do chamador (ex: cnab-uau). Vai no header X-Client-Id
SecretSegredo simetrico associado ao Client ID. Nunca trafega na request. Distribuido fora-de-banda (cofre, secret manager)
Idempotency-KeyUUID v4 unico por requisicao logica. Tem papel duplo: idempotencia e componente da assinatura
String canonicaTexto deterministico construido a partir da requisicao. E o que se assina, nao o body cru
Janela de timestampTempo maximo entre o X-Timestamp enviado e o relogio do servidor. Padrao: 2 minutos

Headers obrigatorios

HeaderFormatoExemplo
Content-Typeapplication/jsonapplication/json
Idempotency-KeyUUID v4550e8400-e29b-41d4-a716-446655440000
X-Client-Idstring ASCIIcnab-uau
X-TimestampISO-8601 UTC com sufixo Z2026-04-26T14:30:00Z
X-Signaturehmac-sha256=<hex-lowercase-64-chars>hmac-sha256=9f86d081...
  • X-Timestamp deve ser UTC (Z), nao usar offset (+00:00 ou -03:00)
  • X-Signature tem prefixo obrigatorio hmac-sha256=. Sem o prefixo a request e rejeitada
  • Idempotency-Key deve ser unico por operacao logica. Reusar gera HTTP 409

String canonica

A assinatura e calculada sobre uma string canonica composta por 5 linhas separadas por \n (LF, byte 0x0A) — nunca \r\n:

<METHOD em uppercase>\n
<path absoluto sem query string>\n
<valor de Idempotency-Key>\n
<valor de X-Timestamp>\n
<sha256(body) em hex lowercase>

Exemplo

Dado:

  • Metodo: POST
  • Path: /api/v1/pix
  • Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
  • Timestamp: 2026-04-26T14:30:00Z
  • Body: {"valor":1.00}

String canonica:

POST
/api/v1/pix
550e8400-e29b-41d4-a716-446655440000
2026-04-26T14:30:00Z
<sha256-hex-do-body>

Regras

ComponenteRegra
MetodoSempre uppercase. POST, nao post
Path/api/v1/pix. Sem query string, sem host, sem trailing slash
Idempotency-KeyMesmo valor enviado no header, byte a byte
TimestampMesmo valor enviado no header, byte a byte
Body hashsha256 dos bytes crus do request body, antes de qualquer parse JSON. Hex em lowercase
Body vazioHash do byte array vazio: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Separador\n (LF, 0x0A). Nunca \r\n
Trailing newlineNao colocar newline depois do hash do body

Passo a passo

  1. Gerar Idempotency-Key (UUID v4 novo por requisicao)
  2. Gerar X-Timestamp no formato YYYY-MM-DDTHH:MM:SSZ (UTC, agora)
  3. Serializar o body para JSON e converter para bytes UTF-8. Guardar esses bytes — sao usados para o hash e para o envio
  4. Calcular bodyHash = lowercase(hex(sha256(bodyBytes)))
  5. Montar a string canonica
  6. Calcular signatureBytes = HMAC_SHA256(secret, canonicalString)
  7. Codificar signatureHex = lowercase(hex(signatureBytes))
  8. Enviar a request com header X-Signature: hmac-sha256=<signatureHex>

O body enviado no passo 3 tem que ser exatamente o mesmo usado para calcular o hash no passo 4. Se voce reserializar o JSON depois (mudando ordem de chaves, espacamento ou numeros), o hash do servidor nao bate.


Exemplos

Em todos os exemplos, substitua SECRET, o body e a URL conforme seu ambiente. O segredo nunca deve ser hard-coded em codigo de producao — leia de variavel de ambiente, AWS Secrets Manager, GCP Secret Manager ou cofre equivalente.

Bash + openssl

#!/usr/bin/env bash
set -euo pipefail

URL="https://api.example.com/api/v1/pix"
CLIENT_ID="cnab-uau"
SECRET="${CNAB_UAU_HMAC_SECRET:?defina CNAB_UAU_HMAC_SECRET}"

BODY='{"dataLancamento":"2026-04-26","valor":1.00,"tipoChave":"CHAVE_PIX_EMAIL","chavePix":"a@b.com"}'
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
IK=$(uuidgen | tr '[:upper:]' '[:lower:]')

BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
CANONICAL=$(printf 'POST\n/api/v1/pix\n%s\n%s\n%s' "$IK" "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')

curl -sS -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IK" \
  -H "X-Client-Id: $CLIENT_ID" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: hmac-sha256=$SIG" \
  --data-raw "$BODY"

Python

import hashlib, hmac, json, os, uuid, datetime, urllib.request

URL = "https://api.example.com/api/v1/pix"
CLIENT_ID = "cnab-uau"
SECRET = os.environ["CNAB_UAU_HMAC_SECRET"].encode("utf-8")

payload = {
    "dataLancamento": "2026-04-26",
    "valor": 1.00,
    "tipoChave": "CHAVE_PIX_EMAIL",
    "chavePix": "a@b.com",
}
body_bytes = json.dumps(payload, separators=(",", ":")).encode("utf-8")

idempotency_key = str(uuid.uuid4())
timestamp = datetime.datetime.now(
    datetime.timezone.utc
).strftime("%Y-%m-%dT%H:%M:%SZ")

body_hash = hashlib.sha256(body_bytes).hexdigest()
canonical = f"POST\n/api/v1/pix\n{idempotency_key}\n{timestamp}\n{body_hash}"
signature = hmac.new(
    SECRET, canonical.encode("utf-8"), hashlib.sha256
).hexdigest()

req = urllib.request.Request(
    URL,
    data=body_bytes,
    method="POST",
    headers={
        "Content-Type": "application/json",
        "Idempotency-Key": idempotency_key,
        "X-Client-Id": CLIENT_ID,
        "X-Timestamp": timestamp,
        "X-Signature": f"hmac-sha256={signature}",
    },
)
with urllib.request.urlopen(req) as resp:
    print(resp.status, resp.read().decode())

Cuidado com json.dumps do Python: o exemplo usa separators=(",", ":") para gerar JSON compacto e deterministico. Se voce usar indent=... ou outras opcoes, os bytes mudam.

Node.js

import crypto from "node:crypto";
import { randomUUID } from "node:crypto";

const URL_API = "https://api.example.com/api/v1/pix";
const CLIENT_ID = "cnab-uau";
const SECRET = process.env.CNAB_UAU_HMAC_SECRET;

const payload = {
  dataLancamento: "2026-04-26",
  valor: 1.0,
  tipoChave: "CHAVE_PIX_EMAIL",
  chavePix: "a@b.com",
};
const bodyString = JSON.stringify(payload);

const idempotencyKey = randomUUID();
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");

const bodyHash = crypto
  .createHash("sha256")
  .update(bodyString, "utf8")
  .digest("hex");
const canonical = `POST\n/api/v1/pix\n${idempotencyKey}\n${timestamp}\n${bodyHash}`;
const signature = crypto
  .createHmac("sha256", SECRET)
  .update(canonical, "utf8")
  .digest("hex");

const resp = await fetch(URL_API, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
    "X-Client-Id": CLIENT_ID,
    "X-Timestamp": timestamp,
    "X-Signature": `hmac-sha256=${signature}`,
  },
  body: bodyString,
});
console.log(resp.status, await resp.text());

Java (JDK 11+)

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.HexFormat;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class PixClientExample {
    public static void main(String[] args) throws Exception {
        String url = "https://api.example.com/api/v1/pix";
        String clientId = "cnab-uau";
        String secret = System.getenv("CNAB_UAU_HMAC_SECRET");

        String body = "{\"dataLancamento\":\"2026-04-26\",\"valor\":1.00,"
                    + "\"tipoChave\":\"CHAVE_PIX_EMAIL\",\"chavePix\":\"a@b.com\"}";
        byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);

        String idempotencyKey = UUID.randomUUID().toString();
        String timestamp = DateTimeFormatter.ISO_INSTANT
                .format(Instant.now().truncatedTo(
                    java.time.temporal.ChronoUnit.SECONDS));

        HexFormat hex = HexFormat.of();
        String bodyHash = hex.formatHex(
            MessageDigest.getInstance("SHA-256").digest(bodyBytes));
        String canonical = "POST\n/api/v1/pix\n"
            + idempotencyKey + "\n" + timestamp + "\n" + bodyHash;

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(
            secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        String signature = hex.formatHex(
            mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));

        HttpRequest request = HttpRequest.newBuilder(URI.create(url))
                .header("Content-Type", "application/json")
                .header("Idempotency-Key", idempotencyKey)
                .header("X-Client-Id", clientId)
                .header("X-Timestamp", timestamp)
                .header("X-Signature", "hmac-sha256=" + signature)
                .POST(HttpRequest.BodyPublishers.ofByteArray(bodyBytes))
                .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}

Postman (Pre-request Script)

Cole no tab Pre-request Script da request:

const secret = pm.environment.get("CNAB_UAU_HMAC_SECRET");
const clientId = pm.environment.get("CNAB_CLIENT_ID") || "cnab-uau";

const idempotencyKey = crypto.randomUUID();
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");

const bodyRaw =
  pm.request.body && pm.request.body.raw ? pm.request.body.raw : "";
const bodyHash = CryptoJS.SHA256(bodyRaw).toString(CryptoJS.enc.Hex);

const canonical = `POST\n/api/v1/pix\n${idempotencyKey}\n${timestamp}\n${bodyHash}`;
const signature = CryptoJS.HmacSHA256(canonical, secret).toString(
  CryptoJS.enc.Hex,
);

pm.request.headers.upsert({ key: "Idempotency-Key", value: idempotencyKey });
pm.request.headers.upsert({ key: "X-Client-Id", value: clientId });
pm.request.headers.upsert({ key: "X-Timestamp", value: timestamp });
pm.request.headers.upsert({
  key: "X-Signature",
  value: `hmac-sha256=${signature}`,
});

Validacoes no servidor

As validacoes sao executadas na seguinte ordem:

  1. Existencia de todos os headers obrigatorios (401 MISSING_HEADER)
  2. X-Client-Id conhecido (403 FORBIDDEN)
  3. X-Signature comeca com hmac-sha256= (401 INVALID_SIGNATURE_FORMAT)
  4. X-Timestamp parseavel como ISO-8601 (401 TIMESTAMP_INVALID)
  5. Diferenca entre now e X-Timestamp dentro de 2 min (401 TIMESTAMP_EXPIRED)
  6. Body ate 1 MB (401 BODY_TOO_LARGE)
  7. HMAC recalculado bate com X-Signature em comparacao constant-time (401 SIGNATURE_MISMATCH)
  8. Idempotency-Key ainda nao usada (409 CONFLICT)

Em caso de falha o servidor responde com mensagem generica "Autenticacao invalida" — o motivo exato fica apenas em logs e metricas (hmac_auth_failures_total), para nao dar pista a atacante.


Codigos de erro

HTTPCodigoQuando aconteceAcao do cliente
401UNAUTHORIZEDHeaders HMAC ausentes, timestamp expirado/invalido, assinatura nao bate, body > 1MBVerificar implementacao
403FORBIDDENX-Client-Id desconhecidoVerificar valor ou pedir provisionamento
409CONFLICTIdempotency-Key ja usadaUsar chave nova para nova operacao

Toda resposta de erro inclui traceId para correlacao:

{
  "code": "UNAUTHORIZED",
  "message": "Autenticacao invalida",
  "traceId": "abc123def456",
  "timestamp": "2026-04-26T14:30:01.234Z"
}

FAQ

Posso reaproveitar a assinatura ao retentar uma chamada que deu timeout?

Sim, se ainda estiver dentro da janela de timestamp (2 min) e usar a mesma Idempotency-Key, mesmo body e mesmo X-Timestamp. O servidor reconhece a chave e devolve resposta idempotente, ou processa pela primeira vez se nunca chegou antes.

Por que Idempotency-Key faz parte da assinatura?

Para impedir que um atacante reutilize uma assinatura valida com Idempotency-Key diferente para forcar nova execucao. Como a chave entra na canonica, mudar a chave invalida a assinatura.

Posso usar offset (-03:00) no timestamp?

Nao. O servidor usa Instant.parse(), que aceita apenas Z (UTC). Sempre converta para UTC antes de formatar.

Como rotacionar o segredo?

Atualmente: redeploy com novo valor da variavel de ambiente. Coordenacao necessaria com o cliente.


Troubleshooting

SintomaCausa provavelComo corrigir
Sempre 401 mesmo com tudo certoReserializacao do body entre calculo do hash e envioLogar o byte array exato usado em ambos os pontos
401 ocasional em horarios de picoDrift de relogioVerificar NTP no host do cliente
401 so atras de proxyProxy reescreve path/methodConfirmar que o servidor recebe POST /api/v1/pix
403 FORBIDDENX-Client-Id nao cadastrado no ambienteValidar valor; pedir provisionamento
409 CONFLICT em retryIdempotency-Key ja usada com sucessoGerar chave nova para nova operacao
Funciona em curl, falha em PostmanPostman re-formatando o bodyUsar pre-request script e conferir pm.request.body.raw

Inclua o traceId da resposta de erro em qualquer abertura de chamado — ele permite correlacionar com os logs do servidor.


Checklist do cliente

  • Segredo lido de variavel de ambiente / secret manager (nunca hard-coded)
  • Segredo nunca aparece em logs nem em mensagens de erro
  • Idempotency-Key e UUID v4 novo a cada operacao logica
  • X-Timestamp em UTC com sufixo Z, sem milissegundos, gerado no momento do envio
  • Mesma string de body usada para hash e para envio (sem reserializacao)
  • Hash hex em lowercase
  • Separador da canonica e \n, nao \r\n
  • Sem trailing newline no fim da canonica
  • Em 5xx ou timeout: reusar a mesma Idempotency-Key. Em 2xx ou 4xx: gerar nova chave
  • Relogio do host sincronizado via NTP (drift < 30s)
  • Comunicacao so por HTTPS

On this page